How to Program in Unity: Hierarchical State Machine Refactor [Built-In Character Controller #5]

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
During development, the scripts in our games are going to get more complex, and that is why it’s so important that our code is organized, easy to understand and expandable. Today we are going to do something incredibly necessary when programming our games to achieve and maintain these characteristics. We are going to refactor our current character controller script by implementing a hierarchical state machine, state factory and adjust for just proper naming conventions for our current variables! Of course, you can grab access to this project’s files and support the channel over on Patreon.com/iheartgamedev. And now! Let’s get started! As a quick refresher, in the previous videos of this series we learned to make our character move and jump! We did so by adding the built in character controller component to our Jammo character model that’s free to use from the hit youtube channel, Mix and Jam. We added animations to Jammo, and integrated Unity’s new input system to monitor the player’s input on both keyboard and controller. In our code, which is where we will be spending the majority of this tutorial, we set up callback functions to handle each of the player’s inputs that we set with the new input system. The callbacks update variables that we use to control Jammo’s movement and transition between its animations in additional functions that are appropriately titled “handleAnimation”, “handleRotation” and within the “Update” function. For jumping specifically, we attempted to replicate Mario’s jump by creating 3 different jump heights and jump durations, that are set when our character is first instantiated into the game. The “handleJump” function is called when the player first presses the jump button and “handleGravity” is always being called in “Update” to constantly enforce gravity on Jammo. If you really want to understand all of this code, I highly suggest checking out the previous videos and learning from there! But, we are now at least on the same page of what each function generally does and have a firm overview of the project. So this is our starting code and by the end of this video, it will be broken down into a hierarchical state machine. And we’ll have a solid understanding of the fundamentals behind that. To begin this code refactor, let’s talk a little bit about C# naming conventions. Variables in C# are typically written in Camel-Case or Pascal-Case. Camel-case is when the first letter of the variable is lowercase and any proceeding words within the variable have their first letter capitalized. Pascal-case is just like Camel-case except the first letter is capitalized. Now, when do we use each when programming in Unity? We will notice that when generating a new script from within the Unity editor, the auto-generated class name is Pascal-Case. That is to say, classes in C# are typically written using Pascal-case. We will also notice that the auto-generated Start and Update variables are capitalized too! This is because Methods and Functions are written using Pascal-Casing, as well. Now let’s say we have an example function and it takes a parameter. The class type of the parameter, as we know, will remain Pascal-case, but the parameter itself will be Camel-case. And if we were to create a variable local to this function, it would also be camel-case. The same can only sometimes be said to the class’s local variables, otherwise known as member variables. If a member variable has the public or protected access type, it is typically Pascal-cased. However, private member variables are denoted by an underscore and then camel-cased. If we want to get more specific, an older method would prefix an “m” prior to the underscore. This prefix is lifted from what’s known as “Hungarian” notation, so go ahead and check that out if you are interested in learning more. A couple of noteworthy specifics include static variables being written at the top of the Class. Interface declaration, prefixes a capital “I” to a Pascal-written name. And Enums are also written using Pascal. Cool! Opening up our current AnimationAndMovementControllerWithJumping script, we will now use this knowledge to refactor the script’s variables. Most noteworthy is that we’ll be adding underscores to the member variables. And already, we can see that in the script’s functions, we can more easily tell the difference between variables defined in the scope of the class versus variables local to the function. Wonderful! Ok! So what is wrong with our current script? As we can see, it clearly works, and is written using relatively understandable functions. However, the logic is already growing to be a little more complex than it should be and there’s not even that much going on: Jammo can move and jump. There is no crouching, swimming, dodging, wall running… none of that. If we were to add all of this functionality for Jammo in this single script, not only would it get much bigger, but it would get so much more complex because we would need to add extra conditions so that the new logic wouldn’t interfere with the old logic, and vice-versa. On top of that, all of the conditional logic would always be running with every update which could lead to performance issues! So, what do we do? If we remember from the explanation of state machines in the original State Machine tutorial on this channel: we can solve this “massive script” problem by essentially splitting the logic into smaller chunks called States. Each State will contain the functionality only relevant to that given state. For example, in a Running state, we’ll have logic that controls Jammo’s movement speed! Jammo will then switch between each of the states we’ve defined so that only one state and it’s associated logic is active at a given time. Clean, efficient and understandable. However, in this tutorial, we’re going to take State Machines to the next level with a Hierarchical state machine. In a hierarchical state machine, within each state, there can be a “substate” and even further, that state can have its own substate and so on! Hierarchical state machines allow an increase in overall complexity of the machine while still providing the same benefits of a finite state machine: only running code necessary to the current states, minimizing conditional statements, easier debugging… etc.. As an example, in our current project: if Jammo is on the ground the gravity will be different than when it’s in the air. But in both, we still want Jammo to be able to move around and rotate. So what we’ll do is have a two tiered hierarchical state machine: the root state will handle Jammo's gravity, the sub-states will handle the transitions between the idle walk and run movement states. To begin, let’s break down how state machines actually work, and don’t worry if this doesn’t all make sense at the start. Hopefully it will by the end of the video after we implement it ourselves. With state machines, we have three different concepts: Context, Abstract State and Concrete States. Each State that Jammo can possibly be in when the game is running will be a Concrete State. As mentioned, this includes running, and we also currently have walking, idle, grounded and jumping. These “Concrete States” are all individual classes that derive from the same Abstract State, or Base Class. This means that they inherit the public and protected methods and variables defined from that Base Class. Deriving from the abstract state guarantees that all of the Concrete States will have particular functions and variables which will keep our code clean. The context is what handles the instances of concrete states. Context will pass data to the currently active concrete state, and in the case of hierarchical state machines: active states, so that the states can react with their specific logic. For example, the context can pass the player’s input to the running state which will then adjust Jammo’s current velocity. Data passed to the concrete states from the context will also determine when one concrete state should switch to another. Like if the jump button is pressed from the grounded state, it should switch to the Jump State and Jammo should jump. And that’s about it for the concepts of a state machine! Now, let’s begin its implementation. In our project, we will create a new folder in our assets titled “StateMachine”. Here we will create a new script “PlayerStateMachine” and move it into the folder. We will continue by adding the rest of the scripts we will need for this entire tutorial: “PlayerBaseState”, “PlayerGroundedState”, “PlayerJumpState”, “PlayerIdleState”, “PlayerWalkState”, “PlayerRunState” and “PlayerStateFactory”. All of our new scripts that we just created will essentially replace our current “AnimationAndMovementControllerWithJumping” script. However, they will retain a lot of the same logic, and if the state machine works as intended, Jammo should function exactly the same. So let’s remember that going forward. Looking at our blueprint of a state machine, we can map these out to their purpose: “PlayerStateMachine” will be considered the “Context”. “PlayerBaseState” refers to the abstractState. Grounded, Jump, Idle Walk and Run are all concrete states. And the StateFactory creates the concrete states from within the context. Let’s start with the context. Opening the PlayerStateMachine script, we know that the context will store a lot of the data that the concrete states need to perform their logic. What this means is that the PlayerStateMachine script will retain all of the variables from the old animationAndMovementController script. Let’s go ahead and copy all of the member variables over now. It’s worth noting, that by keeping the variables in the context instead of within the concrete states themselves, we could technically have multiple state machines running off of the same data. In addition to the variables, we are going to want to keep a few of the functions, as well. We still want the callback functions that we use to track the player’s input using the new input system: onMovementInput, onJump and onRun. Be sure to add the namespace, UnityEngine.InputSystem, at the top of the file. And we’ll need to go ahead and copy the entire Awake method which sets the callbacks, and assigns some of our member variables like the character controller, the input system “player input”, and the animator. We’ll also see the “setupJumpVariables” function being called. Again this function makes sense to keep within the State Machine class because it’s where we first assign these values that the jump state will use. So we’ll copy and paste that function. Our update function will keep the characterController’s call to move because it’s used in all of our movement states, we’ll also keep “handleRotation”. The last we’ll copy is the OnEnable and OnDisable methods which are used to enable and disable the Action map of our input system. Here’s a quick look through our code. And if we replace our original script with our new playerStateMachine script, in play mode we’ll see Jammo currently only able to rotate. Ok! We will come back to the context many times as we go forward, but for now let’s move on to the Abstract State, or PlayerBaseState file. The purpose of the abstract state is to establish methods that all concrete states must have. This is because Concrete states derive from the base state. To start, we will remove all of the “using” namespaces from this file. We won’t need them. Next, we’ll remove “Monobehavior” from the class declaration and add “abstract” after the access type. Abstract means that we cannot create an instance of this class. Remember we only create instances of the concrete states. Next we’ll create the following methods for concrete states and go over each in detail when we use them: EnterState, UpdateState, ExitState, CheckSwitchStates and InitializeSubState. All of these methods will also be declared “abstract” which means that the concrete state classes will need to define their functionality themselves. Below the abstract functions, we will declare the remaining functions to be locally defined later: UpdateStates, ExitStates, SwitchState, SetSuperState and SetSubState. In all five of our concrete states, we will replace “Monobehavior” with “PlayerBaseState”. Immediately, an error will appear in each stating that we need to define the abstract methods from the base state. Let’s write them all as empty methods for now, but this time with the term override instead of abstract. Great! Despite not doing anything, we can now create instances of these concrete states! In our context file, we can declare a new member variable “_currentState”. And, thanks to Jason Storey for the recommendation, we have a clean way of creating concrete states: a State Factory. In our PlayerStateFactory file, we’ll remove the using namespaces and “Monobehavior”. We’ll declare a member variable “_context” at the top of the file of type player state machine and then we’ll write what’s known as a constructor by typing “public PlayerStateFactory that expects a parameter currentContext of type PlayerStateMachine”. The constructor function is what’s called when we create a new instance of a class, so here we’re saying that a new instance of a PlayerSateFactory requires a PlayerStateMachine to be passed in. Inside the constructor function, we’ll set the _context member variable to the passed in currentContext argument! And now the StateFactory will hold reference to our State Machine! Next we’ll declare five new public methods, one for each of our concrete states: Idle, Walk, Run, Jump and Grounded of type playerBaseState. Inside each, we’ll return a new instance of their respective concrete states. For example, Idle returns a new PlayerIdleState instance. Awesome. Back in our context file, let’s declare another member variable titled “_states” and in our Awake method set that equal to a “new PlayerStateFactory” passing in “this”. Remember, in the PlayerStateMachine file, “this” refers to itself, which is a PlayerStateMachine instance. And like we just programmed, PlayerStateFactory’s constructor expects a PlayerStateMachine instance to be passed in! Right below that in the awake method, we’ll set _currentState equal to the invoked _states GroundedState method. And then invoke _currentState’s Enter State method. We now have our groundedState instance set to the current state within the context. If we place a Debug.Log inside the enter state of the PlayerGroundedState, adding the PlayerStateMachine script to Jammo and pressing play -- we will now see the statement logged! Great! We have entered our first instance of state. Now a big feature of state machines is switching between states. In our PlayerBaseState file, we’ll declare and define a method, “SwitchState”, that will accept a parameter “newState” of type “PlayerBaseState”.”SwitchState” will first call “ExitState” which means that whatever the current state is will perform the logic defined in its ExitState method. We’ll then call “EnterState” on the “newState”, and end the method by setting the context’s “_currentState” to the newState. But wait, we don’t currently have access to the context from within our concrete or base state. Fortunately, our playerStateFactory makes this an easy fix! In our PlayerStateFactory class, we can pass in the same context that we stored in the playerStateFactory’s constructor as an argument for each new concrete state. We’ll also pass in “this” as a second argument, giving each concrete state access to the factory itself! Immediately, we’ll be met with an error stating that the constructors of each concrete states do not expect an argument to be passed in. So in each of our concrete states, we’ll go through and add constructor functions that have a parameter “currentContext” of type PlayerStateMachine and “playerStateFactory” of the same type. And to take this even FURTHER we can create a constructor function inside of the base state that does the same thing. In the base state we’ll add a protected variable titled “_ctx”, short for context and a protected variable “_factory”. We’ll create a constructor that expects “currentContext” and a playerStateFactory parameter, the same as the concrete states. Then we’ll set _ctx equal to the currentContext and we’ll do the same for the _factory member variable with the playerStateFactory. Now we’ll get errors in all of the concrete states again. This is because the base constructor now expects these arguments. To fix this, we’ll extend each state’s constructor by writing “: base” and pass in the arguments of from the local state’s constructor. What this does is pass the concrete state’s argument directly to the BaseState’s constructor. Now in the base state, we have access to the context. This means we can try to set the context’s current state to the newState. But wait! Another error. This time it’s because the context’s current state is not publicly accessible. We could just add the “public” access type, but we also have the option to create “getter” and “setter” methods for member variables. Getter and Setters are a cleaner way to access member variables of another class. They allow us to designate whether we want the accessing code to only be able to read a value, by using the getter, and or able to change the value using the setter. The notation to declare a getter setter is to write the access type, in this case “public”. Followed by the type of the variable, “PlayerBaseState” for the currentState, and a Pascal notation variable typically of the same name of the member variable: in this case “CurrentState”. We then use brackets and add whichever of the getter setter methods we want. In this case, we’ll add both, so we’ll type “get brackets return _currentState and set _currentstate is equal to value. Now in the switchState method of the base state, where we have access to the context, we can use the setter to reassign the value! Awesome! We’ll end up using getters and setters for pretty much all of our member variables that are accessed from concrete states. Doing so makes sure that we know exactly how we are using those variables. Ok! Because the _ctx variable is “Protected”, that means that tis variable is inherited by the classes that derive from the PlayerBaseState. This means we have access to the context and state factory in each of our concrete states, and we also have a working “SwitchState” method. But when should we use the switchState method? We’ll remember we created the method titled “checkSwitchStates” on each of our concrete states. When should the Grounded State switch to another state? Well, we can imagine that if the player presses jump when they’re on the ground, it should switch to the playerJumpState. Using our knowledge of getter setters, we can see in our PlayerStateMachine that “_isJumpingPressed” is updated when the player presses the jump button. So we can create a new getter for IsJumpPressed and access that in the groundedState. If IsJumpPressed is true, we’ll call SwitchState and pass in the invoked Jump method of the inherited _factory. That’s the beauty of having access to both the context and the factory from within the concrete states. It encapsulates the state machine logic so that we don’t need to write public state-related functions inside of the context. Instead they can be handled inside the state itself. So what happens when we switch to the new state? From our switchState method, we know that “EnterState” is invoked on the PlayerJumpState. If we look at our original script, we have conditions set in handleJump so that it occurs only one time when the player first presses jump and they are grounded. Well, that’s the point of the GroundedState - Jammo is grounded. And we know that EnterState is only called one time! So we can copy the logic from within that conditional and paste it in it’s own handleJump function of the playerJumpState, then invoke it in the EnterSTate function. However, remember we need to access all of these variables from the context now. This means that we should make getters and setters for each variable. And then replace each variable in the logic of the PlayerJumpState with the new getter or setter. Now remember, we still need to call CheckSwitchStates somewhere for its logic to actually run. As we know, Update is supposed to run every frame of the game and we definitely should check if the state should switch every frame. So in all of our concrete states, we’ll invoke the CheckSwitchStates method. However, because our concrete states do not derive from Monobehaviour anymore, we know that the Update method will not be run. This isn’t the case for the context though! So we just need to invoke the currentStates public update method in the PlayerStateMachine’s update method! Testing in play mode and pressing jump, Jammo will jump! However, as we can see, we’re still missing a lot of logic -- like Gravity! In our original script we had a function handleGravity that had multiple conditional statements. The first condition would be true when the character controller was grounded. That logic would start the coroutine to reset the jump count as well as apply the lower gravity amount. What’s cool about state machines, is now we can keep the logic about the jump tied to the jump state! We’ll take the snippet related to the jump and place it inside the ExitState method of the PlayerJumpState. Like before, we’ll now switch the variables to getters and setters. And we’ll also move the definition of the IJumpResetRoutine into the Jump class as well. ExitState is called during our switchState method, so let’s also finish our checkSwitchState implementation for the JumpState. If the characterController is grounded, we want to switch back to the grounded state. And this will call the EnterState method of the grounded state. With the remainder of that original conditional logic, In the GroundedState’s EnterState method, all we need to do is set the current and applied movement Y values to the grounded gravity value from our context. For these setters, we can target the y value of the Vector3 by declaring the type of the y value: a float. And now, we can just copy and paste the remaining handleGravity logic into the PlayerJumpState and call it inside of Update. Testing in play mode and boom! Our jump logic is working and switching back to our GroundedState! The only oddity is that we can now hold jump and Jammo will continue to jump. To quickly fix this, we also now have an unnecessary variable from our previous implementation: “isJumpAnimating”. With the JumpState’s exit state method, we know Jammo won’t be animating because we disable the boolean with SetBool. So let’s rename this to “requireNewJumpPress”. We’ll then set this to true if jump is pressed on exit of the jump state. In the context, we’ll rename the member variable, getter and setter. And in our onJump callback, set it to false when the jump button is pressed. Then in our groundedState’s CheckSwitchState method, we’ll require the jump button to be pressed and the new RequireNewJumpPress bool to be false. And now our state machine is effectively switching between jumping and grounded exactly like before but only running the logic necessary to do so! Perfect! Let’s finish this up with the hierarchical aspects of the state machine implementation. Currently Jammo can Stand and Jump, and while it’s doing this we want it to be able to move around. This logic will be handled in Jammo’s substates! Back in our PlayerBaseState, we have the remaining methods “InitializeSubState”, “SetSubState”, “SetSuperState” and “UpdateStates”. Let’s first define “SetSubState” and “SetSuperState”. “SetSub” will expect a parameter “newSubState” of type PlayerBaseState, and “SetSuper” will expect a parameter “newSuperState”. “SetSubState” will set a new protected member variable, _currentSubState, equal to this value. And “SetSuper” will set a new member variable _currentSuperState equal to its parameter, as well. SetSub will then call SetSuperState on the newSubState, passing “this” as its argument and therefore setting itself as the super state of its own new substate! What this means is that anytime we call “SetSubState”, we create almost a parent child relationship, but we also create the inverse as well: child to parent. With that in mind, we can now go to our PlayerGroundedState and PlayerJumpState to overwrite the inherited InitializeSubState method. Remember, we have three possible substates: idle, walk and run. And we want to create the correct substate from the moment both parents are constructed. In our original code, we had a method “handleAnimation” that distinctly tells us when we want to transition between each. We can use this similar logic inside of the “IntializeSubState” methods. We want a substate of “Idle” when the character is not moving and not running. We want a subState of “Walk” when the character is moving but not running. And we want a subState of “Run” when the character is moving and running. And as shown, we’ll create the associated getters for these two member variables of the state machine. We’ll then call each InitializeSubState in the constructor of the Grounded and Jump states so that the proper substate, idle walk or run, is created regardless of which superState is active. We can go ahead and add the proper logic in each of these three substates to switch between one another: Idle should switch to both Walk and Run depending on which of the movementPressed and runPressed variables are true. Walk should switch to either Idle or Run. And Run should switch back to Walk or Idle. Same as before, we call “CheckSwitchStates” in our UpdateState method. Let’s quickly finish the movement logic for each of these states. In the EnterState method of each, we will access the context’s animator and appropriately set the IsWalking and IsRunning Hash booleans. In Idle, we’ll set the AppliedMovementX and Z values to 0 in the EnterState method. In Walk, we’ll set AppliedMovementX and Z values to the corresponding CurrentMovementInput values inside of the Update function. And the same for the Run state but multiplying by a larger run multiplier. We’ll then create the appropriate getters and setters in the context. If we enter play mode now, we’ll immediately notice something. The substates aren’t doing anything. That’s because Update is only being called in the context on the “CurrentState” but not its substates. Let’s change invocation to UpdateStates, and back in our playerBaseState we will set updateStates to public and give it the following definition: it will call its own UpdateState method, then if it has a subState will call updateStates on that. This will cause all substates that are a part of the “SuperState-SubState” chain to Update! We can actually do something similar for ExitState with an ExitStates method, but we don’t have the need to with our current machine. Just a couple more things to fix now! Entering play mode once again, we can move around -- however, we can no longer jump! But why? The answer is in our SwitchState method. What happens when we switch state from Idle to Walk is that the context’s current state becomes the sub state! We need a way to tell if the concrete state is at the top of the state hierarchy. To do this, in our playerBaseState, we’ll create a new member variable: _isRootState that will default to false. We can then use this as a condition inside of our SwitchState method so that we only switch the Context’s current state if it’s at the top level of the state chain. And now in our Grounded and Jump States, we’ll set this value to true in their constructors! But what about switching states that are not root states? Well, if it’s not a root state, that means that it’s a sub state and, therefore, will have a super state. So we’ll add a second condition that checks to see if it has a super state, and if it does: to set the superState’s substate to this new state. Essentially transferring the super state over to the next state. Ok! One FINAL change we’ll need to make in this script some of you may have picked up on. We’ve been using the private member variable notation for a protected member variables in our base state. We can go ahead switch these to private, and then create protected getters and setters for IsRootState, CTX and Factory And that’s it! Jammo is now working exactly as it was before, but we now have the power of a hierarchical state machine that splits our previous logic into condensed states and enjoy the many benefits that come with a state machine. Awesome job! There are definitely changes that can be made here, and if you have recommendations, feel free to leave them in the comments. I love learning from the community! To vote on the next tutorial on this channel, be sure to checkout the iheartgamedev patreon! And of course the biggest thank you to all of the current supporters! You help make this happen. If you’re interesting in joining this awesome growing community, we’d love to have you in the channel discord, and for updates on the next video, be sure to follow me on twitter. But that’s all for today, thank you so much for watching and I’ll see you in the next video!
Info
Channel: iHeartGameDev
Views: 9,330
Rating: undefined out of 5
Keywords: How to Program in Unity, Hierarchical State Machine, state machine unity, built in character controller, state machine tutorial, how to program a state machine, c# state machine, programming tutorial unity c#, hierarchical state machine tutorial, unity programming tutorial, unity programming, character controller state machine, state machine explained, unity state machine controller, player state machine unity, unity 3d movement, programming unity c#, unity 3d jump script
Id: kV06GiJgFhc
Channel Id: undefined
Length: 30min 58sec (1858 seconds)
Published: Sun Nov 07 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.