Hi, I'm trying to put together an action platformer and I have a player setup that feels suited to a proper class based finite state machine. Organization has never been my strong suite and I'm also new to Godot so I'm a little lost.

I have a total of 10 states and 25 transitions I'd like to implement, and while I'm CERTAINLY not trying to implement them all at once, I would like to save myself the headache later by getting this sorted out now. I've never had something sprawling enough before that it couldn't be responsibly handled by a switch statement. But the different states also directly relate to player movement in a way that feels difficult to decouple.

What is a responsible way to handle the state machine and prevent two way dependencies when the states are related to player physics? Is there a better, obvious way to handle this that I'm not thinking of?

Bear with me, I'm new to both the forums and Godot, and I haven't done much heavy programming in a while.

I'm trying to put together a prototype for a sidescrolling action platformer and I've mapped out a state machine. Certain actions are able to cancel other actions (You can jump to animation cancel an attack, for example). I've got my state machine mapped out, but I'm noticing some states consist of multiple animations. Is there a clean way to decouple animations from State, or am I better off just splitting them into multiple states?

For example, the player can block. There's a tiny animation when the player presses the block key and starts blocking and a tiny animation when the player releases the key and finishes blocking. I anticipate another animation when an enemy attack actually connects with the block. You can stop blocking by releasing the key, or by jumping or attacking. However, functionally, the block animations are all the same state. The block is effective the moment the player presses the key, and is effective until the release animation finishes. The state transitions are all the same. The only difference is the animations.

I opted to approve and merge in your original post stuck in the moderation queue since it might give extra context to help others hopefully better understand the issue.

Thanks, making that a discussion was actually a misclick and I wasn't sure how to back it out.

Hm.. state machines are a great thing but they have limits. The art is defining what is stateful and what is procedural.

1) High order bit, use states to define how a character response to other events rather than thinking of them as embodying the response itself exclusively.

2) you need to consider how you will manage transitions more than anything else because you are already verging on an explosive growth

so in the case of blocking you could have the concept of enter/exit of a state and invoke animations there. while the block state is active, strikes against it might be a function you call on the state itself to invoke an animation (via collision detection and signals etc).

In this case your state and its transition life cycle effects how the character responds to events like being struck.

Lastly another paradigm of utility 'ai' is also an option.

https://www.gamasutra.com/blogs/JakobRasmussen/20160427/271188/Are_Behavior_Trees_a_Thing_of_the_Past.php

For a simple game, having a single state variable (based on an enum) and some switch/if/else statements can go a long way. It's kind of old-school and error-prone, but if you've never coded a FSM before maybe start there so you can see how it used to be done.

If you want something a little more stable, make an FSM class and then have it hold a State, which is also a class (could be an interface or abstract base class). The State interface just has a few functions, update(), enter(), and exit(). The main FSM class is updated by your main game loop, and mostly just calls current_state.update(), which updates the current state. Then for each state you make a new class which inherits from the State interface, and implements the functions.

You can see the basics of the idea here: https://gameprogrammingpatterns.com/state.html

So to make sure I'm being clear, this state machine is specifically for the player controller. I thought it would be best as a state machine because what actions a player can take at any moment is discretely based on what action they're already taking. For example, they can't switch to blocking in the middle of an attack animation. They can jump; animation canceling like that is part of my gameplay. So I know which actions a player can take in a frame based on their state, and I know the precise conditions for them to exit one state into another.

I have written the FSM as a switch at the moment. My player controller class is 356 lines long right now. Some of that insane length is me splitting things into sub functions, mind. I don't like this, but it works. At the same time, I am acutely aware of how brittle it is if I want to make any changes.

I haven't written a proper object oriented state pattern before, and that's where I tend to get lost. This being a player controller, state needs to somehow account for movement. If the player is in the ATTACK state, they may have a fixed forward motion. It may be different than their runspeed. I have an air dash where gravity may or may not apply. I have configurable values that affect all the actions the player can take because I know I will want/need to tweak them to suit gameplay.

Where I tend to get lost with writing a state class in a game like this is figuring out how to write a state class when state can potentially affect a number of things in different ways. Also, transitions are affected by a number of factors. Maybe I held a key some amount of time, and when I release it my character unleashes a power attack. I just can't seem to wrap my head around how to write a neat state class.

In this case you should construct state machines of nodes and for each state-machine node, its child nodes are state handling nodes.

You can choose to have the state nodes process 'inputs' of your choice (signals, user inputs if you want, direct invocation from some other node), and the state can take on (flexibly) several strategies: 1. if - conditionals for simple stuff 2. switch statements 3. response child nodes where each has a response function and perhaps you use the child node name it identify the 'input' reacted to.

A unifying concept in state-machines is to think of a state responding 'messages' as inputs The choice of strategy is then a matter of implementation.

the basic API for a FSM can be:

    extends Node
    class_name StateMachine
    var initial_state:String
    var current_state:String
    
    func prepare() -- sets current_state to initial state etc
    func transition_to(state_name:String) -- changes state
    func respond_to(m):  -- invokes current_state.respond_to and stays in or alters state depending on what it returns. States usefully have on_enter and and on_exit functions

[valid states are the children of StateMachine]

extends Node
class_name State

func get_machine() -> Node: return get_parent()
func on_enter(): -- utility/debugging
func on_exit(): -- utility/debugging
func respond_to(m) -> String: -- your choice, if/match/or lookup child node and invoke its response function

the response function returns itself or null to stay in the same state or go to another one. State names should be used for de-coupling (don't reference nodes!)

if your states become complex enough then Response classes as children of the state are warranted

extends Node
class_name StateResponse
func get_state()  -> Node: return get_parent()
func get_machine() -> Node: return get_state().get_machine()

-- call these from state\'s on enter/exit to clean up or prepare for use.
func on_state_enter():
func on_state_exit(): 
func respond_to(m) -> String:  handle the one input, returning same state or another state name

Now you can elect to have your StateMachine and States or even the Response nodes maintain internal vars and state. Each lower level has access to its parent context in a systematic way.

The above are base class scripts and you can extend "script-name" from them to create the statemachine of your desire

2 years later