Hello,
as in the title I started designing my new 2d aRPG in Godot 4. I started on to state machines for player/NPCs with states like playerControl, npcIdle, npcChase, npcPatrol, attack etc. I want to design the project with composition principles in mind so that both the states and state manager are kept as separate modules from their parents (NPC/player chars).
The state manager/states need access to the player/NPC animation tree and animation state to help with the transitions while keeping things loosely coupled and portable.
Does anybody have in mind a design solution that worked out for their game (not a tutorial that uses 2 states for one character)?
I want to get it right the first time as we all know that refactoring code in Godot is not the most enjoyable thing to do.
For now, I have a function that is called by the state manager for the current state:

but I do know that it is a poor solution to pass the animation tree as an argument (although I do not have anything against passing that the state acts upon as an argument, as shown in the example). In the end, I will implement signals for entering/exiting a given state and they might require separate animations/data as well.
Thanks for your answers!
Cheers,
Maks

    • You don't need a god state machine to control everything
    • Each node can have its own state machine, the Player the NPC the Enemy etc..
    • The non-nodes way is using enums and match states. It is neat and once you know how to set it up, you can set it up pretty quickly.
      Here is a boilerplate I use myself:
    enum State {INITIALSTATE, STATE1, STATE2, STATE3} # Need another state? Just add one more
    var current_state:= State.INITIALSTATE:
      set(new_state):
        if new_state == current_state: # Nothing happens if you set the state to the same current state
          return
        state_exit(current_state) # The current state last actions
        current_state = new_state
        state_enter(current_state) # The new state, now current state' first actions
    
    func _ready():
      current_state = State.STATE1
    
    # Any action you need to do first thing in current state
    # It can be playing a small animation, flashing some light, playing a sound
    func state_enter(state: State):
      match state:
        State.STATE1:
          # Do stuff when you enter state 1
          pass
        # For the sake of example, you dont need to do anything when you enter any other state
    
    # Any last actions? Flashing some light, play a sound, maybe resetting some variables?
    func state_exit(state: State):
      match state:
        State.STATE2:
          # Do stuff when you exit state 2
          pass
        # For the sake of example, you dont need to do anything when you exit any other state
    
    # So you need to continuously update stuff, but each state behaves differently, no worries
    func _physics_process(delta):
      match current_state:
        State.STATE3:
          print("state3") # for example
    • Whenever you need to do have a separate behavior, you can use a match statement. It is simple, it works, each component of your game can be its own state machine.
    • Player can have idle move attack etc..
    • NPC can have patrol follow return
    • Enemy can have patrol follow aggro attack return
    • Game can be MainMenu Levels Playing (i meant literally anything)
    • You want nodes to be components and want a StateMachine node with State children node, right?
    • I will only go over the general idea, i feel it is overcomplicsting something simple.
    • It is actually the same previous code but separated into different classes
    • StateMachine will store a current_state
    • current_state is not an enum value, but a State object, it extends Node
    • current_state setter will call exit() on current state, change current_state = new_state and enter() on the now new current_state
    • Each State will handle its own entrance, exit, and transition conditions, no need for match statements.
    • When some transition condition is triggered, it signals up to the state machine. StateMachine will change its current_state depending on what the signal sent, triggering the exit() enter() sequence in the current_state setter function.
    • A new state is as simple as adding a new node that extends State.
    • Each state can have entrance animation name variable, which is played in enter() using an animation tree reference
    • You can, in the _ready() function of StateMachine, iterate over every child State, and pass a reference to the parent node, for example the player, so each state can access the animation tree for example.
    • I hope i didn't overcomplicate stuff
    • I am a human and I am sure there is some misinformation above, so take it with a grain of salt, and use whatever suits your needs.

I don't know if i totaly understand your setup!?

Since you will wrap all components of an npc in a seperate .tsnc file and your state and stateManager is a node in this tree you could just add an @export var animation_tree:AnimationTree to the state node. Then the animation_tree would be a local variable of state.

This seems to me is the most flexible way. Also it's usual way you connect nodes in godot.

I don't know much, but whenever I see a system with a class named "something something manager" - it's always overdesigned.
Try to avoid implementing various managers, marshalls, dispatchers etc in advance and stay away from too much abstraction because it often leads to analysis-paralysis. Yeah, I'm not a big fan of design patterns approach to building systems.

It's almost impossible to nail the design at a first go, especially if you don't know the exact state elements it will need to handle. You can't really get away from refactoring in the real world.

One thing that v 4 introduced that's very useful for handling state are callables. So if you're implementing a discrete state machine, you can use state id indexed callables to handle your state in/out/mix/process code instead of a clunky solution with multiple nodes often shown in introductory state machine tutorials.

  • You don't need a god state machine to control everything
  • Each node can have its own state machine, the Player the NPC the Enemy etc..
  • The non-nodes way is using enums and match states. It is neat and once you know how to set it up, you can set it up pretty quickly.
    Here is a boilerplate I use myself:
enum State {INITIALSTATE, STATE1, STATE2, STATE3} # Need another state? Just add one more
var current_state:= State.INITIALSTATE:
  set(new_state):
    if new_state == current_state: # Nothing happens if you set the state to the same current state
      return
    state_exit(current_state) # The current state last actions
    current_state = new_state
    state_enter(current_state) # The new state, now current state' first actions

func _ready():
  current_state = State.STATE1

# Any action you need to do first thing in current state
# It can be playing a small animation, flashing some light, playing a sound
func state_enter(state: State):
  match state:
    State.STATE1:
      # Do stuff when you enter state 1
      pass
    # For the sake of example, you dont need to do anything when you enter any other state

# Any last actions? Flashing some light, play a sound, maybe resetting some variables?
func state_exit(state: State):
  match state:
    State.STATE2:
      # Do stuff when you exit state 2
      pass
    # For the sake of example, you dont need to do anything when you exit any other state

# So you need to continuously update stuff, but each state behaves differently, no worries
func _physics_process(delta):
  match current_state:
    State.STATE3:
      print("state3") # for example
  • Whenever you need to do have a separate behavior, you can use a match statement. It is simple, it works, each component of your game can be its own state machine.
  • Player can have idle move attack etc..
  • NPC can have patrol follow return
  • Enemy can have patrol follow aggro attack return
  • Game can be MainMenu Levels Playing (i meant literally anything)
  • You want nodes to be components and want a StateMachine node with State children node, right?
  • I will only go over the general idea, i feel it is overcomplicsting something simple.
  • It is actually the same previous code but separated into different classes
  • StateMachine will store a current_state
  • current_state is not an enum value, but a State object, it extends Node
  • current_state setter will call exit() on current state, change current_state = new_state and enter() on the now new current_state
  • Each State will handle its own entrance, exit, and transition conditions, no need for match statements.
  • When some transition condition is triggered, it signals up to the state machine. StateMachine will change its current_state depending on what the signal sent, triggering the exit() enter() sequence in the current_state setter function.
  • A new state is as simple as adding a new node that extends State.
  • Each state can have entrance animation name variable, which is played in enter() using an animation tree reference
  • You can, in the _ready() function of StateMachine, iterate over every child State, and pass a reference to the parent node, for example the player, so each state can access the animation tree for example.
  • I hope i didn't overcomplicate stuff
  • I am a human and I am sure there is some misinformation above, so take it with a grain of salt, and use whatever suits your needs.

Thanks for all the answers, I think you are right about my system being slightly overengineered, but I managed to design in it a consistent and uncoupled way (I might have not underlined this feature enough). Now, each game object has its own state manager which has children composed of available states, current state and controller (which provides inputs and directs AI/player controls).