Search Unity

Resolved Help with StateMachine / logic separation

Discussion in 'Scripting' started by johajo, May 16, 2020.

  1. johajo

    johajo

    Joined:
    Apr 1, 2014
    Posts:
    23
    Hello everyone,

    Rather new programmer here trying to get my (first) state machine to work the way I want to. I based my state machine from this thread: https://forum.unity.com/threads/c-proper-state-machine.380612/, specifically the state machine from @KelsoMRK (thank you btw). Also, I apologize if I use the incorrect language for things.

    Might not actually be a state machine question but it surfaced while playing around with the state machine so it is kind of there I need help I guess

    I did a TLDR in the end you can read first if you don't think you will have to read the whole post since you are awesome and can answer my problem just from that.

    So my state machine is fairly straight froward:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. /// <summary>
    5. /// State Machine
    6. /// </summary>
    7. public class StateMachine
    8. {
    9.     IState currentState;
    10.  
    11.     public void ChangeState(IState newState)
    12.     {
    13.         if(currentState != null)
    14.         {
    15.             currentState.Exit();
    16.         }
    17.         currentState = newState;
    18.         currentState.Enter();
    19.     }
    20.  
    21.     public void Update()
    22.     {
    23.         if(currentState != null)
    24.         {
    25.             currentState.HandleInput();
    26.             currentState.LogicUpdate();
    27.         }
    28.     }
    29.  
    30.     public void FixedUpdate()
    31.     {
    32.         if(currentState != null)
    33.         {
    34.             currentState.PhysicsUpdate();
    35.         }
    36.     }
    37. }
    38.  
    And the state interface:

    Code (CSharp):
    1. /// <summary>
    2. /// Interface handling state
    3. /// </summary>
    4. public interface IState
    5. {
    6.     void Enter();
    7.  
    8.     void HandleInput();
    9.  
    10.     void LogicUpdate();
    11.  
    12.     void PhysicsUpdate();
    13.  
    14.     void Exit();
    15. }
    Then on my game object I have a "manager" script which ties state machine to the game object (as well as handles other data for the GO) and creates and initializes the first state. I started with a "slimemanager" (an enemy in the game):

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class SlimeManager : MonoBehaviour
    6. {
    7.     protected StateMachine SlimeStateMachine;
    8.     public SlimeMovingState MovementState;
    9.     public SlimeIdleState IdleState;
    10.  
    11.     private void Start()
    12.     {
    13.  
    14.         SlimeStateMachine = new StateMachine();
    15.  
    16.         IdleState = new SlimeIdleState(this, SlimeStateMachine);
    17.  
    18.         MovementState = new SlimeMovingState(this, SlimeStateMachine);
    19.  
    20.          SlimeStateMachine.ChangeState(IdleState);
    21.  
    22.     }
    23.           private void Update()
    24.     {
    25.         SlimeStateMachine.Update();
    26.     }
    27.  
    28.     private void FixedUpdate()
    29.     {
    30.         SlimeStateMachine.FixedUpdate();
    31.     }
    And a typical Idlestate:

    Code (CSharp):
    1.  
    2. using UnityEditorInternal;
    3. using UnityEngine;
    4. /// <summary>
    5. /// Slime enemy idle state
    6. /// </summary>
    7. public class SlimeIdleState : IState
    8. {
    9.     protected SlimeManager slimeManager;
    10.    
    11.     protected StateMachine stateMachine;
    12.  
    13.     public SlimeIdleState(SlimeManager slimeManager, StateMachine stateMachine)
    14.     {
    15.         this.slimeManager = slimeManager;
    16.         this.stateMachine = stateMachine;
    17.     }
    18.  
    19.     public void Enter()
    20.     {
    21.     }
    22.  
    23.     public void Exit()
    24.     {
    25.     }
    26.  
    27.     public void HandleInput()
    28.     {
    29.         //TESTSWITCH
    30.         if(Input.GetKeyDown(KeyCode.K))
    31.         {
    32.             stateMachine.ChangeState(slimeManager.MovementState);
    33.         }
    34.     }
    35.  
    36.     public void LogicUpdate()
    37.     {
    38.     }
    39.  
    40.     public void PhysicsUpdate()
    41.     {
    42.     }
    43. }
    44.  
    I then started putting logic in the different states but quickly realized that I should probably try to do a general EnemyManager-class (instead of one manager for each enemy type like Slime, Skeleton etc.) that all enemies use. I then wanted to make the states general as well (like all enemies have an IdleState, MovingState and so on). So I moved the logic (in this case movement) code to the manager and then faced the problem of all enemies implementing the same movement behavior.

    TLDR:
    So, now what I would like to do is for the MovementState to call a general Move() method in EnemyManager on the enemy GO which in turn I could attach different movement behaviour scripts, which would be called from the MovementState.

    End of TLDR

    Can't really use the GetComponent in EnemyManger since there will be a bunch of different movement scripts for different enemies using the same manager, like "JumpingMovement" and "SprintingMovement", I guess... ?

    I tried playing around with having the EnemyManager using a virtual methods to be overriden in the specific movement behavior script, but since the State calls the move method in the base class (EnemyManager), the derived method is not called (unless there is a neat way to do this from the base class which I have not found?)...

    I feel like I make this way more complicated than it has to be..
    I hope you understand what I mean and there are probably better way to implement what I'm trying to do but this is what I got so far...

    I mean if I'm way off in my line of thinking let me know aswell!

    Thank you all!
    Jonathan
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,742
    Personally I have never seen the need for a generic state machine script. It just feels like a needless contrivance, since the hard part of state machines isn't the grunt work, but rather these two things:

    1. what states are you even going to be in? (This falls under "understanding your problem")
    2. naming them meaningfully so when you look at it 2 years from now you can understand it

    Also, since Unity's GameObject/Monobehavior structure is a component architecture, you can think about enemies in terms of Components instead of any kind of brittle hierarchy kinda stuff. You can give an enemy a general PatrolWaypoint script, and give each enemy different speed parameters.

    Or you can take it up a level and give the enemies a MovementManager script that has presets for what kind of movement, what kind of intentions, etc. That way you could actually have an enemy that can both walk and fly, and some kind of manager to decide when each thing happens.

    Just a few random thoughts. Every game problem is going to be unique and it might help you to think more flexibly about it if you just start trying things out, see what works, then adjust course.

    Remember, it is software, which means it is SOFT. :)
     
    johajo likes this.
  3. johajo

    johajo

    Joined:
    Apr 1, 2014
    Posts:
    23
    Thank you for solid input! I really appreciate it!

    This is basically what I want to achieve. I think I understand what what you mean and I will try this way out. Not really sure why I didn't do it like this to be honest :)...

    Hehe, it sure is! Thank you for the input!
     
  4. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    Any reason for this? Why not just put the Move logic directly in the state? Any enemy that needs to move can just transition into the state and do its thing.
     
    johajo likes this.
  5. johajo

    johajo

    Joined:
    Apr 1, 2014
    Posts:
    23
    I had a reason in my mind when I started but after investigating and playing around it does not really make sense. After working the code over a couple of times I've now ended up with the logic in the state (as you suggested). At first I had it in a separate movementmanager but it became kind of unnecessary since the state just called a method from there. So i decided to have the different kind of "movement types" (like flying, running, jumping..) in the state and have the enemy manager (which a SO plugs into) have an enum where it is stated what type of movement that enemy should use. I won't have that many types and if I need to do a "complex" behavior on my enemies I think that a state machine is not necessarily the way to go forward. But this will do for now.

    Thank you for the answer and also your other posts which have helped me!
     
  6. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    You could certainly abstract the actual movement out into a Locomotor class or something that does the actual moving and is configured on the entity. Certainly would be more robust than an enum if you end up with any sizable number of different movement types.

    Glad you got it resolved!