"You can also wrap state changes in a function to initialize and do a cleanup, for example, by changing the horizontal acceleration and maximum speed when you are in the air."
I'm following along so far, until I hit this part.
In the code example after the quote above, you store the current state as "previous_state", then change the _state to whatever new_state argument was called.
So now we take the _state and run through a match statement...
That makes sense...
But why do we care about the previous state and store it in a variable?
Why do we then run that previous_state through another match statement and call it a "clean up"? What is it cleaning up?
I know this is just a "how you could do it" example before we even get to the State Machine pattern, but I would love to know the logic behind what's going on here so I can fully appreciate the logistics of the FSM pattern itself.
EDIT: I see that further along with the FSM itself, we still "clean up the state."
But I'm not sure why we need to clean up the state, instead of just flowing from one state to the other by changing the state value?
Thanks so much :)
I'm not sure why we need to clean up the state
Each state typically changes properties during its lifetime, and sometimes a state changes properties other states won't reset.
Say you have a jump state where you can triple jump: in air, you keep track of the jump count. When touching the ground again, you want to reset the jump count to 0.
You can do that either when entering or leaving the jump state. In basic states you may not need it but there always comes a time when you have a value you can only reset in the exit function.
Note there's a different approach to solving that problem that consists of making each state an individual class and instantiating a fresh copy of that class every time you enter the state. That's the approach explained in detail in Game Programming Patterns.
You can probably see how this would grow and become unmaintainable if the code were reused or as more states were added.
As explained in the guide, that's an approach presented for small things, and definitely not for things you'll reuse or that'll grow big.
Now, that's not necessarily a technical debt. A ton of game code is highly specific and trying to make it generic and reusable can be counter-productive. It depends entirely on the game, its scope, content, the team size, and so on, of course.
But having just one function keeps code grouped together, which makes it fast to update and easy to read: you don't have to jump to different places or files. For small pattern-based AIs for instance, that works great.
The idea there is you don't reuse that code. One enemy, one script, no multiple dependencies making your code fragile.
Trying to make the code shared can be necessary at times, but it also comes at a cost: you have to be extremely careful when making updates that updating a state for monster C doesn't introduce a bug in monster A and NPC D that also use it. It means every time you update that state, you need to go back and test all three entities.
If I recall correctly that's a point the guide makes: you want to adapt the code to your precise needs.
Thank you so much for the extremely detailed explanations! I think I understand it a lot better now.
I hadn't considered that those state variables would need to be reset. That makes so much sense!
So I think I understand your replies correctly:
You're saying you can keep everything more simple by referring to one script that handles state transitions, but it can quickly grow unwieldy if the object becomes capable of too many states...
So, maybe suitable for something with a few states basically. (For only 2 states, maybe it's best to just use a boolean, for simplicity.)
Or, if the entity is sufficiently complex, you can uncouple the behaviors by making them generically talk to a common FSM script, but you need to make sure that they can apply to each and every entity they'll be dropped on...and you gotta reference multiple files to track down bugs.
(Being able to just drop an already-scripted AI behavior onto any new character in the game sounds AMAZINGLY COOL though...)
I imagine you could also use inheritance if something has a similar-but-unique behavior? Like a MonsterA_Patrol state that's different from Guard_Patrol state, which both inherit from a generic "Patrol" state script, for instance?
And lastly for the sake of neatness: With this generic and flexible approach, the states are all set up using nodes as classes...so I imagine as a character gets more complex with a ton of other nodes, the states could all be grouped under a "Behavior_States" node so they could be organized and collapsed in the scene tree? (As signals seem to have no problem calling up to a parent-of-a-parent!)
Sorry for the length, and I'm still learning to communicate and describe coding concepts, so I hope I didn't talk in circles. But I wanted to make sure I typed it back out as I interpreted it, to make sure I actually understand what's been said!
Thanks again so much, this has really helped me learn and I can feel the coding part of my brain expanding. :D
Being able to just drop an already-scripted AI behavior onto any new character in the game sounds AMAZINGLY COOL though
It can be nice, although there are some more powerful and flexible systems than state machines. Typically, behavior trees. They're more complex to code and use initially but make it easy to reuse code down the line.
I imagine you could also use inheritance if something has a similar-but-unique behavior?
I imagine as a character gets more complex with a ton of other nodes, the states could all be grouped under a "Behavior_States" node so they could be organized and collapsed in the scene tree? (
Yeah so you can organize however you want and even create hierarchical trees. Like have subtrees of states that work as a sequence (e.g. enemy attack combos, each attack being a separate state) or have nested state machines. That can be really useful and further break down your entities' code.
The main reason to use nodes is so you can visualize your state machine without a dedicated plugin. It's simple and accessible.
I never got to write the signal part of this guide, which we instead approach in dedicated tutorials. You do need to be wary with them: most of the time, you want to connect them when entering a state and disconnect them when leaving it. Just to be sure a state doesn't emit or receive a signal while it should be inactive.
Thank you SO MUCH Nathan!
I really can't wait to get to behavior trees! (one step at a time though right?)
Like have subtrees of states that work as a sequence (e.g. enemy attack combos, each attack being a separate state) or have nested state machines.
I've never considered this was possible for some reason. That realization is blowing my mind. So many possibilities!
most of the time, you want to connect them when entering a state and disconnect them when leaving it. Just to be sure a state doesn't emit or receive a signal while it should be inactive.
That sounds tricky! My first thought would be to make an "_state_active" boolean in each state that sets upon entering the state, and the signal functions check "if _state_active == true" before proceeding, and sets _state_active to false upon exit...Hm, just thinking out loud. :)
But I'll wrap this here. Thank you again so much for helping me understand. I did not expect to grasp the concept as well as I do now, and I can't wait to try out some state machines!
I can't give enough thanks to you and the rest of the GDQuest crew.
We're here to help, I'm glad if the discussion helps you better understand state machines.
Regarding signals, if the nodes are in the tree and you connect, say, an area's area_entered signal to several states, as soon as the signal gets emitted, all connected states receive the callback, whether they're active or not.
Typically in the State pattern you would funnel all signals and callbacks via the StateMachine object, which controls the states and forwards the processing and input calls. But with signals in Godot we've found it more convenient to just connect in the state's enter() function and disconnect in the exit() function.
This also gives you a good example of when to use the state's exit() function for cleanup: disconnecting signals.