Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Using a Finite State Machine for Movement with the new Input System

Discussion in 'Input System' started by dav_ege, Oct 6, 2021.

  1. dav_ege

    dav_ege

    Joined:
    Apr 22, 2019
    Posts:
    17
    Hi,
    i am currently writing a hierarchical state machine to manage player movement and i am using the new input system.

    I am trying to decouple my state machine from player input, so i created an input provider class that reads player input and provides it to my state machine through an inputData struct. That works really well for input types that always contain a meaningfull value, like movement input from the joystick, or if a button is being held down.
    When it comes to specific button presses though it gets tricky. A Button with a tab interaction for example cannot be saved in a bool value, as i would not have a way of resetting the bool back to false.

    Apart from that, the whole conversion of event based information into a struct that then gets checked in the update methods of my states seems like a step back from the advantages of the new input system.

    So i was wondering if there is a more efficient way of combining the event based input system and finite state machines?

    I could subscribe to relevant input events in the enter methods of my states and desubscribe in the exit methods but that seems like a pain, and also quite unefficient.
    The only other way i could think of is creating methods for each input in my base state class and letting my controller class call the corresponding method of the current state resulting in a bloated base state class and a lot of unnecessary calls.

    As FSM's are quite frequently used i was thinking there must be a rather widespread solution to this but i havent found one yet.

    Anyways, thank you for any suggestions!
     
    nickicool likes this.
  2. ippdev

    ippdev

    Joined:
    Feb 7, 2010
    Posts:
    3,799
    Use Mecanim. It was purpose built for this.. And why structs? Bools and floats work just dandy and IIRC are structs under the hood..
     
  3. dav_ege

    dav_ege

    Joined:
    Apr 22, 2019
    Posts:
    17
    Well i use a struct to encapsulate all the bools, vectors and floats that my user input might generate, my input data struct is basically the whole input state at a given time.
    I dont really see how mecanim is going to help me here? the state machine is for movement logic while mecanim is purpose build for animation. Also i have another layer of abstraction between my statemachine and the class that manages animations.
     
  4. ippdev

    ippdev

    Joined:
    Feb 7, 2010
    Posts:
    3,799
    You come from a windiows enterprise background? Beause you are re-inventing the wheel and not using Unity as a component based architecture but seem to be forcing oputside paradigms onto the engine then frustrated you can't find how to make them work. If yer doing this to experiment.then plug away. If yer trying to make a game then using the engine the way it was designed will get you there quicker. Logic fires Mecanim which is a state machine with animations transitions triggered by your logicand a GUI for visual feedback as to how your transitions blend..
     
  5. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,337
    Mecanim is a state machine dirven by provided variables. If you enable root motion or attach components to it, it can drive movement.

    If you haven't properly used it before, I'd recommend to drop whatever it is you're trying to do now, and either read the docs or try a couple of tutorials.
     
  6. dav_ege

    dav_ege

    Joined:
    Apr 22, 2019
    Posts:
    17
    I know about mecanim and i know that it can drive movement. Mecanim is - unless i am terribly mistaken - first and foremost a solution for animation. The state machine i am writing about is designed to handle Movement, not animation.
    I know about root motion animation, but i do not want to move my characters using the root motion of their animations.
     
    lmahieu likes this.
  7. dav_ege

    dav_ege

    Joined:
    Apr 22, 2019
    Posts:
    17
    Now i am not an experienced programmer, but i also dont think this means reinventing the wheel. As i have already stated, finite state machines are widely used to drive character movement (outside of mecanim). I dont mean to say it is not possible to do with mecanim but it certainly is not what i am looking for as i want to separate movement logic and animation logic.
    Also, to be clear, i am asking for advice and experience here, that does not mean i am frustrated or forcing anything.
     
    Last edited: Oct 7, 2021
  8. frosted

    frosted

    Joined:
    Jan 17, 2014
    Posts:
    4,044
    Can you paste some code and a clear goal statement? I don't really understand what your question is.

    "When it comes to specific button presses though it gets tricky. A Button with a tab interaction for example cannot be saved in a bool value, as i would not have a way of resetting the bool back to false."

    For example I have no idea what the above means
     
  9. ippdev

    ippdev

    Joined:
    Feb 7, 2010
    Posts:
    3,799
    Then don't use Root Motion. Use Translate and Rotate or push rigidbodies around. Unless the objects are static single members with no moving parts that is fine. If you are rotating any kind of amature such as a rig then Mecanim is the state machine driving the state to state transition.You are not finding any advice on searches for your methods in Unity as nobody probably has attempted such when tools already exist that are highly performant. You probably have 40_+ years experience deep inthe fields between myself and @neginfinity offerig you advice from long experience and running into brick walls. Logic and animation is decoupled. Try making it work without either though is tricky.
     
  10. Lurking-Ninja

    Lurking-Ninja

    Joined:
    Jan 20, 2015
    Posts:
    9,991
    Mecanim is a finite state machine.
    That's cool. You should be prepared for long and sweaty hours if you have characters with legs and other limbs. If you do not drive your movement with the animations you use, then they will be skating. I only like root motion because it is much more easier to avoid slipping on the surface.
    Because frankly, no one cares. Mecanim is working. BTW, you're looking at it wrong too. Your input should not be part of your FSM. Your input changes should drive the state changes in your FSM. Again, look at Mecanim and how it works.

    Example: user hits W. Your FSM should switch gears and put itself into the "move forward" state until the user releases the button.

    This is why you should learn how Mecanim works first, it offers a solution to this right away. You could implement it in your FSM too if you want. It is called trigger. When you set this trigger to true, it resets itself false after processing. Which means it is a fire and forget trigger, just like what you're looking for with your tap interaction.
     
    ippdev likes this.
  11. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,337
    Your original statement seriously smells like you're trying to make your life more interesting by implementing something you don't really need. And that's waht @ippdev hints at.

    Because the way you're doing things now you'll be making a control structure to control a state machine to control a control structure to control a state machine. There's something wrong with that.

    If the logic is heavily tied to animation state, using mecanim and StateMachineBehaviors is likely the way to go.
    Otherwise you'd need to describe what you're really trying to do. There's a good chance you don't need a state machine either.

    One more thing "Unless I am terribly mistaken" implies that you haven't used Mecanim much. Why don't you go and learn how to use it just so you don't spend inordinate amount of time reinventing a wheel?
     
  12. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    8,194
    What about the case where a player character can enter a vehicle (airplane, helicopter, boat, car, bike) surely a input struct would allow a common way to pass inputs to different vehicle types?

    Does Mechanim have a good way to take inputs and pass them to different vehicles or would this result in a much more complex state diagram tree?
     
  13. JohnnyA

    JohnnyA

    Joined:
    Apr 9, 2010
    Posts:
    5,039
    Really not sure what everyone is on about here, its completely reasonable to track movement state outside of mecanim using a FSM and to drive that FSM through some kind of incoming data object each frame.

    Inputs -> Movement FSM -> Animation System

    In even a moderately complex system, the movment FSM would usually have to take inputs not just from the player but from the objects in the scene too (user presses grab key + hand within 10cm of rock + current state is Jumping -> New state = Swing)

    Trying to build a system like that with state only in mecanim seems like a very bold choice.

    If you want to do the actual translation outside of mecanim, well thats fine too, not every game needs to be controlled by a root animation. For complex movement you may well use both (i.e. you can be moved by things other than animations, for example standing on an escalator).

    ---
    That said your actual problem does seem a bit odd: "When it comes to specific button presses though it gets tricky. A Button with a tab interaction for example cannot be saved in a bool value, as i would not have a way of resetting the bool back to false."

    Why would you have no way of setting the bool back to false?.

    Code (csharp):
    1. frameInputData.someInputAction = false;
    If it has more states than true and false then don't use a bool. Use an enum, or int, or float, or whatever data type is appropriate for the given action.

    In terms of resetting I would imagine you would want to reset input state every frame by default, rather than only updating it in response to events ... for just the reason you mention. (And yes somethings may not reset... a held button for example, but its up to you to decide how your actions work and if they care about press/hold/release/etc).

    So your loop is something like:

    Code (csharp):
    1.  
    2. MainLoop()
    3. {
    4.   UpdateInputs();
    5.   FSM.NextState();
    6.   UpdateMecanimState();  // Pass what the animation system needs to know to turn a state in to an animation
    7.   Translate(); // You may need to do some transaltion outside of mecanim (or all if you don't want to use root motion)
    8.   ResetInputs();
    9. }
    10.  
    11. // Event based methods to update inputs go here, it doesn't matter when they are called the input will be handled in the next processing frame, and reset immediately after.
    12.  
    13.  
    ---

    I would add that if you are doing a complex movement system (something like Zelda BotW for example) you will likely find it simpler to have multiple state machines for movement.

    For example one to control what you are doing with your hands, another to control your core locomotion. This avoids the need for hundreds upon hundreds of states (drinking idle, drinking straffe right, shooting running left, etc, etc).

    It's not that you can't use locomotion as the parent state machine, and then have all these actions in the child state machine, I've just found the abstraction much easier when you have multiple state machines (and use the state of these as the inputs for other state machine).
     
    Last edited: Oct 8, 2021
  14. dav_ege

    dav_ege

    Joined:
    Apr 22, 2019
    Posts:
    17
    Thank you!
    This is exactly what i meant. I think i was not clear about my actual problem, which has led to a lot of confusion in the answers.

    My problem is kind of silly in hindsight and your solution would work greatly. What i have done and what works for now is i created an interface for receiving input that my state machine as well as my states implement. On button press my virtual controller calls the corresponding method of my statemachine which forwards it to the active state.

    I did not know about using multiple state machines for movement, can you recommend any ressources on this topic?
     
  15. ZeBarba

    ZeBarba

    Joined:
    May 28, 2019
    Posts:
    6
    Hey, sort of necroing this topic. Did you ever found the answer to that?
    I do wonder what would be more reasonable:
    1- Subscribe to the events of the input system in the state machine handler and pass it to the current state in some way.
    or..
    2- Subscribe in each state for the events of the input system.

    1 has the advantage of only subscribing once, but there is the need to pass it somehow. But, i think it would be easy to control the active/inactive action maps for each state.
    2 has the advantage of subscribing only for the inputs that are relevant for that state in particular, but does not scale nicely. And probably i could not worry about the activation and deactivation of the action maps.
     
  16. Andy-Touch

    Andy-Touch

    A Moon Shaped Bool Unity Legend

    Joined:
    May 5, 2014
    Posts:
    1,448
    Mecanim was not made to be a generic finite state machine. Purely drive animation only.
    Using it for things other than Animation playback will inevitably fall down; or run into hard-to-debug issues with SMBs.
     
  17. nickicool

    nickicool

    Joined:
    Jan 19, 2022
    Posts:
    10
    I am also looking for a solution using FSM together with a new input system.

    Does anyone have an example of this implementation?
     
  18. ZeBarba

    ZeBarba

    Joined:
    May 28, 2019
    Posts:
    6
    I ended up going for the first case, sort of. The Input Manager class subscribes to the Input Action generated C# class events. And changes something in the FSM Handler (a bool, vector2, etc).

    I am not home, I will post it later, but here is a summary:

    The FSM Handler uses the Input Manager information to decide what state the player should be.

    The base for that was this Jason Weimann video:


    So the exercise was to implement the state machine shown on the video, for a controllable character, using the new input system.

    I liked this implementation because it was very easy to create a new state, and it never broke anything that existed before.
    This needs a bit of refactoring, but I did it just as a case-study, so I don't want to spend more time on it, but there are 3 things I will have in mind for a more serious approach:

    1- You end up creating a lot of control variables in the FSM Handler. Maybe if i could invert the dependencies I could not have this layer of variables between the Input Manager and the FSM Handler and use straight from the input manager.

    2- This implementation of state machines uses delegates, but complexity escalates quickly. So the "context" that triggers two different states could be 90% similar, but 1 variable. In this case, I had to duplicate the code.
    For example: Walking (player moves front and back, rotates around, no strafe), and walking with shield up (player moves front and back, no rotation, strafes left and right). Except for "The shield is up", everything else is the same (isGrounded, noSlope, notDead, etc).
    I would try and make it better somehow. Maybe something with Linq...

    3- This implementation of state machine does not work well with playing audio and particle system. Passing the Audio sources in the constructor is not organized at all. For a state that have only 1 sound, ok. But for states that have 4 or 5 it becomes messy). I would put a "Player Audio Manager" and "Player Particle Manager" instead.
     
  19. mwss1996

    mwss1996

    Joined:
    May 27, 2018
    Posts:
    5
    Hi, do you have any success on this?
     
  20. ZeBarba

    ZeBarba

    Joined:
    May 28, 2019
    Posts:
    6
    I fotgot about this...

    I wont paste the state machine here, as I posted the video that I based that, and the theme here is the transition.

    Also, be aware that by the end of it, I had already proven my concept, but just to finish the game, I used a lot of boiler plate code. Ignore it.

    First create the action maps with your actions, and flag it to generate the C# class.

    After that, insted of using the Player Input component (GameObject components for input | Input System | 1.0.2 (unity3d.com)) I created my own Input Handler:

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.InputSystem;
    5.  
    6. public class PlayerInputHandler : MonoBehaviour
    7. {
    8.     private PlayerInput _playerInput;
    9.     private PlayerFSMHandler _playerFSMHandler;
    10.  
    11.     public static event Action LaunchDebug;
    12.     public static event Action<bool> TryInteraction;
    13.     public static event Action MenuAccepted;
    14.  
    15.     private void Awake()
    16.     {
    17.         _playerInput = new PlayerInput();
    18.         _playerFSMHandler = GetComponent<PlayerFSMHandler>();
    19.     }
    20.  
    21.     private void OnEnable()
    22.     {
    23.         EnableAllGamePlayActionsMaps();
    24.  
    25.         _playerInput.Grounded.Move.performed += TryingToMove;
    26.         _playerInput.Grounded.Move.canceled += TryingToMove;
    27.         _playerInput.Grounded.Jump.started += TryingToJump;
    28.         _playerInput.Grounded.Jump.canceled += TryingToJump;
    29.         _playerInput.Grounded.LockDirection.performed += TryingToLock;
    30.         _playerInput.Grounded.LockDirection.canceled += TryingToLock;
    31.         _playerInput.Grounded.Run.started += TryingToRun;
    32.         _playerInput.Grounded.Run.canceled += TryingToRun;
    33.         _playerInput.Grounded.Attack.started += TryingToAttack;
    34.         _playerInput.Grounded.SpinAttack.performed += TryingToSpinAttack;
    35.         _playerInput.Grounded.SpinAttack.canceled += TryingToAttack;
    36.         _playerInput.Grounded.Interact.performed += TryingToInteract;
    37.         _playerInput.Grounded.Interact.canceled += TryingToInteract;
    38.  
    39.         _playerInput.Menu.Accept.started += AcceptMenu;
    40.  
    41.         _playerInput.Debug.activate.started += TriggerDebug;
    42.     }
    43.  
    44.     private void OnDisable()
    45.     {
    46.         _playerInput.Grounded.Move.Disable();
    47.         _playerInput.Grounded.Jump.Disable();
    48.         _playerInput.Grounded.LockDirection.Disable();
    49.         _playerInput.Grounded.Run.Disable();
    50.         _playerInput.Grounded.Attack.Disable();
    51.         _playerInput.Grounded.SpinAttack.Disable();
    52.  
    53.         _playerInput.Grounded.Move.performed -= TryingToMove;
    54.         _playerInput.Grounded.Move.canceled -= TryingToMove;
    55.         _playerInput.Grounded.Jump.started -= TryingToJump;
    56.         _playerInput.Grounded.Jump.canceled -= TryingToJump;
    57.         _playerInput.Grounded.LockDirection.performed -= TryingToLock;
    58.         _playerInput.Grounded.LockDirection.canceled -= TryingToLock;
    59.         _playerInput.Grounded.Run.started -= TryingToRun;
    60.         _playerInput.Grounded.Run.canceled -= TryingToRun;
    61.         _playerInput.Grounded.Attack.started -= TryingToAttack;
    62.         _playerInput.Grounded.SpinAttack.performed -= TryingToSpinAttack;
    63.         _playerInput.Grounded.SpinAttack.canceled -= TryingToAttack;
    64.         _playerInput.Grounded.Interact.performed -= TryingToInteract;
    65.         _playerInput.Grounded.Interact.canceled -= TryingToInteract;
    66.  
    67.         _playerInput.Menu.Accept.started -= AcceptMenu;
    68.  
    69.         _playerInput.Debug.activate.started -= TriggerDebug;
    70.     }
    71.  
    72.     private void TriggerDebug(InputAction.CallbackContext context)
    73.     {
    74.         LaunchDebug?.Invoke();
    75.     }
    76.  
    77.  
    78.  
    79.    
    80.     public void TryingToMove(InputAction.CallbackContext context)
    81.     {
    82.         _playerFSMHandler.tryingToMove = context.performed;
    83.         _playerFSMHandler.walkInput = context.ReadValue<Vector2>();
    84.     }
    85.  
    86.     public void TryingToJump(InputAction.CallbackContext context)
    87.     {
    88.         _playerFSMHandler.tryingToJump = context.started;
    89.     }
    90.  
    91.     public void TryingToLock(InputAction.CallbackContext context)
    92.     {
    93.         _playerFSMHandler.tryingToLock = context.performed;
    94.     }
    95.  
    96.     public void TryingToRun(InputAction.CallbackContext context)
    97.     {
    98.         _playerFSMHandler.tryingToRun = context.started;
    99.     }
    100.  
    101.     public void TryingToAttack(InputAction.CallbackContext context)
    102.     {
    103.         _playerFSMHandler.tryingToAttack = context.started;
    104.     }
    105.  
    106.     public void TryingToSpinAttack(InputAction.CallbackContext context)
    107.     {
    108.         _playerFSMHandler.tryingToSpinAttack = context.performed;
    109.     }
    110.  
    111.     public void TryingToInteract(InputAction.CallbackContext context)
    112.     {
    113.         TryInteraction?.Invoke(context.performed);
    114.     }
    115.  
    116.  
    117.     public void AcceptMenu(InputAction.CallbackContext context)
    118.     {
    119.         MenuAccepted?.Invoke();
    120.     }
    121.  
    122.  
    123.     public void EnableAllGamePlayActionsMaps()
    124.     {
    125.         _playerInput.Grounded.Move.Enable();
    126.         _playerInput.Grounded.Jump.Enable();
    127.         _playerInput.Grounded.LockDirection.Enable();
    128.         _playerInput.Grounded.Run.Enable();
    129.         _playerInput.Grounded.Attack.Enable();
    130.         _playerInput.Grounded.SpinAttack.Enable();
    131.         _playerInput.Grounded.Interact.Enable();
    132.     }
    133.  
    134.     public void DisableAllGamePlayActionsMaps()
    135.     {
    136.         _playerInput.Grounded.Move.Disable();
    137.         _playerInput.Grounded.Jump.Disable();
    138.         _playerInput.Grounded.LockDirection.Disable();
    139.         _playerInput.Grounded.Run.Disable();
    140.         _playerInput.Grounded.Attack.Disable();
    141.         _playerInput.Grounded.SpinAttack.Disable();
    142.         _playerInput.Grounded.Interact.Disable();
    143.  
    144.     }
    145.  
    146.     private void EnableDebugActionMap()
    147.     {
    148.         _playerInput.Debug.activate.Enable();
    149.     }
    150.  
    151.     private void DisableDebugActionMap()
    152.     {
    153.         _playerInput.Debug.activate.Disable();
    154.     }
    155.  
    156.  
    157.     public void EnableMenuActionMaps()
    158.     {
    159.         _playerInput.Menu.Enable();
    160.     }
    161.  
    162.     public void DisableMenuActionMaps()
    163.     {
    164.         _playerInput.Menu.Disable();
    165.     }
    166. }
    It has a reference to the Action maps (using the generated C# class), and a reference to the FSM Handler.
    It basically gets the input, and passes it to the FSM Handler, using the methods "TrySomething()". For instance:

    Code (CSharp):
    1.     public void TryingToMove(InputAction.CallbackContext context)
    2.     {
    3.         _playerFSMHandler.tryingToMove = context.performed;
    4.         _playerFSMHandler.walkInput = context.ReadValue<Vector2>();
    5.     }
    It just passes if the person is moving the thumbstick or nor (.performed phase) and its value as a Vector2.
    The FSMManager is the one who decides if the character can jump or not, based on its current state.
    So the input system is only a bridge between the phycical action of pressing a button, and the FSM Handler.

    This is the manager I was using:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.InputSystem;
    5. using UnityEngine.Animations;
    6. using System;
    7.  
    8.  
    9. public class PlayerFSMHandler : MonoBehaviour
    10. {
    11.     public StateMachine _playerStateMachine { get; private set; }
    12.     private CharacterController _characterController;
    13.     private Animator _animator;
    14.  
    15.  
    16.  
    17.     //Controller inputs
    18.     //Move
    19.     public bool tryingToMove;
    20.     public Vector2 walkInput;
    21.     //Jump and fall
    22.     public bool tryingToJump;
    23.     public float lastJumpEndTime;
    24.     public float lastLandingTime;
    25.     public bool jumpAnimEnded; //The transition from jump to fall needs this trigger because the jump is done with root motion, but the fall is via script
    26.     public AnimationCurve fallSpeed;
    27.     //Run
    28.     public bool tryingToRun;
    29.     public float lastRunningTime;
    30.     public bool runningEnded = true;
    31.     //Lock (shield)
    32.     public bool tryingToLock;
    33.     //Attack
    34.     public bool tryingToAttack;
    35.     public bool windowToAttack2;
    36.     public bool attack1AnimEnded = true;
    37.     public bool attack2AnimEnded = true;
    38.     public float lastAttackEndTime;
    39.     //Spin attack
    40.     public bool tryingToSpinAttack;
    41.     public bool spinAttackAnimEnded = true;
    42.     public float lastSpinAttackEndTime;
    43.     //LevelUp
    44.     public bool playVictory;
    45.     //GettingHit
    46.     public bool hitAnimEnded = true;
    47.     public bool playerHit;
    48.     //Jump down
    49.     private int jumpDownParam = Animator.StringToHash("flip");
    50.  
    51.     //Camera relative vectors
    52.     public Vector3 cameraRight;
    53.     public Vector3 cameraForward;
    54.  
    55.     //Character controller related to apply a little gravity 100% of the time, to stabilize isGrounded attribute. When root motion is not applied, the gravity should be applied whtin the state (ex.: Falling State)
    56.     private Vector3 gravity = Vector3.up * -1;
    57.     private float coyoteTime = 0.5f;
    58.     private float? coyoteTimeCounter;
    59.  
    60.     //Player powerups and other controls
    61.     private bool SpinAttackUnlocked;
    62.     public bool RunningDashUnlocked { get; private set; }
    63.     private bool _isDead;
    64.  
    65.  
    66.     //Variables for getting root motion, ground motion and slope adjustment
    67.     Vector3 adjFrameVelocity;
    68.     RaycastHit rayHit;
    69.     Vector3 frameVelocity;
    70.  
    71.  
    72.     //Player gameplay colliders
    73.     [SerializeField] private Collider _attackCollider;
    74.     [SerializeField] private Collider _spinAttackCollider;
    75.     [SerializeField] private Collider _shieldCollider;
    76.  
    77.     //Particles systems
    78.     [SerializeField] private ParticleSystem _deathEffect;  //This will be triggered by an animation event on the death animation
    79.     [SerializeField] private ParticleSystem _shieldEffect;
    80.     [SerializeField] private ParticleSystem _slashEfect;
    81.     [SerializeField] private ParticleSystem _stepEffect;
    82.  
    83.     //Audio Sources
    84.     [SerializeField] private AudioSource _stepsAudio;
    85.     [SerializeField] private AudioSource _powerUpAudio;
    86.     [SerializeField] private AudioSource _swingAudio;
    87.  
    88.     //Events
    89.     public static event Action PlayerIsDead;
    90.  
    91.  
    92.     private void Awake()
    93.     {
    94.         //Get components
    95.         _characterController = GetComponent<CharacterController>();
    96.         _animator = GetComponent<Animator>();
    97.        
    98.  
    99.         //Instanciate state machine and states - NOTE: In this area, it is only possible to pass components in the contructors, not variables, because some are no initiated yet.
    100.         _playerStateMachine = new StateMachine(false, false);
    101.         var Walking = new Walking(this, _animator, _stepEffect, _stepsAudio);
    102.         var Jumping = new Jumping(this, _animator, _characterController);
    103.         var LockedWalking = new LockedWalking(this, _animator, _shieldCollider, _shieldEffect, _stepEffect, _stepsAudio);
    104.         var Falling = new Falling(this, _animator, _characterController);
    105.         var Running = new Running(this, _animator, _shieldEffect, _stepEffect, _stepsAudio);
    106.         var Attacking = new Attacking(this, _animator, _slashEfect, _swingAudio);
    107.         var SpinAttacking = new SpinAttacking(this, _animator, _characterController, _slashEfect, _swingAudio);
    108.         var Dead = new Dead(this, _animator, _characterController);
    109.         var Victory = new Victory(this, _animator, _powerUpAudio);
    110.         var GettingHit = new GettingHit(this, _animator);
    111.  
    112.  
    113.         //Create transitions (to-from and any-to)
    114.         _playerStateMachine.AddTransition(Walking, LockedWalking, IsGroundedAndLocked());
    115.         _playerStateMachine.AddTransition(LockedWalking, Walking, IsGroundedAndNotLocked());
    116.  
    117.         _playerStateMachine.AddTransition(Walking, Jumping, JumpingReady());
    118.         _playerStateMachine.AddTransition(Jumping, Walking, IsGroundedAndNotLocked());
    119.  
    120.         _playerStateMachine.AddTransition(Jumping, LockedWalking, IsGroundedAndLocked());
    121.  
    122.         _playerStateMachine.AddTransition(Jumping, Falling, WasJumpingButEnded());
    123.         _playerStateMachine.AddTransition(Falling, Walking, IsGroundedAndNotLocked());
    124.         _playerStateMachine.AddTransition(Falling, LockedWalking, IsGroundedAndLocked());
    125.         _playerStateMachine.AddTransition(Walking, Falling, IsNotGroundedAndNotJumping());
    126.         _playerStateMachine.AddTransition(LockedWalking, Falling, IsNotGroundedAndNotJumping());
    127.  
    128.         _playerStateMachine.AddTransition(Running, LockedWalking, IsGroundedAndLocked());
    129.         _playerStateMachine.AddTransition(Running, Walking, IsGroundedAndNotLocked());
    130.         _playerStateMachine.AddTransition(Running, Falling, IsNotGroundedAndNotJumping());
    131.  
    132.         _playerStateMachine.AddTransition(LockedWalking, Running, RunningReady());
    133.         _playerStateMachine.AddTransition(Walking, Running, RunningReady());
    134.         _playerStateMachine.AddTransition(Jumping, Running, RunningReady());
    135.         _playerStateMachine.AddTransition(Falling, Running, RunningReady());
    136.  
    137.         _playerStateMachine.AddTransition(LockedWalking, Attacking, AttackReady());
    138.         _playerStateMachine.AddTransition(Walking, Attacking, AttackReady());
    139.         _playerStateMachine.AddTransition(Falling, Attacking, AttackReady());
    140.         _playerStateMachine.AddTransition(Attacking, LockedWalking, IsGroundedAndLocked());
    141.         _playerStateMachine.AddTransition(Attacking, Walking, IsGroundedAndNotLocked());
    142.  
    143.         _playerStateMachine.AddTransition(LockedWalking, SpinAttacking, SpinAttackReady());
    144.         _playerStateMachine.AddTransition(Walking, SpinAttacking, SpinAttackReady());
    145.         _playerStateMachine.AddTransition(SpinAttacking, LockedWalking, IsGroundedAndLocked());
    146.         _playerStateMachine.AddTransition(SpinAttacking, Walking, IsGroundedAndNotLocked());
    147.         _playerStateMachine.AddTransition(Victory, Walking, IsGroundedAndNotLocked());
    148.         _playerStateMachine.AddTransition(GettingHit, Walking, IsGroundedAndNotLocked());
    149.         _playerStateMachine.AddTransition(GettingHit, LockedWalking, IsGroundedAndLocked());
    150.  
    151.  
    152.         _playerStateMachine.AddAnyTransition(Dead, PlayerIsDead());
    153.         _playerStateMachine.AddAnyTransition(Victory, PlayVictory());
    154.         _playerStateMachine.AddAnyTransition(GettingHit, PlayerHit());
    155.  
    156.         //Create context controls for transitions
    157.         Func<bool> IsGroundedAndNotLocked() => () => IsGroundedCoyote() && !tryingToLock && runningEnded && attack1AnimEnded && attack2AnimEnded && spinAttackAnimEnded && !playerHit && jumpAnimEnded;
    158.         Func<bool> IsGroundedAndLocked() => () => IsGroundedCoyote() && tryingToLock && runningEnded && attack1AnimEnded && attack2AnimEnded && spinAttackAnimEnded && !playerHit;
    159.         Func<bool> JumpingReady() => () => tryingToJump && (Time.realtimeSinceStartup - lastLandingTime > 0.15f); //(Time.realtimeSinceStartup - lastJumpEndTime > 0.15f)
    160.         Func<bool> IsNotGroundedAndNotJumping() => () => !IsGroundedCoyote();
    161.         Func<bool> WasJumpingButEnded() => () => jumpAnimEnded;
    162.         Func<bool> RunningReady() => () => (Time.realtimeSinceStartup - lastRunningTime > 2f) && tryingToRun;
    163.         Func<bool> AttackReady() => () => IsGroundedCoyote() && runningEnded && tryingToAttack
    164.                                             && (Time.realtimeSinceStartup - lastAttackEndTime > 0.15f)
    165.                                             && (Time.realtimeSinceStartup - lastJumpEndTime > 0.15f)
    166.                                             && (Time.realtimeSinceStartup - lastLandingTime > 0.3f); //There is the need to apply the jumping/landing cooldown so the transition happens
    167.                                                                                                      //it would be good to have a general cooldown between states, or even to deal in a better way with the
    168.         Func<bool> SpinAttackReady() => () => IsGroundedCoyote() && runningEnded
    169.                                             && (Time.realtimeSinceStartup - lastSpinAttackEndTime > 0.3f)
    170.                                             && tryingToSpinAttack
    171.                                             && (Time.realtimeSinceStartup - lastJumpEndTime > 0.15f)
    172.                                             && (Time.realtimeSinceStartup - lastLandingTime > 0.15f)
    173.                                             && SpinAttackUnlocked;
    174.         Func<bool> PlayerIsDead() => () => _isDead;
    175.         Func<bool> PlayVictory() => () => playVictory;
    176.         Func<bool> PlayerHit() => () => playerHit;
    177.  
    178.         //Initialize state machine (needs to be done last, because is depends on all transitions
    179.         _playerStateMachine.InitializeStateMachine(Falling);
    180.     }
    181.  
    182.     private void OnEnable()
    183.     {
    184.         GemManager.NoMoreGems += SetPlayVictorty;
    185.         DashRunSpeaker.DashRunUnlocked += UnlockDashRun;
    186.         DashRunSpeaker.DashRunUnlocked += SetPlayVictorty;
    187.         BonusHeartSpeaker.SpinAttackUnlocked += UnlockSpin;
    188.         BonusHeartSpeaker.SpinAttackUnlocked += SetPlayVictorty;
    189.         HealthManager.PlayerDied += SetPlayerDeath;
    190.         BonusHeartSpeaker.SpinAttackUnlocked += SetPlayVictorty;
    191.         JumpDownSpeaker.JumpDownlocked += SetJumpDown;
    192.     }
    193.  
    194.     private void OnDisable()
    195.     {
    196.         GemManager.NoMoreGems -= SetPlayVictorty;
    197.         DashRunSpeaker.DashRunUnlocked -= UnlockDashRun;
    198.         DashRunSpeaker.DashRunUnlocked -= SetPlayVictorty;
    199.         BonusHeartSpeaker.SpinAttackUnlocked -= UnlockSpin;
    200.         BonusHeartSpeaker.SpinAttackUnlocked -= SetPlayVictorty;
    201.         HealthManager.PlayerDied -= SetPlayerDeath;
    202.         BonusHeartSpeaker.SpinAttackUnlocked -= SetPlayVictorty;
    203.         JumpDownSpeaker.JumpDownlocked -= SetJumpDown;
    204.  
    205.     }
    206.  
    207.     private void Start()
    208.     {
    209.         hitAnimEnded = true;
    210.         lastSpinAttackEndTime = 0;
    211.         lastJumpEndTime = 0;
    212.         lastLandingTime = 0;
    213.         lastAttackEndTime = 0;
    214.         jumpAnimEnded = true;
    215.     }
    216.     public void PlayerGotHit()
    217.     {
    218.         playerHit = true;
    219.     }
    220.  
    221.     void Update()
    222.     {
    223.         _playerStateMachine.Tick();
    224.     }
    225.  
    226.     private void OnAnimatorMove()
    227.     {
    228.         //Now the root motion is managed by this method, and not the Animator component in 100% of the time, even when root motion is applied
    229.         frameVelocity = gravity * Time.deltaTime + _animator.deltaPosition; //This sums the gravity to the root motion of the current clip being played, regarthless of the current state.
    230.  
    231.         if (Physics.Raycast(transform.position, -transform.up, out rayHit, 0.3f) && IsGroundedCoyote())
    232.         {
    233.             _characterController.Move(AdjustFrameMovementOnSlope(frameVelocity)); //This method treats the movement direction in downwards slopes
    234.         }
    235.         else
    236.         {
    237.             _characterController.Move(frameVelocity);
    238.         }
    239.  
    240.  
    241.         //transform.Rotate(_animator.deltaRotation.eulerAngles);  //The rotation is not needed now because no animation has root motion rotation
    242.     }
    243.  
    244.  
    245.     private Vector3 AdjustFrameMovementOnSlope(Vector3 frameVelocity)  //Can be optimized with cached variables
    246.     {
    247.         if (IsGroundedCoyote())
    248.         {
    249.             var slopeRotation = Quaternion.FromToRotation(transform.up, rayHit.normal);
    250.             adjFrameVelocity = slopeRotation * frameVelocity;
    251.             if (adjFrameVelocity.y < 0 && Mathf.Abs(_animator.deltaPosition.magnitude) > 0.01f)
    252.             {
    253.                 return adjFrameVelocity;
    254.             }
    255.         }
    256.         return frameVelocity;
    257.     }
    258.  
    259.     private void UnlockSpin()
    260.     {
    261.         SpinAttackUnlocked = true;
    262.     }
    263.  
    264.     private void UnlockDashRun()
    265.     {
    266.         RunningDashUnlocked = true;
    267.  
    268.     }
    269.  
    270.     private bool IsGroundedCoyote()
    271.     {
    272.         if (_characterController.isGrounded)
    273.         {
    274.             coyoteTimeCounter = coyoteTime;
    275.             return true;
    276.         }
    277.         else
    278.         {
    279.             coyoteTimeCounter -= Time.fixedDeltaTime;
    280.             return coyoteTimeCounter > 0;
    281.         }
    282.  
    283.  
    284.     }
    285.  
    286.     private void SetPlayerDeath()
    287.     {
    288.         _isDead = true;
    289.         PlayerIsDead?.Invoke();
    290.     }
    291.  
    292.  
    293.  
    294.  
    295.     #region "Methods to be called by the animator"  
    296.     public void SetJumpAnimEnded()
    297.     {
    298.         jumpAnimEnded = true;
    299.     }
    300.  
    301.     public void SetWindowToAttack2(int setTo)
    302.     {
    303.         windowToAttack2 = Convert.ToBoolean(setTo);
    304.     }
    305.  
    306.     public void SetAttack1AnimEnded()
    307.     {
    308.         attack1AnimEnded = true;
    309.     }
    310.  
    311.     public void SetAttack2AnimEnded()
    312.     {
    313.         attack2AnimEnded = true;
    314.     }
    315.  
    316.     public void SetAttack4AnimEnded()
    317.     {
    318.         spinAttackAnimEnded = true;
    319.     }
    320.  
    321.     public void SetAttackCollider(int setTo)
    322.     {
    323.         _attackCollider.enabled = Convert.ToBoolean(setTo);
    324.     }
    325.  
    326.     public void SetSpinAttackCollider(int setTo)
    327.     {
    328.         _spinAttackCollider.enabled = Convert.ToBoolean(setTo);
    329.     }
    330.  
    331.     public void SetStopVictorty()
    332.     {
    333.         playVictory = false;
    334.     }
    335.  
    336.     public void SetPlayVictorty()
    337.     {
    338.         playVictory = true;
    339.     }
    340.  
    341.     public void SetHitAnimEnded()
    342.     {
    343.         hitAnimEnded = true;
    344.         playerHit = false;
    345.     }
    346.  
    347.     public void SetJumpDown()
    348.     {
    349.         _animator.SetBool(jumpDownParam, true);
    350.     }
    351.  
    352.     public void SetDieAndGameOver()
    353.     {
    354.         Instantiate(_deathEffect, transform.position, Quaternion.identity);
    355.         var Renderers = GetComponentsInChildren<Renderer>();
    356.         foreach (var renderer in Renderers)
    357.         {
    358.             renderer.enabled = false;
    359.         }
    360.  
    361.         //Destroy(gameObject, 0.5f); //It is better to turn the render off, due to the post processing trigger
    362.         //TO DO: chamar o scene manager para voltar para o menu principal
    363.     }
    364.  
    365.     #endregion "Methods to be called by the animator"
    366.  
    367. }
    368.  
    369.  
    Skiping the FSM implementation (watch the video I posted early, he explains 1000x better I could), this class has several Delegates that are a sum of bool values. They are evaluated to set the FSM state, respecting the existing transitions.

    Code (CSharp):
    1.         Func<bool> IsGroundedAndNotLocked() => () => IsGroundedCoyote() && !tryingToLock && runningEnded && attack1AnimEnded && attack2AnimEnded && spinAttackAnimEnded && !playerHit && jumpAnimEnded;
    2.         Func<bool> IsGroundedAndLocked() => () => IsGroundedCoyote() && tryingToLock && runningEnded && attack1AnimEnded && attack2AnimEnded && spinAttackAnimEnded && !playerHit;
    3.         Func<bool> JumpingReady() => () => tryingToJump && (Time.realtimeSinceStartup - lastLandingTime > 0.15f); //(Time.realtimeSinceStartup - lastJumpEndTime > 0.15f)
    4.         Func<bool> IsNotGroundedAndNotJumping() => () => !IsGroundedCoyote();
    5.         Func<bool> WasJumpingButEnded() => () => jumpAnimEnded;
    6.         Func<bool> RunningReady() => () => (Time.realtimeSinceStartup - lastRunningTime > 2f) && tryingToRun;
    7.         Func<bool> AttackReady() => () => IsGroundedCoyote() && runningEnded && tryingToAttack
    8.                                             && (Time.realtimeSinceStartup - lastAttackEndTime > 0.15f)
    9.                                             && (Time.realtimeSinceStartup - lastJumpEndTime > 0.15f)
    10.                                             && (Time.realtimeSinceStartup - lastLandingTime > 0.3f); //There is the need to apply the jumping/landing cooldown so the transition happens
    11.                                                                                                      //it would be good to have a general cooldown between states, or even to deal in a better way with the
    12.         Func<bool> SpinAttackReady() => () => IsGroundedCoyote() && runningEnded
    13.                                             && (Time.realtimeSinceStartup - lastSpinAttackEndTime > 0.3f)
    14.                                             && tryingToSpinAttack
    15.                                             && (Time.realtimeSinceStartup - lastJumpEndTime > 0.15f)
    16.                                             && (Time.realtimeSinceStartup - lastLandingTime > 0.15f)
    17.                                             && SpinAttackUnlocked;
    18.         Func<bool> PlayerIsDead() => () => _isDead;
    19.         Func<bool> PlayVictory() => () => playVictory;
    20.         Func<bool> PlayerHit() => () => playerHit;

    So, how does the states receive the inputs? The FSM Handler has variables (that could be properties with a better protection level, but... meh) and each state has a reference to the state machine, and gets the information it needs.

    This is the walking state:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.InputSystem;
    5.  
    6. public class Walking : IState
    7. {
    8.     private readonly PlayerFSMHandler _playerFSMHandler;
    9.     private Animator _animator;
    10.     private ParticleSystem _stepEffect;
    11.     private AudioSource _stepsAudio;
    12.  
    13.     //Setup of player input variables
    14.     private Vector3 _characterForwardGoal;
    15.  
    16.  
    17.  
    18.     //Setup of animation and audio clips
    19.     private int animId = Animator.StringToHash("walking");
    20.     private int animFWDInput = Animator.StringToHash("forwardInput");
    21.  
    22.  
    23.  
    24.     public Walking(PlayerFSMHandler playerFSMHandler, Animator animator, ParticleSystem stepEffect, AudioSource stepsAudio)
    25.     {
    26.         _playerFSMHandler = playerFSMHandler;
    27.         _animator = animator;
    28.         _stepEffect = stepEffect;
    29.         _stepsAudio = stepsAudio;
    30.     }
    31.  
    32.     public void OnEnter()
    33.     {
    34.         _animator.SetBool(animId, true);
    35.     }
    36.  
    37.     public void OnExit()
    38.     {
    39.         _animator.ResetBoolParam();
    40.         _stepEffect.Stop();
    41.         _stepsAudio.Stop();
    42.     }
    43.  
    44.     public void Tick()
    45.     {
    46.         LookAtInputRelativeToCamera();
    47.         AnimBlendControl();
    48.         StepEffectControl(_animator.velocity.magnitude);
    49.  
    50.     }
    51.  
    52.  
    53.     private void LookAtInputRelativeToCamera()
    54.     {
    55.         if (_playerFSMHandler.walkInput != Vector2.zero)
    56.         {
    57.             _characterForwardGoal = (_playerFSMHandler.cameraRight * _playerFSMHandler.walkInput.x) + (_playerFSMHandler.cameraForward * _playerFSMHandler.walkInput.y);
    58.             _playerFSMHandler.transform.rotation = Quaternion.RotateTowards(_playerFSMHandler.transform.rotation, Quaternion.LookRotation(_characterForwardGoal), 10f);
    59.  
    60.         }
    61.  
    62.     }
    63.  
    64.     private void AnimBlendControl()
    65.     {
    66.         _animator.SetFloat(animFWDInput, _playerFSMHandler.walkInput.magnitude);
    67.     }
    68.  
    69.  
    70.     private void StepEffectControl(float playerVel)
    71.     {
    72.         if (playerVel > 0 && !_stepEffect.isPlaying)
    73.         {
    74.             _stepEffect.Play();
    75.             _stepsAudio.Play();
    76.         }
    77.        
    78.         if(playerVel <= 0 && _stepEffect.isPlaying)
    79.         {
    80.             _stepEffect.Stop();
    81.             _stepsAudio.Stop();
    82.         }
    83.     }
    84. }
    In the end,d it is the state that sets the animation to be played (and blend trees, layers, etc), the audio to be played, the camera behaviour, the movement, and whatever it is needed in that state. In this walking example i was using root motion, but in the jump state I used physics.


    To sum up:
    1. Action map generates c# class
    2. Custom Input Handler gets the input and passes to FSM Handler without any treatment
    3. FSM Handler uses (or not) the input received based on it current state
    4. FSM Handler decides which state to be based on context of the scene.

    There is a lot of room for improvement, but for a first time implementation of this two things together, I think it was ok.

    If you want, I can create a repo on github with all the scripts. Not the whole project though, it is too big.
     
    blibsproj likes this.