1
\$\begingroup\$

i was wondering if you could help me out with some confusion i have over state machines and SOLID principles! sorry if the post gets too long, i'll try to keep things simple! So i have a huge player controller with dependencies of like 120 references and values (movement, crouch , mouse, jump , gravity etc etc) and i want to make it into a state machine. my confusion is in the whole state thing. i started out by trying to follow the Single responsibility principle so i broke down all functions to their own classes. so now i got movement.cs , jump.cs etc so all dependencies now are in their respective classes. But! my confusion again is, that if i were to make things into a state machine wouldn't the base class need to have those references? wouldnt that also make it become huge again? i.e. this is the new player controller

public class PlayerControllerManager : PortalTraveller, IDamageable
{
    protected PlayerBaseState currentState;
    protected PlayerStateFactory _statesFactory;
    public PlayerBaseState CurrentState { get { return currentState; } set { currentState = value; } }
    PlayerMovement playerMovement; // the first class i made from the previous player controller //
    public Transform GetTransform()
    {
        return transform;
    }
    private void Awake()
    {
        playerMovement = GetComponent<PlayerMovement>();
    }
}

The player movement class only holds values like speed, vectors and things like this. is this how its supposed to be? since the actual functions will go into their own state classes? am i overthinking it? am i doing stupid things??

ie this is the movement state class

public class PlayerMoveState : PlayerBaseState
{
    public PlayerMoveState(PlayerControllerManager currentContext, PlayerStateFactory playerStateFactory) : base(currentContext, playerStateFactory) { }
    public override void CheckSwitchStates() { }
    public override void EnterState() { }
    public override void ExitState() { }
    public override void IntializeSubState() { }
    public override void OnCollisionEnter() { }
    public override void UpdateState() { }
}

can i store dependencies in here? is it good/bad practice? is there a better or well proper way to handle both a state machine and keep the SOLID principles? i did try something out by getting a public variable like this in my Playercontroller class

    public Vector3 Move
    {
        get
        {
            return playerMovement.move;
        }
        set
        {
            Move = value;
        }
    }

and then did this in my moveState class

    public override void UpdateState()
    {
        Vector2 movement = InputManager.GetPlayerMovement();
        _baseContext.Move = new Vector3(movement.x, 0f, movement.y);
    }

Does that mean that i need to declare all my variables again from the movement class to the context class so i can access them? or should i do it like this

    public PlayerMovement playerMovement { get; set; }

and then just call

 _baseContext.playerMovement.move = new Vector3(movement.x, 0f, movement.y);

Thank you for your time!

\$\endgroup\$
4
  • \$\begingroup\$ Have you considered using the built-in StateMachineBehaviour instead of reinventing this wheel yourself? As an aside, there's no particular benefit in implementing your own GetTransform() method that just returns the same thing as the public .transform getter that every component already inherits. \$\endgroup\$
    – DMGregory
    Jun 23, 2022 at 20:04
  • \$\begingroup\$ ah i was not aware of that! didnt know Unity had a build in state machine. thank you for that, but besides the FSM my main concern is about the dependencies as i want to break down my class to smaller ones. how should i reference them? \$\endgroup\$
    – stratos la
    Jun 23, 2022 at 20:06
  • \$\begingroup\$ The premise of using a state machine is that the states are completely mutually exclusive. For basic functions like move/jump/attack, using state machines will limit the scalability of your code. Think about Super Mario Bros and Castlevania, if jump and move are different states, you could not control the character's x-direction movement in the air, just like Castlevania does. Because you can only be in one state at a time. I recommend using composition to manage the code. \$\endgroup\$
    – Mangata
    Jun 24, 2022 at 6:32
  • \$\begingroup\$ thank you for your reply, i just found out hierchial machines are different from normal state machines and im trying to do the former. now im also considering behaviour trees to be honest. I just want to learn and get accustomed to clean code! as i said above my issue is that the base class that does everything is huge. so maybe a better question is how do i refactor it while also making a HFSM or a BT ? \$\endgroup\$
    – stratos la
    Jun 24, 2022 at 6:44

1 Answer 1

1
\$\begingroup\$

One pattern I find very elegant in Unity is to represent each state of a state machine with an own game object. A game object with a state machine gets one child game object per state, which are all inactive by default. When the state machine enters a state, it does so by using SetActive(bool) to deactivate the gameObject representing the current state and activate the gameObject representing the next state.

Those child gameObjects then contain everything that's relevant about their state.

The advantages of this pattern are:

  • The states can not just contain one script but any number of scripts. The state machine does not need to know anything about them.
  • The scripts on the states can use standard Unity event methods, so you don't need to implement an own architecture for managing state transitions beyond deactivating the old and activating the new state gameObject:
    • OnEnable() for the logic which is to be executed when entering this state
    • OnUpdate() / OnFixedUpdate() / OnCollision... etc. for logic to be executed while in this state
    • OnDisable() for the logic which is to be executed when leaving this state
  • These scripts do not need to extend a more specific base class than MonoBehaviour, implement a specific interface or even be aware that they are states within a state machine. As long as a script can deal with being enabled and disabled, it will work as a component of a state gameObject. So you get much better code re-usability and modularity.
  • The states can also contain non-script components. Like for example particle effects which are only supposed to be visible in certain states. They can even contain more child gameObjects. Like a canvas hierarchy with the UI which is only supposed to show up while in a certain state.
  • When states have dependencies (on components of the base object or on something outside of the base object), then you can set up those dependencies in the inspector.
  • When you are testing your game, you can look at the scene hierarchy to see which state is currently active.

The disadvantage is that you have a lot more gameObjects in your hierarchy, and all the enabling and disabling when switching between states can be a tad more expensive than in other implementation patterns. But when you don't have too many objects governed by state machines like that, and state changes do not happen a hundred times per frame, then that should not be an issue.

This architecture applies all 5 solid principles:

  • Single-Responsibility Principle: Each functionality can be put into a separate script which you then put onto one or more states. No other script needs to be changed to make that work. The state machine isn't aware of what scripts are on each state game-object, so it does not need to change either when you add functionality to states.
  • Open-Closed Principle: States are open to extension by adding additional components to them.
  • Liskov Substitution Principle: You can use components on states which were never intended to be used as such. In most cases, it will just work.
  • Interface Segregation Principle: The state objects only implement the interface all MonoBehaviours implement - the standard Unity event methods. Which are all optional. The state machine does not depend on any of them. It just activates and deactivates gameObjects.
  • Dependency Inversion Principle: The high-level state machine only sees GameObjects, and has no dependency to any of the scripts on those game objects which implement the low-level logic of that state.
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .