I’ve used state machines (FSM) in the past for characters, and I see it widely used and recommended, but I have seen some drawbacks to the pattern. I am working on a state machine variant to try and improve the pattern, specifically for characters.
I wanted to reach out and see what other devs think. What are some difficulties you have faced with the pattern or aspects you don’t like?
For me I have found state machines lead to two problem areas due to their typical structure.
First is handling shared functionality across states. I’ve seen two ways to handle this and don’t like either. You can use a monolithic state that toggles off actions with a bool which leaves a lot of dead code and bad re-usability, or use inheritance which can hide functionality in parent classes and lead to inheritance problems.
Second, a lot of states are needed to cover all the states a character can be in, making it difficult to manage characters that may be given temporary, or unlock, abilities. Same thing for transitions to states, that requires another specialized state.
This pattern does not feel dynamic enough to properly handle characters. It feels rigid and I do not like that.
What are your thoughts?
I started this looking as a way to find ways to improve on state machines since I was under the impression they were widely used, due to seeing many tutorials for them and versions of them on the Godot asset library.
This conversation has turned into an insightful sharing of how everyone develops their characters differently, with different views on code structure.
Feel free to join in and share how you make, architect, or design your character functionality.
I do something similar. Instead of lambdas I use nodes with a play function. A temporary effect state is also a good idea. I’ve done that in the animation tree, just changed the node’s animation.
Here I have the game I’m working this week, Rick O’Shea. As you can see, I have separate Movement and an AttackStateMachine objects. The player can shoot anytime, and so it is in its own machine. The AnimationTree also reflects this:
So I can constantly be running a movement animation, and blend in the Shoot animation when it is triggered.
This is why I use a pull state machine instead of a push state machine. You can check out the concept by using my State Machine Plugin. Each state is atomic, and has no knowledge, or dependency on any other state. It handles its own transitions, and tells the machine when it should be active. States can also lock the machine so they cannot be exited.
If I have a special ability, I just add a state for it. When it’s in the tree, it works. When it’s not nothing breaks. And the StateMachine can handle adding and removing of States even when the game is running.
What I meant by shared functionality is states that would cover similar cases. I see you have a walk and fall state. So if the player can move while falling, you now have movement logic in two places. Same thing would happen if you had a dash/dodge that could be triggered on the ground or in air.
I actually use something similar, the responsibility of state switching is on the “state”. Problem is I find it does technically create dependencies on other states as soon as you want a transition to be defined to another state. The state needs to know about the other state if it wants to define special behavior. Unless you found a way around that.
Yes, specifically I have a walk movement speed and an air control speed. Which means the player may be more or less mobile when falling, or jumping. These allow you to create various jump types just by tweaking a few values (which include jump velocity on Jump, and gravity multiplier on Jump and Fall). Specifically though you don’t have movement logic in multiple places, you have different entry points to the movement logic with different values.
Exactly. You seem to be implying this is a bad thing. It’s just a configurable thing.
The only state that knows about another state is Idle. And that’s so that when the player presses the jump button and hasn’t left the ground yet, the Idle state doesn’t take back over. But I could also have solved this by locking the Jump state until it reaches the apex of its height - or at least for a frame. Which now that we have discussed it, I probably will.
For example, this is the Fall state:
# If the player is falling, switch to the Fall state.
func _process(_delta: float) -> void:
if not is_current_state() \
and not character.is_on_floor() \
and character.velocity.y >= 0.0:
switch_state()
You’d have to be more specific about the issues you’re encountering with state dependency for me to help beyond that.
Shared functionality? Here’s a classic java slogan: pick composition over inheritance!
var spider = SpiderBehaviour.new(target_node)
var cannon = CannonBehaviour.new(target_node)
...
func do_state_behaviour():
SpiderBehaviour.lift_leg_to_worldspace(target_node.legs[2].position += Vector3(0,0.41,0))
if(target_node.enemy != null):
cannon.point_towards(target_node.enemy.position)
pass
Too many states? Nest them together (a three-state jump becomes a jump state with three stages) or Run multiple state-machines simultaneously for state combinations (30% attack-up state + punching, or 50% slower-attack-speed state + punching). Instead of having 4*3 = 12 modified attacks, you only have to code 4 + 3 = 7 states total!
Maybe its not as bad as I think but I am implying that. There’s the whole “don’t repeat yourself” saying and “single responsibility” principle. That makes me not like seeing the same thing happen in two places, but maybe I am taking it too far.
When you say “entry” that makes me think you have the actual movement logic pulled out into a different class that states use to move (something I actually do). Is that what you mean or do you do physics in two states?
Since your states control when they are active, how do you handle if two want to be active at once?
My set up is different so maybe its just due to my system but I don’t know how to transition to a state from another without them knowing about each other. If state A is active and B wants to be active, would A not have to know about B so it can define a transition unique to B? Or B knows about A so it can define the transition? It seems like one state would need to be able to check the old/new state so it can do something before activating.
I suppose a typical state machine would have states A and B, then a transition state C.
if A is active and transitioning to B, enter C then B
if B is active and transitioning to A, enter A
Composition is how I am trying to improve on the FSM. I define action components that combine to make a state. States can be general and just composed of actions. This of course only cuts down on states if there is a lot of shared functionality.
I was a professional Java developer for years. I have never heard that as a Java-specific slogan. I have heard it as the start of an argument of composition vs. inheritance. IMO, the answer depends on what you’re doing.
DRY and SRP are good rules of thumb, but sometimes they don’t fit. Working code is more important. Also, you sound like you’re future proofing. Make it work, then refactor.
I actually do physics in multiple states. The only code in the player’s _physics_process() is move_and_slide(). The states actually manipulate the velocity. Is that what you’re asking?
I tweak the conditions until they don’t. Or, if I’m in a hurry I just see if the state I shouldn’t interrupt is running, like in Idle:
## If the player stops moving, move to the Idle state.
func _process(_delta: float) -> void:
if not is_current_state() \
and character.is_on_floor() \
and character.direction == Vector3.ZERO \
and not get_current_state() is JumpPlayerState:
switch_state()
Or, I set the State’s can_transition variable to false, and the StateMachine won’t let anything else take over. So, three options there.
If a state wants to activevate itself, it calls switch_state()
## Asks the [StateMachine] to switch to this [State]. Should always be used
## instead of calling _enter_state() when a [State] wants to switch to itself.
func switch_state() -> void:
_state_machine.switch_state(self)
Which as you can see, is a helper function that calls the same function on the StateMachine. Which is more complex:
## Should ideally be called from [method State.switch_state][br][br]
## Switch to the target [State] from the current [State]. Fails if:[br]
## 1. The [StateMachine] does not have the passed [State].[br]
## 2. The [StateMachine] is already in that [State].[br]
## 3. The current [State] won't allow a transition to happen because its [member State.can_transition] = false.[br]
## 4. The target [State] won't allow a transition to happen because its [member State.can_transition] = false (e.g. cooldown timers).
func switch_state(state: State) -> void:
if not is_running:
print_rich("[color=red][b]ERROR[/b][/color]: %s State Machine is off! Cannot enter %s!" % [subject.name, state.name])
return # The StateMachine is not running.
if not _machine_has_state(state): return # The StateMachine does not have the passed state.
if _current_state == state: return # The StateMachine is already in that state.
if not state.can_transition: return # The target State won't allow a transition to happen (e.g. cooldown timers).
if _current_state:
if not _current_state.can_transition: return # The current State won't allow a transition to happen.
_current_state._exit_state() # Run the exit code for the current state.
_current_state = state # Assign the new state we are transitioning to as the current state.
_current_state._enter_state() # Run the enter code for the new current state.
state_changed.emit()
## Should ideally be called from [method State.is_current_state][br][br]
## Returns true if the passed [State] is the current [State].
func is_current_state(state: State) -> bool:
return _current_state == state
But luckily fully commented.
So the answer is, the State doesn’t know other states exist or are running. It just asks the StateMachine to enter. The StateMachine says, yes or no. On no, the state asks again next frame if the conditions are still right. On yes, the StateMachine tells the previous State it is exiting, and tells the requesting State it is entering. That’s it.
I’ve seen you talk about the pull state machine and your state machine plugin a few times and I am interested in learning more about how a pull state machine works. The ease of use of just adding or removing states from the scene hierarchy and not having to set a state variable in the player script for each new state added (which is what I currently do) seems great.
But whenever I start thinking about refactoring my state machine (which isn’t even node-based) into a node-based pull state machine, I realize there are things I wouldn’t know how to do.
Think of a simple example:
If I press jump when in the idle state, the character jumps (enters jump state).
But if I press jump when in the crouched state, the character does a forward roll (enters roll state).
Wouldn’t the jump and roll states need to check if the current state is crouched or not? So wouldn’t it need to know about those states?
Or would you just create a ‘can_roll’ and a ‘can_jump’ flag somewhere and then they can check for that? And if so, where would you put those flags, in the player script?
How do you handle this type of situation?
I am learning this. With this project of mine I have come to see that even monolithic classes have their merits.
I am. I have a working system but I am refining it now. That’s why I made this post. I’m looking for situations state machines are used in and fail in, or at least make complicated, so I can attempt to address them and see how my system would handle the same situations.
That was. In my own project I just complicate it more by having a class with an apply_force()function since multiple sources can effect velocity at a time. This prevents one overriding the other. But I can see you don’t need that if only one state is active at a time, only one would have control over the physics.
So I am still confused on how your system handles transition states like my A, B, C example. It sounds like the state machine would handle transitions since it is where the switching happens. But that doesn’t seem right since that would cause coupling. I’ve been under the impression that the states would handle transitions, but that would require knowledge of other states from what I can tell. And if you did use a transition state, wouldn’t that cause more issues since now the transition state and state being transitioned to need to know about other states so they know which should be active?
Like if a character is running and the player hits crouch, the character would slide then crouch. Sliding is the transition state. But if the character is idle and the player hits crouch, the character just crouches. Since the transition depends on the previous state, do the states not need to know about each other?
May I also ask where you handle input? In or out of the states?
I don’t want to steal you interest away from their state machine but the system I have been working on would handle this pretty easily and is also node based.
You don’t make state classes, you make action nodes that do one action (move, jump, crouch). States are composites of action nodes with requests mapped to the actions, like IDs.
I don’t know where input is intended to be handled in the pull state machine but looking at your example it may be possible to have the crouch state activate over the roll state if no input other than “crouch” is given. But this probably won’t work with a toggle-able crouch, which makes me think you would need to check the current state, or have a global “state” variable all states can read. Then the crouch and roll states would use that to determine which can go.
I’ve been looking into @dragonforge-dev state machine for my own needs. After studying it, I think it’s a really solid implementation.
That said, another idea I’ve been considering is something similar to Unreal Engines GAS/gameplay tag system. It’s like an FSM, but has a hierarchical nature that allows for state combinations. In a sense, it’s basically what Dragonforge is doing with having a movement state machine and an action state machine.
And now you have a system where you can only be in one state from the first level catagories, but the others can be active simultaneously. (eg moving and falling and attacking)
There’s a bit more nuance in Unreal’s system, but if one were to implement something similar in Godot, I believe it’d be basically that, yeah. The main point, though is not considering idle and moving to be of the same category type as falling and crouching. This would then avoid your concern of duplicate code.
This actually makes me think what I am doing is correct then. I am breaking states into action components that get put together to make states. Multiple actions can be active at once.
You could detect how high your character’s head is. Or you could just see what state they are in.
## If the player stops moving, move to the Idle state.
func _process(_delta: float) -> void:
if not is_current_state() \
and character.is_on_floor() \
and character.direction == Vector3.ZERO \
and not get_current_state() is JumpPlayerState:
switch_state()
That last line in the if statement tests for the JumpPlayerState as a Type, and if it’s running, doesn’t run. So name your crouch state something like CrouchState and do this:
Jump State
# If the player presses the Jump action, switch to the Jump state.
func _process(_delta: float) -> void:
if not is_current_state() \
and Input.is_action_just_pressed(GameConstants.INPUT_JUMP) \
and character.is_on_floor()
and not get_current_state() is CrouchState:
switch_state()
Roll State
# If the player presses the Jump action, switch to the Jump state.
func _process(_delta: float) -> void:
if not is_current_state() \
and Input.is_action_just_pressed(GameConstants.INPUT_JUMP) \
and character.is_on_floor()
and get_current_state() is CrouchState:
switch_state()
Yes and no. They would need to know that the state exist, but they can just poll the StateMachine through get_current_state() to see what state is active. That’s all they need to know. And if that state isn’t in the machine, it doesn’t matter because it’s a Type check.
An older version of the machine had these in there. @wchc and I actually discussed this at length and we ended up making a change to it. In fact @wchc made a PR that added in the ability to store these flags in the StateMachine. Ultimately though, Type checking seemed less invasive. But that took a few months of trying out the other system to get there.
I did not mean to encourage monolithic classes. Those are typically a result of feature creep. At some point, it’s helpful to refactor or you end up with increased cognitive load trying to debug monolithic classes. All I’m saying is that SRP taken to the extreme can be…infeasible to implement. And DRY should be the result of refactoring - not architectural in nature. I.E. you don’t plan to DRY up your code, you do it when you see where it needs it.
Ok, well I’d recommend implementing things you’re actually going to use. Hypotheticals lead to hypothetical work that gets thrown out. as long as this is just for funsies, you should be ok. But trying to future proof your code is an anti-pattern that prevents you from building your systems.
I actually at one point had the _physics_process() of the Fall state always applying gravity. In the end, I split it up because I wanted more control when jumping up and falling down. The Move state (before it became the Walk/Run states) also used to always have _physics_process() active to allow for air control. But over time, splitting it up made more sense to me.
I ended up changing it so that only one state at a time could modify it. But you can add forces from multiple different places without having a router to handle it. The player’s velocity variable can be the router.
I bolded that last sentence there. Because I believe that’s what you’re not getting. That is an untrue statement. The transition might depend on the currentState. But there is no need for a “transition state”. Each State knows what conditions allow it to enter. And for most states, exiting is handled by another State pulling the StateMachine and saying “It’s my turn now!”
Let’s take a StateMachine from another game. In this game there are States for Crouch, CrouchWalk and Slide. Note, this game has reverse gravity fields and I made custom code for dealing with that. Also, the function set_move_state() is just a call that is passed to the AnimationPlayer in more recent games that call is now trigger_animation() and tied directly to the AnimationTree - where before it was being routed through the character/player object because I thought I needed to centralize it.
class_name CrouchPlayerState extends PlayerState
func _activate_state() -> void:
super()
set_process(true)
func _enter_state() -> void:
super()
character.player_statistics.add(GameConstants.PLAYER_STATISTIC_CROUCHES, 1)
character.set_move_state(GameConstants.MoveState.CROUCH)
func _exit_state() -> void:
super()
# If the player releases the Crouch action, exit the Crouch state.
# If the player presses the Crouch action, switch to the Crouch state.
func _process(_delta: float) -> void:
if is_current_state():
if Input.is_action_just_released(GameConstants.INPUT_CROUCH):
clear_state()
elif Input.is_action_pressed(GameConstants.INPUT_CROUCH):
if character.is_on_floor_or_ceiling() and character.direction == 0.0:
switch_state()
CrouchWalkPlayerState
class_name CrouchWalkPlayerState extends PlayerState
# Player speed is reduced by this amount while crouch walking.
@export var speed_reduction: float = 0.5
func _activate_state() -> void:
super()
set_process(true)
func _enter_state() -> void:
super()
character.set_move_state(GameConstants.MoveState.CROUCH)
set_physics_process(true)
func _exit_state() -> void:
super()
set_physics_process(false)
## If the player has directional input and is crouching, move to the Crouch Walk state.
func _process(_delta: float) -> void:
if is_current_state():
if Input.is_action_just_released(GameConstants.INPUT_CROUCH):
clear_state()
# Enter only if we have directional input and we are already crouching, otherwise this overrides slide
elif Input.is_action_pressed(GameConstants.INPUT_CROUCH):
if character.is_on_floor_or_ceiling() \
and (Input.is_action_pressed(GameConstants.INPUT_MOVE_LEFT) or Input.is_action_pressed(GameConstants.INPUT_MOVE_RIGHT)) \
and not get_current_state() is SlidePlayerState:
switch_state()
## Handles movement
func _physics_process(_delta: float) -> void:
character.direction = Input.get_axis(GameConstants.INPUT_MOVE_LEFT, GameConstants.INPUT_MOVE_RIGHT)
character.velocity.x = lerp(character.velocity.x, character.direction * character.speed * speed_reduction, character.friction_coefficient)
SlidePlayerState
class_name SlidePlayerState extends PlayerState
## Maximum amount of time a player can slide.
@export var max_slide_duration: float = 0.8
@onready var slide_timer: Timer
func _activate_state() -> void:
super()
set_process(true)
slide_timer = Timer.new()
add_child(slide_timer)
slide_timer.timeout.connect(_on_slide_timer_timeout)
func _deactivate_state() -> void:
super()
slide_timer.timeout.disconnect(_on_slide_timer_timeout)
func _enter_state() -> void:
super()
character.player_statistics.add("slides", 1)
character.set_move_state(GameConstants.MoveState.SLIDE)
slide_timer.start(max_slide_duration)
set_physics_process(true)
func _exit_state() -> void:
super()
set_physics_process(false)
slide_timer.stop()
func _process(_delta: float) -> void:
# Enter only if we have directional input, and the character is on the floor or ceiling
if not is_current_state() \
and character.is_on_floor_or_ceiling()\
and abs(character.velocity.x) > 0.0 \
and Input.is_action_just_pressed(GameConstants.INPUT_CROUCH) \
and not get_current_state() is CrouchPlayerState:
switch_state()
func _physics_process(_delta: float) -> void:
character.direction = Input.get_axis(GameConstants.INPUT_MOVE_LEFT, GameConstants.INPUT_MOVE_RIGHT)
if character.direction == 0 or not Input.is_action_pressed(GameConstants.INPUT_CROUCH):
clear_state()
character.velocity.x = lerp(character.velocity.x, character.direction * character.speed, character.friction_coefficient)
func _on_slide_timer_timeout() -> void:
if is_current_state():
clear_state()
In the states. Though I have considered a Command design pattern approach. In that case, input would be handled outside the states, so that player and enemy/NPC states would be identical, and player input and Enemy/NPC AI would create commands to send to the states. I don’t know what that looks like yet, but it allows a lot of things - including protection from hacking in an online game, and DRYing up the code.
If a state machine doesn’t adequately model what’s happening with the character - don’t use that abstraction to think about it or implement it. Imo, state machines are useful abstractions only for very simple character controlers and you’re totally right in concluding they’re not very flexible.
If you abandon the notion that character is required to “be in” a singular “state”, you won’t have to deal with problems related to that, like do states have to “know” about each other etc.
You’re in no way obligated to control your characters via state machines.