A clarification about State Machines

Hi,
I implemented a State Machine pattern in my game (or I think I did).
I updated how it works many times, trying to find the best possible solution (that it doesn’t exist, I know).
With the last update, I came up with a doubt and I would like to know what do you think or if you know what’s the best practice.

Should the logic be kept in the state or in the main node that has the State Machine??? 0_0

Let me explain it better:
I have a Player node of type CharacterBody2D.
I attach a script to the player, with all his methods, like jump() (makes the player jump), or move() (apply a velocity and move the player) or hurt() (flash the sprite and reduce health) etc…

My question is: all this methods should be kept in the player script, so the State Machine job is just to call a certain method when the right state is enabled (eg: I enable the state “jump” that on enter calls the player’s method jump() and makes the player to jump) or should the logic and the methods be kept inside the states, so I will have the state “jump” with the jump() method (and all its logic to make the player jump), the state “hurt” with its method and logic to handle the player hurting and so on???

This doubt is really puzzling me and I can’t arriving at a solution :face_with_diagonal_mouth:
What do you think is it the best approach?

1 Like

I think it depends on how you have implemented your state machine. My preference is to keep all enemy specific logic in the enemy scene. The state machine may have logic in it, but that is when it has to make a decision about how to behave, or to exit etc. The Player is different though, since it reacts to inputs and is unique, so I approach this differently.

The state machine I wrote for enemies and objects, looks at all the children of the state machine and uses those for the available states. Thus I can then re-use this anywhere I need to add states. States are nodes themselves that I can share between different actors, especially enemies that might share the same Manoeuvring state (getting out of each others way) or Approaching state (approaching the player), Exploding, Attacking, Firing etc.

The state machine I use for my Player is different though. Since there will only ever be one player, I have put a lot more logic in that for the player. So all the logic for doing the player jump, say, is in the state itself.

It is a great question though, and I am looking forward to hearing what others have to say on the topic.

Paul

1 Like

The pros of the first approach (the one I would like to adopt) are, for instance:
I have a state “attack”. The logic inside the state is to just call the method attack() on my entity (an entity could be a player or an enemy). Each entity will have its own implementation of the attack method, the important thing is they have a method called “attack” that I will trigger from the state.
On the other side, if I would adopt the second approach, I would have two (or more) different attack states, one for the implementation of the player and one of the enemy because, let’s say, on attack I want to add a short dash to the player but I don’t want the same for the enemy. Using the second approach it wouldn’t be possible, if not using different states for each entity, and I don’t find it very convenient.
The pros could be having little chunks of code, each kept in the respective state, instead of having a big long script with all the logic for the entity. But is it a bad thing?
And what do you think could be the cons of the first approach instead?

Hey, what I am doing currently is having a state machine more like your 1st approach, every state call a function in the entity (player or different type of enemy).
This scales up better to reuse your state machine, but as you said, the code in the player entity starts to be a big spagetti code. I was having that problem.
What I did is create a set of “actions” (more like a command design pattern), those actions are implementing all the logic to execute the action of the curent state. Then I have a Jumping state and a JumpAction class, which executes the Jump action with all “coyote time” and “jump buffer” logic inside, then I have a EnemyJumpAction which dont need that logic, and executes the Jump in a different way, then for a Bat Enemy, I have a BatJumpAction so it can kind of fly. And so on.

Maybe that helps.

Also, I recomend to have a look into this AddOn for StateCharts (a more roboust state machine), that is very cool to create state machines without programming too much:

1 Like

I’m interested in exploring this method of using actions and the command design pattern. Where can I find some information about implementing it in Godot?
Thank you for the video, I’ll watch it soon :+1:

You can find a way to implement the command pattern it in Godot in this video, there might be another videos also:

1 Like

That’s really cool, thank you for sharing it!

1 Like

This question has bothered me for a long time and I tried both approaches and I’m still not sure. But I decided to put all required code inside the object and call it from the states. This allowed me to use some functions of the object outside states and made creating new states a bit easier, because I don’t need to inherit/extend from other states to use functions I already created for another state. For example a function that handles movement. Basically I ended up with a character with all relevant functions and variables and in the states just set movement speed and call the move function etc.

On the other hand, if you create states that are generic enough and include all required code, you can use them for other things without changing anything. It’s a really tough choice. :sweat_smile:

1 Like

It bothers me too. I was putting all the code into functions and calling them from states, but the states were often devoid of anything meaningful.

Now I am starting to use the states for more code, only moving the code into character functions when it gets repeated. So an actor might have this set up:

PirateShip

  • StateManager
  • VisualsManager
  • MovementManager
  • WeaponManager

The MovementManger for example might have two handling functions:
func handle_move_to_target()
func handle_move_in_set_direction()

The statemanager might have a state for Attacking, Evading and Drifting. These states will create either a target position or a direction, and then signal the movement manager to deal with the movement, either to move in a certain direction or move towards a target (so I am not repeating movement code in the states) but the states retain the calculation of which direction they want to move to or where they want to move to.

So far, this seems to be working well. I only do _process(delta) functions in the states, and these processes are enabled or disabled when you enter or exit the state. So the movement manager does not have any processing itself, just handling functions to handle the movements (as an example).

Hopefully that all made sense and helps someone else.

Paul

1 Like