gale.state
This module contains two ways to organize the states of your game (or of
any object in it, such as a character): StateMachine, which holds a
single active state, and StateStack, which stacks states on top of
each other (useful, for instance, to open a pause menu on top of the
gameplay state without destroying it). In practice, StateMachine is
by far the most used of the two: at the game level, to drive its overall
flow (title screen, play, pause, game over, …), and at the entity
level, to drive a single character’s behavior (idle, walk, jump, …).
StateMachine
from gale.state import BaseState, StateMachine
class TitleState(BaseState):
def enter(self) -> None:
print("Showing title screen")
def on_input(self, input_id, input_data) -> None:
if input_id == "start" and input_data.pressed:
self.state_machine.change("play")
class PlayState(BaseState):
def enter(self, level: int = 1) -> None:
self.level = level
def update(self, dt: float) -> None:
...
state_machine = StateMachine({"title": TitleState, "play": PlayState})
state_machine.change("title")
# Later on, for instance when the player presses "start":
state_machine.change("play", level=1)
States are meant to be disposable: StateMachine.change throws the
previous state away and builds a brand new one every time, so any data a
state needs to keep going has to flow through enter(**params). A
common pattern is passing a state’s entire live context forward when
transitioning away from it, so it can be restored unchanged when coming
back, for instance to implement pause/resume:
class PlayState(BaseState):
def enter(self, level=1, score=0, lives=3) -> None:
self.level, self.score, self.lives = level, score, lives
def on_input(self, input_id, input_data) -> None:
if input_id == "pause" and input_data.pressed:
self.state_machine.change(
"pause", level=self.level, score=self.score, lives=self.lives
)
class PauseState(BaseState):
def enter(self, **play_state_params) -> None:
self.play_state_params = play_state_params
def on_input(self, input_id, input_data) -> None:
if input_id == "pause" and input_data.pressed:
self.state_machine.change("play", **self.play_state_params)
When a state needs a dependency other than the state machine itself
(the owning Game instance, or the entity a state belongs to), pass a
lambda instead of the class directly, and override __init__ to accept
the extra argument:
class StartState(BaseState):
def __init__(self, state_machine: StateMachine, game) -> None:
super().__init__(state_machine)
self.game = game
state_machine = StateMachine({
"start": lambda sm: StartState(sm, game=my_game),
"play": PlayState,
})
The same trick is handy to give every character of a given type its own
StateMachine built from a shared dictionary of state classes:
class Creature:
def __init__(self, states: dict) -> None:
self.state_machine = StateMachine(
{name: (lambda sm, cls=cls: cls(self, sm)) for name, cls in states.items()}
)
self.state_machine.change("walk")
HierarchicalState
A HierarchicalState is a BaseState that owns its own nested
StateMachine (its “substate machine”). It is used to build hierarchical
state machines (HFSM): a “superstate” such as Patrol can itself be made
of finer-grained substates, without needing any machinery beyond
StateMachine and HierarchicalState composed together. Nesting a
HierarchicalState inside another one is enough to build deeper
hierarchies.
from gale.state import BaseState, HierarchicalState, StateMachine
class Walking(BaseState):
def update(self, dt: float) -> None:
... # move towards the next waypoint
if self.reached_waypoint():
self.state_machine.change("looking_around")
class LookingAround(BaseState):
def update(self, dt: float) -> None:
... # rotate in place scanning the surroundings
if self.done_looking():
self.state_machine.change("walking")
class Patrol(HierarchicalState):
def __init__(self, state_machine: StateMachine) -> None:
super().__init__(
state_machine,
substates={"walking": Walking, "looking_around": LookingAround},
initial_substate="walking",
)
class Suspicious(BaseState):
...
class Alert(BaseState):
...
class Search(BaseState):
...
guard_fsm = StateMachine({
"patrol": Patrol,
"suspicious": Suspicious,
"alert": Alert,
"search": Search,
})
guard_fsm.change("patrol")
Because Patrol.enter (inherited from HierarchicalState) selects
"walking" as soon as the guard enters the patrol state, calling
guard_fsm.update(dt) every frame automatically drives whichever substate
(Walking or LookingAround) is currently active, in addition to
Patrol itself. If Patrol overrode update to check for the
player, it would call super().update(dt) to keep that delegation
working.
StateStack
from gale.state import BaseState, StateStack
class PlayState(BaseState):
def render(self, surface) -> None:
...
class PauseState(BaseState):
def render(self, surface) -> None:
... # draw a translucent overlay on top of PlayState
state_stack = StateStack()
play_state = PlayState(state_stack)
state_stack.push(play_state)
# Opening the pause menu keeps PlayState in the stack, so it still renders
# underneath, but only PauseState (the top of the stack) gets updated.
pause_state = PauseState(state_stack)
state_stack.push(pause_state)
# Closing the pause menu.
state_stack.pop()