I’m using Godot 4.5 and hoping for pointers on how to make a state machine for movement and an attack for a 2D platformer.
Thank you in advance.
I’m using Godot 4.5 and hoping for pointers on how to make a state machine for movement and an attack for a 2D platformer.
Thank you in advance.
Check out @dragonforge-dev StateMachine
State machine is really similar to the Strategy pattern. It’s just a way to structure your code, you don’t need to overcomplicate it with plugins and node trees, in my opinion. Best see some examples on how others do it and then reinvent the wheel!
State machine is very simple and extremely powerful. It’s worth it to learn it properly
Both the State Pattern and the Strategy Pattern were introduced in the Gang of Four’s book Design Patterns in 1994. So while they are really similar, that are also quite distinct.
So the plugin is two files. (Well and two icons.) It’s intentionally been kept simple. And you can in fact just drag and drop them into your project without installing the plugin. One is a StateMachine object and the other is a State object.
I get where you’re coming from, because I started there too when I first started using Godot. But as I continued to use Godot, I realized it made a lot of sense to use nodes because that’s how Godot is structured. And once you get used to using nodes, it makes seeing the structure of a StateMachine much easier in the context of the editor.
Nodes have very little additional overhead, but they create structure for add/initializing/removing/shutting down a State. Which means a lot LESS code than if you did it without nodes. It also helps enforce atomic, encapsulated code because each State must be its own inherited object.
Tak a look at that node tree, You’ll notice that it is very clear what states the Player object has at a glance. Need a new one? Add it. Want to take one away? Remove it. Want to add new ones during gameplay like for double jumps? Just add the node - which Godot makes really easy to do.
This seems to be antithetical to the idea of Object-Oriented Programming and reuse of code. Sure, one can re-invent the wheel, but then we have engines like Godot because people got tired of re-inventing the wheel. That’s also why we have plugins.
Agreed. But, you gotta know your audience. And while you have a Master’s degree in Game Development, most people here asking questions do not. And while there are a number of us who have been programming for decades and love to teach people about this stuff, we also know that sometimes people just want to get on with making their game.
While we are on the subject, the plugin @wchc mentioned above follows a pull model, instead of the standard push model most state machines follow. Following the Kanban method of project management, it requires each State to tell the StateMachine when it should switch. The StateMachine is then just there to make sure we only have one State active at a time and make the switch. And so it has some things in common with the Strategy Pattern.
+1 for the @dragonforge-dev state machine. I started using it recently and after some initial head scratching I quite like it. Since its a pull model state machine the states are easier to reuse and more modular. Great for hierarchical state machines too as state machines can be enabled and disabled.
I have a question: when the states are nodes, how do you get references to the state you want to switch to? Like, if the character is falling (falling state) and lands, it has to change to, say, grounded state. How does falling state get a reference to grounded state to switch to it?
And you mention adding new states in-game, then how do other states know when to switch to the newly added state and how do they get a reference to it?
That’s the whole point of a Pull approach. They don’t know about other states.
CharacterStateFall only does something if the Character isn’t on the floor. Then it switches to itself and applies gravity. CharacterStateIdle checks to see if the Character is on the floor and not moving, and if so, switches to itself. PlayerStateMove looks for player input and that the character is on the floor, and if so switches to itself and handles velocity calculations.
Discrete state machine pattern is inadequate abstraction for modeling player state in most cases. Using it for that will result in poor play feel. The player typically doesn’t behave like a state machine except in most simplistic games. It may be fine for npc state management and perhaps for educational purposes when explaining the concept of a state.
People seem to like playing my games.
What alternative are you recommending?
I’m not big on cookbooks and recipes. Use whatever models the problem domain adequately. Boxing the player into discrete states model from the get go severely limits your gameplay/interaction design options.
Ok, but OP asked for advice on a state machine.
I agree that a state machine doesn’t fit the model for every player, but it does for some. I also believe that the work required to keep the state machine atomic helps people keep their code encapsulated and really think about their architecture.
When I made Katamari Mech Spacey last month, I took a different approach. Instead of a state machine, I put the player controls into components. So when you turn, the attitude thrusters affect how fast you turn. The more attached, the faster you turn. When you press the fire button, every laser you have attached fires. It’s a very different architecture, but having split all that functionality into states allowed me to come up with that architecture.
For most simple game examples though, there’s a state machine example with an Enum. It’s a giant spaghetti ball of code all in player.gd that quickly gets cumbersome. Having states as nodes enforces encapsulation - which in turn enforces more readable code, and thinking about interfaces.
How so? Could you give a concrete example?
I forgot to mention, I also use multiple state machines inside complex characters. So one for movement, another for actions like attacks, spells, etc so that you can combine them.
I didn’t know about the pull approach and I’m still trying to wrap my head around it.
If every state has to constantly check for all the conditions to see if it should transition to itself, doesn’t it defeat the purpose, at least in part?
Isn’t one of the benefits of using state machines that you don’t need to check every condition? For instance, if you are in idle state and press crouch you don’t have to check anything, just switch to idle_crouched. So no need to check if character is on floor, if there’s movement input, etc.
I guess that’s a trade-off for the benefit of being able to easily add new states at any time. Did I understand it correctly?
Ever heard of XY problem? ![]()
How we you know a state machine is an adequate tool in this specific case when we don’t know what exactly is being modelled.
Multiple state machine would almost always be a requirement, which has a fancy name “concurrent(or collaborative) state machine”. And his too may be inadequate. Not to mention that with multiple state machines, the neatness of node based implementation goes qucikly away.
How about you give an exact description on everything the player could do and each thing relates to other. Then we can try to evaluate if the state machine is a useful abstraction for implementing your player.
I actually never had a name for it before, and I often ask the same probing questions when I think that people don’t know the problem they are trying to solve.
Because I believe that a state machine for movement and another for attack is a valid architecture.
Sure. But that’s not really an issue.
I disagree, based on my implementing multiple state machines on characters. If you use a pull model, where the StateMachine knows nothing of the states, and the States handle everything, then they can be atomic and you can have multiple running at the same time without impacting (or knowing about) one another.
I get that. It’s a more advanced approach IMO.
What purpose is it defeating exactly?
No. That is a false assumption. You just do all those checks in the State Machine before making the switch. The pull method just moves where the code for those checks is. If they aren’t in the State Machine at all, then they are in the character controller, and that code is a large and messy.
You did not. The benefits of the pull method is all your objects have less code. This in turn means the code is easier to read, reducing cognitive load and resulting in an easier (and faster) time fixing bugs. The ability to add states at any time is a side-benefit of atomic code, not a core design feature of the pull state machine.
All you’re doing is moving the decision-making code to the states. It’s gotta be in there somewhere.
Yes, but I still feel that the decision making is simpler when the current state is the one deciding.
For instance, imagine a 3D platformer.
The player is in IdleState. Crouch button is pressed.
IdleState gets the input of the button press and just calls a transition to crouched state. It doesn’t have to check if it’s grounded or not, moving or not. I know the character is grounded and not moving because it’s in IdleState, else it would be in some other state.
If it’s in FallingState when the crouch button is pressed, it knows to transition to, say, GroundPoundState.
It doesn’t have to check if the player is not grounded.
To complicate the issue, there might be other states that are airborne (not grounded) and that you can’t transition to groundpound. Maybe if the player is in LongJumpState, it can’t ground pound, even though it’s not grounded, so it just ignores the crouch button press.
In the pull approach, the GroundPoundState would have to check if the player is not grounded, but also somehow check if the player is in LongJumpState or in some other state that doesn’t allow for a ground pound. So it seems more complicated and a bit messy, harder to mantain, no?
Maybe I’m still missing something.
Ok. Let’s diagram what you’ve created. But we should add in a few simple states. Walk, Run and Jump.
[*] --> Idle
Idle --> Walk: Move input
Idle --> Crouch: Crouch button pressed
Idle --> Falling: Not on floor
Idle --> Jump: Jump button pressed (on floor)
Walk --> Idle: No move input
Walk --> Run: Run button pressed
Walk --> Crouch: Crouch button pressed
Walk --> Falling: Not on floor
Walk --> Jump: Jump button pressed (on floor)
Run --> Walk: Run button released
Run --> Idle: No move input
Run --> Crouch: Crouch button pressed
Run --> Falling: Not on floor
Run --> Jump: Jump button pressed (on floor)
Crouch --> Idle: Crouch button released
Jump --> Falling: Reached apex
Falling --> Idle: On floor
Falling --> GroundPound: Crouch button pressed
GroundPound --> Idle: On floor
Idle --> [*]
So now to make sure you can use the crouch button, you have to add that code to the Idle, Walk, Run, and Falling states. Every state in fact, except Jump. Sure, it’s only a few lines to make the switch, but you have to not forget to put it in any new transitions.
You also need to do the same with the Jump state. It needs to be called from all the states except Falling and Crouch. You now have duplicated blocks of code in every state except Crouch. And it’s only going to get worse when you add Crouch Walk, Slide, etc.
Or you put that code in one function, and you don’t care what state its in, only if it can enter the state. If you’re in the Falling state you set can_transition to false and no other state can transition until it hits the ground and sets it back to true. Then whatever states are trying to transition will do so.
Just all in where you want your mess. In one place or in many.
If you implement LongJumpState, and you don’t want it to Ground Pound, you can use set_arg() to set a custom variable of say can_ground_pound to false. Then when the button is pressed, nothing happens.
You may just have to use the push method until you get something complicated enough that it makes sense to switch to the pull method.
How do you know that if the problem is not described precisely? Movement and attack are very broad concepts. You’re making assumptions there that may not match the actual problem domain.
For a fluid movement feel, modeling it as a set of mutually exclusive states is often inadequate, or you risk ending up with very wooden controls. For example Run/Jump, Run/Fall, Walk/Fall Crouch/Fall, Crouch/Run etc… all might not be mutually exclusive. Add some combo attacks to the mix and your state machines will quickly start falling apart under their own weight.