Genshin Impact A game that most of you likely
have heard by now or even played. In this game, there are around 4 to 5 main ways
of roaming around the map. We'll be focusing on the first 3 of these systems:
Moving, Gliding and Swimming. Of course, we'll start with their base: The Movement System. Because it is the base,
it also means it will take the longest. Now, I planned on splitting the tutorial
in 1 video per system, but as I was writing the first tutorial,
I noticed it would take quite longer than I expected. So, I made a "Poll" in the "Community" tab
and while not by a long shot, most people voted to split it into smaller videos,
much like my Dialogue System, so I re-wrote it to make more sense that way. However, because it was quite close in votes, I attempted to re-write it in a way
that made sense when put together, so whenever I release the last video of the first part, I'll also release one video with all of those videos
together as soon as possible, for those that prefer it that way. Of course, because the video will be quite long,
it will likely take hours to render it and would take quite a while to re-watch it as well, so I'll just hope that it renders with no audio artifacts. If there happens to be any, I apologize for it now. Before we head into our tutorial,
lets first take a look at what are we going to be using: We'll be using Cinemachine, which is an Unity Package that easily allows us to add a Camera System
with pre-made algorithms. The New Input System, also an Unity Package,
is the new way of creating and reading Input. The Animator, Animator Controllers and Animations
allow us to add animations to our Character. We'll be taking a look at Reusable Sub-State Machines, but won't be taking a look at Blend Trees. Physics-Based Movement
simply means Movement using a Rigidbody. We'll not be using the built-in Character Controller. A Floating Capsule is a technique that easily allows our
player to move up or down slopes as well as steps without any weird jumps or problems. And finally, State Machines are an easy way of
adding, removing and changing states. In our case, they will be our Player States. All of these will be introduced in the
Base Movement System Part of the series, which is the main reason why it will take the longest. I'll try to explain as much as I can for what we need, but if you're interested in knowing more about these topics
in whatever areas I don't cover, I'll be leaving a few links in the description. Regarding State Machines, it is very likely that you'll see
different ways of using them throughout other tutorials, so don't worry if mine is somewhat different from theirs,
as that's normal. If you have already finished the series,
if you did enjoy it, leaving a like and subscribing would help me out a lot
as it allows me to reach more people. Feel free to also leave any feedback whatsoever
so that I can improve my future videos. With that out of the way,
I really hope you enjoy this series. Lets then start by creating our Project. All we need for this System is a 3D Project. Unless there's any current problem with using HDRP or URP
with Cinemachine or the New Input System, we should be able to choose any 3D Template we want. I'll go with a simple normal 3D Template. For the Unity version I'm using, I'm currently using 2020.3
but you should be able to use any you want. I do recommend at least 2020 as we'll be using serialized
properties, but, if you are using 2019, that should be fine as well, simply swap the properties with public variables instead or use non-automatic properties. For the name and folder of the Project, choose the one you
desire. We'll start by setting up a base namespace to make sure no
name conflicts happen. To do that, we go to "Edit > Project Settings > Editor >
Root Namespace". I'll name mine "GenshinImpactMovementSystem". Of course, give it a name you like. For simplicity, this will be the only namespace we'll be
adding in this tutorial. I also went ahead to Kenney's website and downloaded his
Prototype Textures. You can use normal materials if you prefer, in case you
don't feel like downloading them. I'll have mine be inside of a folder named "Materials". Next, lets download Cinemachine. To do that, we go to "Window > Package Manager > Packages:
Unity Registry" and search for "Cinemachine". Make sure you select the latest version here, which by the
time of this video is "2.8.4". This is simply because there are some bugs happening in the
older versions so we're just making sure we've got a working one. If by the time you watch this video the default version is
at least "2.8.4", then you should be fine. When that's done, press "Install". When Cinemachine is done installing and importing,
go ahead and search for "Input System". Select it and feel free to Install it with the default
version. A warning should show up asking you
if you want to disable the old Input System. While it's not necessary, we don't need the old Input System
for this tutorial so I'll go ahead and press "Yes". Do note that this will restart your project. If you chose "No", make sure that in
"Edit > Project Settings > Player > Other Settings" your "Active Input Handling" is set to
"Both" or "Input System Package (New)". Your project will also restart when you change this option. That's all for our Packages, so we'll now go ahead and start downloading
our Character Model and its Animations. We'll of course be using a free model and free animations, so do not expect them to look as good
as Genshin Impact Animations. To do that, we'll be using a website
owned by Adobe named "Mixamo". So go ahead and open up "https://www.mixamo.com/". You'll need to login with an Adobe account to download the
assets but you can create an account using Google or Facebook if
you do have them. When you're logged in, go ahead and open the "Characters"
tab. This is the list of character models we can choose from. You can choose the character you want, but I'll go ahead
and search for "Y Bot" and select it. Once you have your character model selected,
press "Download" at the top right. For the "Format", make sure you choose "FBX for Unity". I'll leave the pose as "T-Pose". When that's done, press "Download" again. I'll be renaming the file to "CharacterModel". Then, in Unity, I'll create a new folder named "Models", followed by "Characters" and "Player" inside, and then drag our Character Model here. With our Character Model in Unity,
we of course need its animations. So back into Mixamo, go on to the "Animations" tab. We now have a list of animations we can download. For now, we'll only be downloading animations
for our Base Movement System. Note that the only important setting in the "Download" tab
is the "Format", which should be set to "FBX for Unity". I'll also be renaming the files but
I'll leave the new names in the screen. Here is the list of Animations I'll be downloading: "Idle". "Walking", with an "Overdrive" of "15"
and the "In Place" option enabled. "Running" around the bottom with
the "In Place" option enabled. "Running" above a bit, with the "In Place" option enabled. "Idle To Sprint". "Run To Stop" at the right. "Stop Walking", with a "Starting Trim" of around "45". "Stop Walking" again but with an "Overdrive" of "25",
"Character Arm-Space" of "55" and "Starting Trim" of "50". "Jumping" on the second tab at the right, with a "Starting
Trim" of around "37". "Falling To Landing" with a "Starting Trim" of around "22". We have all of the animations we need,
so it's time to add their files into Unity. Before we do that, make sure you have
the following names in your Animation Files. Back into Unity, select all of our Animation Files
and drag them into our Models Folder. Now, these are Models that come with an animation. We already have our base model so we
of course only want the animation clips, so we'll have to extract them out
of the assets we've just imported. If we go ahead and open up
one of the assets by clicking on the arrow icon we can see a bunch of other assets inside. The one we want is the last asset,
which is an animation clip. Now, if you kept the "ybot@" part of the file name, your animation clip should have the same name
as the text that follows it. This is why I've asked you to rename the files. If you did rename the files to not contain the "ybot@" part
or whatever name your character model has, then the animation clip will come named as "mixamo.com" and
you'll have to rename them through Unity instead, so I recommend just renaming and redragging
the assets with the correct names. To get these animations out of our assets, we simply open up
every asset, ctrl click each of the animation clips and then press "Ctrl + D" to duplicate them. When that's done, we won't be needing the downloaded assets
anymore so feel free to remove them. Do not remove the "CharacterModel" asset though. We now have our animations,
so lets add them to a new folder. In the Assets folder, create a new folder named
"Animations", followed by "Characters", "Player", "Clips" and "States". I'll drag every single animation to the "States" folder. Then, inside of this "States" folder,
I'll create a few more folders: "Moving", "Stopping", "Landing", "Grounded" and "Aiborne". I'll add the "Walk", "Run" and "Sprint" animations to the
"Moving" folder. Then, I'll add the "Light", "Medium" and "HardStop"
animations to the "Stopping" folder. The "LightLanding" animation
will go to the "Landing" folder. When that's done, drag the "Jump" animation
to the "Airborne" folder. Whatever remains here except the "Airborne" folder
will be moved onto the "Grounded" folder. It's alright if you have no idea what these folder names
mean as it will become clear later on, we're just adding them now for organization purposes
and to not need to do it later. Before we keep going, there were some animations that we've
downloaded that didn't really have the "In Place" option. This means that they will move the player forward without
our input, which is something we do not want. We will change that in the following animations. Start by right clicking in the Project tab
and add a new "Animation" tab. I'll dock it to the right for a bit and resize it. Then, double click on the "Dash" animation
on the "Grounded" folder. The Animation Window should now have
all the keyframes of this specific animation. Open up the "Hips: Position" tab
and select all "x" and "z" keyframes. Then, press the "Delete" key to delete them. Repeat these steps for the "LightStop", "MediumStop" and
"HardStop" animations in the "Stopping" folder as well. Next, we'll need to some some of the Animations
to keep looping after they run once. We can do that by enabling the "Loop Time"
option in the Animation Clip. We'll do it for the "Idle" Animation
and the "Walk", "Run" and "Sprint" Animations. Our animations are now set-up properly. We have our packages, character model
and animations imported into Unity, but, to be able to try out our game later on,
it's best for us to have a small test map. We'll go ahead and create one now,
but don't worry as it's easy and fast. If you do not feel it's worth it though, I'll leave a link down below where
you can download the final map. We'll be using Pro Builder to easily build it so go to "Window > Package Manager > Packages: Unity Registry". In here, search for "ProBuilder", with no space in between. When it shows up, select the default version and Install it. When it's done installing, go to
"Tools > ProBuilder > ProBuilder Window" and dock it to the same tab as the Animator tab. We'll start by creating the Ground by pressing
on the "New Shape" square with the plus (+) icon. This will open a Shape Tool to
select our Object Shape and its size. Do note that for this to show up, you need to
press directly on the plus (+) icon square. You can also open this Window by pressing "Ctrl + Shift +
K". We now have a preview of our Shape in blue. Leave it as a "Cube" and set its
size to be "2000" by "100" by "2000". Press "Build" when you're done and close the window. Then, I'll go to the Kenney Prototype Textures folder and open up the "Dark" PNG Folder
and drag the "texture_05" to the Ground. This will create a material so now press
on the ProBuilder "Material Editor" Window and assign this new material to the "Alt + 2" shortcut. If we now select an object and press "Alt + 2",
our material will be applied to it. I'll add this ground to a new parent Game Object named
"Environment" and also rename it to "Ground". I'll set the Environment Transform Position to
"-1000, -100, 1000" to center it. I'll also drag our Character Model into the Scene. When that's done, add a parent Game Object
to the Character Model and name it "Player". Reset both Game Objects Transforms. This will make it so that our Player has
a different object for its main component and its graphics. It doesn't matter too much but you can
also set the camera to "0, 1.5, -1". Next, we'll add one small ramp and one
big ramp with a small area to walk on. To do so, press "Ctrl + Shift + K" and choose the "Cube"
shape. I'll have the size of the small
ramp be "5, 1, 15" and "Build" it. Close the window and select the "Edge Selection" icon. Select the top-end Edge and also the Move Tool. Then, hold "Ctrl" and pull the arrow
upwards until you got around 6 squares. Then, select the top-start Edge without holding "Ctrl" and then move it down until it's on the same level
or close to the same level as the bottom ledge. We now have a ramp, but lets add our
base to walk on by going to its back, select the "Face Selection"
option and select the back face. Then, press "Ctrl + E" to extrude and move
it with the "Ctrl" key held around 5 squares. When that's done, add this object
to the "Environment" Game Object and rename it "SmallRamp". Then, choose the "Object Selection" icon and
move the whole object somewhere else you'd like. I'll also press "Alt + 2" to give it the dark texture. I would like for the top of the ramp to have
another color though, so go to the "Orange" folder and drag the "texture_02" into the ramp
and then press "Ctrl + Z" to undo it. This created a new material so open the "Material Editor" Window and add
this new material to the "Alt + 3" shortcut. Then, select the "Face Selection" icon and
then select the top face and press "Alt + 3". Lets also expand this small ramp to have
a big center by extracting its side faces. Make it as large as you want,
I'll have each side be 5 squares. Although we won't use it in the first part
of the tutorial, our Water will stay here. When you're done, lets add a
ramp that goes down to the Water. I'll select the bottom edge of
the side I want to add a ramp on and press "Alt + U". This creates an edge on the opposite orientation, so now Drag the newly created edge
to be 5 squares from the wall. Then, select the new Face and extrude it with
"Ctrl + E" and drag it a bunch of squares. Now, select the top Edge and drag it down. Then, select the top Face and give it
the orange material with "Alt + 3". Lets build another ramp by pressing "Ctrl + Shift
+ K" and make the "Z" be "50" and "Build" it. Make it a child of the "Environment"
Game Object and rename it to "BigRamp". Move it to a place you desire,
I'll place it to the left of our "SmallRamp". Then, select its top-end Edge and drag
it up as many squares as you want. When that's done, drag the top-start
Edge down until there's a small step, which will serve as a test for our Step Climb. I'll also extrude the Back Face around 5 squares. Select the whole object now and
give it the "Alt + 2" material and then select the ramp face and
give it the "Alt + 3" material. With that done, we'll now add a new Cube
with a size that fits our Water area. Make it a child of the "Environment"
Game Object and rename it to "Water". Then, go to our "Materials" folder and a
create a new folder named "Environment". Inside, right click the project
window and go to "Create > Material". I'll name it "Water". Change the "Rendering Mode" to "Transparent" and make its "Albedo" color
a blue with a lower Alpha. Then, drag the material into the Water Game Object. Next, remove its mesh collider and add a
new Box Collider and set it to "Trigger". We'll also add 2 small rectangles
to test something later on so press "Ctrl + Shift + K" and build
two "Cubes" with a size of "4, 0.9, 10". Name them "Obstacle" and add them as child
objects of the "Environment" Game Object. Then, move them somewhere close to our Player and separate them, leaving just
a small gap in between them. I'll give these 2 the "Red" folder "texture_04" material. And that's our base map created. Feel free to close or dock the
Tabs somewhere else if you want to. If you don't want to close the "ProBuilder" Window, make sure the "Object Selection" option is selected. We are all set but before we go ahead and start coding I think it's best for you to learn
about States and State Machines first. When opening up and loading a game, the first thing you might notice is that our player is
"Idling". This is what we call a "State". In this example, the current "Player State" is the "Idling
State". If we were to move, lets say by pressing "W",
our player would start "Running". "Running" is yet another "Player State". We've simply changed or transitioned from
the "Idling State" to the "Running State" by pressing a movement key. This gives us two states: "Idling" and "Running". Both of these states are implemented as
their own classes with their own logic. This means that the "Idling State" doesn't
care about what the "Running State" does, but only that it should transition to it
whenever we press a movement key, like "W". The "Running State" is the one that takes
care of giving the player some desired speed. However, that transition needs to somehow happen and the current state logic needs to somehow run as well. That's what "State Machines" are for. They hold the current state,
a method to change that current state and, if the state isn't public,
one or more methods to run its logic. This brings us to one of the State Machine
System Advantages: Each state has its own logic. An example on why this is a good thing is
the typical "Jumping" and being "Grounded". Lets have an imaginary situation
where our player is "Idling". Whenever we press on the "Space" key,
our player should "Jump". One common problem when doing this
without State Machines is that if we keep on pressing the "Space" key,
our player will keep on Jumping. The common way of fixing this problem
is by adding an "isGrounded" check. If when the player presses "Space" there
is ground right underneath the player, then the player can "Jump". Otherwise, our Player shouldn't be able to "Jump". While this is alright, not only are the "Idling"
logic and the "Jumping" logic in the same script, the "Jump" is also caring about whether we are Grounded or
not while it should only care
about adding an upwards force. However, lets take a look at the same
example using States and State Machines. In our "Idling State", we have
the logic of transitioning to the "Jumping State" whenever we press the "Space" key. When we do so, the State Machine changes
the current State to the "Jumping State". In the "Jumping State", the only
logic we have is to jump upwards, there is no logic telling us that we
should "Jump" whenever we press "Space". This simply means that our player won't
keep Jumping if we keep on pressing "Space". Of course, if we did want to double or triple
jump, we would simply add a count of used jumps and add a new upwards force
whenever we pressed "Space". Something to note as well is that we do not need
to check whether the Player is "Grounded" or not. The reason for that is quite simple: Our player
can only be "Idling" when it is in the "Ground", as otherwise, our Player would be
in another state like "Falling". One other big advantage is that it's quite
easy to add, remove or change a state. With one or a few scripts, we would have
multiple states tangled within each other and it can be a bit of a mess to
add or remove an existing state, at least if you end up having quite a lot of them. With State Machines, we add a new one by creating
a new class and add its own logic to that class and we can change that state implementation
by updating that isolated logic. To remove one, we simply remove the file, the state transitions and whatever
code was necessary for that transition. This does however bring us to a
disadvantage: Each State is its own file. If we end up having 200 States, we'll end up having 200
Files. This can easily add up a lot of files which
makes it harder to keep things organized. However, it does sound better to me than
having those 200 States in just a few files. Another disadvantage is that the Player
can only be in one State at a time. If we're Moving, then we can't be Shooting, for example. Or, if we're Shooting, then we can't be Moving. The first fix you might think is to have
the movement logic in the "Shooting State" so that you can Shoot and Move at the same time. This works but "Shooting" shouldn't really be related to "Moving" as they are
two complete different systems, although it's fine if you decide it to be that way. The actual fix though, is quite simple:
Simply create two State Machines. This means that you'll have a State Machine
for Movement and a State Machine for Combat, which allows you to be in two States at the
same time, in case that's what you desire. As far as I know, we can't move while attacking in Genshin, so they either doit in the same State Machine
or disable the other one. One last disadvantage is that State Classes
do not inherit from the "MonoBehaviour" Class. This means that we cannot add them
as components in our Inspector, which also means that any variable
we want to set in the Inspector needs to be done through another script. An example of this would be having a
Player Script that holds all of that data and then passes that data to the
State Machines and their States. Furthermore, because we need access to those variables, we'll either need to make them public or somehow reference them by passing in every single variable. To keep things as simple as possible, we'll
be making each variable a public property and alternate between private, omitted and public setters. Now that we have an idea on what
States and State Machines are, lets take a look at what States our Player
will end up having for our base Movement System. Our very first State will of course be the "Idling State". As we've seen before, it's a simple
State where the Player stands still. From the "Idling State" we can start "Moving". In Genshin Impact, this is the equivalent of 3 States: "Walking", "Running" and "Sprinting". The main difference between these states
is the speed at what the Player moves at. In Genshin Impact however, we can't
directly go from "Idling" to "Sprinting" but instead need to go to "Dashing" first. "Dashing" is a low-time but
faster movement speed State. When we stop movement in any of these 4
States, they will transition to 3 other States: "Light", "Medium" and "Hard Stopping". These simply decelerate the Player until it comes to a Stop, then transitioning to the "Idling State". We have 2 last States, which are the
"Jumping" and "Light Landing". "Jumping" is a simple upwards force while "Light Landing" happens
when the Player falls from a small height. There are 2 other "Landing States" that will
be introduced with the "Gliding System", as we'll also introduce "Falling"
there, but for now this one suffices. These are all of the States we'll be
needing for our base Movement System. We, however, can go a bit further. Each one of these States can be part of a Group. And by Group, I basically mean "inherit"
from a Base Class that groups common logic. This is what's called
"Hierarchical State Machines". Much like State Machines though, there
are several ways you can use them, so if you ever watch another video, it's quite possible that they'll have
a different implementation than mine, but, we'll go with the simplest of
them all, which is "State Inheritance". To make things easier, we'll start
Grouping States from the Bottom to the Top. The first States we want to consider are
"Walking", "Running" and "Sprinting". These States can be considered "Moving States", as they're the States the player will be when "Moving". With that same logic, our "Light", "Medium" and "Hard Stopping" States
can be considered "Stopping States". While there's only one of them right now, our "Light Landing" State can also
be considered a "Landing State". All of our current States, besides the "Jumping" State, are States that the Player will be when he's on the
"Ground". This means that we can group all
of them into a "Grounded State". Following the same logic, our "Jumping" State can be grouped into a State for when the Player is in the air,
which we'll be calling an "Airborne State". As we already know, the Movement System
is the base of the other 3 systems. The reason why is because Gliding and Swimming all
use the same movement but with different settings. This means that we can group every
existing State into a Base State, to which we'll call "MovementState". This will simply take care of Moving our Player around. And this is how our State Hierarchy will look like. As I've said before, each of these
States will have to run their own logic. This means that every single one of them
needs to have common methods that run that logic. Because of that, we'll be defining an
Interface that defines those methods and implement that Interface into our Base State. Because every other State will end
up inheriting from our Base State, they will be able to use those methods and
also override them with their own logic. To create that Interface, we'll go into Unity and create a new folder named "Scripts"
in the "Assets" folder. Inside of this new folder, we'll create
yet another one named "StateMachine". Here, we can now create a new C# Script
by right clicking in the Project Window and going to "Create > C# Script". I'll name mine "IState". Open it up and start by removing the default
methods and the MonoBehaviour inheritance as well. To make it an Interface, simply swap the
"class" keyword with "interface" instead. There are a few methods in State
Machines that people commonly use. The first two are a simple "Enter" and "Exit" methods. The logic in these methods is supposed to run
whenever we transition from a "State" to another. "Enter" will run whenever this State
becomes the current Player State. "Exit" is the opposite and will run whenever
this State becomes the previous Player State. This is good for setting and resetting
data whenever we "Enter" or "Exit" a State. Lets then define a method for each of these. To do that, simply type in "public void Enter();"
and "public void Exit();". Remember, we're only defining the
methods here and not implementing them. If you did want an implementation, you
would likely use an Abstract Class instead. It does seem that Unity 2021 does support C#
8, which supports default interface methods but I'm not entirely sure what's the line between
Abstract Classes and Interfaces in that case. Regardless, we now have two method definitions. Of course, we can't really run any constantly
on-going logic with these two methods. For that logic, we often have 3 other methods: "HandleInput", which will allow us to
run any logic regarding reading Input, "Update", which will allow us to
run any non-physics related logic and "PhysicsUpdate", which will allow
us to run our physics related logic. You are likely familiar with the
"Update" and "PhysicsUpdate" methods, which are the equivalent of the MonoBehaviour
"Update" and "FixedUpdate" methods. The only new one here is the "HandleInput" method, which is us simply separating it from the "Update" method. We'll be adding a few more methods here as we need them, but these will work fine for now. That's our State Interface done, so now,
we'll be creating our base State Machine. We'll be using an "Abstract Class". So, back into Unity, in the exact
same folder, create a new C# Script. I'll name it "StateMachine". Open it up and remove the default methods
and the "MonoBehaviour" inheritance. To make this an "abstract class", we simply need
to type in "abstract" before the "class" keyword. We'll start by creating the variable
that will hold the current state. To do that, type in "protected IState currentState;". Note that this is the class that every
State Machine we create will inherit from, so we don't want the "current Player State" but the "current State" of the context we're getting it
from. "Protected" is here to make it accessible
in classes that inherit from this one. We now need a few things: The first one is a method that allows us to change
the State Machine current State for another State. The second one, because our current state variable isn't
public, are methods that access the current state logic methods. If you don't understand why, don't worry, you'll do later. Simply put though: we'll need them so that we
can call them from the "MonoBehaviour" methods. Lets start with the first one by
typing in "public void ChangeState()". We'll pass in "IState newState" as a parameter. What we need to do here is quite simple. We'll start by calling in "currentState.Exit();". This resets any Data that needs to
be reset before changing states. Then, we set the "currentState"
to be the "newState". When that's done, we call in "currentState.Enter();". This will set any Data that
needs to be set on the new state. That's really all we need to change states. However, we currently have a problem: Because this will need to be called once to set the initial
state,
our "currentState" variable will be null. This of course means that our "Exit" method
won't be called as it will throw an error, as "null" doesn't contain an "Exit" method. The simplest fix for that is to add an if
statement checking if the currentState is null and if it isn't, we can then call in the "Exit" method. C# however has an handy operator which
is the "null-conditional" operator. Its use is quite simple: Simply add a question mark (?) right
after the code that can return null. In this case, we add it right after the "currentState". Now, if "currentState" returns "null",
C# will not call the "Exit" method. Our "Enter" method doesn't need it as our "currentState" is set to the "newState" right before we call it. It would throw an error if we were
to pass in a "null" "newState", but you probably want to know if that's the case. That finishes up our "ChangeState" method. We should now create a method for
each of our State logic methods. To do that, start by typing in "public void HandleInput()" and inside, call in "currentState?.HandleInput();". That's all we need to do, so duplicate this method twice and swap the name with
"Update" and "PhysicsUpdate". We now have a way to call the current
state logic from a MonoBehaviour class, so our base "State Machine" is done. With our abstract class done, we can now create our Player Movement State Machine. This State Machine will of course take
care of all of the Player Movement States. To create it, in Unity, go back to the "Scripts"
folder and create a new folder named "Characters". Then, inside, create another one named "Player". Inside of that "Player" folder, we'll have
yet another folder named "StateMachines", followed by another folder named "Movement". In there, create a new C# Script to which
I'll name "PlayerMovementStateMachine". Open it up and remove the default methods and swap the "MonoBehaviour" inheritance with "StateMachine" instead. This State Machine can now be used to
change between Player Movement States and run their logic. Of course, we don't really have any State
yet as we still need to create them. Before we do that though, lets
first take a look at the differences between Caching States or Instantiating New States. Whenever we call our "ChangeState"
method, we need to pass in our new State. We can do this by either creating a new instance
of the new State Class every time we Change States or create it once and cache that instance in our State
Machine. Creating a new instance every time we change
States ensures that if a state isn't being used, resources won't be wasted unnecessarily, as it will be
removed. Caching the instance will always have
resources, like memory, being used, as the instance is still needed
and won't be automatically removed but, you won't need to
instantiate new States everytime. One thing to note here as well is that creating a new
instance always creates the variables again so their values
will be reset to their initial default values, while caching the instance will
always have them the way they were. Although, I don't think it
will matter for us too much. Now, which we should use depends in our use case: When a State will be entered quite a lot,
then caching it might be a good idea. Lets take the example of the Player Jumping. It's extremely common for someone to keep pressing
the Jump button while roaming around the map. This means that the Player State will be
changed to the "Jumping State" quite a lot, which makes it a good reason to cache that
State instead of instantiating it every time. Now, lets take an example of
an NPC which has 2 or 3 states that only get changed every 30 minutes. For example, one would be "Idling",
another one would be "Walking" around and another one would be
"Talking" with another NPC. Because they only change every so often,
there isn't much reason to keep them cached and would probably be better for us to
create a new instance at every state change, or, every 30 minutes. This would also make it if there were 100 NPCs in
the map, we wouldn't have 300 States cached in, although we would have 100 States
being instantiated every 30 minutes. Of course, you can mix both usages if you prefer. For our Player however, we'll be caching in all of our
States. Now that we know that, lets start by
creating our first Movement States. We'll start with the "Moving States" without the
Group State, but including the "Idling State" and the "Movement State" as well,
as that will be our Base State. So, back in Unity, in the same folder,
create a new folder named "States". Inside, we'll create a new C# Script, to
which I'll name "PlayerMovementState". When that's done, create another folder
named "Grounded", for our "Grounded States". Inside, create a new C# Script
named "PlayerIdlingState". When you're done doing that, create yet another
folder named "Moving", for our "Moving States". Inside, we'll create 3 new C# Scripts:
"PlayerWalkingState", "PlayerRunningState" and "PlayerSprintingState". When you're done creating them, go back 2 folders
and open up the "PlayerMovementState" script. Remove the default methods and swap the "MonoBehaviour"
inheritance with an "IState" interface implementation. If you're using an IDE, it should
show an error that says that we should implement the interface methods,
so I'll press "Alt + Enter" and implement them. To make it easier for us to know what
State the Player is currently in, we'll log the current state
class name in the "Enter" method. To do that, type in "Debug.Log();"
and pass in ""State: " + GetType().Name". This will show the name of the correct Class Type. Other options like "typeof()" or "nameof()"
would always show the parent class name. When that's done, we want to make sure we can override these methods with
their own logic in each state, so we'll need to add the "virtual" keyword
to every single one of the methods. Then, we'll open up all of the
other 4 States we've created and remove their default methods, making sure we swap their inheritance from
"MonoBehaviour" to "PlayerMovementState" as well. We now have our Initial States
so we can start caching them. To do that, head back to the
"PlayerMovementStateMachine" script. Remember that we'll only need to cache in
States that are not considered "Group States". This is because the player will never be
in the "Player Movement State" but instead on the "Idling State" or "Running State", as the "Movement State" simply
exists to group common logic. So, start by typing in
"public PlayerIdlingState IdlingState". I'll make it a property with an ommited set. In case you are wondering, here are the differences between writing "private set;"
or simply "omitting" the "set". If you don't want to think about it, simply make it "private" for all properties
that don't use a "public set". Duplicate this property 3 times and swap
their names to be "Walking", "Running" and "Sprinting". We now of course need to instantiate each one
of them, so create a constructor by typing in "public PlayerMovementStateMachine()" and inside simply type in
"IdlingState = new PlayerIdlingState();". Do the same thing for all of the other 3 States. Our States are now Cached. That's great, but you have likely
noticed we aren't yet calling any of the State methods in any "MonoBehaviour" class, which means their logic will never
run when we start playing the game. To do that, we need to first
create that "MonoBehaviour" class. So, back in Unity, go all the way back to the "Player"
folder and create a new C# Script. I'll name it "Player". Opening it up, remove the default methods. All we need to do now is to create an instance
of our state machine and call its methods. To do that, create a new "private" variable of type "PlayerMovementStateMachine",
to which I'll name "movementStateMachine". We now have the "Constructor", "ChangeState", "HandleInput", "Update"
and "PhysicsUpdate" methods to call. We'll call them in the "Awake"
method, the "Start" method, the "Update" method
and the "FixedUpdate" method. In the "Awake" method we'll need to create
a new instance of our state machine, so type in "movementStateMachine =
new PlayerMovementStateMachine();". In the "Start" method we need to
define the player starting State. We'll start our Player in the "Idling State". To do that, type in "movementStateMachine.ChangeState();" and pass in "movementStateMachine.IdlingState". Our player should now enter the "Idling
State" whenever the game starts. In the "Update" method we'll need to handle our
input and run our non-physics related logic, so call in "movementStateMachine.HandleInput();"
and "movementStateMachine.Update();". The order is important as we want to make
sure we've got our "Input Data" updated before we do anything with
it in the "Update" method. In the "FixedUpdate" method we'll
need to run our physics related logic, so call in
"movementStateMachine.PhysicsUpdate();". We're now running our current State
logic through our Player Script. Of course, this will never happen unless we
add this script as a component of the Player, so head back into Unity, select the "Player" Game Object and add the "Player" script as a new component. If we now go ahead and enter play mode, a Debug.Log should be called saying that
the player is in the "Idling State". We are now able to run our State logic
but of course we don't still have any. Before we go ahead and start adding some logic
though, we first need to create our Player Input. The reason why is that we're
using the new Input System, so we'll need to create
the Player Input ourselves. Thankfully, it's quite simple to do that. Lets start by creating a folder in our "Assets"
folder, to which I'll name "InputActions". Inside, we'll create 2 new folders, one inside of the other: "Characters" and then "Player". Inside the "Player" folder, we'll create its Input
Actions by right clicking in the Project Window, going to "Create" and "Input
Actions" at the very bottom. I'll name them "PlayerInputActions". When you're done doing that,
double click on the asset file to open up the Input Actions Window
or press on the "Edit asset" button. I'll dock the Window right next to the Game tab. Now, if you never used the new Input System,
you likely have no idea what is what. While I won't do a deep dive
into what each option does, I'll at least try to explain what
we need to know for this tutorial. The first thing we want to create
is what Unity calls an "Action Map". This is nothing more than a group of Inputs. You can divide your Inputs in whatever way you desire, but we'll create a "Player Action Map"
that groups every Player Input. So, lets go ahead and press on the plus
(+) icon, which adds a new Action Map. I'll name it "Player". When we select an Action Map,
Unity shows us the existing Inputs, known as Actions, of that Action Map at the right side. It should come with a default Action already but
feel free to right click on it and press "Delete". Right now we'll only be needing two inputs. One is for our "Movement", which
we'll be making it be our "WASD" keys and the other one will be to toggle
between our "Walk" or "Run" States, which will be bound to the "Left Ctrl" key. To do that, press the plus (+) icon
and name the first one "Movement". Then, press it again and name
the second one "WalkToggle". Select the "Movement" Action again. At the right side we now have a "Properties"
area for the currently selected "Action". This area allows us to choose a few things, such as the type of value that we want our Input to return, what type of interaction is needed for the Input to be
called and if we want to process that data,
such as Clamp the returned value. For our "Movement" Input we want it
to be constantly reading for changes so that we can get an updated
value in our "HandleInput" method. The way we do that is by setting
the "Action Type" to "Value". The "Value" type is an Action Type that
continuously tracks changes on that action. This is different from the "Button" Action Type that only
tracks changes whenever we press or un-press an Input. Now, we want our "Movement" Input to return
something that we can use to move horizontally. If you've ever used the old Input System
that would be the "GetAxis" or "GetAxisRaw" of the "Horizontal" and "Vertical" axis,
in which you would then set the "Vertical" axis value to move on the "z" axis, as "x"
and "z" are our Player Horizontal Axis in 3D. So, all we need here is a "Vector2",
as it holds the 2 values we need. We can choose that returned value
type in the "Control Type" dropdown. We now have the type of Input and Value we want to receive. However, we haven't really set any key to this Action. To do that, we use "Bindings". When creating an Action, Unity already adds
a default Binding to it with no Key attached. However, we want our Movement Input to be the "WASD" keys. Currently, our Binding is just one key, so we can't do that. Thankfully, there are other Binding Types
depending on the chosen Action Type, so go ahead and right click in the
current Movement Binding and "Delete" it. Then, press on the plus (+) icon
at the right side of the Action and choose "Add 2D Vector Composite". I'll name it "Keyboard". This now allows us to bind one key for each direction. To bind a key, we need to use the "Path" dropdown. This has quite a lot of keys to choose
from and you could go ahead and go to "Keyboard" and search for the Key you want. However, we'll do something simpler and faster
and that's to "Listen" for our own Input. If we go ahead and press "Listen"
and then press "W" in our keyboard, an option saying "W [Keyboard]" should now appear. Go ahead and select that one. Our "Up" direction is now bound to the "W" key. All we need now is to do the exact
same thing for our other 3 directions, so go ahead and do just that. Just in case you didn't understand, binding
a key to a direction here means that "W", for example, will get the value
of "Up", which is "1" in the "z" axis, which of course in our "Vector2"
will be "1" in the "y" variable. If we press lets say "WD", then we'll get a value
in the "x" and in the "y" variables of the Vector2 and that value will already be normalized, as in our "Keyboard" Binding we have
the "Digital Normalized" Mode selected. That's it for our "Movement" Input,
so lets now do our "WalkToggle". In Genshin, whenever we are
Walking or Running, we can switch between both by pressing the "Ctrl" key. This Input is reading like a button, waiting
for us to press on it and nothing else. There is no reason for us to continuously track this Input
value as we don't really need to use it anywhere else
besides when switching between these two States. Because of that, we'll choose the "Button" Action Type. For our Binding, we'll be "Listening"
for the "Left Control" key and choose it. We now have the necessary Inputs to start
moving so all we need to do now is to save them and somehow read them. To save these Input Actions, simply
press on the "Save Asset" button above. If you wanted every change you do to automatically save, then you would enable the "Auto-Save" at the right. I will however not do that. We now have our Inputs saved but
we still need a way to access them. If we select the Input Actions asset
file we can see in the Inspector that we have an option to "Generate a C# Class". This class is automatically generated by Unity and provides us a way to access these
Input Actions we've just created. So, feel free to enable it. Now, there are a few fields you can update but
we'll leave everything to their default values and instead just press "Apply". This should create a new C# Script
in the same folder with the same name as our Input Actions asset file,
as well as automatically generate code of our Action Maps and Actions, including
their Bindings, Properties, etc.... That's great, but we of course still need an
instance of this class to be able to use it. To do that, we'll create our own Player Input Class. So, go back to the "Player" Scripts folder. In here, create a new folder named
"Utilities", followed by "Input". Inside, we'll create a new C# Script,
to which I'll name "PlayerInput". Open it up and start by
removing the default methods. We'll leave the "MonoBehaviour"
inheritance as we'll need it later. We need a few things to start
using our Input Actions. The first is of course an instance
of our "PlayerInputActions" class. To do that, create a new "public" property of type "PlayerInputActions",
to which I'll name "InputActions". I'll give it a "private set;". I'll also create a reference for our Player
Action Map so that we can access it right away. To do that, type in "public PlayerInputActions",
which is our generated C# class, ".PlayerActions". I'll name it "PlayerActions" as
our Action Map is named "Player". The ".PlayerActions" we've added
is automatically generated by Unity and represents the struct
type of our Player Action Map. If you've named your Action Map something different, then it should be your Action Map name
with "Actions" attached to it at the end. With that done, we can start by creating
a new instance of our Input Actions. We'll do that in the "Awake" method. In here, simply type in
"InputActions = new PlayerInputActions();". Then, set our "PlayerActions = InputActions.Player;". Note that while "PlayerActions" is the
name of the Action Map struct type, "Player" is the name of the
variable of that struct type. We now have a reference for our Inputs but there's
a mistake I see some people make quite often, which is trying to read their Inputs now. While we indeed have an instance of our Player
Actions, Unity requires us to first "Enable" them. To do that, we'll use the
MonoBehaviour "OnEnable" method and also the "OnDisable" method to disable them, as we don't want them to be enabled if the
Player Game Object is no longer enabled. So, add in the "OnEnable()" method
and inside type in "InputActions.Enable();". Then, add the "OnDisable" method
and call "InputActions.Disable();". We are now able to read our Inputs. Of course, we still need to add this
class as a component of the player, but we'll also need to add a
reference to it in our "Player" script so that we can access it in our States. So, go back to our "Player" script. In here, start by creating a new "public" property of type "PlayerInput". I'll name it "Input". Make sure you type the same
property "set" as I do, but I'll most of the time make
it "private" unless we need to be able to set the value from other classes. Then, in the "Awake" method, we'll
get its reference by typing in "Input = GetComponent();". We can now add this "PlayerInput"
class as a component of our Player, but lets first add the
"RequireComponent" attribute. Above of our "Player" class, type in
"[RequireComponent(typeof(PlayerInput))]". Now, every time we add the "Player"
component to a Game Object, it will automatically add the
"PlayerInput" component with it as well. Save and go back to Unity and
select the "Player" Game Object. If Unity didn't add the "PlayerInput"
as a component automatically, add a new component of its type yourself. We are now able to read Input
through our "PlayerInput" class, which makes it almost possible
for us to start moving our Player. "Almost" because we still need to set a few
small things before we are able to do it. The first one is of course adding a "Rigidbody". Without it, our Physics-based Movement won't work. That's quite simple to do, we just
select our "Player" Game Object and add a new component of type "Rigidbody". You could of course use the
"RequireComponent" here as well if you'd like. We now have a Rigidbody which means
our player can move using Physics, but it also needs a collider. This is because if we go
ahead and enter play mode, our Player will start falling due to
Gravity but won't collide with the Ground. To add one, exit Play Mode and add a new
"Capsule Collider" Component to the player. If we Inspect the player, we can see that
our Capsule Collider is more like a Sphere. The reason why is that we're not adding it to
the Character Model but to an empty Game Object, which doesn't really have a defined size. We'll have to get the "CharacterModel" height ourselves and then define the
values of this Capsule Collider. The way we know our "CharacterModel" size is by
using its "Mesh Renderer" bounds size variable. I've already got it myself and I know that
my "Character Model" has an height of "1.8", so we'll select the "Player" Game Object
again and give our "Capsule Collider" an "Height" of "1.8". Of course we do have a new problem here, which is the Capsule Collider
being half way into the ground. That's actually correct as
it's centered in the origin, but because we need it to fit our Character Model,
instead of moving the Character Model Object down, we'll update the center of
our Capsule Collider to go up. We simply need it to go up half of the
height of the collider, which is "0.9". It should now be in the correct
height but it's still looking quite thick, so lets lower the
radius to be around "0.2". If we now enter Play Mode, our player
should no longer fall through the Ground. Of course, if we were to rotate it a bit, which can also happen when we
collide with another Object, it would fall as we're not restricting any rotations. To fix that, exit Play Mode and
in the "Rigidbody" Component, open up the "Constraints" area and freeze all rotations. We'll also set "Interpolate" to "Interpolate" and "Collision Detection" to "Continuous" for our Player. Interpolation removes some possible jittering while Collision Detection makes it so our
player detects other colliders better, which makes it less likely to go through those colliders. These are recommended to use on your Player but
most often only there as they are less performant. That's basically it but we'll need a reference
of our Rigidbody to be able to use it, so go to the "Player" Script and when you're here,
add a new public property of type "Rigidbody", to which I'll name "Rigidbody". I'll give it a "private set;". Then, in the "Awake" method, type in
"Rigidbody = GetComponent();". We now have every required
Component to move our Player, but we don't really have a way to
access that Player in our States. Thankfully, that's quite easy
to do as we just need to pass in our Player to the State Machine constructor. We have two options here: Pass it into the
State Machine and then into each State, which makes it so we can
access a variable right away, or leave it in the state machine itself so that
we can access through the "stateMachine" variable. I don't really see any worthy advantage
in any of them besides in one not needing to type in "stateMachine"before the Player variable and in the other one not needing to pass
in the Player to the States constructors. Other than that, it should really be the same. I'll leave the Player reference in the State Machine as I'm
going to use the State Machine to hold reusable data. So, in the "Player" script, go to the State Machine initialization and pass in "this",
which represents the Player class. Then, in Visual Studio, I'll press "Alt + Enter" and choose "Add parameter to
PlayerMovementStateMachine()". Our Movement State Machine constructor
should now have a Player parameter. Next, create a public property above of type "Player", to which I'll name "Player".
I'll omit the set as well. When that's done, in the Constructor, set
this "Player" to be the "player" parameter. Our State Machine can now reference the Player, but, States don't yet have a
reference to our State Machine. To do that, lets first go to our "Movement State". In here, add a constructor for this class by typing in "public PlayerMovementState()" and accept a
parameter of type "PlayerMovementStateMachine" named "playerMovementStateMachine". Then, create a "protected" variable above
of type "PlayerMovementStateMachine" named "stateMachine". When that's done, simply set it in
our constructor to be the parameter. We now need to generate a constructor for
each of our States that inherit from this one. We'll actually do that by going back
to our State Machine and pass in "this" for each of the constructors. When you're done doing that,
press "Alt + Enter" and choose "Generate constructor in State". Do it for all of the 4 States. With that done, we are finally
ready to start moving our Player. Our movement logic will stay at the
"PlayerMovementState" base class. So, start by opening it up. The first thing we want to do is
to read the Player Movement Input. We'll save the Input value into a variable, so above create a new "protected"
variable of type "Vector2", which is our Input Action Control
Type, and I'll name it "movementInput". Then, we'll set its value
in our "HandleInput" method, so in there, call in a new method
named "ReadMovementInput();". For organization purposes, I'll be adding this
method to a new region named "Main Methods". I'll go ahead and also add the other methods
to a new region named "IState Methods". You don't need to do this if you don't want
to, or, if you're not using Visual Studio, it's possible that you can't do it. In this new method, we'll
read our Input by typing in "movementInput =
stateMachine.Player.Input.PlayerActions", which gets us our "Action Map". Through this "Action Map",
we can get our "Actions". Our Movement Input was named "Movement", so
type in ".Movement" and to read its value we use the ".ReadValue();" method. This method accepts the type of
the value we're trying to read, which in our case is the "Vector2" type. When that's done, lets go to our "PhysicsUpdate"
method and call in a new method named "Move();". I'll also add it to the "Main Methods" region. Inside, start by checking "if
(movementInput == Vector2.zero)", if that's the case, we "return;",
as it means we aren't moving. We can now start moving our
Player in case there's any Input. The way we'll do that is quite simple: Whenever there's input, we'll add
a force to our Player Rigidbody towards that direction, at a certain speed. We are already reading our Input, so lets also
create a variable that holds our speed value. So above, create two new variables:
"protected float baseSpeed;", defaulted to "5f" and "protected float speedModifier;", defaulted to "1f". This second variable makes it so we can edit
our speed without modifying the base speed. Back to our Move method, add to the
if statement "|| speedModifier == 0f". We can now add a force to our Player. To make it more understandable,
I'll be creating a new "Vector3" named "movementDirection" and set it to be equal to a new method
named "GetMovementInputDirection();". We'll be returning a new "Vector3" which
simply holds the value of our movement input "y" variable in the "z" axis. So, type in "return new
Vector3(movementInput.x, 0f, movementInput.y);". I'll also be making this method "protected" and I'll add it to a new region named "Reusable Methods". We now have our Movement Input in 3D coordinates,
as "x" and "z" are our 3D Horizontal Axis. Back to our Move method, we'll now
get our movement speed by typing in "float movementSpeed = GetMovementSpeed();". Inside, we'll simply
"return baseSpeed * speedModifier;". I'll also make it "protected" and add
it to the "Reusable Methods" region. All that's left to do now is to
add a force for our player to move. Now, commonly, what I see in other tutorials is people setting the Rigidbody
"velocity" variable right away. However, when going into Unity's Documentation, they do not recommend doing that and
recommend using "AddForce" instead. From my experience while doing this
System, I've found that while that works, I did need to set the
"velocity" variable sometimes. The reason why was because adding
a force isn't instantaneous and will only happen in the next Physics Update, while setting the velocity right away
will be an instant change of the force. We'll understand more on the
reason why when we get there, but we'll be using the "velocity"
when resetting our velocity or whenever we can have more than one
"AddForce" running at the same time and use "AddForce" for the remaining
forces, as so far, it seems to work fine. That of course means our Movement
will use the "AddForce" method, so in our "Move" method, under our
"movementSpeed" variable, type in "stateMachine.Player.Rigidbody.AddForce();". For the force, we'll pass in
"movementDirection * movementSpeed". This adds a force, but right now, by default, Unity is adding a ForceMode of "Force",
which isn't really what we want. If we take a look at a nice
image made by "nothke", that was posted on Reddit,
we see that the "Force" Force Mode not only depends on time but
it's also dependent on our player mass. We don't really want that. Besides not caring about our Player mass, we
don't want it to be time dependent either. We need an instant force that
moves our player at the same speed at the start or middle of the Input. Which leads us to the Force
Mode known as "VelocityChange". Just like it says in the formula, this
is the same as "velocity += force", which is almost the same as using our
"velocity" variable to add a force, but of course using the "AddForce" method,
which gets called in the Physics Update. Note that this is a 3D Force Mode and as
far as I know, this one doesn't exist in 2D, so it's quite possible you'll need to
use the "velocity" variable instead. Do note that it's also apparently not recommended
to use "deltaTime" when setting a velocity. Now that we know that, pass
in "ForceMode.VelocityChange" as the second parameter of the "AddForce" method. It might seem that we're done, but
remember that the "AddForce" method adds a force to the already existing force. This means that if we move our Player
and don't stop pressing our Input Keys, our Player will become Speedy Gonzales. To fix that problem, we simply need to remove the existing velocity from the
force we're going to be adding. To do that, type in, just before adding the force: "Vector3 currentPlayerHorizontalVelocity
= GetPlayerHorizontalVelocity();". Inside of this new method, create a new
"Vector3" named "playerHorizontalVelocity" and set it to be
"stateMachine.Player.Rigidbody.velocity;". Then, set the "playerHorizontalVelocity.y" to be "0f"
and return the variable at the end. I'll also make this method "protected" and
add it to the "Reusable Methods" region. Back to our "Move" method, subtract
this horizontal velocity from our Force. If we save and go back to Unity, entering Play Mode should have our Player moving at a constant speed. Remember that we don't need to normalize
our input through code as the Input Action already does that automatically for us. That's the base of our Player Movement
done, but just as a small tip, lets say we were to add another float
to our "AddForce" multiplication. While this might seem normal at first
glance, because our first variable in the multiplication is a "Vector",
it means that it will multiply with "movementSpeed" and return another Vector,
and then multiply with this new variable, which also returns a Vector. The problem here is that we're
doing two Vector multiplications, which are less performant
than normal multiplications. If we were, for example, to swap the order
and make our Vector the last variable, we would now multiply the new
float with "movementSpeed", which would just be a normal float multiplication, and only then multiply by our Vector,
which would be a Vector multiplication. We now have one Vector multiplication only
instead of two, which means it's more optimized. This can be a problem when you're
calling the multiplication multiple times like we're doing for our AddForce. It would be even worse if it was in the
"Update" method, as it's called once per frame. Adding the Vector as the last
value of the multiplication basically ensures you only
have one Vector multiplication, unless of course you have
more Vectors in the formula. In our example, we would now have 10
Vector Multiplications instead of 20 in 10 calls just by swapping the order. This doesn't really matter for our system
as we're only multiplying by one value. But, it might be a nice thing for you to keep in mind in case you ever multiply
vectors by multiple float values. In our case though, we can
leave the current order. With that, we've got the base of our Movement done, but if we take a look at Genshin Impact, we quickly notice that this
isn't how Movement works there. Not only does the Player rotate
towards the Movement Direction, that Movement Direction is relative
to the Camera and not to the Input. If we press "W" to move forwards, we'll always
move forwards towards where we are looking at. That also means that if we
press "S", we'll move backwards, or to the opposite of where we are looking at. To be able to do the same, we
need to create a Camera System. Thankfully for us, we're going to be using
"Cinemachine", which easily allows us to add one. This seems to be what Genshin Impact uses as well. We've already downloaded it
when setting up the project, so we can now easily create our Camera
by right clicking in the Hierarchy Window and going to "Cinemachine > Virtual Camera". I'll name it "PlayerCamera". While I was able to somewhat replicate
Genshin Camera with a "FreeLook" camera, there was a missing option that we would
need to add ourselves and apparently "Virtual Cameras" are more performant as they
only use 1 rig while "FreeLook Cameras" use 3. To start off with our Camera, we need a
target for the "Follow" and "Look At" fields. "Follow" is the target that
our Camera will follow around. "Look At" is the target that
our Aim will be relative to. By "relative" here, I mean that the center
of our Aim will always be on the target. To add these, select our "Player"
Game Object and add a new child Game Object named "CameraLookPoint". I'll add an icon to this game object so
that we can easily see where it's placed. Then, I'll make its "Y" axis be at "1.4". When that's done, we'll set our
Camera targets to be this new object. So, back into our "PlayerCamera", drag this new
Game Object to the "Follow" and "Look At" fields. Note that Cinemachine should add a Cinemachine
Brain to your Main Camera automatically, but if it didn't, make sure you do it yourself. This simply makes it so Cinemachine
Cameras can control your Main Camera. Our Camera is now looking
towards our Camera Look Point, but it's still using the old Input System,
so our current mouse Input won't work. Thankfully, it's quite simple to
swap it with the new Input System, as all that we need to do is to add a new
component named "Cinemachine Input Provider". This component accepts an "XY" axis, which will represent the camera rotation
and a "Z" axis, which will represent the distance. Of course, to assign an Input
here we need to create it first. So, go ahead and open the Input Actions Window. In here, lets first create a new
Action for our mouse movement by pressing on the plus (+) icon
and naming it "Look". For our types, we'll have it
be a "Value" of "Vector2", as we want to constantly get our X and Y axis. In its binding, select "Mouse > Delta". Delta is apparently the change in pixels in Vector2 since the
last rendered frame mouse position. For our "Zoom", we'll add another action
named "Zoom" and set it to "Value" as well. We simply want a float here to know
whether we are scrolling up or down. The Value Type for float is called "Axis". Now, we'll use the value this input returns
to know how much we should scroll up or down but it currently comes with high values such
as "120" so we'll clamp it to a lower value. To do that, add a "Processor" of type "Clamp"
and set it to be from "-0.1" to "0.1". This Input also comes inverted, which
means scrolling up would return positive and scrolling down would return negative. We want the opposite of that, so add yet another
"Processor" but this time of type "Invert". When that's done, set its
Binding to be "Mouse > Scroll Y". Then, save the asset. Back in our "PlayerCamera", in the Input Provider
Component, select the corresponding Inputs, so "Look" for the "XY" axis and
then "Zoom" for the "Z" axis. We're now using the new
Input System for our Camera. We're only a few settings away of having
our Camera behaving like Genshin's camera. The first thing we'll be
doing is set our FOV to "60". The next thing is setting both
the camera "Body" and "Aim". The "Body" sets the algorithm that the
Camera uses to "Follow" its target. The "Aim" sets the algorithm that the
Camera uses to "Look At" its target. If we take a look at Genshin Impact camera, we
can see that we can rotate around the target. In the "FreeLook" Camera this is
known as an "orbital transposer". However, in the "Virtual"
Camera, the "orbital transposer" does not contain a vertical
rotation, but only horizontal. Thankfully, we can imitate
the "FreeLook" Camera setting using the "Framing Transposer"
Body together with the "POV" Aim. The "Framing Transposer" tries
to keep the camera centered to the Follow Object at the provided distance. The "POV" aim moves the camera according
to your input, which should translate to "where you are looking at",
relative to the "Look At" target. If we enter Play Mode, our Camera should have the same orbital
behaviour that Genshin Impact camera offers. However, there are a few problems: Besides the camera movement being too fast, the faster you move your mouse
around, the slower the movement gets. What we want here is for the camera sensitivity
to be as close to the mouse speed as possible. To fix that problem, open up the "Aim" area and swap both inputs from "Max
Speed" to "Input Value Gain". Make sure your Vertical Axis also
has the "Inverted" option enabled, as otherwise we'll be rotating towards
the opposite vertical direction. Next, we'll set the speed on the vertical axis to
be "0.1" and on the horizontal axis to be "0.16", as Genshin Camera has a
faster horizontal movement. If we now move our Camera around, it should be slower and also have a
closer 1 to 1 sensitivity with our mouse. If you wanted a fully 1 to 1 sensitivity,
then you would need to set the speed to "1". There still are some problems left to resolve. One example is our vertical axis, as we
aren't able to rotate 90 degrees up or down. That's solvable by updating the "Value
Range" field from the vertical axis. We'll set it to be from "-90" to "90". For our Horizontal Axis,
we'll go with "0" to "360". I'm not entirely sure if we actually need this one but I'll leave it as is to
make sure nothing breaks. I'll also enable "Wrap" to allow the camera to keep rotating once we get
to the "360" degree rotation, as otherwise it would stop there and
we would need to move the camera back. The next thing is that if you start
moving your camera around and stop it, you might notice something: The acceleration and deceleration
of the camera are instantaneous. In Genshin Impact however there is a
small acceleration and deceleration. We can easily set that up by updating
the Vertical and Horizontal Axis "Accel Time" and "Decel Time" fields. For the vertical axis we'll go with "0.8"
acceleration and "0.05" deceleration time. For the horizontal axis, we'll also go with
"0.8" acceleration but "0.25" deceleration time. Our camera should now start or stop a bit slower. I'm not really sure what the
"Recenter Target" does here but I have mine set to "Follow Target Forward"
and it works fine so I'll add it here as well. In this case, it seems to be the same as the
"Look" one because both have the same target, but I don't really know what this "Axis
angles are relative to Target" means. I'm assuming it has something to do with
the "Vertical" and "Horizontal Recentering" and that it recenters relative to that
target, but I'm not completely sure. If you do know, please leave a comment below explaining it so that we
know what it actually does. This "Horizontal Recentering" option is
what Free Look Cameras don't provide us with but we'll only need this quite a bit
later so leave it disabled for now. For our "Body", we'll be setting
both the "X Damping" to be "0.2" and the "Y Damping" to be around "0.4". Damping is how fast or slow the camera tries
to keep itself centered with the target, which in this case, should be the "Follow" target. While we won't be needing to set this here, I'll also go ahead and set
the "Camera Distance" to "6". We now have a Camera working quite nicely but
we still need 2 more things to take care off. The first one is that currently our Camera goes
through the ground instead of colliding with it. The second one is that we
can't yet zoom in or zoom out. Thankfully, both are quite easy to do. Lets start by exiting Play Mode. For the Camera Collisions, go to the
bottom of the Cinemachine Component and open up the "Add Extension" dropdown. There should be an option
named "CinemachineCollider". Feel free to press on it. This should add a new component to our Camera Object that allows us to set
Collisions with the Camera. What we want here is to set the "Collide
Against" layer to something to collide with. To do that, we'll create a layer for our
Environment so that the Camera collides with every existing Environment instead
of just the Ground. So, add in a new layer named "Environment". Then, go to all of our Environment
Game Objects besides the "Water" and set their layer as "Environment". When that's done, go back to the Camera and
now set the "Collide Against" to "Environment" and remove the "Default" layer from there. For the "Ignore Tag" we'll set it to "Player". Of course, we need to add this tag to
the player so select its Game Object and add the "Player" tag. Next, back into our Camera, we'll be changing
the "Strategy" of the Camera Collider. This is simply how the camera behaves
when colliding with something. In our case, we want the camera to "Pull
forward" when we collide with the Environment, which simply pulls the camera
to the front of the collider. I'll leave the rest with its default values
but feel free to change them if you want to. If we enter Play Mode, our Camera
Collisions should be working. That means that all that's
left is our Camera Zoom. Unfortunately, I don't think there's
a built-in functionality for this so we'll have to do it ourselves. Thankfully, with the Input Provider "Z"
axis and the Body "Camera Distance", this is quite simple to achieve. That's because the input provider "Z" axis gives
us the value of when we scroll our mouse wheel, while the "Camera Distance" is
how far we are from the target. So, go back to the "Scripts" folder
and create a new folder named "Camera". Inside, we'll create a new C# Script,
to which I'll name "CameraZoom". When you're done creating it, add it
as a component of our Player Camera. Then, open it up and remove the default methods. We'll start by creating 3 variables:
The default distance that the camera will start at and the minimum and maximum
distances that the Camera can go to. So, type in "[SerializeField]
private float defaultDistance;" and default it to "6f". Then, duplicate the variable line twice and swap "default" with "minimum"
and then "default" with "maximum". I'll set their default values
to "1f" and "6f" respectively. We'll also make it so we can update
these variables with a slider, so to do that simply add in after the
"SerializeField" attribute: "[Range(0f, 10f)]", meaning we can choose a value
from 0 to 10 through a slider. Next, we'll add two more variables:
A value to smooth our distance lerp and a value to multiply our "Z" axis value with. So, duplicate one of the variables above twice and
then change the first one to be named "smoothing" and the second one to be "zoomSensitivity". I'll default the "smoothing" to "4f"
and the "zoomSensitivity" to "1f". That's all the data we need to be able to set
in the inspector so now we need two more things: A reference to the Cinemachine Framing
Transposer, which represents our "Body" and a reference to the Cinemachine Input
Provider, which contains our "Z" axis value. So, create a new "private" variable
of type "CinemachineFramingTransposer" named "framingTransposer". Make sure you import the "Cinemachine" namespace. Then, create another "private" variable of type
"CinemachineInputProvider" named "inputProvider". To get their references, we'll
use the "GetComponent" method so type in "Awake" and inside type in "framingTransposer = GetComponent()", which gets our Virtual Camera. We'll now get our framing transposer by using the
".GetCinemachineComponent()" method instead. For the type, we'll of course pass
in "CinemachineFramingTransposer". We use the "GetCinemachineComponent" method here because the "Body" is part
of the Cinemachine Component. For our provider, type in "inputProvider =
GetComponent();". When that's done, call in the
MonoBehaviour "Update" method and call in a new method named "Zoom();". We can now start zooming our camera. The way we'll do that is by
retrieving the value of our scroll, which we can do from the input provider "Z" axis. To do that, type in "float zoomValue
= inputProvider.GetAxisValue();". Here, we need to pass in an axis index,
which is the index of "2" for the "Z" axis. Then, we multiply this value by our "zoomSensitivity". We now need to add this value
to our current distance target, which we currently have no variable of. So, above, create a new "private
float" named "currentTargetDistance". This will start with a default value of "0" but
we want it to start with the "defaultDistance" so in the "Awake" method type in
"currentTargetDistance = defaultDistance;". Then, back in our "Zoom" method, type in "currentTargetDistance =
currentTargetDistance + zoomValue;". We need to make sure we clamp this target
distance to not pass our maximumDistance, so add in "Mathf.Clamp()"
surrounding our assignment and then pass in "minimumDistance"
as the second parameter and "maximumDistance" as the third one. We can now lerp the current
distance towards the target distance so that we slowly get there
instead of it being an instant change. To do that, create a "float" named "currentDistance" and assign to it
"framingTransposer.m_CameraDistance;". Then, type in "if (currentDistance ==
currentTargetDistance)", we "return;". This makes it so that if we're already at
the target distance, we won't do anything. To lerp our value, type in
"float lerpedZoomValue = Mathf.Lerp(currentDistance, currentTargetDistance,
smoothing * Time.deltaTime);". We pass in "smoothing * Time.deltaTime"
here because we don't really want it to be an amount of seconds
for the whole lerp but just keep it as consistent as possible at every
change and get there whenever it gets there. Then, finish it up by setting the "framingTransposer.m_CameraDistance = lerpedZoomValue;". That's all we need to Zoom our camera,
so save it up and go back to Unity. If we now enter play mode, we
should be able to zoom our Camera. With our camera working
like Genshin Impact Camera, we can finally use it to rotate our character
and move it towards the right direction. Because we'll need to know our
Camera rotation to do that, we'll need a reference to our Camera. So, open up the "Player" script. In here, create a new public
property of type "Transform" to which I'll name "MainCameraTransform". Then, in the "Awake" method, type in
"MainCameraTransform = Camera.main.transform;". We're getting the main camera
and not the Cinemachine camera because Cinemachine controls the Main Camera,
so we are safe to get our data from there. Now, "Camera.main" was something that wasn't very performant before and that's the
reason why we're caching it here. This is because it used to search for all
objects with tags and add it to a temporary list, which would then be searched again for an
object with the Camera Component enabled. However, seems like in Unity
2020.2, that was optimized and now it stores a dedicated list of
objects with the "MainCamera" tag instead and then searches on that list only. And as it says there, they've found the new
way of doing things to be way more performant. However, we'll still cache it ourselves not
only because we're also getting the transform, but also caching it means we can
assign any value to this variable whenever we want and it's changed everywhere, and likely makes the code a
bit cleaner and organized. With that done, lets head
back to our "Movement State". We'll be adding our Player Rotation here. Just under our "Move" method declaration,
create another one by typing in "private float Rotate()". We'll accept a parameter of type
"Vector3" named "direction", which is simply the direction that
our Player will rotate towards. Before we start coding it though lets first
take a look at how it works in Genshin Impact. Lets start by pressing "W". As one might expect, this
will move the player forwards. However, the moment we rotate the camera
around, the player will start rotating with it and move forwards towards
where the camera is looking at. If we instead start pressing
"S" to move backwards, it will keep on doing the same but
towards the opposite direction. The same happens when we move sideways. What's happening here is that our
player is moving towards the sum of our "Input" Direction plus the Rotation of the Camera. So if we press "W" and move the camera "45º", we want to go 0º (Up) from our forward
direction plus 45 degrees from our camera. If we instead press "S", we
want to go 180º (Down) degrees from our backwards direction
plus 45 degrees from our camera. We'll understand more in a bit on why
"W" and "S" are "0" and "180" degrees. Lets then start by getting that sum. The first thing we'll need will be our
movement input angle, or its direction angle. We can get that through a
"Math" method named "Atan2". "Atan2" receives two parameters: the "y"
and "x" of a coordinate, in that order. Lets say we were moving forward, which
translates to the coordinate of "(0, 1)". Remember that our "y" axis here is
the equivalent of the 3D "z" axis. If we pass in "1, 0" to the "Atan2" method, it will be the same as having
the "(0, 1)" coordinate. The angle this returns is "90"
degrees, which should be correct. However, Unity forward axis is the "z" axis. Contrary to the normal degree circle,
it increments its angles clockwise, starting at the top. This means that our "(0, 1)" coordinate should've
been "0" degrees instead of "90" degrees, as "90" degrees in Unity would be
towards the right and not forwards. We can actually make it so that "Atan2"
returns relative to the "z" axis by simply swapping the order
of the parameter values. If we now pass in "0, 1" to
our "Atan2" method instead, which is the same order as the forward coordinate,
the coordinate will now become "(1, 0)". If we take a look at the degree circle now, that coordinate is at "0" degrees,
which is exactly what we need. Now, I don't completely understand
the reason why this happens, but I guess that's just how math works. It's like we're saying "give us the
angle relative to the 'z' axis instead", or "relative to the second parameter axis". Lets also take a look at a few others
coordinates: "(0, -1) - Backwards", "(-1, 0) - Left", "(-0.7, 0.7) - Left Forward". With that in mind, lets
get that angle by typing in "float directionAngle =
Mathf.Atan2(direction.x, direction.z);". This method however returns the value in Radians. To transform it into degrees we just
need to multiply it by a certain value, which is by "Mathf.Rad2Deg". This works because we know that "1 Radian"
is the equivalent of an amount of degrees. That amount is stored in the
"Mathf Rad2Deg" variable, so by multiplying it with the amount of
radians, we get the angle in degrees. We however have yet another problem:
Atan2 can return negative values, as it ranges from "-180" to "180". If I remember correctly, there were some
problems with rotations that I needed to make sure that negative angles
were converted into positive angles. I believe it was because a rotation
wouldn't always follow the shortest path to a certain angle,
so it could rotate wrongly. Thankfully, it's quite easy to turn
negative angles into positive angles. To do that, simply type in
"if (directionAngle then we sum to the
"directionAngle += 360f" degrees. This means that "-90" will equal to
"270", "-180" will equal to "180", etc.... With that, we have our input direction angle
and simply need to add our camera angle to it. Now, if any of you is wondering why
do we need to add our camera angle if we already have the input direction in degrees, that's because the movement input is always the
same regardless of where the player is looking at. That's of course because that's how
it is defined in the Input Actions, "W" is always "Up", "S" is always "Down", etc... Not only that, but no matter
how much the player rotates, "90" degrees in Unity is always to the same
direction (right), as it is in world space. That means that if we tell
our player to go "90" degrees, it will always go to the right in World Space and not to the Right relative to
where the Player is facing. And that's when our camera angle comes in. Lets say we move "90" degrees,
or to the right by pressing "D". Then, we also move our Camera
90 degrees to the right. Currently, our "Player" would keep
on moving towards the same direction as it's going to the World
Space "90 degrees" direction. However, lets also add the
"90" degrees of our camera to make a total of "180" degrees. Our player will now move "90"
degrees to the right of the camera, which is considered "Down" or "Backwards" in
World Space, the equivalent of "180 degrees", which is exactly what we want. So, to add our camera angle, simply type in "directionAngle +=
stateMachine.Player.MainCameraTransform.eulerAngles.y;". Note that we need to use "eulerAngles" and not
"rotation", as "rotation" returns a Quaternion. The "y" axis of a Camera
is the horizontal rotation. Of course, when adding the camera
angle to our direction angle, it will be possible to go over 360 degrees. To fix that problem, we simply
check "if (directionAngle > 360f)". If that's the case, we subtract to
the "directionAngle -= 360f" degrees. So, "600" degrees would lead to "240" degrees
or "380" degrees would lead to "20" degrees. With that done, we have our final direction angle. We can now return it by typing
in "return directionAngle;". To keep things organized, lets do two things: Select the first if statement
and assignment and extract it to a new method named "GetDirectionAngle();". Then, select the second if statement
and assignment and extract it to a new method named "AddCameraRotationToAngle();". I'll rename the last method
parameter to be "angle". We now have the direction angle we
want to rotate our player towards. But if we take a look at Genshin
Impact, whenever the player rotates, either by pressing one of the Movement
Keys or by rotating the Camera, it takes some time to get to the target rotation. You might not notice it too much when pressing
the Movement Keys because that reach time is quite low, but you can see that
it isn't null as it isn't snappy. If you do rotate the camera, then you can see
that the player is a bit delayed on its rotation. We'll be smoothing our player rotation much like
they do using a method named "SmoothDampAngle". This method requires a few things from us: The "current angle", which we'll be
able to obtain from our Player Rotation, the "target angle", which is our "directionAngle", a "velocity", which the method
takes care of updating its value and the "amount of time" it should
take to reach the target angle. We'll create one variable for each of
these, including for our "directionAngle", as we'll need it somewhere
else further into this series. So, above, create a few new variables:
"protected Vector3 currentTargetRotation;", "protected Vector3 timeToReachTargetRotation;", "protected Vector3
dampedTargetRotationCurrentVelocity;" and "protected Vector3
dampedTargetRotationPassedTime;". We are creating "Vector3"'s because in our "Gliding System" we'll need a
value for the "x" and "z" axis, so we might as well just make
it a "Vector3" right away. We now need to initialize our reach time
to a certain value so in our constructor call in a new method named "InitializeData();". Inside, we'll type in
"timeToReachTargetRotation.y = 0.14f;". We'll later on understand how I
got to this value but just know that it is the time it takes for
the rotation to happen in Genshin. When that's done, back to our "Rotate" method, we'll call in a new method named
"RotateTowardsTargetRotation();". I'll make this method protected and move
it to the "Reusable Methods" region. We'll start by getting the
current angle by typing in "float currentYAngle =
stateMachine.Player.Rigidbody.rotation.eulerAngles.y;". We can now start smoothing our player rotation but
only if we aren't already at our target rotation, so, type in "if (currentYAngle == currentTargetRotation.y)" If that's the case, we "return;". Otherwise, we'll smooth our angle by typing in
"float smoothedYAngle = Mathf.SmoothDampAngle();". When that's done, pass in "currentYAngle"
and "currentTargetRotation.y". Then, we'll need to pass in the velocity
variable but with the "ref" keyword, as Unity automatically updates and uses this
variable as it wishes inside of the method, so pass in "ref dampedTargetRotationCurrentVelocity.y". For the amount of time it should
take to reach the target rotation, we'll pass in "timeToReachTargetRotation.y
- dampedTargetRotationPassedTime.y". If we were to pass in
"timeToReachTargetRotation" alone here, it would always take "0.14"
seconds for each smooth method call instead of "0.14" seconds for the whole rotation. Of course, we need to increment something to the passed time variable
as otherwise it will keep on being "0", so type in "dampedTargetRotationPassedTime.y
+= Time.deltaTime;". Note that because this method is being
called in the "FixedUpdate" method, Unity will automatically return "fixedDeltaTime"
when we use the "deltaTime" variable. We now have our smoothed value, so
we can rotate our player with it. To do that, we'll create a new "Quaternion",
to which I'll name "targetRotation". Then, we'll set it to be "Quaternion.Euler()", which accepts euler angles and
transforms them into a quaternion, and pass in "0f" for the "x", "smoothedYAngle"
for the "y" and "0f" for the "z". When that's done, we'll set the
player rotation by typing in "stateMachine.Player.Rigidbody.MoveRotation(targetRotation);
". That takes care of rotating our Player. However, we need to remember that the moment
our direction angle changes to something else, we need to reset the passed time and also
set the target rotation to our new angle, as we're not yet doing that. So, back in our "Rotate" method,
between the last 2 methods, type in "if (directionAngle != currentTargetRotation.y)", we'll call in a new method named
"UpdateTargetRotationData();" with "directionAngle" as an argument. I'll update the name of the parameter
to be "targetAngle" instead. In here, we'll set the "currentTargetRotation.y"
to be the "targetAngle". Then, we'll reset the passed time by typing in
"dampedTargetRotationPassedTime.y = 0f;". We don't really need to reset anything else. We don't need to reset our velocity either, as the "SmoothDampAngle" method
automatically takes care of that for us. Back in our "Rotate" method, I'll select the code of the if statement
up until the "GetDirectionAngle" method and extract it to another method
named "UpdateTargetRotation();". Then, I'll make it "protected" and add
it to the "Reusable Methods" region. I'll also add a new parameter of type "bool" named "shouldConsiderCameraRotation" defaulted to "true". Then, before we add our camera angle, type in "if (shouldConsiderCameraRotation)" and
put the line inside of this if statement. We'll need this later for our "Dashing State". We now have everything
needed to rotate our Player. Of course, we aren't yet calling
the "Rotate" method anywhere. If we take a look at Genshin Impact, if we
move the camera around while Idling or Jumping, our player won't rotate with it. This simply means that our rotation
only happens when we are moving. The Idling and Jumping are not considered "Moving"
because we set their "Speed Modifiers" to "0". So, in our "Move" method, right after
we get the movement direction, type in "float targetRotationYAngle = Rotate(movementDirection);". We now need to transform this angle into a
direction that the player can move towards, as right now we're moving
towards the input direction, which will always be the same
regardless of the camera angle. To do that, type in "Vector3 targetRotationDirection
= GetTargetRotationDirection();", passing in "targetRotationYAngle" as an argument. I'll rename the method parameter
name to be "targetAngle" instead. I'll also make it be "protected" and
add it to the "Reusable Methods" region. In here, we'll "return Quaternion.Euler(0f,
targetAngle, 0f) * Vector3.forward;". Now, if I'm being completely honest, I'm
not too sure what actually happens here. If I understood something, it's that we have
a point, which is our forward direction. And then, we want to know what point would
we get if we rotated that forward direction by an amount of degrees,
lets say "45 degrees". With this multiplication of a Quaternion, which
represents our "45 degrees angle", by a Vector, some black magic happens
and we get our normalized point, which is the direction we want to go to. We pass in "Vector3.forward" here because
we always want a rotation from the "z" axis, which is the player forward axis. Do note, that it's apparently important for the
multiplication to be a Quaternion by a Vector and not the opposite, so order matters here. With that done, lets go back to our "Move" method. In the "AddForce" method, we
now swap the "movementDirection" with our new "targetRotationDirection",
which already comes normalized. If we save and go back to Unity, entering Play Mode should allow us to move
and rotate our Player around correctly. We even get a thumbs up from our shadow. Great! Our player now rotates and moves properly. But, while we're doing that, we're not
really using any of our actual States. We're doing all of this while on the Idling
State, which of course shouldn't be happening, as Idling means standing
still doing nothing at all. To fix this problem, lets go
to our "Idling State" script. We have a few things to do here. The first thing is to set the speed modifier to "0f" when we "Enter" this State
so to not allow our player to Move. We can do that by "overriding" the "Enter" method
and set the "speedModifier = 0f;". We don't need to set this variable
back to "1f" when "Exiting" as every State will update this
variable to a specific value. The second thing we need to do
is to reset our Player Velocity. This is because we can come from
a State that's still moving. If we did not reset the Velocity, our player
would continue to slide due to Physics. We'll be creating a reusable method for this so
head back to the "PlayerMovementState" Script and in the "Reusable Methods" region, create a
new "protected" method named "ResetVelocity()". Here, type in
"stateMachine.Player.Rigidbody.velocity = Vector3.zero;". As I've previously explained,
we'll be setting this through the "velocity" variable to make
sure it is an instantaneous change. Back to our "Idling State", we
now call in "ResetVelocity();". I'll also add this method to a
new region named "IState Methods". That's most of what we need in our "Idling State". Right now, we'll only add the
transitions to other States and later on in the series we'll
finish whatever is left to do here. Taking a look at our Movement System States, the "Idling State" can transition
to the following states: "Walking", "Running", "Dashing" and "Jumping". Because we'll only do "Dashing"
and "Jumping" later on, we'll only transition to the
"Walking" and "Running" States. The way we'll be doing this is by using something
from the new Input System, which are callbacks. Every action should have 3 phases:
"started", "performed" and "canceled". When these are called depend on the "Action Type"
that we chose when creating the Input Actions. In our case, we need the "Movement"
Input which is of type "Value". For the "Value" Action Type: "started" means when we first
pressed one of the keys. "performed" means whenever we press a key after
the first one without actually releasing it, like going from "W" to "WD". And "canceled" means releasing
the Input Keys completely. We'll be using these actions
for most of our Transitions, but in some cases, this leads us to a problem. Lets say we were "Idling" and decided to "Jump". Whenever we are in the "Jump" state,
moving in the air is not possible, but we're still be able to press the
"Movement" Input Keys if we don't disable them, which we won't. In that case, if we hold one of the "Movement"
Keys while in the "Jumping State" and then land, the expected outcome is for us to start moving. However, because the "Movement Input" has
already "started" when we were "Jumping", the callback we'll add on the "Idling
State" won't be called anymore, which means we would stay in the "Idling State"
even though our "Movement" Keys are pressed. If we were to disable the Input when we
"Jump" and then enable it when we "Land", then the "started" would be called again, but because we won't be doing that
here, we need to find another solution. Thankfully, this is quite simple to solve: In our "Update" method, we simply need to check
if our "movementInput" variable is not "zero", and if it isn't, it means
we should start "Moving". This works because our "Movement Input" is of
"Value" Type, which continuously tracks changes. And if you remember, we're constantly reading for those changes
in the "PlayerMovementState" "Update" method. So, lets do that by typing in
"override Update" and pressing "Enter". Here, type in
"if (movementInput == Vector2.zero)". If that's the case, we can "return;". Otherwise, we'll call in a
new method named "OnMove();". Note that even though this
is in our "Update" method, it will only be called once as we'll
be transitioning States in this method, meaning our "currentState" will change
to another State on its first call. Now, in Genshin Impact, we can go from
"Idling" to "Walking" or "Running". The transition depends on our "WalkToggle". If the walk is toggled, then we'll
transition to the "Walking State" and if it isn't, we'll transition
to the "Running State". Of course, we don't really have a way
to know in what toggle state we are, so we'll start by creating a variable
in our "PlayerMovementState" Script. In here, create a new variable by
typing in "protected bool shouldWalk;". To update it, we'll use the "WalkToggle"
Input Action "started" action. For the Button type: "started" is called whenever
we press on the Input Key, "performed" is called whenever the Input
Action is performed, which can change if you add an Interaction, like "Hold". An "Hold" interaction of 1 second here
would only call the "performed" action when the Button is held for that 1 second. This will be useful for us later on. For an Input Action without interactions though, this will be called right
after the "started" action. "canceled" is called whenever we release the Input Key. Start by going into our "Enter" method and call in
a new method named "AddInputActionsCallbacks();". In our "Exit" method, we'll also call in another
new method named "RemoveInputActionsCallbacks();". Make both of these methods
"protected" and "virtual" and move them to the "Reusable Methods" region. The way we can add a callback to the
Input "started" action is by typing in "stateMachine.Player.Input.PlayerActions.WalkToggle.started
+= OnWalkToggleStarted;". If we generate a method for this, it
should also come with a parameter. We do not need to pass this parameter when we
add this callback as C# does that for us already. I'll rename it to "context". I'll also make this method
be "protected" and "virtual" and add it to a new region named "Input Methods". What we'll do here is quite simple: every
time we press the "WalkToggle" Input Key, we'll set the "shouldWalk"
variable to its opposite value, which we can do by typing in
"shouldWalk = !shouldWalk;". This is how you add a callback to an Input Action. Now, we aren't really using the context variable
here, so do we really need to pass it in? The answer is actually yes. Whenever we add a callback, we need
to make sure we also remove it. The reason why is because we're adding a
callback every time we "Enter" a State, which means that if we were
to "Enter" it 10 times, we would have 10 of the
same callback being called. However, Unity needs to know what's
the callback we are trying to remove and apparently it needs this
"context" variable to know it. I've tried in a project before
removing the "context" parameter and it didn't really remove the callback,
so we need to make sure it is there. Now, to actually remove the callback,
we simply copy our line that adds it and paste it in the
"RemoveInputActionsCallbacks" method. Then, we swap the plus (+) operator with the minus
(-) operator to subtract, or remove this callback. And that's our "WalkToggle" callback done. So, go back to our "PlayerIdlingState" and in here we can now use the
"shouldWalk" variable for our transitions. To do that, type in "if (shouldWalk)", we should then transition to
the "Walking State" by typing in "stateMachine.ChangeState(stateMachine.WalkingState);". We can of course "return;" from
the method right after that. Otherwise, if we aren't supposed to be walking,
we should go to the "Running State", so type in "stateMachine.ChangeState(stateMachine.RunningState);". We are now able to transition to
another State from the "Idling State". Of course, we haven't yet done the other States
logic so if we were to change States now, our Player would stand still due to
the speed modifier still being "0". Lets start by adding our
logic to the "Walking State". To do that, open up the
"PlayerWalkingState" Script. We'll start by "overriding" our "Enter" method and then set our "speedModifier"
to be around "0.225f;". This will make it so our Player moves
slowly, much like he was walking. I'll add this method to a new
region named "IState Methods". Note that every value that
I'll write are values that I found to be somewhat close to Genshin Impact. And that's really it for our Walking State
logic, we simply needed to make it move slowly. Of course, we also need to add
transitions to other States, so lets take a look at our Movement System States. From the "Walking State", we can transition to: "Running", "Light Stopping",
"Dashing" and "Jumping". Again, we'll add "Dashing" and
"Jumping" later on in the series. For our "Light Stopping", we'll also add it later so we'll instead
transition to the "Idling State" for now. We'll use callbacks for these transitions. The first one is to the "Running State". This happens when we are walking and
"untoggle" the "shouldWalk" variable. Thankfully, we can do that very easily by
"overriding" the "OnWalkToggleStarted" method. I'll add this method to a new
region named "Input Methods". We need to call our base method logic here as it takes care of setting the
"shouldWalk" variable for us and all we need to do here is to transition
to the "Running State" by typing in "stateMachine.ChangeState(stateMachine.RunningState);". That's it for our Running Transition, so for
the other, we'll be adding a new callback. We'll be using the "Movement"
"canceled" action for this. So, start by overriding the "Add" and
"RemoveInputActionsCallbacks" methods. I'll add them to a new region
named "Reusable Methods". Then, call in "stateMachine.Player.Input.PlayerActions.Movement.canceled
+= OnMovementCanceled;". I'll make this new method "protected", rename the parameter to "context" and
add it to the "Input Methods" region. Make sure you also remove the callback
we've just added in the "Remove" method. In the callback, we'll simply type in "stateMachine.ChangeState(stateMachine.IdlingState);". That's all we need for our "Walking State" so
lets next do the logic of our "Running State". So, open up the "PlayerRunningState" script. Start by "overriding" the "Enter" method
and set the "speedModifier" to be "1f;". I'll add this method to a new
region named "IState Methods". Again, that's all the logic we need so all
that's left are the transitions to other States. Looking at our Movement System States,
our "Running State" can transition to: "Walking", "Medium Stopping",
"Dashing" and "Jumping". This is much like the situation
of our "Walking State", so lets head back to the "PlayerWalkingState"
and copy the whole "Input Methods" region. Then, back into the "PlayerRunningState",
paste the code we've just copied. Our "OnMovementCanceled" callback
is as it should be for now. For our "OnWalkToggleStarted", we'll swap
"RunningState" with "WalkingState" instead. And that's all we need to
do in our "Running State". Go ahead and save all the files
and then go back to Unity. Entering Play Mode, our Player
should now be working fine and our "Debug.Log" should show the correct State. If we start "Running" and "toggle" our
"Walk" by pressing the "Left Control" Key, we should start moving slowly. Of course, "toggling" it again should now make
the Player go back to its "Running" speed. However, we currently have two problems: The first one is that we're starting to
unnecessarily repeat some code in our States, like our "OnMovementCanceled" callback. We are also hardcoding our values,
such as for our "speedModifier". The second one is regarding
our "shouldWalk" variable and how we are currently
reusing data between States. Lets exit and enter Play Mode again to make
sure everything is reset to its default values. Right now, our "shouldWalk"
variable should be set to "false". That means that if we start moving,
we should enter the "Running State", which is what's happening. But, lets now "toggle" our "Walking State". With this, not only are we moving slowly,
our "shouldWalk" variable should now be true. Lets now stop moving to enter the "Idling State". What do you think will happen
if we start moving again? From what we've done so far, because
the "shouldWalk" variable is now "true", we should start "Walking". However, we can see that's not the
case and are instead "Running". The reason why that's happening is quite simple:
the "shouldWalk" variable is not "static", which means each State has
its own "shouldWalk" variable. So, while in our "Running State" the
"shouldWalk" variable became "true", it remained as "false" in our "Idling State",
hence why we started "Running" instead. We can further see this if we stop running,
press "Left Control" and start moving again. We should now be "Walking". One way of possibly fixing this is by setting the
"shouldWalk" variable to be a "static" variable, meaning it would be shared across all
instances of the "PlayerMovementState" class. However, that isn't a great solution when we can
have more than one Player, like in Co-Op games, as it would make it so every Player would share
the "shouldWalk" variable with each other. What we'll end up doing instead is
to create a class that holds data that can be reused between all of the States. Before we do that though, we'll
start by fixing our first problem, which also involves removing the
hard coded values of our code. We don't really have a lot of code that can
be reused in our "Moving States" right now, but we can still Update a few things. In our case, we'll be moving our
"OnMove", "OnMovementCanceled" and the "InputActionsCallbacks" methods to a new State,
which will be our "Grounded State". Of course, we still need to create that State, so go to the Player States
folder in the "Grounded" folder. In here, create a new C# Script, to
which I'll name "PlayerGroundedState". Open it up and remove the default methods. We'll also swap the "MonoBehaviour"
inheritance with "PlayerMovementState" instead and generate the constructor it needs. When we're done doing that, we'll go ahead
and swap our "Moving" and "Idling" States to inherit from this State instead, so go to the
"PlayerIdlingState" and swap the inheritance. When that's done, do the same for our
"Walking", "Running" and "Sprinting" States. Then, go to the "PlayerWalkingState" Script and
copy or cut the "InputActionsCallbacks" methods. Next, paste that code into the
"PlayerGroundedState" script. Then, we'll go get the "OnMovementCanceled" method
from the "PlayerWalkingState" Script as well and paste it here with its region. Make it a "virtual" method as well. When that's done, remove them from
the "PlayerRunningState" Script. Next, we'll go to the "PlayerIdlingState"
Script and copy or cut the "OnMove" method and paste it in in the "Reusable Methods"
region of the "Grounded State" script. We'll also make it a "protected virtual" method. That's all we need to reuse for now. All that's left is to create a way for us to set
our current hardcoded values through the Inspector and also create the Reusable Data class
for us to reuse data between States. So, go back to Unity and move back
to the "Player" Scripts folder. In here, we'll create another folder named "Data". Inside, create two other folders:
"ScriptableObjects" and "States". Inside of the "ScriptableObjects"
folder, create a new C# Script. I'll name it "PlayerSO". Inside of the "States" folder,
create two new C# Scripts: "PlayerStateReusableData"
and "PlayerRotationData". Then, create a new folder named "Grounded". Inside of the "Grounded" folder, create
a new C# Script: "PlayerGroundedData". Then, create yet another folder named "Moving" and inside of that folder, 2 new C# Scripts: "PlayerWalkData" and "PlayerRunData". The "PlayerSO", or "Player Scriptable Object", will hold most of the values we'll
be using throughout our System, such as the speed modifiers
or rotation reach times. The "PlayerStateReusableData" will hold the data
that needs to be reused through multiple States much like our "shouldWalk" variable
or our "movementInput" variable. Start by going back to the "ScriptableObjects"
folder and open up the "PlayerSO" script. Remove the default methods and swap
the "MonoBehaviour" inheritance with "ScriptableObject" instead. Then, create a new "public" property of type
"PlayerGroundedData" named "GroundedData". I'll give it a private set. Because we'll need to set this in the Inspector,
add the "[field: SerializeField]" attribute to it. The "[field: SerializeField]" attribute
should work in Unity 2020 or above but if it doesn't work for you,
use public variables instead. I believe you can also use it with
non-automatic properties as well. That's all the Data we need here, so now, we
need a way to create this Scriptable Object. We can do that by adding, just above
the class name, "[CreateAssetMenu()]". I'll pass in the "fileName" of "Player" and
the "menuName" of "Custom/Characters/Player". When that's done, open up the
"PlayerGroundedData" script. In here, remove the default
methods and inheritance. We'll hold the Base Speed of
the player here, so type in "public float BaseSpeed" and give it a
private set and a default value of "5f". Then, give it the "[field: SerializeField]" attribute, together with the "[field: Range(0f, 25f)]" attribute. Then, we'll add our rotation data by typing in "[field: SerializeField] public PlayerRotationData
BaseRotationData { get; private set; }". Next, we'll add our "Walk Data"
and "Run Data", so type in "[field: SerializeField] public
PlayerWalkData WalkData { get; private set; }" and duplicate this line, swapping
the "Walk" with "Run" instead. To make this class show up in the Inspector
we need to make it serializable by typing in just above the class name
"[Serializable]", which requires the "System" namespace. We'll now set our "PlayerWalkData" so open it up
and remove the default methods and inheritance. Then, type in "[field: SerializeField] [field:
Range(0f, 1f)] public float SpeedModifier", with a private set
and a default value of "0.225f;". Make this class "[Serializable]" as well. That's all for our "Walk Data" and
our "Run Data" will be the same so copy the "SpeedModifier" variable
and open up the "Run Data" script. In here, remove the default
methods and inheritance and then paste the variable
line and swap the range to be from "1f" to "2f" and
default the value to be "1f". Don't forget to make this
class "[Serializable]" as well. All that's left for our SO data is the Rotation
Data so open up the "PlayerRotationData" Script. Start by removing the default
methods and inheritance. When that's done, add a new "public Vector3" named "TargetRotationReachTime",
giving it the "[field: SerializeField]" attribute. Make this class "[Serializable]" as well. That's it for our Scriptable Object Data, so for our Reusable Data, open up
the "PlayerStateReusableData" Script. In here, remove the default methods
and the "MonoBehaviour" inheritance. I'll be showing on the screen the necessary data to be
reused. Lets start with our movement input by typing in
"public Vector2 MovementInput { get; set; }". We'll be giving a public "set"
to all of the properties here, as we need to be able to set their values. Then, add the speed modifier by typing in
"public float MovementSpeedModifier { get; set; }" and default it to "1f;". For our "shouldWalk", type in "public
bool ShouldWalk { get; set; }". With that done, we now need a
property for all of our "Vector3"'s. However, we have a problem when doing that. Whenever we have a Vector3 property, we
cannot set its variables from another class. So creating a Vector3 here
wouldn't allow us to change its "x", "y" and "z" variables from our States. This only happens when it is a property,
because properties return a copy of the private variable they create
when its Type is of "Value Type", which means changing its "x", "y", or "z"
wouldn't do anything in the original Vector3. We could fix this by making it a
public variable instead of a property, but we'll actually fix it by making it
so that the property returns a reference of our private variable. This means that we can change its
variables as it's not a copy anymore but a reference to the actual Vector3. To do that, first create the private
variable by typing in "private
Vector3 currentTargetRotation;". Then, under it, type in "public
ref Vector3 CurrentTargetRotation". We'll have to set the "get" ourselves and "return"
a "reference" of our "currentTargetRotation". We don't nor can give it a "set;"
because it is by reference, which basically means "do what
you want with it" already. Now, the reason why we also need the "ref"
in our Property and not just in the "return" is because properties are methods, so our "ref Vector3" is basically
our property, or method return type, which requires the "ref" keyword if
we're returning a value by "reference". All that's left to do is to do the
same for the other 3 variables, so duplicate the existing one 3 more times and
change the variables and properties name to "timeToReachTargetRotation", "dampedTargetRotationCurrentVelocity" and "dampedTargetRotationPassedTime". Make sure you make all the properties public. We now have our Reusable Data. Of course, we do still need to create our
Player Scriptable Object in Unity and also swap the current data usage
with our data classes. For our Player Scriptable Object, head back
to Unity and go to the "Assets" folder. In here, create 3 folders, each inside of the other: "ScriptableObjects", "Characters" and "Player". Then, we'll create the Scriptable Object by
going to "Create > Custom > Characters > Player". I'll name it "Player". You can also access this menu
through the "Assets" top menu. We already have most of our
default values set to the correct values but because our base rotation reach time is a Vector3 we weren't
able to set a default value. So, change the "Y" value to be "0.14". That's all we need, but I'll
also add a new Inspector Tab by right clicking in the Inspector
Tab and going "Add Tab > Inspector". Then, I'll lock this tab so that if we
ever need to access the Scriptable Object, we can come here without needing
to go through all of the folders. When that's done, go back
to the other Inspector Tab. Our Scriptable Object is now created so
we need a reference to it in our Player. To do that, open up the "Player" Script. In here, create a new "public" property of
type "PlayerSO", to which I'll name "Data". Add the "[field: SerializeField]"
attribute to it as well. I'll also add a "[field:
Header()]" for "References". When that's done, go to Unity,
select the "Player" Game Object and add the Player SO to the respective field. That's it for our Scriptable Object. We now need to add our Reusable Data and
we'll do that in our "State Machine". We could add it for every single
State but it's unnecessary. So, open up the
"PlayerMovementStateMachine" Script. In here, type in "public PlayerStateReusableData ReusableData { get; }". Then, in the constructor, type in
"ReusableData = new PlayerStateReusableData();". We can now start swapping our
values with our Data variables. Start by opening up the
"PlayerMovementState" script. We'll be using the "Rename"
function of Visual Studio for this, which other IDE's should also have. However, it doesn't really let
us add a dot (.) ourselves, but we can cheat that by typing
in "stateMachine.ReusableData.", select it and then copy or cut it. Then, we'll go to our variables, and
we'll select the "movementInput" variable and right click on it and press
"Rename", or just use the "F2" shortcut. When that's done, put the cursor on
the beginning of the variable name and paste our "stateMachine.ReusableData." text. Of course, change the "m" to be
upper case as well and press "Enter". This renamed our "movementInput"
variable usages to what we needed, so feel free to delete the variable now. When that's done, do the exact same thing for the
other existing variables except the "baseSpeed". Rename the "speedModifier" to
"MovementSpeedModifier" as well. Once we're done doing that, we need to
swap our hardcoded values with our SO data. For our base speed, we'll
remove the current variable and create a new "protected" variable of type
"PlayerGroundedData" named "movementData". Then, in the constructor,
we'll set the "movementData" to be equal to the
"stateMachine.Player.Data.GroundedData;". When that's done, go to the "GetMovementSpeed"
method and swap the old "baseSpeed" with "movementData.BaseSpeed". Now, in our "InitializeData" method,
we'll swap the hardcoded "0.14f" with "movementData.BaseRotationData.TargetRotationReachTime". We'll also remove the ".y" from
the "TimeToReachTargetRotation". All that's left now is to do the same thing
for our hardcoded speed modifier values, so go to the "Walking State" and swap the value
with "movementData.WalkData.SpeedModifier" and then go to the "Running State" and swap the
value with "movementData.RunData.SpeedModifier". We don't need to set our "Idling State"
modifier because it is always "0". We don't need to make the "movementData" reusable either because it's always referencing
to the same Scriptable Object Data. We are now reusing our Data and not hardcoding
any values besides the speed modifiers of "0". If we go into Unity and enter Play Mode, you might've noticed when going against
a wall that we kinda get stuck in there. We'll be fixing that by giving our Player
a Physics Material with no friction so that we don't get stuck anymore. So, exit Play Mode and in our "Materials"
folder, create another folder named "Physics". Inside, create a new "Physics Material". I'll name it "PlayerPhysics". Then, set every field to be "0". When that's done, drag it into our
Player Capsule Collider Material field. If we enter Play Mode again, going against
a wall should no longer get us stuck. As a last thing, while we won't really
be reusing any data or logic with it, we'll create our "PlayerMovingState". This is because we'll need it when animating
so we might as well just create it now. So, go to the Player "Grounded" States
Scripts folder and in the "Moving" folder create a new C# Script named "PlayerMovingState". Open it up and remove the default methods and
swap the inheritance with "PlayerGroundedState". Then, go to the "Walking",
"Running" and "Sprinting" States and swap their inheritance to
inherit from "PlayerMovingState". That's all we'll need for now. Saving it all and going back to Unity, entering Play Mode should now have our
"shouldWalk" variable working fine. If we start running and press "Left Control", our Walking will be toggled
and we'll start moving slowly. If we now stop and then start moving again,
we should start walking instead of running, which means that our variables are indeed
being reused between the different States. That's great! Our data can now be reused throughout all of the States. Before we start adding the remaining States
though, we'll take a look at how are we going to allow our Player to move up
and down Slopes and also "climb" steps up until a certain height. For the reason why, lets enter "Play Mode"
and move up and down in our small ramp. Even though for the most part it works,
when going down or going up our ramps, we sometimes jump. If we head to the other ramp, we can't climb
it either as there's a small step. The first one happens because Unity Gravity
isn't fast enough to keep with our horizontal movement speed,
so it will look like we've jumped. The second one happens because we're colliding
our Player Capsule Collider with the Ramp Collider, which in this case is like colliding with a Wall. It would work if the step was a bit smaller
as it would hit the rounded part of the Capsule Collider, but it would be best for us to decide the maximum height we want our Player to climb instead. From what I've found when doing this system,
there are 2 common ways of solving the slope problem: Floating Capsules and Normals Floating Capsules, which is the solution we'll
be using, consists in adding a force to the player in a way that the capsule constantly floats or, more
specifically, the center of the capsule stays
at a certain distance from the ground. We can mix this with a resizable Capsule Collider
to allow our Player to climb steps up until a certain
height. Normals however involve getting the normal
of the ground that's underneath the Player and set our Player direction relative to that normal. "Normal" is simply the direction that the Object is facing. However, I don't think Normals solve the "step climb"
problem, so we'll go with the "Floating Capsule" technique. With that in mind, we now have two things to do: Allow our Capsule Collider "Height" to be
changed through a slider, removing the Height from the bottom only and apply a force to our Player
to keep him from falling due to Gravity. Lets start with the collider height. To do that, go to the "Scripts" folder and
create a new folder named "Utilities". Inside, create another folder named "Colliders". In here, we'll create a new C# Script, to
which I'll name "CapsuleColliderUtility". Open it up and remove the default methods
and inheritance. Because we'll need it to show in the Inspector,
we'll make this class "[Serializable]". In this script we'll basically be recalculating
our Collider Height every time a slider that goes from "0" to "1"
is updated in the Inspector, "0" being "0%" and "1" being "100%"
of the original Collider Height. To do that, we'll need a few things: A reference to our Capsule Collider,
the default Collider Data, and the Slope Data, like our percentage Slider. We'll be creating a Data class for each of
these, including our Capsule Collider reference as we'll need to cache some data from it as well. So, back into Unity, go to the "Scripts" folder
and create a new folder named "Data". Inside, create yet another folder named "Colliders". In here, we'll create 3 new C# Scripts: "CapsuleColliderData", "DefaultColliderData" and
"SlopeData". Start by opening up the "CapsuleColliderData" script. Remove the default methods and inheritance. We'll be storing not only our Capsule Collider
reference but also its local space center. So, type in
"public CapsuleCollider Collider { get; private set; }" and then
"public Vector3 ColliderCenterInLocalSpace { get; private
set; }". To initialize this data, we'll create a new method by typing
in "public void Initialize()" and pass in "GameObject
gameObject" as a parameter. This is because we won't be setting
our reference through the Inspector but instead through a Game Object we pass in. If you do want to set it from the Inspector
instead, then the next 3 lines won't be required, but you'll need to add the "[field: SerializeField]"
attribute to the "Collider" variable as well as the "[Serializable]" attribute to the class. Making it a "GameObject" instead of a "Player"
makes this script reusable. Inside, type in "if (Collider != null)",
we "return;", as it means it's already Initialized. Otherwise, we'll get it by typing in
"Collider = gameObject.GetComponent();". For our Capsule Center in local space we have
two ways of doing it. One is by using the
"transform.InverseTransformPoint()" method to transform a world space position to a local space
position. However, the "Capsule Collider" already contains
a "center" variable in local space. We'll be setting it to our own property thought
as that makes it so that if we ever need to update this center to be something else,
we only need to change it here and it applies everywhere, or, if we ever need to remove it, errors will be thrown wherever we were using the property, which makes it easier for us to know where
we need to remove it from. So, type in "ColliderCenterInLocalSpace = Collider.center;". We'll extract this last line to a new method
named "UpdateColliderData();" and make it "public". That's it for our Capsule Collider Data for
now, so open up the "DefaultColliderData" Script. We'll need this data when recalculating the
Capsule Collider new dimensions. Remove the default methods together with the
"MonoBehaviour" inheritance. We'll be making this class "[Serializable]"
to be able to set the default data through the Inspector. We'll be needing 3 things: Its "Height", its
"Center" and its "Radius". So, type in
"[field: SerializeField] public float Height { get; private
set; }". Then, duplicate this line twice and swap the
names to be "CenterY" and "Radius". I'll give the "Height" a default value of "1.8f", the "CenterY" a default value of "0.9f" and the "Radius" a value of "0.2f". The "Height" of our Character Model is known
through its "Mesh Renderer" "bounds.size" variable. The "CenterY" is simply half of that "Height" and our "Radius" is something we've defined ourselves
before. That's it for our default data, so next,
open up the "SlopeData" script. Remove the default methods and the inheritance
and make the class "[Serializable]". We'll only need one variable for now which is our Step
Height, so type in "[field: SerializeField] [field: Range(0f, 1f)]
public float StepHeightPercentage { get; private set; }". I'll default it to "0.25f". We'll add the rest of our variables whenever we need them. That's it for now, so go back to the
"CapsuleColliderUtility" Script. We'll create a variable for each of our Data classes. To do that, start by typing in "public CapsuleColliderData CapsuleColliderData { get;
private set; }". Again, if you are setting this one through
the Inspector, add the "[field: SerializeField]" attribute
to it. Then, duplicate this line and swap "CapsuleCollider"
with "DefaultCollider". When that's done, add the "[field: SerializeField]"
attribute to this property. Duplicate this new line and swap the "DefaultCollider"
with "Slope" instead. We now have our data variables so we can start creating the
methods needed to recalculate our Collider Dimensions. To do that, create a new "public" method named
"CalculateCapsuleColliderDimensions()". We'll be calling this method every time our
Inspector values are updated, so, we'll need to recalculate the "Height", "Center" and
"Radius". We'll start with our "Radius" by typing in
"SetCapsuleColliderRadius();" and pass in "DefaultColliderData.Radius". Then, I'll make the method "public" just in
case we ever want to reuse it. Inside, type in "CapsuleColliderData.Collider.radius =
radius;". This is needed because we'll need to update
the radius to something else later on under a certain
condition. Once that's done, we'll need to set our collider
height depending on our step height percentage, so duplicate this method and swap the "Radius"
with "Height" instead. Then, in our "Calculate" method, call in
"SetCapsuleColliderHeight();" and pass in "DefaultColliderData.Height". Of course, we need to multiply this with our
step height percentage, which goes from "0" to "1", where "1" is "100%". So, multiply the Height by
"* (1f - SlopeData.StepHeightPercentage)". We add the "1f -" here because the "step height
percentage" is the "percentage" we want to remove, so removing "0.25", or "25%", means that our new "Height"
should be "75%" of its default "Height", which we can get by multiplying the "Default Height" with
"0.75". With that done, we now need to recalculate our "Center". While our "Center" is half of our Height when
the Height is at 100%, this isn't true when we start lowering the Height. The reason why is because we'll only remove
"Height" from the bottom to represent the "Step Height", which means we want the Capsule Collider to center itself at
the top and not in the middle of the Character. Thankfully, it's quite easy to do that: We simply need to get the difference between
the default height and the current height and then add half of that difference to the "Center". "Half" here is the same as the "bottom" or the "top" part and this basically makes it so that our "top"
difference will add itself to the "bottom" instead. So, call in a new method named
"RecalculateCapsuleColliderCenter();". I'll make this method "public" as well. Inside, type in "float colliderHeightDifference = DefaultColliderData.Height
- CapsuleColliderData.Collider.height;". We get the default height first because our
current height will never be bigger than our default height. When that's done, create a new "Vector3" named
"newColliderCenter" and set it to be "= new Vector3(0f, DefaultColliderData.CenterY +
(colliderHeightDifference / 2f), 0f);". Again, we're making it go up half of our Height, which is
the same as adding our Top Height difference to the bottom. Then, we set the center by typing in
"CapsuleColliderData.Collider.center = newColliderCenter;". Note that this "Collider.center" is measured in local space, so we can assign our new center without transforming
it into World Space, which if you ever need, you can do it by using
the "transform.TransformPoint" method. That's mostly it when recalculating the dimensions
of our Capsule Collider, but we currently have a problem. Whenever we update our Step Height,
it will come to a certain point where our Capsule Collider becomes a Sphere from how small it is and will start moving up when we keep removing height from
it. This seems to happen whenever our "Height"
is less than double of our "Radius". It's really simply because a "Capsule Collider"
top and bottom are half spherical, which makes it become a sphere when there's
no more middle ground to remove, so it starts going up instead, likely because
it starts becoming a negative-like middle ground. What we'll do to fix it, is simply set our
"Radius" to half of our "Height" whenever the height gets to that point we've
mentioned earlier. This makes it so that instead of going up,
it will keep being where it was but will instead become smaller. It isn't a perfect solution, but in my opinion, it's better. So, to do that, type in "if (CapsuleColliderData.Collider.height / 2f If this is confusing, we basically need to
know if our "Height" is double or less of our "Radius", so if we divide our "Height" by "2", we get half of it. If half of our "Height" is equal to the "Radius",
then it means that the full Height is Double of the
"Radius". If half of our "Height" is less than the "Radius", then it
means that the full Height is less than Double of the
"Radius". In the last case, we need to Update the "Radius"
to that half "Height", which keeps our "Height" at least double of
our "Radius" at any given time. To do that, type in "SetCapsuleColliderRadius(CapsuleColliderData.Collider.heigh
t / 2f);". I'll also extract this half height into its
own variable named "halfColliderHeight" and swap it in the method call as well. That fixes our problem but we also can't forget
that when we recalculate our center, we also need to cache our center in local space again, so call in "CapsuleColliderData.UpdateColliderData();". With that done, we now only need an initialization
method so above type in "public void Initialize()" and accept a "GameObject"
named "gameObject". Then, call in "CapsuleColliderData.Initialize(gameObject);". Of course, in case this class isn't "Serializable",
which in my case isn't, we'll also need to instantiate it, so above, type in "CapsuleColliderData = new CapsuleColliderData();". And above that, type in
"if (CapsuleColliderData != null)", we "return;", as to not keep calling this once it's already initialized. That's it for our Collider Utility so lets
now add it to our Player. To do that, open up the "Player" script. In here, create a new "[field: SerializeField] public"
property of type "CapsuleColliderUtility" named "ColliderUtility",
with a private "set". I'll give it a "[field: Header("Collisions")]". When that's done, in the "Awake" method, call
in "ColliderUtility.Initialize(gameObject);". Then, to make sure our collider dimensions
are updated, call in "ColliderUtility.CalculateCapsuleColliderDimensions();". This takes care of setting our collider data
whenever we enter Play Mode, but doesn't take care of setting it in the
Editor whenever we update the slider value. To do that, we'll call in the MonoBehaviour
"OnValidate()" method. We'll copy the two lines we've just added
in the "Awake" method and paste them here. Saving and going back to Unity,
every value update we do in the Inspector should now recalculate our Capsule Data. Our Capsule Collider Height is indeed updating fine. Of course, if we go ahead and enter Play Mode,
our Player will fall and have its legs be stuck in the
Ground as our Capsule Collider
is no longer the size of our Character Model. That's where the Floating Capsule technique
comes to the rescue. Simply put, we'll add a force to counter the
gravity pulling us down. The way we'll be doing that is somewhat simple. Lets take a look at our Player Model with its Capsule. In the Editor Mode, we are able to update
the Capsule Collider Height by only taking away Height from the bottom. Of course, because we're in the Editor Mode,
there is no gravity and that's why it stays up. What we want is to keep this place even when
we enter Play Mode. However, as we've seen just now, if we do enter Play Mode, our Player will fall until it collides with the ground. This is simply because our "Character Model"
is just plain Graphics and doesn't take care of any collisions,
as we've left that for our "Capsule Collider". The "Capsule Collider" then sees nothing underneath
so it will start falling down due to Gravity until it collides with something. So, how do we make it so that the Player doesn't
fall down due to Gravity and stays at the distance it was in the beginning? We have our "Capsule Collider Center" in Local Space, which currently, for our values, should be around "1.125". We'll then cast a ray down from the Capsule
Collider World Space Center, as "Raycast" requires a world space origin,
up until a certain distance. If this ray hits something, which should be our "Ground", it will gives us the "distance" between the
origin and the ground hit point. We then should simply remove that distance
from our Capsule Collider Local Space Center. There are 3 possible outcomes here: A "positive" value, a "null" value and a "negative" value. A "positive" value happens when our Capsule
Collider isn't yet where it should be and should still go
up. For example, lets say that in this position,
the distance from our ray to the ground is "1". "1.125 - 1" results in "0.125". This means that our player still needs to
go up "0.125" units for it to be at our desired point, which can be done by adding a positive vertical force. A "negative" value is quite the opposite,
it means that our Capsule Collider is above where it should be and should go down. For example, lets say that in this position,
the distance from our ray to the ground is "1.5". "1.125 - 1.5" results in "-0.375". This means that our player went "0.375" units
above its desired point and needs to go down "0.375" units to be where it should be. This can be done by adding a negative vertical force. A "null" value happens when our Capsule Collider
is at its desired point. For example, lets say that in this position,
the distance from our ray to the ground is "1.125". "1.125 - 1.125" results in "0". This means that our Player is at the desired point, as it's both up or down "0" units, which is perfect. Of course, we need to keep a few things in mind here. The first one is that we need to use our local
space center for this subtraction. If we were to pass in the world space center,
then the "Y" could be "0", "1", "1000", "-250", etc. depending on the Player position at that time. The second thing is that we need to add the
negative vertical force ourselves because if we let Unity Gravity take care of it, it would be too slow and we would still have
our Slope "Jump" problem. The third thing is that these values that we'll receive will be smaller than that "1.125" value,
meaning that they will be extremely small. Adding a force with such a small number
will not be strong enough to overpower Unity Gravity. Because of that, we'll need to multiply that
value with a force multiplier to make sure it has enough force to go Up
or Down as fast as it can, instantaneously to the Human Eye if possible. Also, we need to remember that "AddForce"
keeps on adding a force, so we need to make sure
we remove the current vertical velocity as otherwise our Player would start bouncing. It doesn't end up flying because we'll also
be adding a negative force for when we pass through the desired point,
so it will keep coming back and start bouncing instead. The fourth and last thing is that it is very
unlikely that our value will be "null", or "0". The reason why is because we're using Physics. So, by the time we call another "AddForce",
Unity's Gravity will have added a tiny bit of negative
velocity. But, we'll still check if it is "0" so that
we can return and do nothing if that's ever the case. We'll do all of this in our "Grounded State",
so open up the "PlayerGroundedState" Script. The reason why we'll do it here is because
we really only want our Capsule to "Float" whenever we're on the "Ground", as the rest
of the States, "Airborne" and "Aquatic" won't really need it and would probably not
work very well if we added it there. With that in mind, start by "overriding" the
"PhysicsUpdate" method. I'll add this method to a new region named
"IState Methods". Then, call in a new method named "Float();". You can name it "FloatCapsule();" or anything
else if you prefer. I'll add this "Float" method to a new region
named "Main Methods". We'll start this off by casting our Ray Downwards
towards the "Ground". To do that, we have the "Physics.Raycast" method. This method requires us to send the origin
of the Ray in World Space, so create a new "Vector3" named
"capsuleColliderCenterInWorldSpace" and set it to be "stateMachine.Player.ColliderUtility.CapsuleColliderData.Col
lider
.bounds.center;", which returns the center in World Space. Then, we'll create a new variable of type
"Ray" named "downwardsRayFromCapsuleCenter" and assign it to
"new Ray(capsuleColliderCenterInWorldSpace, Vector3.down);". As far as I understood, the difference between
"Vector3.down" and "-transform.up" is that "Vector3.down" will always be in World Space, while "-transform.up" will be relative to the transform
rotation. When that's done, type in "if (Physics.Raycast())". The first argument is our Ray, so pass in
"downwardsRayFromCapsuleCenter". For the second argument we need a "RaycastHit"
variable, which we can pass in "out RaycastHit hit", which allows us to use the "hit" variable
inside of the if statement. This variable is what we'll use to know our distance. Of course, we now need a distance for the
ray, so lets go to our "SlopeData" script and when you're here, add in a new "[field: SerializeField] [field: Range(0f, 5f)] public float
FloatRayDistance { get; private set; }". I'll also default it to "2f;". You don't really need it to be "2f" here but
just a bit above half of the Capsule Height. Giving it an higher number ensures it works
and will also be used for something we'll see later on. Back into the "Grounded State", we'll need
the slope data so lets go to our variables and type in
"private SlopeData slopeData;". This is so we don't need to type in a long
line, as we'll use it more than once. Then, in our constructor, set the "slopeData"
to be "= stateMachine.Player.ColliderUtility.SlopeData;". Back to our "Float" method, we can now pass
in "slopeData.FloatRayDistance". For our fourth argument we need the Layer
or Layers that this ray will collide with. We'll only be colliding with our "Environment"
layer but we don't yet have a way to get it. We could pass in its layer number or string
but that isn't a great way of doing it. We'll instead create a new script to hold
this data so head back to Unity. In here, open up the Player Data Scripts folder
and create a new folder named "Layers". Inside, create a new C# Script named "PlayerLayerData". Open it up and remove the default methods and inheritance. We'll make this class "[Serializable]". When that's done, create a new "[field: SerializeField] public LayerMask GroundLayer { get;
private set; }". Then, go to the "Player" Script and add a
new property under our collider utility by typing in "[field: SerializeField] public PlayerLayerData LayerData {
get; private set; }". Save and head back to Unity. Selecting the "Player" Object, go to the "Layer Data" area and set the "Ground Layer" to be "Environment". Back to our "Grounded State", pass in
"stateMachine.Player.LayerData.GroundLayer". For our fifth argument, we'll pass in
"QueryTriggerInteraction.Ignore", which ignores objects that have the "Environment" Layer
but are Trigger Colliders, as we don't want to consider triggers a "Ground" we walk on. That's it for our if statement, so we can
now start floating our Capsule. To do that, create a new variable to hold
our remaining distance by typing in "float distanceToFloatingPoint = stateMachine.Player.ColliderUtility.CapsuleColliderData
.ColliderCenterInLocalSpace.y", as we want the vertical
axis. Now, this isn't extremely important for us
but if we ever scale our Player Game Object, our capsule won't stay afloat at the point that we want. To fix this, we can multiply this collider
center "y" with the player "y" local scale. So, multiply it by
"* stateMachine.Player.transform.localScale.y". Of course, if the scale becomes too big, the
distance will be more than "2" so the raycast won't find anything
and our Player will enter the "Ground", which is solvable by increasing the distance value. We now simply need to subtract the distance
from our ray hit, so type in "- hit.distance". This is of course the distance from the center
of the capsule collider to the ground. We can now float if this value isn't "0", so first type in "if (distanceToFloatingPoint == 0f)", we "return;". Then, we'll need our lift force, which means we need a
Vector3. Start by typing in
"float amountToLift = distanceToFloatingPoint;". We now need to multiply this value with that
extra force we've talked about previously and also remove the current vertical velocity. For the force, head back to the "SlopeData" script. In here, duplicate the ray distance variable
and set the range to be from "0f" to "50f" and name it "StepReachForce". I'll default its value to "25f;". When that's done, go back to the "Grounded State" script and multiply our distance with "* slopeData.StepReachForce". We'll need to subtract it with the player
vertical velocity now so lets create a method to get us
that. Go to the "PlayerMovementState" Script
and under the "GetPlayerHorizontalVelocity" type in
"protected Vector3 GetPlayerVerticalVelocity()" and inside type in "return new Vector3(0f,
stateMachine.Player.Rigidbody.velocity.y 0f);", You could also just return the "y" value if you'd prefer, which would return a float instead of a Vector3. Back into the "Grounded State" Script, subtract the
"amountToLift" with "- GetPlayerVerticalVelocity().y". We now need to convert this float force into
a Vector3 force, so type in "Vector3 liftForce = new Vector3(0f, amountToLift, 0f);". Then, to add this Force, we call in "stateMachine.Player.Rigidbody.AddForce(liftForce,
ForceMode.VelocityChange);". "AddForce" here is fine even though we have
another "AddForce" when moving because this one is a vertical force while
the other one is an horizontal force. We would likely need to use the "velocity"
variable if both "AddForces" were for the same axis. If we now save everything and go back to Unity
and enter Play Mode, our Player should be floating, meaning that
our Capsule Collider won't be touching the ground. We can also go up and down Slopes without any Jumps. Furthermore, it's now possible for our Player
to climb the other Ramp Step without a problem. Of course, we can see our feet entering the
Ground when going up the Ramp, but Genshin Impact likely fixes this using
"IK", or "Inverse Kinematics", which we won't be covering. Regarding what I've previously said about
our "FloatRayDistance", which is "2f", this ray distance is also used when the Player
needs to go down. This means that if we Jump or Fall down something,
lets say we fall off the ramp, if the distance from the Center of the Capsule
Collider to the Ground is enough to be caught in the Raycast,
we'll instantaneously teleport to our desired floating
point, as we've added a negative Force to move down. This, however, won't be too noticeable once
we add "Falling" in our "Gliding System". With that, we are now able to correctly move
on Slopes, so we'll add something else as well. If we take a look at Genshin Impact, the speed
of the player is reduced when we enter a Slope. This happens both when going up or down the Slope. So, back into our "Grounded State", in the
Raycast if statement, we need to get the angle of the ground. That's easily obtainable by using the "Vector3.Angle"
method, which returns the angle in degrees between two Vectors. So type in "float groundAngle = Vector3.Angle();". For our first Vector, we'll pass in the "Ground" facing
direction, which we can get through the "hit.normal"
variable. For our second vector, we'll pass in our
"downwardsRayFromCapsuleCenter.direction". However, this direction is going down, which
is the opposite of the ground direction, so, we'll instead do its opposite direction
by adding a minus (-) at the beginning. With this value, we can now set our speed
modifier to something else depending on the given angle, so call in a new method named
"SetSlopeSpeedModifierOnAngle();" and pass in "groundAngle". I'll rename the parameter to be named "angle" instead. We'll now need to create a new property for
our Speed Modifier when on Slopes, as we'll want to make it be relative to the
current State Speed. To do that, go to the "PlayerStateReusableData" script and when you're here, duplicate the "MovementSpeedModifier"
property and name it "MovementOnSlopesSpeedModifier". This will act as a percentage, from "0" to "1". Back to our "Grounded State", we need to check
if this angle is at a certain angle and only update our slope speed modifier when that's true. However, while adding an "if statement" is fine, we'll learn something a bit cooler: "Animation Curves". This easily allows us to add different speeds
for different angle ranges through the Inspector. To get our Animation Curve, go ahead and open
up the "PlayerGroundedData" script. In here, under our Base Speed, create a new "[field: SerializeField] public AnimationCurve
SlopeSpeedAngles { get; private set; }". Save it up and go to Unity. Then, open up the second "Inspector" Tab to
see our "Player" Scriptable Object. If we open the "Animation Curve", we can see
an empty graph-like window with a value from "0" to "1" on the left and
"0" to "1" on the bottom. The "bottom" value is considered the "time",
while the "left" value is considered the "value". We can read it as "it having a certain value
at a given time". What we'll do is convert this "time" into
our "angle" and the "value" into our "slope speed modifier", so, it will end up
"having a certain slope speed modifier at a given angle". We could leave the "time", or "angle" to be
from "0" to "1" for it to be normalized, but that would make it harder to understand angle ranges, so instead, we'll change our "time" to be from "0" to "90". To do that, select the first curve preset at the bottom. Then, right click on the last key and choose "Edit Key". In here, press "Tab" to go to the "Time" field
and set it to be "90" instead. Press "Enter" when you're done. Then, press "F" to recenter the Graph. This made it so that our "time", or "angle",
will now go from "0" up to "90" degrees. Right now though, our "slope speed modifier"
is always "1" at any given "angle". Lets make it so that our "slope speed modifier"
is "0.75" when the ground "angle" is within "15" and "55" degrees. To do that, right click in the curve and press "Add Key". Then, right click on the key and edit to be
"0.75" on the "Value" and "15" on the "Time". When that's done, press "Enter" and "F" to recenter the
Graph. Make sure the graph itself is selected or
you'll recenter on the selected key instead. Now, add yet another key and edit it to be
at "0.75" for the "Value" and "55" for the "Time". Press "F" again to recenter. Right now our curve is looking like an actual
curve, which makes it so that the "slope speed modifier" is actually descending from "1" to "0.75" as the angle
increases instead of being either "1" or "0.75". This can be what you desire and is what makes
Animation Curves quite powerful, but we want it to only be one of the two values. To do that is thankfully quite simple: We right click on a key and open up the
"Both Tangents" dropdown. We then set it to be "Constant". Do the same for the other key. Our values are now either "1" or "0.75" and
not something in-between. We do however have another problem: From our angle "55" upwards,
it keeps being "0.75" instead of "1". To fix this, we'll edit the second key and
set the "Value" to be "1". The problem with this outcome though, is that
now our angle "55" is also "1". So, edit it again, and make the "Time" "55.1" instead. This now translates to the following: If the angle is on the range of "0" to "14.9",
the speed modifier is "1". If the angle is on the range of "15" to "55",
the speed modifier is "0.75". If the angle is on the range of "55.1" to "90",
the speed modifier is "1". In Genshin however, we can actually very slowly
climb high angled grounds up until a certain angle. After that angle, we'll fall or slide instead. So, swap the "55.1" key value to be "0.4"
and then add another key for "65.1" and make its value be "0". Make the "90" degrees key have a value of "0" as well. That's done for our slope modifier so feel
free to close the Animation Curve Window and head back to the "Grounded State" Script. In here, we need a way to get the slope speed
modifier at the given ground angle. That translates to "get the value of the curve at a given
time". Thankfully, we can easily do that by using
the Animation Curve "Evaluate" method. So, type in "float slopeSpeedModifier =
movementData.SlopeSpeedAngles.Evaluate();" and pass in "angle". We now have our curve value, or "slope speed
modifier", so we need to set it up by typing in "stateMachine.ReusableData.MovementOnSlopesSpeedModifier =
slopeSpeedModifier;". We are now setting our slope speed modifier
using an Animation Curve. We'll also return this value so type in
"return slopeSpeedModifier;" and update the return type to be "float". In our "Float" method, get its returned value
by assigning it to a new "float" variable named "slopeSpeedModifier". Then, under that, we'll check
"if (slopeSpeedModifier == 0f)", we "return;". This makes it so we don't float on Grounds
that have an angle that's too high, which makes it so that we can't walk on them
and will fall or slide instead. Of course, we still need to multiply our Movement
Speed with this new variable, so open up the "PlayerMovementState" Script
and go to the "GetMovementSpeed" method. In here, simply add to the multiplication "* stateMachine.ReusableData.MovementOnSlopesSpeedModifier". When that's done, save everything and go back to Unity. Entering Play Mode and going to a slope should
now update the speed that our player is moving at depending on the "Ground" Angle. We now have our slopes working so it's
time to start adding the remaining States. We'll be starting with our "Dashing State". To do that, we'll need to first
add an Input Action for our Dash, so open up the docked Input Actions Window. In here, add a new Action and name it "Dash". I'll leave the "Action Type" as "Button". Make sure you remove the "Clamp" and "Invert"
Processors Unity automatically added from our previous Input. I assume it's a bug. For our Binding, I'll bind it to the "Left Shift" Key. We'll also add another binding and
set it to be the mouse "Right Click". This is simply because you can Dash
with any of these 2 keys in Genshin. When you're done setting the
Bindings, feel free to save the asset. Still in Unity, open up the "Grounded"
States Folder in the "Player" Scripts Folder. In here, we'll create a new C# Script,
to which I'll name "PlayerDashingState". Open it up and remove the default methods. Swap the inheritance with "PlayerGroundedState"
instead and generate the constructor. We'll need to cache this State so open up
our "PlayerMovementStateMachine" Script. In here, duplicate one of the properties
and swap it with the "DashingState". Don't forget to initialize it as well. When that's done, go back to
the "Dashing State" Script. When entering this State, we'll
have two possible outcomes: When we Dash while moving, which
is the equivalent of transitioning from a "Moving State" to the "Dashing State",
we'll dash towards our movement direction. When we Dash while standing still, which
is the equivalent of transitioning from the "Idling" or the "Stopping States" to the "Dashing
State", we'll dash towards the direction that our player is facing. To do that, start by "overriding" the "Enter" method. If our dash comes from a "Moving State", we'll simply update the speed modifier to be a bit higher. If it comes from a "Stopping State", we'll add a force
instead, as our player isn't able to move
without pressing a Movement Input Key. Of course, that means that we
need a speed modifier value, which also means that we'll need
to create a Dash Data script. So, back in Unity, go to the States Data
folder and in the "Grounded" folder, create a new C# Script, to which I'll name "PlayerDashData". Open it up, remove the default methods and the
inheritance and make the class "[Serializable]". Then, add the speed modifier by typing in "[field: SerializeField] [field: Range(1f, 3f)] public float
SpeedModifier { get; private set; }". I'll also default it to "2f;". Then, go to our "PlayerGroundedData" Script. In here, create a new "[field: SerializeField] public PlayerDashData DashData
{ get; private set; }". We can now return to our
"PlayerDashingState" script and set the "stateMachine.ReusableData.MovementSpeedModifier =
movementData.DashData.SpeedModifier;". We'll be needing more data from this class later
on, so above create a new variable by typing in "private PlayerDashData dashData;". In the constructor, set the "dashData"
to be equal to "movementData.DashData;". Then, swap it in our "Enter" method. This does it for when we're
transitioning from a "Moving State", so for the from a "Stopping State" case, we'll call in a new method named
"AddForceOnTransitionFromStationaryState();". I'll add this method to a new
region named "Main Methods". I'll also add the "Enter" method to
a new region named "IState Methods". In our new method, we only want to add a force
if there was no Input by the time we got into the "Dashing State", so type in "if (stateMachine.ReusableData.MovementInput
!= Vector2.zero)". If that's the case, then we can "return;". For the force, we'll add it towards the player
facing direction multiplied by the movement speed. To do that, start by typing in "Vector3 characterRotationDirection =
stateMachine.Player.transform.forward;". We only need our horizontal rotation direction, so now type in
"characterRotationDirection.y = 0f;". Here, we'll actually set the force through
the ".velocity" variable of the "Rigidbody". The reason why we'll be doing that is
because we can "Move" while "Dashing". That means that it's possible that our "Move
AddForce" will be called before "Dashing". Even if we were to reset the
velocity before we add this force, it's possible that it will "Reset >
Add Movement Force > Add Dash Force", which again, because we're "Adding",
it would become double the velocity. For some reason, doing the same as in the "Move Add Force"
of getting the "Player Horizontal Velocity" and subtracting it from the "AddForce", only
removed half of the velocity it needed to remove. I couldn't really understand why, which is why I decided to set the
".velocity" variable right away, which seems to fix our problems, as it
completely overrides the current velocity. So, type in "stateMachine.Player.Rigidbody.velocity =
characterRotationDirection * GetMovementSpeed();". This should be enough to get
the base of our "Dash" working. However, in Genshin, if we dash
twice in a small amount of time, we'll get into a cooldown where
we can't dash for a second or two. We'll be calling this limit
our "consecutive dashes limit". The way they will work is as follows: When we "Enter" our "Dashing State", we'll
save the current time into a variable. Before we save that time, we'll check if the "current time is less than our saved time + a time we've
defined", which is the time to be
considered a consecutive dash. If the current time is less than that sum, then it is a consecutive dash so we add
"1" to the consecutive dashes count. Whenever that count is equal to the limit
of consecutive dashes we've defined, we'll disable the Dashing Input for a few seconds. We'll be setting this data in our
"PlayerDashData" script, so open it up again. We'll need 3 new properties so
duplicate the existing one 3 times. We'll name the first one
"TimeToBeConsideredConsecutive" and default it to "1f".
We'll also set its range to be from "0f" to "2f". We'll name the second one "ConsecutiveDashesLimitAmount" and
make it an integer. We'll default it to "2" and make the range be from "1" to
"10", without the "f", as it's no longer a "float". For the third one, we'll name
it "DashLimitReachedCooldown" and default it to "1.75f". We'll also change the range
to be from "0f" to "5f". When that's done, head back to
the "PlayerDashingState" script. We'll start by creating our
saved time variable so type in "private float startTime;". Then, we'll create one to count how many
consecutive dashes we've done so type in "private int consecutiveDashesUsed;". In our "Enter" method, call in a new method
we'll create named "UpdateConsecutiveDashes();". I'll add this method to the "Main Methods" region. Inside, we'll start by checking if the Dash
we're in is a consecutive dash, so type in "if (IsConsecutive())" and generate the method. Inside, we'll "return Time.time", which is the current game time,
" of when we've entered the previous dash,
"+ dashData.TimeToBeConsideredConsecutive". So, if we entered the current "Dashing State"
not too long after our previous "Dashing State", we'll consider the current
"Dash" a "Consecutive Dash". That's done, so in our
"UpdateConsecutiveDashes" method, we'll add the not (!) operator to the if statement and reset our dash count if
it's not a consecutive dash. We do that by typing in
"consecutiveDashesUsed = 0;". After that, outside of the if statement, we'll add "1" to the used dashes count
by typing in "++consecutiveDashesUsed;". We now need to check if our used dashes count
is equal to our dash limit amount, so type in "if (consecutiveDashesUsed ==
dashData.ConsecutiveDashesLimitAmount)". If that's true, we'll need to
reset our used dashes count. To do that, type in "consecutiveDashesUsed = 0;". This is likely not necessary if our cooldown is
higher than the "TimeToBeConsideredConsecutive", but this makes it a safe reset. We'll also need to disable our
Dash Input for a few seconds. Unfortunately, it doesn't
seem like the new Input System has a built in way of disabling an Input for an amount of seconds
so we'll need to do it ourselves. We'll do that in the "PlayerInput" Script. In here, we'll create a new "public
void" method named "DisableActionFor()" and pass in an "InputAction" named
"action" and a "float" named "seconds". Don't forget to import the necessary
namespace for the "InputAction" type, which is the type of our "Movement"
Input, "Dash" Input, etc.... Simply put, in this method, we'll disable
an action for an amount of seconds. We could add a list of Inputs that
we wanted to disable and then iterate through that list in the "Update" method
to see if we should enable any again, but we'll instead use a Coroutine. This is simply because in this
use case it seems better for me to use a Coroutine than to loop a list
of disabled Actions every Update call. So, under our method, create
a new Coroutine by typing in "private IEnumerator DisableAction()"
and pass in the same parameters as above. Import any necessary namespace
for the "IEnumerator" type. Inside, we'll disable the action, wait
a few seconds and then enable it again. To do that, simply type in "action.Disable();". Then, type in "yield return
new WaitForSeconds(seconds);". And then, we enable the action again
by typing in "action.Enable();". That's all we need to do, so in our public
method, we'll call this Coroutine by typing in "StartCoroutine(DisableAction(action, seconds));". In case you didn't know, Coroutines can
only be called in a MonoBehaviour class, which is why we're doing it here. With that done, go back to the
"PlayerDashingState" Script. We can now call in
"stateMachine.Player.Input.DisableActionFor();" and pass in the action of
"stateMachine.Player.Input.PlayerActions.Dash" and the seconds of
"dashData.DashLimitReachedCooldown". All that's left to finish this up is
to set our "startTime" variable value. We'll do this after we "Update the consecutive
dashes" because we need to know the "startTime" of the last "Dash" and not the current one. If we did set it before we check
if it's a "Consecutive Dash", then we would be comparing the
current passed time with itself, which would always lead to the same result. So, in our "Enter" method, right after we
call the "UpdateConsecutiveDashes();" method, type in "startTime = Time.time;". We're now finished adding consecutive dashes but we of course still need to add the
transitions to and from this State. We'll start by adding the
transitions to the "Dashing State". If we take a look at our "Movement System States", the States that transition to the "Dashing State" are: "Idling", all "Moving States", all "Stopping
States" and the "Light Landing State". In other words, every "Grounded
State" besides the "Dashing State". The reason why the "Dashing State"
doesn't transition to itself is that in Genshin if we try to dash, we can actually only transition when we enter the
"Sprinting State" or the "Hard Stopping State". It's a bit hard to understand
it as the "Dashing State" only happens for quite a small period of time, but if you keep pressing "Shift" on that
period of time, it will not "Dash" again. So, knowing that, lets go to
our "PlayerGroundedState". Then, in the "AddInputActionsCallbacks" method, call in "stateMachine.Player.Input.PlayerActions.Dash.started
+= OnDashStarted;". I'll also remove the callback in the
"RemoveInputActionsCallbacks" method. When that's done, I'll make the
method be "protected virtual" and add it to the "Input Methods" region. I'll also rename the parameter to "context". Inside, we'll simply update the
state to be our Dashing State, so type in "stateMachine.ChangeState(stateMachine.DashingState);". Of course, right now this also adds
the callback to the "Dashing State", which is why we've made this a "virtual" method, so open up the "Dashing State" again and
add a new region named "Input Methods". In here, "override" the "OnDashStarted"
method and leave it empty. Regarding the "Dashing State" transitions to other States, if we take a look at our "Movement System States": At the end of the Dashing Animation, we can transition to the "Sprinting State"
if we keep holding the Movement Input Key or to the "Hard Stopping State" if
there was no Movement Input Key pressed. We can also transition to the "Jumping State". Of course, we still don't have our "Stopping States" so we'll instead transition to the "Idling State" and won't yet transition to the "Jumping
State" either as we haven't done it yet. However, as I've just said, these transitions
will happen at the "end" of the "Animation". The way we'll be doing that is by using
something called "Animation Events". Simply put, we can add an event at a certain
frame of the Animation in the "Animator" tab and call a specific method for when
our Animation enters that frame. That means that we won't actually be able to test our transitions to those
States until we add Animations, but we can at least start creating the methods
that we'll need to call in our Animation Events. To do that, we'll add 3 new methods to our
States, so open up the "IState" Interface. The methods we'll be adding are: "public void OnAnimationEnterEvent();", which we can use for things like "make the player immune to
damage when the animation enters its first frame". and "public void OnAnimationExitEvent();", which we can use for things like "make the player vulnerable
to damage again when the animation enters its last frame". In Genshin, an example of this would
be the Characters Ultimate ability, which makes the character immune to
damage when the animation is playing. For our last one, we have the
"public void OnAnimationTransitionEvent();", which we can use for transitioning to other
States when the animation enters a certain frame. An example of this would be our Dashing State, which is the reason why we're
adding these events right now. When that's done, open up the
"PlayerMovementState" script. In here, implement the 3 new methods we've added. I'll add them at the end of the "IState
Methods" region and also make them "virtual". While we'll only be using
them when we add Animations, I'll also open up the "StateMachine" script. In here, I'll create a new method by typing
in "public void OnAnimationEnterEvent()" and call in
"currentState?.OnAnimationEnterEvent();". Then, I'll duplicate this method twice and
swap "Enter" with "Exit" for the first one and then "Enter" with
"Transition" for the second one. When that's done, we can add the
transition to our "Dashing State". To do that, open up the
"PlayerDashingState" Script. In the "IState Methods" region, start by "overriding" the
"OnAnimationTransitionEvent" method. Inside, we'll check "if (stateMachine.ReusableData.MovementInput
== Vector2.zero)", we'll then transition to
our "Hard Stopping State". Of course, we don't yet have it so lets transition
to the "Idling State" instead by typing in "stateMachine.ChangeState(stateMachine.IdlingState);". We can then "return;". If that isn't the case and we have
something in our "MovementInput" variable, which means we're pressing a Movement Key, we
need to change to the "SprintingState" instead, so copy the line above and
paste it under the if statement. Then, swap the "IdlingState"
with "SprintingState". There is one last thing we need to do
and that's to "override" the "OnMovementCanceled" callback
method and leave it empty. This makes it so we don't go to the "Idling State"
if we press and release a Movement Input Key. Our "Dashing State" should now be finalized. Do note though, that it will only completely
work whenever we add Animations to our System. But, if we enter Play Mode, we
should at least be able to dash once. Of course, it will never
leave the "Dashing State", which has an higher speed modifier as well. With our "Dashing State" done, we can now do our next State, which will be the "Sprinting State". This State has a few things to consider, but
before we go ahead and talk about them, lets first see how can we enter this State. We enter the "Dashing State" by pressing "Shift" or "Right
Click" and then at a certain frame of its animation
we transition to the "Sprinting State". The "Sprinting State" will make the player
go faster for a while but only keeps sprinting if we didn't stop "holding"
the "Shift" or "Right Click" keys for a while. Lets look at an example in Genshin where we are running. While we're doing that, we press on the "Shift"
key to "Dash" and then unpress it right away. The "Dashing State" will still enter the "Sprinting State"
and the player will run faster for a second or two before the "Sprinting State" transitions back
to the "Running State". This is because we haven't held the "Shift"
key long enough for our Player to keep "Sprinting". Lets take a look at the same example,
but we now don't let go of the "Shift" key for a while. Our Player will now keep on "Sprinting". This stays true even if we let go of the "Shift" key, as we've held the key long enough already. Thankfully, the new "Input System" has an
"Hold" Interaction that only calls the "performed" action once we've held the Key for a desired amount of time, which makes this system quite simple to achieve. Of course, we should start by creating our Sprint Input. To do that, open up the docked "Input Actions" Window. We'll add a new Action named "Sprint". I'll leave it with the "Button" Action Type
as we don't need to continuously read its value. In the "Properties" area, we'll add the "Hold" Interaction. We should now get a "Press Point" and an "Hold Time". I don't really know what the "Press Point" is, but I assume it's how much force we need to
apply to a key or button for it to be considered pressed. We'll leave it with the default value. The "Hold Time" is what we need, which is how long do we
need to press the key for it to be considered pressed, which translates to how long does it need to be pressed
to call the "performed" action. We'll untick the "Default" from this one and
set the amount of time to be "1" second. For our "Binding", we'll listen for the "Left Shift" key and then add another binding for the mouse "Right Click". When that's done, feel free to "Save the Asset". Then, open up the "PlayerSprintingState" Script
in the "Moving" States folder. We'll start by setting the speed modifier
for this State, so "override" the "Enter" method. I'll add it to a new region named "IState Methods". We need a value for our Speed Modifier, so back in Unity, go to the "Grounded Moving" State "Data" folder
and create a new C# Script named "PlayerSprintData". Open it up and remove the default methods
and the "MonoBehaviour" inheritance. We'll also make it "[Serializable]". Then, create a new "[field: SerializeField] [field: Range(1f, 3f)] public float
SpeedModifier { get; private set; }". I'll also default it to "1.7f;". Next, open up the "PlayerGroundedData" script. Duplicate the "RunData" property and swap
the "Run" with "Sprint" instead. When that's done, go back to the "Sprinting State" script. Lets start by creating a variable to hold our data by typing
in "private PlayerSprintData sprintData;". Then, in the constructor, type in
"sprintData = movementData.SprintData;". Next, in our "Enter" method,
we set the speed modifier by typing in "stateMachine.ReusableData.MovementSpeedModifier
= sprintData.SpeedModifier;". Now, this is basically it for our "Sprinting"
logic and the rest is mainly transition logic. Like we've seen before,
if our "Sprint" Key isn't held enough time, our "Sprinting State" will go to the "Running State". To know if our Key was held enough time, lets
go to our variables and create a new "private" variable of type "bool" named "keepSprinting;". This variable will start as false, but if
our "Sprint performed" action is ever called, it will become "true",
as it means that the Key was held long enough, so we need to keep sprinting. So, add a new region named "Reusable Methods". In here, start by overriding the "Add" and
"RemoveInputActionsCallbacks" methods. Then, add a new callback by typing in "stateMachine.Player.Input.PlayerActions.Sprint.performed +=
OnSprintPerformed;". Don't forget to remove the callback as well. I'll add this method to a new region named "Input Methods". I'll also update the parameter name to be "context". Inside, we'll simply set the "keepSprinting" variable to
"true". The way we'll transition to the "Running State",
in case this variable keeps on being false, is by checking if this variable is false and
if a certain amount of time has passed on the "Update"
method. This certain amount of time passed is that
1 or 2 seconds we've seen in our example before it transitions into the "Running State". So, in our "IState Methods" region, "override"
the "Update" method. In here, type in "if (keepSprinting)", we "return;", as that means we don't want to transition to another State. For our time, we need two variables: One for when have we entered the "Sprinting State" and the other one for how long can we stay
here before transitioning. So, above in our variables, create a new
"private float" named "startTime". Then, in the "Enter" method, type in "startTime =
Time.time;". For the other one we'll do it in our Data
Class, so open up the "PlayerSprintData" Script. In here, duplicate the speed modifier property
and name it "SprintToRunTime" and default it to "1f". We'll also make the range be from "0f" to "5f". When that's done, go back to our "PlayerSprintingState"
Script. In the "Update" method, under our current
if statement, we'll check "if (Time.time If this is the case, it means not enough time
has passed for us to transition to the "Running State", so we need to "return;". Otherwise, we'll call in a new method named
"StopSprinting();". I'll add this method to a new region named
"Main Methods". In here, we'll either transition to the "Running
State" or the "Hard Stopping State", just in case we've entered this method at
the same time that we've stopped any movement input, which will be extremely rare but can still happen. Of course, we don't have our "Stopping States"
yet, so we'll transition to the "Idling State" instead. To do that, type in "if
(stateMachine.ReusableData.MovementInput == Vector2.zero)", then, we transition to the "Idling State" by typing in "stateMachine.ChangeState(stateMachine.IdlingState);". We of course "return;" right after. Otherwise, we'll need to transition to the
"Running State", so copy the line above and swap "IdlingState" with "RunningState". That's it for our transition from "Sprinting" to "Running". However, there is yet another use case. We've seen us going from "Running" to "Sprinting" to
"Running", but we haven't yet seen what happens when
we go from "Walking" to "Sprinting" to "Walking". What happens is a bit weird but likely the
way they've decided to "smooth" the transition. If we take a look at a Genshin Impact example, we can see
that our "Sprinting" still changes to the "Running State" but then, our "Running State" changes to the "Walking State"
after a small amount of time. We'll add this logic of swapping to our "Walking
State" in the "Running State", so open up the "PlayerRunningState" Script. The logic will be quite simple. If we entered the "Running State" while the
"ShouldWalk" property was true, then it means we entered the State through
the "Sprinting State". This is because if that was not the case,
we should've entered the "Walking State" instead, as our "ShouldWalk" property is set to true. So, start by "overriding" the "Update" method. In here, type in "if
(!stateMachine.ReusableData.ShouldWalk)",
we can "return;", as it means we should be running,
as the "ShouldWalk" property is set to false. Otherwise, after an amount of time passes,
we'll need to change the State to the "Walking State". The logic is much like our "Sprinting" to "Running" logic. So above, we'll create a variable for the start time by
typing in "private float startTime;". Then, in the "Enter" method, set the "startTime"
to be "Time.time;". We now need the time it takes to transition. We'll do that in the "PlayerSprintData" as
that's the reason why we need this logic here, so open it up. In here, duplicate the "SprintToRunTime" property
and change its name to "RunToWalkTime". I'll default it to "0.5f" and update the range
to be from "0f" to "2f". Feel free to do this in the "PlayerRunData"
if you prefer it that way. We can now go back to the "Running State". In here, we'll create a variable for our Sprint Data so type
in "private PlayerSprintData sprintData;" and
then in the Constructor, type in "sprintData = movementData.SprintData;". Then, in the "Update" method, we'll type in
"if (Time.time we "return;". Otherwise, we can call in a new method named
"StopRunning();". I'll add this method to a new region named
"Main Methods". We'll do the exact same as we did in our "Sprinting State"
here but transition to the "Medium Stopping State" instead, which will need to be our "Idling State" for now, so type in "if (stateMachine.ReusableData.MovementInput ==
Vector2.zero)" If that's the case, we change the state to be the
"IdlingState". We should also "return;" right after. Otherwise, we copy this line and swap the
"IdlingState" with "WalkingState" instead. When that's done, go back to the "PlayerSprintingState". We'll still need to do one thing here and
that's setting our "keepSprinting" variable to "false" whenever we exit the "Sprinting State", as otherwise it'll always keep on sprinting
when we come back to this State, because we're caching in our States and not
initializing new ones, meaning our variable data will remain intact. So, in the "IState Methods" region, start
by "overriding" the "Exit" method. In here, simply type in "keepSprinting = false;". And that's all we need for our "Sprinting State". We can also transition to the "Jumping State"
from the "Sprinting State", but we haven't done it yet so we can't do it for now. We can't really test it either as we don't
have Animation Events, but it should be working. If you wanted to try it out, you could always
change from the "Dashing State" to the "Sprinting State" after a certain amount of time has passed instead of it being on the Animation Event. With our "Dashing State" and "Sprinting State" done, we can now step ahead into our "Stopping States". These are the States that will be used whenever
we stop pressing our "Movement" Input Keys and will represent the different stopping motions. Thankfully, these won't need any Input Action themselves as all we need to know is if the
"Movement" Input Action was "canceled". Of course, we still need to create their State Scripts. So, head over to the Player
Scripts "Grounded" States folder. In here, create a new folder named "Stopping". Inside of this new folder, create 4 new C# Scripts: "PlayerStoppingState", "PlayerLightStoppingState", "PlayerMediumStoppingState" and "PlayerHardStoppingState". Start by opening up the
"PlayerStoppingState" Script. Remove its default methods and swap the inheritance with "PlayerGroundedState" instead,
generating the necessary constructor. When that's done, go to the other 3 "Stopping States" and do the same but inherit from
the "PlayerStoppingState" instead. Then, open up the
"PlayerMovementStateMachine" Script and cache the 3 new "Stopping States": "LightStoppingState", "MediumStoppingState" and "HardStoppingState". Initialize them as well. Head back to the "PlayerStoppingState"
Script when you're done. The way "Stopping" works in Genshin
is by slowly coming to an halt and not by instantaneously "Stopping". In other words, the Player starts decelerating
the moment we stop pressing a "Movement" Key, or, at the end of the "Dashing State" animation if we didn't
press the "Movement" Key in the first place. Now, all that changes between the "Stopping States" logic is at what force the Player will decelerate. Because of that, we can add the deceleration
logic in our "PlayerStoppingState" Script and individually set the force in each State. We'll start by actually setting the speed
modifier by "overriding" the "Enter" method and typing in "stateMachine.ReusableData.MovementSpeedModifier
= 0f;". Although likely not necessary due to
a few transitions we'll be adding, this makes it so that we can't move
while in our "Stopping States". I'll add this method to a new
region named "IState Methods". For our Deceleration, we'll be creating a
method in our "PlayerMovementState" Script as we'll later on need to decelerate
the Player in our "Swimming System", so feel free to open it up. We'll add it in our "Reusable Methods"
region, so when you're there, type in "protected void DecelerateHorizontally()". Remember that we only need to Decelerate
our Player in the Horizontal Axis. We'll be doing that by
using the "AddForce" method. Also remember that because
our speed modifier is 0, the Move method "AddForce" won't be called,
which means we can call this AddForce here without any problems
even if it is a Force in the same axis. However, we want our Player to slowly decelerate and not for it to have an
instant change on its velocity, so we can't or shouldn't use the
"VelocityChange" Force Mode here. If we take a look at the same image
we've seen previously made by @nothke, we can get our desired
ForceMode by knowing two things: We want our Player to slow down overtime,
which means we want our Force to depend on how much time has passed,
or in other words, to be "time dependent". Not only that, we don't want our Player
mass to have any matter on the deceleration, so we know that our Force is "mass independent". This leads us to the "Force
Mode" known as "Acceleration". Of course, we don't want to "Accelerate"
our Player but to "Decelerate". The way we do that is simple:
We'll simply slowly accelerate our Player towards its opposite direction,
which we can get from its current velocity. Of course, we'll only add this Force
while the Player velocity is over 0 or over a close enough value to 0. This is because if we didn't
check for our velocity, we would keep Decelerating the Player
even if we had already stopped. Of course, this means that we
would start moving backwards. Not only that, but because we're using Physics, we can't expect our velocity to reach
exactly "0" when we're adding Forces, so we'll instead check for a
bit of an higher value than "0". Of course, even if we do stop
adding more deceleration forces, the forces we've added before will still be there. Physics of course takes care of slowing it down,
but it does so slowly if there are no collisions, so our Player would slide for a while. We could reset the player velocity once we've
come to the point where we're almost at "0", but because these States will transition to the
"Idling State" at the end of their animations, we'll actually not need to
reset the Player Velocity here, as we already reset the Player Velocity
when we enter the "Idling State". With that in mind, in our "Decelerate"
method, start by typing in "Vector3 playerHorizontalVelocity
= GetPlayerHorizontalVelocity();". Then, we'll add a force by typing in
"stateMachine.Player.Rigidbody.AddForce();" and pass in "-playerHorizontalVelocity",
which gets the opposite direction of our velocity and pass in "ForceMode.Acceleration"
for our ForceMode. Note that this "ForceMode"
is multiplied by "deltaTime", as it's a "time dependent" Force Mode. Of course, we want to set how fast
we want to decelerate our Player, so we'll need to multiply our horizontal velocity with some other value that we'll
set in each State individually. To do that, start by opening up the
"PlayerStateReusableData" Script. In here, duplicate the last
speed modifier property and rename it to "MovementDecelerationForce". Then, go back to the "PlayerMovementState"
Script and multiply the horizontal velocity with
"* stateMachine.ReusableData.MovementDecelerationForce;". With that done, we now need to create the necessary data property to be able to
set this force through the Inspector. Before we do that though, we'll
first create another method. This method will simply tell us if
the Player is Moving Horizontally, because again, we'll only Decelerate
our Player if it isn't Moving. So, under our "DecelerateHorizontally"
method, type in "protected bool IsMovingHorizontally()" and pass
in "float minimumMagnitude = 0.1f" as a parameter. The parameter is required because
we'll know if the Player is Moving by checking the magnitude
of its horizontal velocity. The "magnitude" of a Vector is simply the distance between the Vector origin (0,
0, 0) and the Vector Point. In this case, it will be the distance
between the origin and our velocity. If the distance is "0" or close to "0", then it means we aren't moving or
are almost completely stopping. This is mostly so that we don't check
if the "x" or the "z" is over "0" and can instead check one variable only. So, type in "Vector3 playerHorizontalVelocity
= GetPlayerHorizontalVelocity();". However, this velocity comes with an "y" value. Even when this "y" is "0", it
changes our magnitude results. This is because we're checking for a
Point, so "0" is part of its coordinate. To fix that, create a new "Vector2"
named "playerHorizontalMovement" and set it to be "= new Vector2(playerHorizontalVelocity.x,
playerHorizontalVelocity.z);". Then, "return playerHorizontalMovement.magnitude
> minimumMagnitude;". These are all the methods we'll need, so now
go back to the "PlayerStoppingState" Script. In here, we'll do both the "Is Moving" check and
the Deceleration in the "PhysicsUpdate" method, so override it in the "IState Methods" region. Then, start by checking "if
(!IsMovingHorizontally())". In case this is true, meaning that
we aren't moving, we can "return;". Otherwise, we can call in our
"DecelerateHorizontally();" method. This takes care of Decelerating our Player. However, we still aren't setting the
Force we want to Decelerate with. To do that, lets head back into Unity and go
to the Player "Grounded" States Data folder. In here, create a new folder named "Stopping". Inside, create a new C#
Script named "PlayerStopData". Open it up and remove the
default methods and inheritance. We'll make it a "[Serializable]" class. We'll be creating 3 Forces, one for each
"Stopping State", so start by typing in "[field: SerializeField] [field: Range(0f, 15f)] public
float
LightDecelerationForce { get; private set; }". Then, duplicate this line twice and swap
"Light" with "Medium" and then with "Hard". I'll default the "Light" Force to "5f", the "Medium" Force to "6.5f"
and the "Hard" Force to "5f" as well. When that's done, open up the
"PlayerGroundedData" Script. In here, add a new property by typing in "[field: SerializeField] public PlayerStopData StopData
{ get; private set; }". That's all the data we'll need so lets now set the Deceleration Force
Property to its corresponding values. Start by opening up the "LightStoppingState". Here, "override" the "Enter" method and set the "stateMachine.ReusableData.MovementDecelerationForce" to be
"movementData.StopData.LightDecelerationForce;". I'll add this method into a new
region named "IState Methods". When that's done, copy this whole region and
open up the "PlayerMediumStoppingState" Script. In here, paste the region and swap the "Light"
force with the "MediumDecelerationForce". Then, do the same in the
"PlayerHardStoppingState" Script. That's all we need for our Deceleration logic, so we'll now take care of the
transitions to and from these States. We'll start with our transitions to these States
by taking a look at our Movement System States. As we've seen before, we had multiple States
that transitioned to a "Stopping State" but that we've decided to transition
to the "Idling State" instead while we didn't have them. Those states are: "Walking",
"Running", "Sprinting" and "Dashing". The "Walking State" will transition
to the "Light Stopping State". The "Running State" will transition
to the "Medium Stopping State". The "Sprinting" and "Dashing States" will
transition to the "Hard Stopping State". Start by opening up the
"PlayerWalkingState" Script. In here, we'll "override" our "OnMovementCanceled"
method in the "Input Methods" region. Remove the base method call and transition
to the "Light Stopping State" by typing in "stateMachine.ChangeState(stateMachine.LightStoppingState);"
. Then, copy this method and go to
the "PlayerRunningState" Script. Paste the copied method into
the "Input Methods" region and swap the "LightStoppingState"
with "MediumStoppingState" instead. When that's done, go to the
"PlayerSprintingState" Script. Paste the copied method into
the "Input Methods" region again and swap it to the "HardStoppingState" instead. The "Dashing State" will also
transition to the "HardStoppingState" so open it up and in our
"OnAnimationTransitionEvent" method, swap the "IdlingState" with
"HardStoppingState" instead. That's it for the States that
transition to the "Stopping States", so we now need to set the State
transitions from the "Stopping States". Taking a look at our "Movement
System States" again, our "Stopping States" can transition to: "Idling", "Walking", "Running",
"Dashing" and "Jumping". Whenever any "Stopping State"
animation reaches its last frame, we'll transition to the "Idling State". Whenever we press a "Movement" Key again,
we'll transition to the "Walking" or "Running State",
depending on the "Walk Toggle" value. We'll also be able to transition to the
"Dashing" and "Jumping States" as usual, although we won't be able to add the
transition to the "Jumping State" yet, as we don't really have it. With that in mind, start by opening
up the "PlayerStoppingState" Script. In here, in the "IState Methods" region, we'll "override" the
"OnAnimationTransitionEvent" method. Inside, we'll transition to
the "Idling State" by typing in "stateMachine.ChangeState(stateMachine.IdlingState);". This method override makes our
"OnMovementCanceled" method useless and although it should never be called because
not only will we only enter a "Stopping State" when we have already Stopped Input
but also add transitions to the "Moving States", lets "override" it and leave it empty. I'll also add it to a new
region named "Input Methods". Now, previously we've talked about going
to "Moving States" through the callback and the "Update" method for situations
where we could press the "Movement" Input in a State like "Jumping", where the callback won't be called as
the Input had already been pressed. In our "Stopping States" case, we'll
never enter them from the "Jumping State". We'll also never enter our "Stopping States"
until we release our "Movement" Input Keys. This simply means that we'll never
be able to enter a "Stopping State" with our "Movement" Input Keys already pressed. For that reason, we can add a callback
to our "Movement" "started" action instead of using the "Update" method. To do that, add a new region
named "Reusable Methods". In here, "override" the "Add" and
"RemoveInputActionsCallbacks" methods. Inside, type in "stateMachine.Player.Input.PlayerActions.Movement.started
+= OnMovementStarted;". Don't forget to remove the callback as well. When that's done, I'll add the new
method to the "Input Methods" region. I'll also rename the parameter to "context". Inside, we can simply call the
"Grounded State" "OnMove();" method, which takes care of transitioning
to the "Moving States". For our "Dash" transition, it's already set up in the "Grounded
State" so we don't need to add it here. That means that we're now
transitioning to the "Idling State", the "Moving States" and the "Dashing State". Of course, the "Jumping State" will be added
whenever we add "Jumping" to our System. However, there's one more thing to keep in mind. In Genshin, in the "Light"
and "Medium Stopping States", we are able to transition to the
"Walking State" at any given moment. However, that doesn't seem to be the
case for the "Hard Stopping State". Whenever we try to do it, the Player
doesn't start Walking or Moving at all, meaning that we can only
transition to the "Running State". To copy this situation, open up the
"PlayerHardStoppingState" script. In here, we'll simply
"override" the "OnMove" method, which we've made "virtual"
in our "Grounded State". I'll add it to a new region
named "Reusable Methods". In here, remove the base method call
and then check "if (stateMachine.ReusableData.ShouldWalk)" and if that's true, we "return;", as we
can't transition to the "Walking State". Otherwise, we should transition to
the "Running State" by typing in "stateMachine.ChangeState(stateMachine.RunningState);". That's all we need for our "Stopping States",
so save it all and go back to Unity. Entering Play Mode, we should now be able to
stop after "Walking", "Running" or "Sprinting" by slowly decelerating instead
of instantaneously stopping. Of course, because we
haven't yet added Animations, our Animation Event is never going to be called,
which means we'll never enter the "Idling State". Because of that, our Player will
very slowly slide for a while. We also won't be able to test
our "Dashing" transition yet. With our "Stopping States" added into our System, the only remaining States are the
"Jumping" and "Light Landing States". Before we take a look at those States though, we need to add something else
first: Automatic Rotations. If we take a look at Genshin Impact,
if we press a "Movement" Key like "S" when looking forward
and unpress it right after, the Player will still finish rotating itself
even though we're not pressing on the Key anymore. Genshin offers this automatic
rotation in multiple places: When Dashing, When Stopping, And when Jumping. In the "Dashing State", if
we ever tap a "Movement" Key to change its direction in the middle
of the dashing or even before dashing, the Player will still finish rotating. In the "Stopping States",
the player simply finishes rotating towards the direction that
the player was going to move. This means that it actually automatically
rotates when stopping and not when moving, which makes sense as we
already rotate while moving. In the "Jumping State", we
have 2 possible outcomes: If by the time that we "Enter" the "Jumping
State" we are pressing on a "Movement" Key, then the Player will rotate and "Jump"
towards the "Movement Direction" even if we were looking somewhere else. This happens either we leave
the Key pressed or unpress it, as long as it's after we
"Enter" the "Jumping State". If by the time that we "Enter" the "Jumping
State" we are not pressing any "Movement" Key, then the Player will "Jump" towards
its "Facing" or "Forward Direction", which doesn't really need any extra rotation. We'll be adding the first two cases here
and then add the "Jumping" case whenever we add "Jumping" into our System,
which should be right after this. Thankfully, when we've added
Movement to our Player, we already created reusable methods
that allow us to rotate our Player, so it will be quite simple to add this feature. We'll start with our "Stopping States",
so open up the "PlayerStoppingState" Script in the "Stopping" States folder. What we'll do here is quite simple:
In the "PhysicsUpdate" method, we'll simply make sure our Player keeps rotating
until it reaches the target rotation. We'll use the "PhysicsUpdate" method as
our reusable methods use the Rigidbody "MoveRotation" method, which involves Physics. So, in the "PhysicsUpdate" method,
above the rest of the code, call in "RotateTowardsTargetRotation();". We don't need to check if it
reached the target rotation already because we already do that
inside of the Rotation method. And that's really all we need to do. Whenever we Enter a "Stopping State" now, it will finish rotating even though
we're not pressing on a "Movement" Key. When that's done, open up the
"PlayerDashingState" Script. When "Dashing", we want to
keep rotating in two occasions: When we "Enter" the "Dashing State"
with a "Movement" Key already pressed and when we press a "Movement" Key
while already in the "Dashing State". Now, in the first case, because
we'll be calling our "Move" method, we'll already be rotating our Player. However, it's possible that we
release the key right after, which means that our Player will stop rotating. Because of that, we need to make
sure we somehow keep rotating. For the second case, lets say we "Entered" the "Dashing State"
without any "Movement" Key pressed at all and then pressed and unpressed
a "Movement" Key mid-dash. For example, we could go from "Idle" to
"Dash" and press and unpress "S" quite fast. Because we aren't reading for
that change in Input anywhere, our Player won't keep rotating towards
where it should when we release the Key. With that in mind, lets start by
creating a new variable by typing in "private bool shouldKeepRotating;". Then, in our "Enter" method,
after we add the Force, we'll type in "shouldKeepRotating
= stateMachine.ReusableData.MovementInput != Vector2.zero;". This will become true if we
are pressing any "Movement" Key or false if we're pressing none. This is just in case we
release the key right after to make sure it keeps rotating regardless. For our second case, we'll need to make this
variable true whenever we press a "Movement" Key, which we can know through the "performed" action. So, add a new region named "Reusable Methods". In here, "override" the "Add" and
"RemoveInputActionsCallbacks" methods. Then, add a new callback by typing in "stateMachine.Player.Input.PlayerActions.Movement.performed
+= OnMovementPerformed;". Don't forget to remove it as well. Remember that "performed"
for the "Value" Action Type will be called right after "started"
and every time a new Key is pressed, even if we didn't release the
old one, which is what we want. I'll add this method to the "Input Methods" region
and also rename the parameter to "context". Inside, all we need to do is to set the
"shouldKeepRotating" variable to "true". When that's done, we need
to make our Player rotate, so head over to the "IState Methods" region
and "override" the "PhysicsUpdate" method. In here, first check "if (!shouldKeepRotating)"
and if we shouldn't, we can then "return;". Otherwise, we call in
"RotateTowardsTargetRotation();". That's mostly it, but we currently have a problem. Lets say we are facing forwards and tap "S". This updates our target rotation to be backwards. In the middle of this rotation however, we
press "Shift" to "Enter" the "Dashing State". Because by the time we've "Entered" the "Dashing
State" there was no "Movement" Key pressed, our Player will "Dash"
towards its facing direction. However, at the end of our "Dash", we'll
transition to the "Hard Stopping State", simply because we were not
pressing any "Movement" Key. What happens next is that our "Stopping State"
will rotate towards its target rotation, which is still backwards from
when we've pressed our "S" Key. This happens because we are only
updating our target rotation when we Move and we haven't pressed any "Movement"
Key since the first "S" Key press. Thankfully, this problem is quite simple to solve:
Whenever we "Dash", we simply need to update our target rotation
to be our Player "Forward Direction". So, in our
"AddForceOnTransitionFromStationaryState" method, type in right after we get our
character rotation direction, "UpdateTargetRotation();". For the direction, pass in
"characterRotationDirection". For the second argument, we'll pass in "false"
as we don't want to consider the camera rotation but only the Player "Forward Direction"
or our "Movement Input Direction". There's one more thing
regarding our Dash Rotation, which is at what speed our Player rotates. If we take a look at Genshin, our "Movement" and "Stopping" rotations
should be around "0.14" seconds. This is how I obtained the "0.14"
from our existing "Target Reach Time". However, in both the "Dashing"
and "Jumping" rotations, the rotation takes only about "0.02" seconds. Lets update this rotation reach time by
opening up our "PlayerDashData" Script. In here, after our "Speed Modifier", add a new "[field: SerializeField] public PlayerRotationData
RotationData { get; private set; }". When that's done, go back to
the "PlayerDashingState" Script. In the "Enter" method, we now simply type in "stateMachine.ReusableData.RotationData
= dashData.RotationData;". Of course, we don't still have this property, so generate it by pressing "Alt +
Enter" and choose "Generate Property". Then, go to that Property
and make its set "public". We of course need to set its default value
so open up the "PlayerMovementState" Script and in the "InitializeData" method, just
before setting our reach time, type in "stateMachine.ReusableData.RotationData
= movementData.BaseRotationData;". Then, swap the "movementData.BaseRotationData" from our reach time line
with "stateMachine.ReusableData.RotationData". When that's done, select these 2 lines and extract them to a new method
named "SetBaseRotationData();". I'll make this method protected and add
it to the "Reusable Methods" region. The reason why we've made this a new
method is because we need to make sure we set our rotation data back when
we "Exit" the "Dashing State", as otherwise our rotation would
keep on being "0.02" seconds. To do that, open up the
"PlayerDashingState" Script. Then, in the "IState Methods"
region, "override" the "Exit" method. In here, call in "SetBaseRotationData();". When that's done, save it
up and head back to Unity. In the second "Inspector" tab, update the "Dash
Data" Rotation "Y" value to be "0.02" seconds. If we enter Play Mode now, our
Player should rotate correctly even when we unpress our "Movement" Keys. The next State we'll be taking care of is the "Jumping
State", as we now have everything we need to add it to our System. Before we start doing that though,
we of course need to know how "Jumping" works in Genshin. There are several outcomes when it comes to "Jumping", depending on the current Player Input or State. The most basic "Jump" is when "Idling". This will add an upwards force and make the
Player "Jump" in the same place, without moving horizontally. The outcome that follows it is "Jumping" while "Moving". This will still add a normal upwards force
but also "Jump" towards our "Movement" direction. One thing to note is that Genshin seems to have a different
Force for each "Moving State". This "Force" doesn't seem to have anything
to do with the "Speed Modifier" or with the velocity that our Player is moving at. Another outcome is when rotating. We've already seen this previously but if we press "Space" when we have a "Movement" Input Key pressed, then the Player will "Jump" towards
the "Movement Input Direction". If we "Jump" to the opposite side,
then we'll rotate our Player and "Jump" towards that side. We also know from our Automatic Rotation part
that it takes "0.02" seconds for this rotation to happen. If we pressed a "Movement" Key and unpressed it right after, if we end up pressing "Space" while the Player is still
rotating, then the Player will stop rotating and "Jump"
towards its "Facing Direction". This is because the "Stopping States" will
have their own Forces. Of course, if we were back to the "Idling State" already, we would "Jump" in the same place. And last but not least,
we can "Jump" while on a slope. In Genshin, when on a slope, we "Jump" with
a lower force. This is because if we were to "Jump" with our normal force when we're facing a slope, it would be too much of a force and we wouldn't really "Jump" but instead "slide" on the
slope. This of course happens when Moving,
as we're adding an horizontal velocity towards the slope. Now that we know the possible outcomes of
the "Jumping State", we'll start by creating its Input. To do that, head over to the docked Input
Actions Window. In here, we'll add a new "Action" named "Jump". If Unity added the "Hold" Interaction to it,
feel free to remove it. We can leave the "Action Type" as "Button". For our "Binding", we'll Listen for the "Spacebar"
Key. When that's done, feel free to "Save the Asset". Next, we'll create our "Jumping State" Script. To do that, head back to the Player "States"
Scripts folder. In here, create a new folder named "Airborne". Inside of the "Airborne" folder, we'll create 2 new C#
Scripts: "PlayerAirborneState" and "PlayerJumpingState". Start by opening up the "PlayerAirborneState"
Script. In here, remove the default methods and swap the inheritance
from "MonoBehaviour" to "PlayerMovementState". Generate the necessary constructor as well. When that's done, open up the "PlayerJumpingState"
Script. Remove the default methods and swap the "MonoBehaviour"
inheritance with "PlayerAirborneState" instead. Make sure you generate the necessary constructor. We now need to cache the "Jumping State",
so open up the "PlayerMovementStateMachine" Script. In here, create a new property for our "Jumping State". Make sure you initialize it as well. With that done, go back to the "PlayerJumpingState"
Script. When "Jumping" in Genshin, we aren't able to move, simply meaning there's no air movement
like there's when Gliding. We can easily not allow movement in our "Jumping
State" by setting our speed modifier to 0, so "override" the "Enter" method. Then, inside, type in
"stateMachine.ReusableData.MovementSpeedModifier= 0f;". I'll add this method to a new region named
"IState Methods". We can now start thinking on how to add the
Jump Force. The first thing we need is the Force that
we'll want to add when "Jumping", which changes depending on the current Player State. To create it, head over to the "PlayerStateReusableData"
Script. In here, under our other Vector3's Properties, type in "public Vector3 CurrentJumpForce { get; set; }". Now, because we'll be assigning this property
value to a temporary variable, we don't really need to do the same thing
we did in our other Vector3 properties. This is of course because we'll not actually
update the "x", "y" or "z" of this Vector3 but only the one of our temporary variable. With that done, go back to the "PlayerJumpingState" Script. In our "Enter" method, we'll now call in a
new method named "Jump();". I'll add this method to a new region named
"Main Methods". We'll start by assigning our current force
into a temporary variable, so type in "Vector3 jumpForce =
stateMachine.ReusableData.CurrentJumpForce;". The reason why we're adding a temporary variable
here is because the jump force will need to be changed depending
on the slope angle as well as the Jump Direction. For example, lets say we want to add a force
of "3" in the Horizontal Axis. "3" will always be a positive force, which
will always jump towards the same side, even if we intended to jump in the opposite direction. We'll need to update it to be relative to
our desired direction, which we might need to make it become "-3"
instead. We'll also need to reduce it when on slopes
to be a certain percentage of the original force, which requires us to hold that original force somewhere. By creating a temporary variable, we can update it as we desire
without updating the original property value. With that in mind, we'll now add our force by typing in "stateMachine.Player.Rigidbody.AddForce(jumpForce);". We'll also be setting the "Force Mode" to
be "ForceMode.VelocityChange", as it should be both time and mass independent. Of course, we need to update our jump force
to be able to "Jump" in the direction that our Player is moving and not towards
the positive force direction only. Not only that, but currently, if we set a force of "3" while we're only moving in one of the "Horizontal Axis", we'll still jump towards both "Horizontal Axis" as we're adding a force of "3" to both Axis. To solve these 2 problems,
we can simply multiply our Jump Force with the Player Forward Vector3. The "Player Forward" is basically our
Player normalized direction, which is the equivalent of where our Player is facing. This means that if we're only moving in one "Horizontal
Axis", the force of the other Axis will be multiplied by "0", which means we won't "Jump" towards that direction. To be able to do that, just before adding our Force, type in "Vector3 playerForward =
stateMachine.Player.transform.forward;". Then, we multiply our
"jumpForce.x *= playerForward.x;" and our "jumpForce.z *= playerForward.z;". We don't want to multiply our "y" axis as
our player forward direction has no effect whatsoever in our upwards velocity. That's the very base of our Jump done
and we can now "Jump" with a given Force. However, before we Jump,
we should reset any velocity we have, as we don't want our current Player velocity
to impact our "Jump" Force. To do that, above our "AddForce" method,
call in "ResetVelocity();". This situation is one of the reasons why our
"ResetVelocity" method sets the "velocity" variable right away for
an instantaneous change. If we didn't do that, "Jumping" multiple times in a row could make the Player "Jump" more than it should. Again, I'm not entirely sure why that happens,
but I assume the order of the "AddForces" is being swapped or the force we were trying to add was now more than the player force at the time that the
"AddForce" method was actually called. Of course, we still need to create the data classes to be able to set the corresponding "Jump Forces". To do that, save everything and head back to Unity. When you're there, go to the Player States "Data" folder. In here, create a new folder named "Airborne". Inside, we'll create 2 new C# Scripts: "PlayerAirborneData" and "PlayerJumpData". Start by opening up the "PlayerJumpData" Script. In here, remove the default methods and inheritance
and make this class "[Serializable]". We'll be adding one property for each force strength. To do that, start by typing in "[field: SerializeField] public Vector3 StationaryForce {
get; private set; }". Then, duplicate this property 3 times. Swap their names to:
"WeakForce", "MediumForce" and "StrongForce". When that's done, open up the "PlayerAirborneData" Script. Remove the default methods and inheritance
and make this class "[Serializable]". We'll add a jump data property here by typing in "[field: SerializeField] public PlayerJumpData
JumpData { get; private set; }". When that's done, open up the "PlayerSO" Script. Duplicate the existing "GroundedData" property
and swap the "Grounded" with "Airborne" instead. Once you're done doing that, save it all up
and head back to Unity. We'll be setting our forces values so open
up the second "Inspector" tab. In the "Airborne Jump Data" area, we'll set the following
forces: For the "Stationary Force", we'll set the "y" to a value of
"5". For the "Weak Force", we'll set the Vector to "1, 5, 1". For our "Medium Force", we'll set the Vector to "3.5, 5,
3.5". For the "Strong Force", we'll set the Vector to "5, 5, 5". With that done, we now need to assign these
values in the corresponding States. If we take a look at Genshin, we can see our
Forces being applied to the following States: "Idling", which gets set to the "Stationary Force". "Walking" and "Light Stopping", which get
set to the "Weak Force". "Running" and "Medium Stopping", which get
set to the "Medium Force". "Sprinting", "Dashing" and "Hard Stopping",
which get set to the "Hard Force". Regarding the Forces, they also seem to depend
on the size of the selected Character. For example, smaller characters seem to have
the same "Medium" and "Strong" "Jumping Force", while taller characters seem to have a stronger
"Strong Force". Thankfully, if you ever need to differentiate
their Forces like that, you can simply create another Scriptable Object
for smaller or taller characters. We will however not do that and just use our
existing Player Scriptable Object. Now, sometimes the "Stopping States" used
the "Stationary Force" at any given frame and sometimes they use the "Weak", "Medium"
and "Hard Forces" at any given frame as well. I couldn't really understand what was the
factor that decided which "Force" to use, as it didn't seem to be at a certain frame. To make it easier for us, we'll leave the
same "Force" for the whole "Animation" duration. If you did want to set the "Stationary Force" in a certain
frame, you could do it by "overriding" one of the
"Animation Event" methods and set the "Current Jump Force"
to the "Stationary Force" there. Upon further Inspection after recording everything,
I noticed it would depend on the last Jump we've made. So if we've "Jumped" while "Idling", the "Stopping
State" would "Jump" with that Force. It wasn't the case for every single "Stopping
State" as the "Lighter Stopping States" for example, wouldn't transition to the "Stronger Force". It seems to cause a bug sometimes where something
like "Dashing" and "Jumping" would cause the "Jump" to use a "Stationary Force" as well, so because I don't find this way of doing it to be correct and mainly because I've already recorded everything,
I'll go with my current implementation. If you do want to try to be as close as possible
to the actual implementation, you would likely not set the Forces in the
"Stopping States". With that in mind, we'll now assign the Forces
in the corresponding States. We'll start with the "Idling State", but because we'll need
to reference the Airborne Data a few times, start by opening up the "PlayerMovementState" Script first. In here, we'll create a new variable by typing in "protected PlayerAirborneData airborneData;". Then, in the constructor, we type in
"airborneData = stateMachine.Player.Data.AirborneData;". When that's done, open up the "PlayerIdlingState" Script. In the "Enter" method, we'll set the "Jump" Force by typing
in "stateMachine.ReusableData.CurrentJumpForce =
airborneData.JumpData.StationaryForce;". When you're done doing that,
copy this line and open up the "PlayerWalkingState" Script. Paste the line we've just copied in the "Enter" method and swap the "StationaryForce" with "WeakForce" instead. Copy the line again and open up
the "PlayerLightStoppingState" Script. Paste the line in its "Enter" method. Next, open the "PlayerRunningState" Script. Paste the line in the "Enter" method
and swap it with "MediumForce" instead. Copy the line again and open up the
"PlayerMediumStoppingState" Script. In here, paste the line in the "Enter" method as well. When you're done doing that, do the same for the
"Sprinting", "Dashing" and "Hard Stopping States" but swap the "MediumForce" with "StrongForce" instead. That's it for our Forces so before we add the remaining
features we'll actually make it so that we can transition
to and from the "Jumping State". This will allow us to test what we have done so far. Taking a look at our "Movement System States",
we can transition to the "Jumping State" through the following States: Every "Grounded State". This basically means that we can add the transition
to the "Jumping State" in our "PlayerGroundedState" Script. So, knowing that, open it up. In here, in the "AddInputActionsCallbacks"
method, found in the "Reusable Methods" region, we'll add a new callback by typing in "stateMachine.Player.Input.PlayerActions.Jump.started +=
OnJumpStarted;". Don't forget to remove the callback as well. I'll make this method "protected virtual"
and add it to the "Input Methods" region. I'll also rename the parameter to "context". To transition to the "Jumping State", we simply type in "stateMachine.ChangeState(stateMachine.JumpingState);". That's it for the transitions to the "Jumping State" so we'll now add the transition from the "Jumping State". For now, because we don't have the "Falling"
and the "Landing Systems" in our System, we'll simply transition to the "Idling State" whenever we
"Jump" and touch the "Ground" again. The way this transition works is by having a "Trigger
Collider"
at the feet of the Player waiting for the "OnTriggerEnter" call whenever the collider
enters the "Ground" after "Jumping". Knowing that, we need 2 things: A "Trigger Collider" and a way to call the
"OnTriggerEnter" method. Lets start with our "Trigger Collider" by
going back into Unity. Selecting our "Player" Game Object,
create a new Child Game Object and name it "TriggerChecks" or any other name you'd like for colliders
that will check for something. Inside of this "TriggerChecks" Game Object,
create yet another Child Game Object named "GroundCheck". Adding a collider in a different game object
allows us to add a specific layer to it, which means we can control
what that specific collider collides with. In the "GroundCheck" Game Object, add a new
Component of type "Box Collider". A Sphere would likely be fine as well, but
I'll go with a Box. When that's done, make the Collider a "Trigger"
by ticking the "Is Trigger" checkbox. Right now though, its center and size aren't the most
correct, so edit the "Center" to be "0, 0.1, 0" and
the "Size" to be "0.25, 0.2, 0.25". It should now be positioned around our feet
and just slightly touching the "Ground". If your "Capsule Collider" Height is still at 100%, make sure this "Collider" is able to touch
the Ground by updating its "Center" or "Y Size" if
necessary. When that's done,
we'll add a Layer to this "Collider" Game Object, so add in a new layer named "GroundCheck". When you're done adding it,
go to "Edit > Project Settings > Physics" and in the layers at the bottom make sure
you disable every collision for the "GroundCheck" layer besides collisions
with the "Environment" layer. This ensures that our "Trigger Collider" can
only collide with the "Environment", meaning that other Game Objects such as other
Players or Enemies won't call the "OnTriggerEnter" method when being collided with. With our "Trigger Collider" added, we can
now add the "OnTriggerEnter" method. To do that, open up the "IState" Script. In here, add in a new "public void" method
named "OnTriggerEnter". Unity MonoBehaviour "OnTriggerEnter" method
requires a "Collider" parameter so pass in "Collider collider" as the method parameter. Make sure you import any necessary namespace. When that's done, open up the "PlayerMovementState" Script and implement the "OnTriggerEnter" method,
making sure you add it to the "IState Methods" region and remove whatever code is inside. Make it a "virtual" method as well. With that done, open up the "StateMachine" Script and create a new method to call the "OnTriggerEnter"
method by typing in "public void OnTriggerEnter(Collider collider)". Make sure you import the necessary namespace. Inside, call in "currentState.OnTriggerEnter(collider);". Then, open up the "Player" Script. In here, under the "Start" method,
call in the MonoBehaviour "OnTriggerEnter" method. I'll rename the parameter to "collider" instead. Inside, we can simply call in
"movementStateMachine.OnTriggerEnter(collider);". All that's left now is to check if we collided with the
"Ground" and transition to the "Idling State" if that's the case. To check for that collision, we can either
use "Tags" or "Layers". To be honest, I couldn't find which one is
the best for these comparisons, so I'll go with "Layers" by using a "LayerMask" to compare
them, simply because "Layers"
seem to be the most related to Collisions. Unfortunately, we can't or shouldn't really
compare a "LayerMask" with the collider "layer" right away and it's a bit bothersome to compare them when you don't
understand how it works, but I'll try to explain it as best as I can. Before I explain it though, open up the "PlayerLayerData"
Script. This is where our "GroundLayer" reference is in and we'll now add a new method that allows
us to compare layers, or more specifically, check if a "LayerMask"
contains a certain "Layer". To do that, create a new method by typing in "public bool ContainsLayer(LayerMask layerMask, int layer)". Inside, we simply
"return (1 Now, if you've never seen this before or never
compared layers before, you're likely completely lost on the meaning
of this "return" statement. To be honest, I was too and had no idea what
this meant. To be able to fully understand it, lets go bit by bit. We have "LayerMasks" and we have "Layers". If you remember the "Layer" Inspector,
you know that we can have up to "32 Layers", some already used by Unity. This is basically a "32 bit bitmask". In "Human Readable Numbers", these "Layers"
will be represented from "0" to "31", which is what people often use in their code. However, lets take a look at our "Environment
Layer" in "Computer Numbers". Using the Inspector "32 bit bitmask",
it would be represented by a bitmask with a "1" at its "7th" position. This is because our "Environment Layer" is
the "Layer 6" in the Inspector and because Layers in the Inspector start from "0", the actual position is that number "+ 1". Of course, our "GroundCheck Layer" would have
a "1" in the "8th" position instead and so on. So, that's how our "Layers" are represented
in the "32 bit bitmask". A "LayerMask" is also a "32 bit bitmask". If you remember from our Inspector, a "LayerMask"
can be used to select multiple "Layers". So while it starts with "0"'s for the whole "bitmask", the moment we select one or more "Layers" in the
"Inspector", the "32 bit bitmask" would start turning "0"'s
to "1"'s in the respective "Layer" position. For example, selecting the "Environment Layer"
would have a "1" at the "7th" position. Now, if our "LayerMask bitmask" is looking the same as our
Layers "bitmask" with our "Environment Layer" selected, why can't we compare our "Layer" with our "LayerMask"? The reason why is that while "LayerMasks"
are represented with "Computer Numbers", Layers are represented with "Human Numbers",
simply to make it easier for us to understand them. So, when we compare the "Environment Layer"
with a "LayerMask", we're comparing a "Human Number"
with a "Computer Number", which won't work out very well. For "Raycast" cases, because we're passing
in the "Human Number" to the "layerMask" argument, that "Human Number" would be converted to
a "Computer Number" represented by the "Layer" number. For example, if we passed in "6", as that's
our "Environment Layer" number, it would be transformed into binary and would
be represented by a "32 bit bitmask" with a "1" at its "2nd" and "3d" positions. This is because "6" in binary is represented by "110". That "110", would then be transformed into a "32 bit
bitmask" as the "Raycast" requires a "Layer Mask". Of course, this would simply add the remaining bits,
or "0"'s, to its left side. If we now compare it with the "LayerMask bitmask",
we can see that they do not look the same at all, even though we've passed in the correct "Layer" Number. The fix to all of this is quite simple:
We simply need to convert our "Human Number" to a "Computer Number",
which is what our weird formula does. Starting from the beginning, our "1" represents
the first position of the bitmask. The " Simply put, we tell our bitmask to "shift"
an amount of times to the left. So, our "1" that was in our first position,
will now shift itself "layer" amounts to the left. If our "Layer" is the "Environment Layer",
then it will shift itself "6" times to the left, which means it will now be in the "7th" position. With this, our "Human Number" is now a "Computer Number" and represents the same bitmask
that our "LayerMask" represents. This means that comparing them would give
us the desired result. However, remember that a "LayerMask"
can contain multiple "Layers". If we were to also add in the "GroundCheck Layer", it would now have a "1" in the "7th" and "8th" position, which would now be different
than our "Environment Layer bitmask". That's where the "& layerMask" comes in play. The "&" sign is a "bitwise operator"
and represents the "AND" operator. This operator will compare all bits in both of our
"bitmasks" and return "0" when one of the same positioned bits is "0", or "1" when both bits are "1". Simply put, its keeps the common "1"'s
between both "bitmasks". If the returned bitmask has any "1" in it,
then it means that if we convert this "bitmask Computer Number" into a "Human Number",
it will return a value over "0". For example, in this case, it would be "64",
as it's a "1" in the "7th" position. This means that our "LayerMask"
contains the passed in "Layer". If the final "bitmask" were to return a bitmask with only
"0"'s, then it would mean that the "Layer"
wasn't part of the "LayerMask". That's what our "!= 0" is for. Again: If the final "Human Number" is "0", then it
means the "LayerMask" didn't contain the passed in "Layer", as there was no "1" in the same bit in both "bitmasks". If the final "Human Number" is "!= 0", then
it means the "LayerMask" contains the passed in "Layer", as there was a "1" in the same bit in both "bitmasks". I'll leave a few links in the description
if you want to dive deeper into it, but this should be enough for you to understand our use
case. With that in mind, we'll also create another
method to check if it's a "Ground Layer", so type in "public bool IsGroundLayer(int layer)". Inside, "return ContainsLayer(GroundLayer, layer);". That's all we need here so now open up the
"PlayerMovementState" Script. In our "OnTriggerEnter" method,
we'll check if the object we've collided with is part of the "Environment" by typing in "if (stateMachine.Player.LayerData.IsGroundLayer
(collider.gameObject.layer))". Inside of this if statement, call in a new
method named "OnContactWithGround();" and pass in "collider" as an argument. "return;" right after, to make sure nothing
else gets called. I'll make the method "protected virtual"
and add it to the "Reusable Methods" region. We'll override this method wherever we need
something to happen when we enter in contact with the ground,
which we don't need in our "Movement State". We can use this method to transition to the
"Idling State" in our "Jumping State", but because I know we'll need to transition
more times to the "Idling State", or more specifically to the "Light Landing
State" whenever we add it, we'll actually add it to the "PlayerAirborneState" Script,
so open it up. In here, create a new region named "Reusable Methods". Inside, "override" the "OnContactWithGround" method. Then, transition to the "Idling State" by typing in "stateMachine.ChangeState(stateMachine.IdlingState);". Feel free to remove the "base" method call as well. This should be enough to test our current
Jump, so save it all up and go back to Unity. If we enter "Play Mode", our "Jump" should
now work and its force should depend on the current Player State. We also cannot move mid-air. However, we still need to finish up a few
features from the "Jumping State": Automatic Rotation, Force Modifier when on
a Slope, Fixing the Floaty Jump Feeling and Keep Sprinting after Jumping. We'll start with the "Automatic Rotation"
so go back to the "PlayerJumpingState" Script. We need to automatically rotate our Player
if we've entered the "Jumping State" with a "Movement" Input Key pressed. Not only that, but currently we are always
jumping to the Player Facing Direction. This is not what should happen
when we press a "Movement" Key and should instead "Jump" towards
our "Movement Input Direction" if that's the case. We'll start by creating a variable to know
if we should rotate, so above, type in "private bool shouldKeepRotating;". Then, in the "Enter" method, set the "shouldKeepRotating =
stateMachine.ReusableData.MovementInput != Vector2.zero;". This makes it so we keep rotating if we have Input. With that, we can now "override" the "PhysicsUpdate" method and inside type in "if (shouldKeepRotating)". If that's the case, we can call in
"RotateTowardsTargetRotation();". We're adding it inside of the if statement
as we'll have another if statement in this method later on. We should now make it so that the Player "Jumps" towards the "Movement Input Direction" instead of
the Player Forward Direction if we entered the "Jumping State"
with a "Movement" Key pressed. To do that, go to the "Jump" method. The way we'll do this is by updating
our "playerForward" variable value to be the Movement Input Direction instead
when we have a "Movement" Key pressed. We can use the "shouldKeepRotating" variable to know if our Movement Input Key was pressed as that's the
condition we gave it in our "Enter" method. So, after our "playerForward" variable, type in "if (shouldKeepRotating)". If that's the case, we'll update our "playerForward"
to be the Movement Input Direction. However, we actually want the direction to
be relative to the Camera, as the "Movement Input" would be in "World Space". We can do this by getting the direction of our "target
rotation", which was updated when we "Moved" before "Jumping". We also have already created a method before
that gets us a direction given a rotation. So, simply type in "playerForward =
GetTargetRotatioDirection();" and pass in "stateMachine.ReusableData.CurrentTargetRotation.y". This is the method that transforms a quaternion
with a certain angle into a Vector Direction. Remember that even if we come here from a "Stopping State", we "Enter" them with an updated target rotation already, so we don't need to Update it here. Our "playerForward" variable name
no longer makes much sense, so we'll select it and rename it by pressing
"F2" or right clicking on it and choosing "Rename" and set its name to
"jumpDirection". Of course, like we've seen before, both "Dashing" and
"Jumping" have a different reach time, so we need to set that up. To set it up, open up the "PlayerJumpData" Script. In here, create a new property by typing in "[field: SerializeField] public PlayerRotationData
RotationData { get; private set; }". When that's done, save it up and go back to Unity. In the second "Inspector" tab, set the "Jump
Data" "Y Rotation" value to be "0.02" seconds. Then, go back to the "PlayerJumpingState" Script and create a new variable above by typing in "private PlayerJumpData jumpData;". Then, in the constructor, set it to be
"jumpData = airborneData.JumpData;". In our "Enter" method, we can now type in "stateMachine.ReusableData.RotationData =
jumpData.RotationData;". We should also not forget to reset this rotation
data when "Exiting" so "override" the "Exit" method
and call in "SetBaseRotationData();". If we save it all and head over to Unity,
entering "Play Mode" should now have our "Jump" working
fine. That's great, but we should now make sure
we have a lower Jump Force when on Slopes to make sure we don't get into
the Slopes when "Jumping", which would make the Player look like it's sliding. This mostly happens on steeper slopes. To do that, head back to the "PlayerJumpingState" Script. We'll do this by casting a "Ray" in our "Jump" method to know if we're on a slope and then update
the Forces if that's the case. To do that, before resetting the velocity, type in "Vector3 capsuleColliderCenterInWorldSpace = stateMachine.Player.ColliderUtility.CapsuleColliderData.
Collider.bounds.center;". Then, create the "Ray" by typing in "Ray downwardsRayFromCapsuleCenter = new
Ray(capsuleColliderCenterInWorldSpace, Vector3.down);". We can now cast our "Ray" by typing in "if (Physics.Raycast(downwardsRayFromCapsuleCenter, out
RaycastHit hit))". We need the distance of our "Ray" now,
so open up the "PlayerJumpData" Script. In here, type in "[field: SerializeField] [field: Range(0f, 5f)] public float
JumpToGroundRayDistance { get; private set; }". I'll default it to "2f". When that's done, go back to the "PlayerJumpingState"
Script. Pass in to the "Raycast" "jumpData.JumpToGroundRayDistance". For our "LayerMask", we'll pass in
"stateMachine.Player.LayerData.GroundLayer". Then, pass in "QueryTriggerInteraction.Ignore"
to ignore "Trigger Colliders". If we hit something with this ray we then
need to know if we're on a slope. We'll of course be using the "Vector3.Angle" method again. To do that, type in "float groundAngle = Vector3.Angle(hit.normal,
-downwardsRayFromCapsuleCenter.direction);". We now need a way to check for the angles
and set a force accordingly, which we'll be doing by using "Animation Curves". However, we'll need to create an "Animation Curve" for both
going up and down slopes. This is because that's how it works in Genshin. When going up we update the "Horizontal Forces"
to make sure we don't slide in the slope. When going down we don't really need that
as there's no ramp in front of us but it seems that they update the "Vertical
Force" to be a bit lower. To know whether we're going up or down, we
simply need to check the Player vertical velocity. Going up means it's positive, while going
down means it's negative. We'll create 2 methods for that so open up
the "PlayerMovementState" Script. In the "Reusable Methods" region, head over
to the "IsMoving" method. Then, create a new method by typing in "protected bool IsMovingUp(float minimumVelocity = 0.1f)". Inside, we can simply
"return GetPlayerVerticalVelocity().y > minimumVelocity;". That's all we need so now duplicate this method
and rename it to "IsMovingDown" instead. Then, we'll swap the ">" with "
a minus (-) sign before the "minimumVelocity". This makes it so that we can pass in a positive
velocity as an argument while the method takes care of it being checked as negative, which is what should happen. When that's done, go back to the "PlayerJumpingState"
Script. In the raycast if statement, now check
"if (IsMovingUp())" and also "if (IsMovingDown())". We can now start creating and setting the
Animation Curves for each situation. To do that, open up the "PlayerJumpData" Script. In here, create a new "[field: SerializeField] public AnimationCurve
JumpForceModifierOnSlopeUpwards { get; private set; }". You can give it any other name if you find something better. Then, duplicate the property and
swap "Upwards" with "Downwards". When you're done, save everything and head back to Unity. In here, open up the second "Inspector" tab. In the "Jump Data" area, start by opening
up the "Upwards" Force Animation Curve. Select the first curve preset. Then, right click on the last key, press "Edit
Key" and set its "Time" to be "90". We'll have 2 different ranges: From "20" to "35", the force modifier will
be "0.75", or "75%" of the original "Jump Force". From "35.1" to "75", the force modifier will
be "0.5", or "50%" of the original "Jump Force". This is because the steeper the slope, the
easier it is to look like we're sliding, so we need to make sure we have a lower "Horizontal Force". Knowing that, add in 3 keys: "20", with a value of "0.75". "35.1", with a value of "0.5". And "75.1", with a value of "1". Of course, we don't want this to look like an actual Curve so select all Keys and set "Both Tangents"
to "Constant" for all Keys. If all has been done correctly,
"20" to "35" should be at "0.75", "35.1" to "75" should be "0.5"
and the rest should be "1". With that done, close the current "Animation Curve" and open up the "Downwards Animation Curve". Choose the first curve preset and change the
last key "Time" to be "90". We'll only have 1 range here: From "20" to "70", the force modifier will
be "0.85", or "85%" of the original "Jump Force". Make sure the "70" key is actually "70.1" and set to "1", for the same reason as all the other "Animation Curves". Set both keys "Tangents" to be "Constant". We should be left with one single area from
"20" to "70" set to "0.85" and the rest set to "1". That's all we need for our "Animation Curves" so feel free
to close it and head back to the "PlayerJumpingState"
Script. In our "IsMovingUp()" if statement,
we'll need to apply the forces only on the "Horizontal
Axis". To do that, start by typing in "float forceModifier = jumpData.
JumpForceModifierOnSlopeUpwards.Evaluate(groundAngle);". Then, apply the modifier by typing in
"jumpForce.x *= forceModifier;" and "jumpForce.z *= forceModifier;". That's all we need when moving up, so in our
"IsMovingDown" if statement we'll need to apply the forces only for the "Vertical Axis". To do that,
copy the first 2 lines of the "IsMovingUp" if statement and paste them in in the "IsMovingDown" if statement. Then, swap "Upwards" with "Downwards" and "x" with "y". This should be all that's necessary
to correctly "Jump" on a Slope so save it all and go back to Unity. Entering "Play Mode", we should now be able to "Jump"
correctly in both of our Ramps. We should also be "Jumping" differently when
going up or down the Ramps. Do note that animations also play a part on how good this
looks so you might need to update the values once
you add animations into the System. If it isn't working in your own Game Objects,
make sure that the Ramp angles are within the ranges we've added,
as if they aren't, they will "Jump" normally. I think you could ramp up the "20" to "30" if you'd like, but I made it "20" so that we could test it
out in our current Ramp, as "20" shouldn't be steep enough to need
the "Jump" force modifier. With that done, you might have also noticed
that our "Jump" looks kinda "Floaty". If we take a look at Genshin, if we "Jump",
we see that it gets to the top quite fast, not really having much of a floating feeling. And yes, I'm still talking about the "Jump". The reason why the "Jump" feels "Floaty" is
because it's taking its time to get to the top and then Gravity also takes its time to create a strong
enough negative velocity. The way we'll make it less floaty is by adding
a small negative force while the Player is going up. This makes it so that we get to the top faster,
which makes it look less floaty. You could also add a small negative force
when going down, which would make the Player fall faster, but
I'll just add one when going up. To do this, go back to the "PlayerJumpingState" Script. In here, in our "PhysicsUpdate" method,
type in "if (IsMovingUp())". If that's the case, we need to add the force
to our Player "Vertical Axis". We'll do that using the "Acceleration" Force Mode, as we want to slowly decelerate our Player over time. Of course, we currently only have an
"Horizontal Deceleration" method, so we'll need to create one for a "Vertical Deceleration". To do that, open up the "PlayerMovementState" Script. In the "Reusable Methods" region,
go to our "DecelerateHorizontally" method and duplicate it. Then, rename it to "DecelerateVertically". Inside, swap the "GetPlayerHorizontalVelocity"
with "GetPlayerVerticalVelocity" instead and rename the variable to "playerVerticalVelocity". If you want this "Decelerate" to always "Go Down", then make sure you swap the minus (-) sign
to be on the Force instead as otherwise a negative velocity will become positive, which means it would make the player go up. Because I named it "Decelerate", this is what I'll have it
do. This of course means that if our Player was going down, "Decelerating" it "Vertically" would make
it fall slower and slower over time. With that done, go back to the "PlayerJumpingState" Script and inside of the if statement, call in
"DecelerateVertically();". Of course, we need a value for the "Deceleration Force", so open up the "PlayerJumpData" Script. In here, type in "[field: SerializeField] [field: Range(0f, 10f)] public
float DecelerationForce { get; private set; }". I'll default it to "1.5f". Back into the "PlayerJumpingState" Script,
in the "Enter" method, set the "stateMachine.ReusableData.MovementDecelerationForce =
jumpData.DecelerationForce;". And that's all we need to make it less floaty,
so save it all up and go into Unity. If we enter "Play Mode", we should now be
getting to the top a bit faster. If you wanted the Player to get there faster,
you would simply make the force stronger, although, if you make it too strong,
it will reduce the Jump Height. We're almost done with our "Jumping State",
but there's one more situation to solve. If we take a look at Genshin, if we "Jump"
when we were "Sprinting", we will keep "Sprinting" after "Landing". However, this only happens if we have held
the "Sprint" key long enough. If we didn't, then we'll go back to the
"Running" or "Walking State", depending on the "Walk Toggle" value. We also do not keep sprinting with other States
like Gliding or Hard Landing or Rolling. Of course, we still don't have these in our System, but we'll make it so that it only works for the "Jumping
State". We'll be doing this by creating a "Reusable Data Property" that defines whether we should keep "Sprinting" or not. Then, in our "OnMove" method, if this property is true, we'll simply transition to the "Sprinting State". The property will only become true when we
are "Sprinting" and decide to "Jump". So, to do that, start by opening up the
"PlayerStateReusableData" Script. In here, duplicate the "ShouldWalk" property
and name it "ShouldSprint". When that's done, open up the "PlayerGroundedState" Script. In the "Reusable Methods" region, go to the "OnMove" method. In here, above everything else, type in
"if (stateMachine.ReusableData.ShouldSprint)". If that's the case, then we can transition
to the "Sprinting State" by typing in "stateMachine.ChangeState(stateMachine.SprintingState);". At the end, we "return;", as to not call the remaining
statements. When you're done doing that,
open up the "PlayerSprintingState" Script. In the "Input Methods" region,
in our "OnSprintStarted" method, type in "stateMachine.ReusableData.ShouldSprint = true;". The reason why we're adding this here instead
of in the "OnJumpStarted" method is because we only want to keep "Sprinting"
if the "Sprint" Key was held enough time, which we won't know if that's the case in
the "OnJumpStarted" method. You could do it in the "OnJumpStarted" method
by checking the "keepSprinting" variable value if you prefer it to be that way. Above in our variables, we'll create a new
variable by typing in "private bool shouldResetSprintState;". Then, when we "Enter" the "Sprinting State",
we'll set the "shouldResetSprintState" variable to "true". When we "Exit" the "Sprinting State", we'll
check "if (shouldResetSprintState)". If we should reset it, then we set our "keepSprinting" to
"false" and our "stateMachine.ReusableData.ShouldSprint"
to "false" as well. In case we shouldn't reset it, we want to
keep this variables as they were, which makes it so we start "Sprinting" once we "Land". Of course, we need to set this variable to "false" somewhere and we'll be doing that when we "Jump". So, in our "Input Methods" region, "override"
the "OnJumpStarted" method. Then, before our "base" method call, we type
in "shouldResetSprintState = false;". The reason why we are doing this before our "base" method
call is because on the base "OnJumpStarted" method
we're transitioning to the "Jumping State". This means that our "Sprinting State" "Exit"
method would be called before we set the "shouldResetSprintState"
variable value to what we want. Now, while everything seems okay so far, lets
say we "Enter" the "Jumping State" with the "ShouldSprint" property set to "true". Whenever we come back to a "Grounded State"
we'll "Enter" the "Sprinting State" due to the "OnMove" method. However, lets say that while in the "Jumping
State" we were to start "Gliding" whenever we add "Gliding" to our System. In Genshin, this makes it so that we no longer
go to the "Sprinting State" whenever we "Land". But because we are not resetting it anywhere
in our "Jumping" or "Airborne States", right now we'll always come back to "Sprinting". To fix this, open up the "PlayerAirborneState" Script. In the "Reusable Methods" region, create a
new method by typing in "protected virtual void ResetSprintState()". Inside, we'll call in
"stateMachine.ReusableData.ShouldSprint = false;". Then, call this method in the "Enter" method,
which we need to "override". I'll also add the "Enter" method to a new
region named "IState Methods". This makes it so that every time we "Enter" an "Airborne
State", the "ShouldSprint" property gets reset to "false". Of course, we don't want that to be the case
on the "Jumping State" so open up the "PlayerJumpingState" Script
and create a new region named "Reusable Methods". In here, "override" the "ResetSprintState"
method and leave it empty. Now every "Airborne State" besides the "Jumping
State" will reset the "ShouldSprint" property. However, we still have another problem. While we'll indeed keep "Sprinting" after
"Jumping" and "Landing", this currently stays true even if we stop
pressing the "Movement" Keys. In Genshin, we'll only keep "Sprinting" if
we never let go of the "Movement" Keys, or, more specifically, if a "Movement" Key
was pressed when we "Landed". If we do "Land" with no "Movement" Key pressed,
then when we move again, we'll go back to the normal "Running" or "Walking States". To fix this, open up the "PlayerGroundedState" Script. In here, "override" the "Enter" method. Inside, we'll call in a new method named
"UpdateShouldSprintState();". Inside of this method, we'll check "if (!stateMachine.ReusableData.ShouldSprint)" and "return;" if that's the case. We'll also check "if
(stateMachine.ReusableData.MovementInput != Vector2.zero)", and "return;" if that's the case as well. If none of these are "true", then we'll reset
the "ShouldSprint" property by typing in "stateMachine.ReusableData.ShouldSprint = false;". This makes it so that every time we "Enter" a "Grounded
State", if our "ShouldSprint" property is set to "true"
and there's no "Movement" Key pressed, we set the "ShouldSprint" property to "false",
which means we won't transition to the "Sprinting State". I'll also add this method to the "Main Methods" region. That's all we need to do for our "Jump". Of course, we can't really test this out until we add
"Animations", but it should be working fine. Our Player is now able to "Jump" well but if we enter Play Mode and
fall down from one of the Ramps, pressing "Space" while in the
air will make our Player "Jump". This is because right now,
when we leave the "Ground", our Player keeps being in a "Grounded State". We'll solve this problem by
adding a new State: "Falling". While I didn't intend to add "Falling"
until I introduced the "Gliding System", due to a few reasons I've decided to
add it to the first part of the series. Because it wasn't intended to be added now,
we haven't yet downloaded any "Falling Animation". To do that, open up "mixamo.com". In here, after logging in, make sure
you select your Game Character Model and then in the Animations
Tab search for "Falling Idle". We'll download it as is with "FBX for Unity"
selected and rename it to "ybot@Fall". In Unity, open up the "Airborne" Animations Folder and drag the downloaded
Animation into this folder. Then, open up the asset and select the
Animation Clip and press "Ctrl + D". We now have our Animation Clip so feel
free to remove the asset we've dragged in. Select the "Animation Clip" and check the
"Loop Time" option to loop the Animation. With that done, lets now take
a look at how Falling works. We have our "Ground Check" Collider that
we already use in our "Jumping State". This simply allows us to go back to the "Idling
State" when we touch the "Ground" again. To do that, we use the "OnTriggerEnter" method. In the "Falling" case, it
will be quite the opposite. If when we're in a "Grounded State", our
Trigger Collider "Exits" the "Ground", then we should start "Falling". When "Falling", the moment we enter
in contact with the "Ground" again, we'll go back to the "Idling State", only
until we don't have our "Landing States". We'll have to care about a few
things regarding our "Falling State" but we'll understand them later whenever
we finish the base of our "Falling". We'll start by creating the "Falling State" Script
so head over to the "Airborne" States folder. In here, create a new C# Script
named "PlayerFallingState". Open it up and remove the default methods. Then, swap the inheritance to inherit
from "PlayerAirborneState" instead. Generate the necessary constructor. We'll need to cache this State so open up
the "PlayerMovementStateMachine" Script. Duplicate the "Jumping" Property
and rename it to "Falling" instead. Don't forget to initialize it as well. Because we need the "OnTriggerExit"
method, we'll need to add it to our States, so open up the "IState" Script. In here, duplicate the "OnTriggerEnter" method
and rename it to be "OnTriggerExit" instead. When that's done, open up the
"PlayerMovementState" Script. Implement the necessary interface method
and place it under our "OnTriggerEnter" method in the "IState Methods" region. Once you're done doing that, open
up the "StateMachine" Script. In here, duplicate the "OnTriggerEnter" method
and rename the "Enter"'s to "Exit". Then, open up the "Player" Script. Under the MonoBehaviour "OnTriggerEnter",
call in the MonoBehaviour "OnTriggerExit" method. I'll rename the parameter to "collider". Inside, call in
"movementStateMachine.OnTriggerExit(collider);". We now need to check if our
Collider exited the Ground so go back to the "PlayerMovementState" Script. In the "OnTriggerExit" method, we'll start
by checking if we exited the "Ground" by typing in if (stateMachine.Player.LayerData
.IsGroundLayer(collider.gameObject.layer))". If it is a "Ground" Layer, we then call in a
new method named "OnContactWithGroundExited();", with "collider" as an argument. Make sure you "return;" right after as well. I'll make this method "protected virtual" and
add it under our "OnContactWithGround" method in the "Reusable Methods" region. You can name it "OnGroundExit" or
something else if you'd prefer, I've just named it like this to be something
close to the "OnContactWithGround" method. We'll now transition to the
"Falling State" with this method but we'll do it in our "PlayerGroundedState", as we can only exit the "Ground"
when we're "Grounded", so open it up. In here, in the "Reusable Methods" region, start by "overriding" the
"OnContactWithGroundExited" method. Inside, call in "stateMachine.ChangeState(stateMachine.FallingState);". We'll extract this line into its own
method, to which I'll name "OnFall". I'll make it a "protected virtual" method as well. This is just because we'll need
to do something with it later on. Now, while this seems fine at first glance, we're currently starting to "Fall"
the moment we leave the "Ground". This means that we'll fall even if we have "Ground" underneath
not too far from the Player feet. We don't really want that and don't mind it
being considered a "Grounded State" in this case as the distance is small, so
it won't really look weird. The "Floating Capsule" also helps it looking
better as we'll somewhat teleport down. We'll check for ground underneath using
a "Raycast", so start by typing in "Vector3 capsuleColliderCenterInWorldSpace = stateMachine.Player.ColliderUtility
.CapsuleColliderData.Collider.bounds.center;". Then, we'll create our "Ray" by typing in
"Ray downwardsRayFromCapsuleBottom", as we'll cast a ray from the bottom
of our capsule and not the center, and assign it to "= new
Ray(capsuleColliderCenterInWorldSpace, Vector3.down);". For us to get our Capsule Collider bottom, we'll get its center and then
add to it half of its height. Of course, we need to somehow get it. We'll cache this half height into
a property so go to the Script where the "CapsuleColliderData"
property is at by "Ctrl" clicking it. Then, "Ctrl + Click" the
"CapsuleColliderData" Class. If you can't do this, just go to
the "CapsuleColliderData" Script. In here, right after our
local center property, type in "public Vector3 ColliderVerticalExtents
{ get; private set; }". We've named it "Extents" because the
Collider has a "bounds.extents" variable which returns half of the Collider size and
because we only need half of the height. So, in our "UpdateColliderData"
method, now type in "ColliderVerticalExtents = new Vector3(0f,
Collider.bounds.extents.y, 0f);". We're now caching this every time our Capsule
Collider is changed through the Inspector. With that done, back to our
"PlayerGroundedState" Script, in our "Ray", we now subtract
(-) to the collider center "stateMachine.Player.ColliderUtility
.CapsuleColliderData.ColliderVerticalExtents". We need to subtract because we want the
"Capsule Collider" "Bottom" and not the "Top". For our "Raycast", we type in
"if (!Physics.Raycast())", which means we found no "Ground",
we pass in "downwardsRayFromCapsuleBottom" for the first parameter
and "out _" for the second one. This is known as "discards". The "Raycast" method requires a "RaycastHit"
here, but we won't really need it, so instead we are telling C# to use a placeholder
and say there's no value for this variable, which might even make it
so storage isn't allocated. We now need a distance from
our bottom to the ground, so open up the "PlayerGroundedData" Script. In here, duplicate our "Base Speed"
and rename it to "GroundToFallRayDistance". We'll also update the range
to be from "0f" to "5f" and default it to "1f". Back to our "PlayerGroundedState" Script,
we now pass in "movementData.GroundToFallRayDistance"
to the "Raycast". For our LayerMask, we pass in
"stateMachine.Player.LayerData.GroundLayer". And then "QueryTriggerInteraction.Ignore" as we
don't want to consider "Ground Trigger Colliders". Make sure you move the "OnFall" method
call inside of the if statement. This looks like it's over but we'll
actually need to do yet another check. Lets take a look at those 2
red obstacles we've created. We can see that there's a
small gap in between them. Now, when we exit this Obstacle we'll enter the "Falling State" as the
"Ground" is still a bit far. The problem here is that small gap. There are a few possible outcomes
from this but the main cause is "Entering" the "Falling State" because our
Raycast saw no Ground in the ray it casted, as it's a really thin ray. Problems like getting stuck in the
Obstacle or between the Obstacles are possible because even though
our Capsule Collider is big enough to pass this gap without "Falling",
our Raycast didn't realize that there was Ground a bit further,
so we started "Falling" and got stuck instead. What we'll do to fix this is cast
not only our Ray but also a Box. This makes it so if there's
"Ground" really close to each other or the Player is going to
"Fall" a really small distance, we'll keep being on the "Grounded State". The Box can have any size you want but
the best would be around the same size as the Capsule Collider or a bit smaller. We'll go with the size of our "Ground
Check Trigger Collider" for this. Of course, we don't yet have a
reference to our Trigger Collider so we'll need to create a Script to hold it. So, in Unity, head over to the
Player Scripts "Data" folder. In here, create a new folder named "Colliders". Inside, create a new C# Script
named "PlayerTriggerColliderData". Open it up and remove the
default methods and inheritance. Make the class "[Serializable]" as well. We'll create a property to hold the "Ground
Check Trigger Collider" reference so type in "[field: SerializeField] public BoxCollider
GroundCheckCollider { get; private set; }". We'll need to add this to our
"CapsuleColliderUtility" Script, but it currently is a reusable class, while
this new class is for the Player only. Because of that, we'll create another class
to hold it together with the Collider Utility. So, back into Unity, go to
the Player "Utilities" folder and create a new folder named "Colliders". Inside, create a new C# Script named
"PlayerCapsuleColliderUtility". Open it up and remove the default methods. Swap the inheritance from "MonoBehaviour"
to "CapsuleColliderUtility". Make the class "[Serializable]" as well. Then, add in a new property by typing in "[field: SerializeField] public PlayerTriggerColliderData
TriggerColliderData { get; private set; }". When that's done, go to the "Player" Script and swap our "CapsuleColliderUtility" type
with "PlayerCapsuleColliderUtility" instead. This basically allows us to add
more things into the Utility that are specific to the Player, without
needing to change anything anywhere. You could and likely should go for Composition instead, which is simply adding the
"CapsuleColliderUtility" as a property instead of it being inheritance,
but for simplicity I'll go with inheritance, as again, that makes it so we don't
really need to change anything else. If you prefer the Composition way, add it as a property and make sure you update all
usages to get there correctly. With that done, save everything and go to Unity. In the "Player" Game Object, open up the "Trigger Collider Data" Area
and assign to it the "GroundCheck" Game Object. When that's done, open up the
"PlayerGroundedState" Script. In the same method, above our
if statement, we'll type in "if (IsThereGroundUnderneath())"
and generate the method. Inside of the if statement, we'll "return;"
as that means we shouldn't "Fall". I'll add this method to the "Main Methods" region. In here, we'll check for a box
using the "OverlapBox" method. There are other methods to check for a
Box but at least the "CheckBox" method seems to count the transform we cast it
from as well, even with a Layer Mask, so we'll use the "OverlapBox" instead. This method requires a center in World Space from
us, which is where the box will be positioned. We'll use our "Ground Check" Box center for this. So, start by typing in
"Vector3 groundColliderCenterInWorldSpace = stateMachine.Player.ColliderUtility
.TriggerColliderData.GroundCheckCollider.bounds.center;". Then, we'll create a "Collider[]" named
"overlappedGroundColliders" and assign it to "= Physics.OverlapBox();". For our center, we'll pass in
"groundColliderCenterInWorldSpace". Next we have the size, but it actually
requires half of it, so pass in "stateMachine.Player.ColliderUtility
.TriggerColliderData.GroundCheckCollider.bounds.extents". We're repeating this quite
a bit so above our "Vector", create another variable of type
"BoxCollider" named "groundCheckCollider" and assign to it the "GroundCheckCollider". Then, swap it with where we use
our "Ground Check Collider". For the third argument, we need
the box rotation, so pass in "groundCheckCollider.transform.rotation". For our "LayerMask", we want
the "Ground Layer" so type in "stateMachine.Player.LayerData.GroundLayer". Pass in "QueryTriggerInteraction.Ignore"
for the last argument. Now, we can "return overlappedGroundColliders.Length > 0;", as that means we've found
ground underneath our Player. That should get our "Falling" working
whenever we exit the "Ground", but we still have a few things to do. The first one is to not allow
air movement while "Falling", which we can do by setting
the speed modifier to 0, so open up the "PlayerFallingState" Script. In here, "override" the "Enter" method. Inside, type in "stateMachine.ReusableData.MovementSpeedModifier = 0f;". I'll add this method to a new
region named "IState Methods". The second thing is to make sure
we limit our "Falling" velocity. This is because when falling, Gravity
will keep on increasing the velocity. When using ground colliders like "Mesh Colliders"
or Colliders that don't really have a lot of depth,
because our velocity can be quite high, our collider will go through the ground
at a speed that the collision won't be detected, even with our Rigidbody collisions
set to "Continuous" and all that. This means that we would simply
keep on the "Falling State" even though we have already hit the "Ground". To fix that problem, we simply
limit our vertical velocity, which Genshin seems to do as well. We can do that by adding a vertical Force to it
in a way that it doesn't go over a certain limit, so "override" the "PhysicsUpdate" method and call in a new method named
"LimitVerticalVelocity();". I'll add this method to a new
region named "Main Methods". We'll need to know what's our velocity limit,
which means we need a Data Class for "Falling". So, back in Unity, in the "Airborne" Data folder,
create a new C# Script named "PlayerFallData". Open it up and remove the
default methods and inheritance. We'll make it "[Serializable]". In here, create a new "[field: SerializeField] [field: Range(1f, 15f)] public
float FallSpeedLimit { get; private set; }". I'll default it to "15f". Higher values than this might not
work so beware when setting it higher. Then, open up the "PlayerAirborneData" Script and duplicate the "JumpData" property
and swap "Jump" with "Fall" instead. When that's done, go back to
the "PlayerFallingState" Script. In our variables, create a
new variable by typing in "private PlayerFallData fallData;" Then, in the constructor, type in
"fallData = airborneData.FallData;" Then, back to our "LimitVerticalVelocity" method, We'll check if we've reached
the speed limit by typing in "if (stateMachine.Player.Rigidbody.velocity.y
>= -fallData.FallSpeedLimit)". If that's the case, we "return". Note that our "speed limit" is turned to negative
here, so "-14" will be greater than "-15". "-16" for example, would be an
higher speed fall than "-15", but because it's a negative number,
it means that it's lower than "-15". Otherwise, if we've passed the limit, we'll set
the current Y velocity to be our max fall speed. To do that, type in
"Vector3 limitedVelocity = new Vector3(0f, -fallData.FallSpeedLimit -
stateMachine.Player.Rigidbody.velocity.y, 0f);". We're simply getting the
difference between both here. If we're falling at "-16" and the max is "-15",
then we'll add "1" to make sure we go to "-15". Because we only enter this
statement when the Player velocity is stronger than the fall speed
limit, doing the subtraction this way ensures we always receive a positive
value, not needing to use "Abs"olutes. We're also getting our velocity
twice now so above everything type in "Vector3 playerVerticalVelocity
= GetPlayerVerticalVelocity();". Then, swap the "velocity.y" lines with
"playerVerticalVelocity.y" instead. With that done, we simply need to add
the Force, so at the bottom, type in "stateMachine.Player.Rigidbody.AddForce(limitedVelocity,
ForceMode.VelocityChange);". We pass in "VelocityChange" as we don't want it
to be over time nor to depend on the Player mass. That solves one bug so we only
have 2 things left to do or solve. The first one is to transition
from "Jumping" to "Falling". This is because when we "Jump" right now, we'll never start "Falling" as there's
nothing telling us to change States. Now, we could have this happen in 2 possible ways. The first one is by setting an
Animation Transition Event on the frame that the "Jump Animation" gets to the top. However, while this may seem fine
at first glance, there's a problem. Lets say we "Jump" and there's an
obstacle right above our Player head. When we hit this Obstacle we'll
start moving down due to Physics but because our "Jump Animation"
still hasn't reached the top frame, we'll be falling while in the "Jumping State". The second way of doing it is the
way we'll be fixing this problem: Simply transition to the "Falling State"
whenever our velocity becomes negative, as that means we're "Falling". So, start by opening up the
"PlayerJumpingState" Script. In here, in the "IState Methods"
region, "override" the "Update" method. Inside, we'll check "if
(GetPlayerVerticalVelocity().y > 0)" and "return;" if that's the case. Otherwise, we can transition to
the "Falling State" by typing in "stateMachine.ChangeState(stateMachine.FallingState);". This is basically what it's
normally needed to transition from the "Jumping State" to the "Falling State", but, we need to keep in mind that we're
using the "Floating Capsule" technique. This technique tries to keep the "Capsule
Collider" afloat by adding a Force to it. This is needed because the "Capsule Collider"
would otherwise fall due to Gravity. This means that our velocity
will always become negative until the next "AddForce" method is called. Because of that, if we leave
things as they are right now, it will "Enter" the "Falling
State" right after we "Jump". Thankfully, it's quite simple to solve that. In our variables, create a new variable by typing in
"private bool canStartFalling;". Then, in our "Update" method, we'll check
"if (!canStartFalling && IsMovingUp())". We'll also pass in "0f" to the "IsMovingUp" method to make sure we check for the
actual moment it starts going up. If that's the case, we'll set
the "canStartFalling = true;". Then, in our state change if statement,
we'll add in "!canStartFalling ||" The transition will now only
happen if we "canStartFalling" and the Player velocity is
under "0", or, negative. Of course, because we're caching in our States, we need to make sure we reset this
variable to false when "Exiting". So, in our "Exit" method, simply
type in "canStartFalling = false;". With that, we should have our transition
to the "Falling State" working correctly. However, I had a problem when falling off ramps that
we would sometimes just randomly Jump. I don't really remember the cause,
but I remember that to fix it, I had to reset our vertical velocity
when we "Entered" the "Falling State". We don't reset our horizontal velocity because
we still want to keep our Horizontal Momentum. So, open up the "PlayerFallingState" Script. Then, in our "Enter" method, type
in "ResetVerticalVelocity();". Of course, we don't still have this method,
so open up the "PlayerMovementState" Script. Go to our "Reusable Methods" region
to our "ResetVelocity" method. Under it, create a new method by typing in
"protected void ResetVerticalVelocity()". Inside, we'll make it so that our
vertical "velocity" becomes "0", so start by typing in "Vector3 playerHorizontalVelocity =
GetPlayerHorizontalVelocity();". Then, set the "stateMachine.Player.Rigidbody.velocity
= playerHorizontalVelocity;". The "GetPlayerHorizontalVelocity" method returns a Vector
with the "y" value as "0", which makes it so we're resetting the vertical
velocity while keeping the rest being the same. If we now go to the "PlayerFallingState" Script,
the method should no longer be throwing an error. This should be enough so now
there's only one thing left to do, which is to "override" the
"ResetSprintState" method and leave it empty. I'll add this method to a new
region named "Reusable Methods". This override is required because now our "Jumping
State" only happens until we're at the top, so we need to make sure we keep
sprinting after we "Fall" as well. Of course, because we can "Jump"
and go to the "Idling State" in certain situations where we
"Jump" to an higher leveled "Ground", we can keep the "JumpingState" "override" as well. However, we need to remember that we can also go
from a "Grounded State" to the "Falling State". This means that in our "Sprinting State",
we also need to set the "shouldResetSprintState" variable to "false" when we start "Falling",
much like what we did for when we "Jumped". Thankfully, we've previously created
a "virtual" method named "OnFall" that will make it easy for us to do that. So, open up the "PlayerSprintingState" Script. In here, in the "Reusable Methods" region,
"override" the "OnFall" method. Before we call the "base" method, we simply type in
"shouldResetSprintState = false;". This means that if our
"ShouldSprint" becomes "true", it will now not be reset when we fall. Note that the "ShouldSprint"
should only become "true" if the "Sprint" Key was held long enough,
which is how it works in Genshin and is how it's working for us as well. That should be all that's needed for "Falling",
so save it up and go to Unity. Entering Play Mode, we should
now be able to "Fall" correctly. With our "Falling State" done, we can now
add in our last States, which are the "Landing States". Originally, the "Light Landing State" was
the only "Landing State" that we would add in the first part of the Series, but with the addition of the "Falling State",
I think it's fair if we add the other 2 "Landing States" as
well. Of course, for the same reason as our "Falling
State", we haven't yet downloaded the 2 extra "Landing States Animations", so
we'll need to do it now. Start by opening up "mixamo.com". Make sure you're logged in and your Character
Model is selected. Then, in the "Animations" tab, search for: "Hard Landing". Download it with the "FBX For Unity" option selected. I'll rename it to not contain the white space it comes with. And also search for "Sprinting Forward Roll". Download it with the "In Place" option enabled. I'll rename it to "ybot@Roll". Of course, "ybot" here would be your selected
Character Model name. When that's done, in Unity, go to the "Landing"
Grounded Animations folder. Then, drag the two downloaded Animations to this folder. Open both by clicking on the arrow key and
"Ctrl + Click" both "Animation Clips". Then, press "Ctrl + D" to duplicate them. Once that's done, remove the imported Assets
as we no longer need them. Regarding our "Landing States", we now know
that we have 3 possible States: "Light Landing", which happens when we fall
from a small Height, like after "Jumping" or when touching the
"Ground" from "Gliding". "Hard Landing", which happens when we fall
from a higher Height. And "Rolling", which also happens when we
fall from a higher Height but only if we have a "Movement" Key being pressed. With that in mind, lets start by creating their Scripts. To do that, go to the Player "Grounded" States Scripts
folder. In here, create a new folder named "Landing". Inside, we'll create 4 new C# Scripts: "PlayerLandingState", "PlayerLightLandingState", "PlayerRollingState", and "PlayerHardLandingState". Start by opening up the "PlayerLandingState" Script. Remove the default methods and swap the inheritance
from "MonoBehaviour" to "PlayerGroundedState". Generate the necessary constructor. When that's done, go to all of the other "Landing States" and remove their default methods and swap
their inheritance with "PlayerLandingState" instead, generating the necessary constructor for each one of them. When that's all done, we'll cache our States
so open up the "PlayerMovementStateMachine" Script. In here, we'll create 3 new properties, one
for each of the "Landing States". To do that, type in
"public PlayerLightLandingState LightLandingState { get; }". Then, duplicate this property twice and swap
the "LightLandingState" with "PlayerRollingState"
and "PlayerHardLandingState". Don't forget to initialize them as well. When you're done, go back to the "PlayerLandingState"
Script. We will transition to a "Landing State" from
an "Airborne State", which means we can "Enter" the "Landing State"
with a "Movement" Key pressed. That means that if we let the Key go,
our "OnMovementCanceled" method callback will be called and we'll transition to the "Idling State". Of course, we only want to transition to the "Idling State" at the end of their animations. So, to remove the current behaviour,
"override" the "OnMovementCanceled" method and leave it empty. I'll also add it to a new region named "Input Methods". The next thing we'll do is to update the current
"Idling State" transitions that were supposed to be to the "Light Landing State". We only need to do it in the "Jumping" and "Falling States". Thankfully, we've added that transition in
our "Airborne State", so open it up. In here, in the "OnContactWithGround" method,
swap the "IdlingState" with "LightLandingState" instead. Of course, we don't really do anything in
our "Light Landing State" yet. The first thing we'll do is make sure we can't
move while in this State, so open it up. In here, "override" the "Enter" method. Then, set the
"stateMachine.ReusableData.MovementSpeedModifier = 0f;". I'll add this method to a new region named
"IState Methods". The reason why we're not doing this in the
"Landing State" is because "Rolling" will have an actual speed,
as we'll be able to move while "Rolling". I'll also reset the velocity when we "Enter" this State as we want to make sure we stand still and
don't keep any previous velocity. So, call in "ResetVelocity();". And that's all we really need regarding the
"Light Landing State" logic, so all that's left to do here is to set the
transitions from this State. If we take a look at our "Movement System States", we can transition from the "Light Landing State" to: "Idling", all "Moving States", "Dashing" and "Jumping". When the "Light Landing State" reaches its
last frame, it will transition to the "Idling State". Whenever a "Movement" Key is pressed while
in the "Light Landing State", it will transition to the respective "Moving
State", including the "Sprinting State". At any given moment of the "Light Landing
State", we can "Dash" or "Jump". With that in mind, lets transition to the
"Idling State" by "overriding" the "OnAnimationTransitionEvent" method. Inside, type in
"stateMachine.ChangeState(stateMachine.IdlingState);". Make sure you remove the base method call. For our "Moving" States, we'll have to follow
the "Update" method approach, as we'll be getting here through an "Airborne State" like
"Jumping". So, above, "override" the "Update" method. Inside, check "if (stateMachine.ReusableData.MovementInput
== Vector2.zero)" and "return;" if that's the case. Otherwise, we call in "OnMove();". For our "Dashing" and "Jumping" transitions,
because a "Landing State" is a "Grounded State", they are already set up. That's all for our "Light Landing State",
so we can now do our "Hard Landing State". In Genshin, if we "Fall" from a high enough height and
"Land", if there's no "Movement" Input Key being pressed,
we'll enter the "Hard Landing State". In this State, much like our "Light Landing State", we don't nor can "Move" while in it. Lets start by doing that by opening up the
"PlayerHardLandingState" Script. In here, "override" the "Enter" method. Inside, we'll set the
"stateMachine.ReusableData.MovementSpeedModifier = 0f;". We'll also call in "ResetVelocity();" to reset
any velocity we had before "Landing", making sure we don't accidentally move horizontally
due to Physics. I'll add this method to a new region named
"IState Methods". That's most of the necessary logic for our
"Hard Landing State" and the only remaining logic is regarding
the transitions to other States. Because only the "Falling State" will transition
to the "Hard Landing State", we'll do that transition after we also add the "Rolling
State". Regarding Transitions to other States, if
we take a look at our "Movement System States", we'll be able to transition to: "Idling", "Running" and "Dashing". Whenever the "Hard Landing State" reaches its final frame, the Player will transition to the "Idling State". Regarding the "Running State", the way the
"Hard Landing State" handles moving to a "Moving State" is somewhat different. Whenever we first "Land", we'll actually not be able to
"Move", or, press a "Movement" Input Key. However, at a certain frame of its animation,
we'll be able to start "Moving" by pressing a "Movement" Input Key. This simply means that we'll have to disable
the Input for a few frames. One thing to note as well is that we cannot
transition to the "Walking State" and only the "Running State". And last but not least, we can "Dash" at any
given moment of the "Hard Landing State". Regarding "Jumping", we cannot "Jump" again
while we're on the "Hard Landing State". With that in mind, we'll start with the "Idling State"
transition by "overriding" the "OnAnimationTransitionEvent" method. Inside, we'll remove the "base" method call and type in "stateMachine.ChangeState(stateMachine.IdlingState);". For our "Moving States" transition, we'll start by "overriding" the "OnMove" method. I'll add this method to a new region named
"Reusable Methods". Then, we'll remove the base method call and then check "if (stateMachine.ReusableData.ShouldWalk)"
and if that's the case, we "return;". Otherwise, we'll call in
"stateMachine.ChangeState(stateMachine.RunningState);". To call the "OnMove" method we'll use the
"Movement" "started" callback. Now, we can do this here even though we can
come from the "Falling State" because we'll be disabling and enabling the
"Movement" Input, which means the "started" action will be called
when enabling again if a Key is being pressed. So, above our "OnMove" method, "override" the "Add" and
"RemoveInputActionsCallbacks" methods. Then, add a new callback by typing in "stateMachine.Player.Input.PlayerActions.Movement.started +=
OnMovementStarted;". Don't forget to remove it as well. I'll add this method to a new region named
"Input Methods". I'll also rename the parameter to "context". Inside, we simply type in "OnMove();". Regarding our "Dashing State" transition,
it's already coded in the "Grounded State". Now, right now, we are able to "Jump" because of our
"OnJumpStarted" method in the "Grounded State". So, in our "Input Methods" region, "override"
the "OnJumpStarted" method. Then, simply leave it empty, meaning we won't
transition to the "Jumping State". Regarding our "Movement" Input, we'll Disable
it when we "Enter" this State and then Enable it both when "Exiting" the
State and at a certain frame of its Animation. We need to make sure we also enable it when "Exiting" because it's possible that we never reach
the defined Animation frame, in case we decide to do something like "Dash",
which we can at any given frame. So, to do that, in the "Enter" method, type in "stateMachine.Player.Input.PlayerActions.Movement.Disable();
". Then, "override" the "Exit" method and type in
"stateMachine.Player.Input.PlayerActions.Movement.Enable();"
. Copy this line and now
"override" the "OnAnimationExitEvent" method. We'll use this one to enable the Input at
a certain frame of the Animation, so remove the "base" method call and paste
the line we've copied inside. And that's all that's needed for our "Hard Landing State", so next we'll do our "Rolling State". Of course, lets first understand what this "Rolling State"
is. In Genshin, if we "Fall" from a high enough height and
"Land", if there's any "Movement" Input Key being pressed, we'll "Roll" instead of "Hard Landing". In this "Rolling State", we can also "Move"
around while "Rolling". With that in mind, we know we'll need 2 things:
A Movement Speed & a Rotation Data. Thankfully, it uses the same rotation as our
normal "Movement", so we don't really need to do anything extra regarding it. We'll however, still need our movement speed modifier, so we'll need to create its Data class. To do that, go back into Unity. In here, in the Player "Grounded" Data folder,
create yet another folder named "Landing". Inside, we'll create a new C# Script to which
I'll name "PlayerRollData". Open it up and remove the default methods and inheritance. We'll make the class "[Serializable]" as well. Then, type in "[field: SerializeField] [field: Range(0f, 3f)] public float
SpeedModifier { get; private set; }". I'll default it to "1f". We'll then open up the "PlayerGroundedData" Script. Duplicate the "Stop" property and swap "Stop"
with "Roll" instead. With that done,
we can now open up the "PlayerRollingState" Script. In here, create a new variable by typing in
"private PlayerRollData rollData;". Then, in the constructor, type in
"rollData = movementData.RollData;". We can now set our speed modifier,
so start by "overriding" the "Enter" method. Inside, type in "stateMachine.ReusableData.MovementSpeedModifier =
rollData.SpeedModifier;". I'll add this method to a new region named
"IState Methods". Now, our "Rolling State" will keep "Rolling"
until its last frame even if we stop pressing any "Movement" Key. This of course means that if we fast press a Key, our Player will stop Rotating as the "Move"
method isn't being called anymore. Because of that, we'll need to add an Automatic Rotation to
it. Thankfully, that's quite simple to do here,
so start by "overriding" the "PhysicsUpdate" method. Inside, we simply call in "RotateTowardsTargetRotation();". That should take care of it, but let's also add an "if (stateMachine.ReusableData.MovementInput
!= Vector2.zero)" and "return;" inside. This ensures we only rotate when we are not
calling the "Move" method, as our previous implementation would call the
"RotateTowardsTargetRotation" method twice while "Moving". Right now, because we are able to "Move" in the "Rolling
State" and also because we can only "Enter" here
if we were pressing a "Movement" Input Key, our "Grounded State" "ShouldSprint" if statement
will never "Enter" due to its condition. However, in Genshin, we do not keep sprinting after
"Rolling". To fix this, we'll just go to our "Enter" method and type in "stateMachine.ReusableData.ShouldSprint =
false;". This should take care of the problem
and we should no longer keep sprinting once we "Enter" the "Rolling State". We'll now take a look at the transitions from
the "Rolling State". Regarding transitions to the "Rolling State",
much like the "Hard Landing State", we can only come here from the "Falling State",
so we'll add that transition when we're finished doing the "Rolling State". Regarding transitions from this State, if
we take a look at our "Movement System States", we can transition to: "Walking", "Running", "Dashing" and "Medium Stopping". Whenever the "Rolling State" reaches its last frame, if there's no "Movement" Input Key being pressed,
it will transition to the "Medium Stopping State". If it reached its last frame but we had a
"Movement" Input Key being pressed, then we would transition either to the "Walking"
or the "Running State" depending on the "WalkToggle" value. In Genshin, it seems that you transition to
the "Walking State" a bit earlier, but I didn't like the way it looks so I decided
to transition at the last frame. If you do want to completely replicate it,
then you could "override" another "Animation Event" like the "Enter"
or "Exit" method and do that transition there. I say "Enter" or "Exit" because we'll already
be using the "Transition" Event to transition at the last frame, so unless
you kept a variable that would check if we reached the last frame already, it would
possibly transition to the other States earlier as well. Feel free to do it as you prefer. We can also "Dash" at any given moment of
the "Rolling Animation". Much like our "Hard Landing State", we cannot
"Jump" in the "Rolling State". With that in mind, lets start by setting our
Animation Transition Event by "overriding" the "OnAnimationTransitionEvent" method. Inside, remove the "base" method call and
start by checking "if (stateMachine.ReusableData.MovementInput
== Vector2.zero)". If that's true, we can then transition to
the "Medium Stopping State" by typing in "stateMachine.ChangeState(stateMachine
.MediumStoppingState);". Of course, "return;" right after to not call the rest of the
code. Otherwise, if we are "Moving", then we'll just call in
"OnMove();". Because we're setting the
"ShouldSprint" property to "false" above, the "OnMove" here will never transition to
the "Sprinting State", so we don't need to "override" it. Because we're only adding this transition at the last frame, you could also very likely simply transition
to the "Medium Stopping State" and let it take care of the transitions to
the "Moving States". However, I'll leave them here as they are right now. The "Dashing State" is of course already done
in our "Grounded State". For our "Jump", lets "override" the "OnJumpStarted" method and then leave it empty, as we don't want to be able to
"Jump". I'll also add this method to a new region
named "Input Methods". With that done, all that's left is to add the transition from the "Falling State" to the "Landing States". To do that, open up the "PlayerFallingState" Script. The way the transitions from the "Falling
State" to the "Landing States" work is as follows: If we fall from a small height, we'll transition
to the "Light Landing State". If we fall from a bigger height, we'll transition
to the "Hard Landing State" or the "Rolling State" depending on whether we are pressing
a "Movement" Input Key or not. This means that we need to do 2 things: Get the distance that the Player fell
and override the "OnContactWithGround" method, as right now it's only transitioning to the
"Light Landing State". Knowing that, in the "Reusable Methods" region,
start by "overriding" the "OnContactWithGround" method and remove the "base" method call. We now need to know our distance. The way we'll be knowing it is by saving the
position of the player when it "Entered" the "Falling State" and then the position of the Player
when it collided with the "Ground". Knowing both, we can then subtract the "Y"
value of one with the "Y" value of the other, which gets us the vertical distance between both positions. Of course, we should also always make this value positive. This is because the "Jumping State" transitions
to the "Falling State" when it reaches its top point, which means we can "Fall" into a "Ground" that was above our previous "Ground", which
gets us a negative distance. So, knowing that, in our variables area, create
a new variable by typing in "private Vector3 playerPositionOnEnter;". Then, in our "Enter" method, we type in
"playerPositionOnEnter =
stateMachine.Player.transform.position;". When that's done,
go back to our "OnContactWithGround" method. In here, we'll get the distance by typing in "float fallDistance = Mathf.Abs();", for "Absolute", which makes it become positive
when the subtraction returns negative and pass in "playerPositionOnEnter.y -
stateMachine.Player.transform.position.y;". This gets us the difference between the player
position on "Enter" and its current position. When that's done, we now need to know what's
the distance to be considered a "small" or "big" fall. So, to know that, open up the "PlayerFallData" Script. In here, duplicate the existing property
and name it "MinimumDistanceToBeConsideredHardFall". I'll also make the range be from "0f" to "100f"
and then default its value to "3f". When that's done, go back to the "PlayerFallingState"
Script. In our "OnContactWithGround" method, now check "if (fallDistance If that's the case, we'll call in
"stateMachine.ChangeState(stateMachine.LightLandingState);". We could instead call the "base" method here
as it does the same as this transition line, but, we won't because the "base" method can
be changed at any moment to contain something else. We of course need to "return;" right after
to not call the remaining code. Under this, if we hit the minimum distance,
we'll either transition to the "Hard Landing State" or the "Rolling State". We'll start with the "Hard Landing State" by typing in "if (stateMachine.ReusableData.MovementInput ==
Vector2.zero)". Inside, we call in
"stateMachine.ChangeState(stateMachine.HardLandingState);". We "return;" right after as well. Otherwise, if we have movement, we'll transition
to the "Rolling State" by typing in "stateMachine.ChangeState(stateMachine.RollingState);". And that's all we need for our transitions. With this, our "Landing States" should now be working. As a tip, if you wanted to add "Fall Damage" to your Player, this would likely be the Place for you to do it. Now, regarding our transition to the Landing States, in Genshin, if we have the "WalkToggle" on,
we'll not be able to transition to the "Rolling State". However, that's only true if we're not "Sprinting". If we are, then we'll be able to transition
to the "Rolling State". So, in our "Hard Landing State" Transition
if statement, add in "stateMachine.ReusableData.ShouldWalk &&
!stateMachine.ReusableData.ShouldSprint ||". This makes it so that if we have the "WalkToggle" on and are not "Sprinting", we'll transition
to the "Hard Landing State". One last thing to do is to set the Jump Force
to the Stationary Force when we Light Land, so open up the "PlayerLightLandingState" Script. In here, in the "Enter" method, we type in "stateMachine.ReusableData.CurrentJumpForce =
airborneData.JumpData.StationaryForce;". Saving and going back to Unity,
entering Play Mode should allow us to "Fall" and "Land"
well. Of course, until we add Animations, we won't
be able to fully test our "Landing" System. There's now one last feature we need to take care of, which we've left to do later when we added our Player
Camera. If you don't remember it by now,
I've previously said that we would be enabling the Virtual Camera "Horizontal Recentering" option, which was the option
that the "FreeLook Camera" didn't offer us. Now, what exactly is this "Horizontal Recentering" option and why do we really need it? Lets take a look at Genshin Impact. Currently, we're facing forwards while in
the "Idling State". Lets say that we start moving to one of the sides, right or
left. The moment we do so, we can notice that our
Player slowly rotates and moves in a circular motion. We can further notice this if we look down on the Player at around a 90 degrees angle and start moving. What happens is that our Player will rotate extremely fast. This happens even if we move "backwards"
while we're looking down. This is achievable through the "Horizontal Recentering"
option. Simply put: it automatically recenters our
"Camera" at a certain speed, also allowing us to set a time it should wait
before it starts recentering. The speed of the Recenter is called "Recentering Time", which is how long it takes to recenter the Camera. The time it should wait before it starts Recentering
our Camera is called "Wait Time". Of course, our Player only rotates
and moves in a circular motion because its movement is dependent
on the Camera "Horizontal Axis". So by Recentering the Camera "Horizontally",
or, in the "Horizontal Axis", our Player will start rotating itself and
move with that Rotation in mind. The higher the "Recentering Time", the longer
it takes to Recenter the Camera, which means the slower the Player Rotates. Of course, as we've seen before, different
angles can have different settings or even enable or disable the Recentering option. For example, moving "backwards" while looking
forwards disables the option while moving "backwards" while looking up
or down enables it. To do this, we'll need to know the direction we're moving, which we can know through the "Movement Input" variable and also the settings we want for each angle in each
direction. To know those settings
we'll actually not use "Animation Curves" and use our own class this time,
in which we can simply have a list of angle ranges and their Recentering values. This is because I haven't found a way to have
2 different values in one Animation Curve, which would require us to have 2 "Animation
Curves", one for each value, which I found somewhat unnecessary
as the values are updated for the same ranges. You are free to do it using 2 "Animation Curves" if you
prefer to. With that in mind, we'll start by creating
the class that will hold this Data. To do that, go to the Player "Data" folder. In here, create a new folder named "Cameras". Inside, create a new C# Script to which I'll
name "PlayerCameraRecenteringData". When that's done, open it up. Remove the default methods
and the "MonoBehaviour" inheritance and make this class "[Serializable]". We'll create the angle ranges by typing in "[field: SerializeField] [field: Range(0f, 360f)] public
float MinimumAngle { get; private set; }" Then, duplicate this property and name it
"MaximumAngle" instead. Next we'll need one property for each of our Recentering
values, so duplicate the last property. I'll make the range be from "-1f" to "20f"
and rename it to "WaitTime". Then, duplicate this new property and rename
it to "RecenteringTime" instead. We made the minimum range value "-1" as a
way to specify that we want to use the default setting
value, which we'll define later. Next we need to create a method that tells
us if we're within the angle range, so type in "public bool IsWithinRange(float angle)". Inside, we simply
"return angle >= MinimumAngle && angle When that's done, open up the "PlayerGroundedData" Script. We'll be creating 2 "Lists" of our class,
one for Recentering Settings when moving Sideways and another one for Settings
when moving Backwards, or "Downwards". So, to do that, type in "[field: SerializeField] public List SidewaysCameraRecenteringData { get; private set; }". Make sure you import the necessary namespace. Then, duplicate the property
and swap "Sideways" with "Backwards", or "Downwards" if you prefer. When that's done, save everything and go back to Unity. Open up the second "Inspector" tab. In our "Sideways Recentering Data", we need
2 ranges, so add 2 elements to the List. The first one will be from "0" to "80", to
which we'll pass in "-1" for both settings. This of course means they'll use the default
values we'll define later. The second one will be from "80" to "90", to which we'll pass in "0" for the "Wait Time"
and "0.3" for the "Recentering Time". That's it for our "Sideways Data", so for
our "Backwards Data", we'll be having 1 range, so add in 1 element to the List. The range will be from "80" to "90" degrees, to which we'll pass in "0" for the "Wait Time"
and "0.5" for the "Recentering Time". The "Horizontal Recentering" is disabled for
any other angle range when moving "backwards". Also note that while we're setting the "Horizontal
Recentering", these angle ranges are for the Camera "Vertical Angles". With that done, we'll set the Player Camera
settings to their default values. So, select the "PlayerCamera" Game Object in the "Hierarchy" and open up the "Aim" Area. Don't forget to go to the first "Inspector" tab. In here, set the default "Horizontal Recentering"
settings to "0" and "4". These are not really necessary as we'll be
overriding them later, but it's always nice to have them set to the
default values right away. We'll be defining these default values somewhere else as
well, which will be in an Utility Script we'll be creating. This Utility Script will allow us to enable or disable our "Horizontal Recentering" option whenever we want to. To do that, open up the Player "Utilities" Scripts folder. In here, create a new folder named "Cameras". Inside, we'll create a new C# Script to which
I'll name "PlayerCameraUtility". Open it up when you're done. Remove the default methods and inheritance
and make it "[Serializable]". To get a reference to our "Horizontal Recenter" option, we'll need a reference to our Camera "POV" Aim. To get that, type in
"private CinemachinePOV cinemachinePOV;". Make sure you import the "Cinemachine" namespace. Above, we'll get a reference to our Camera by typing in "[field: SerializeField] public CinemachineVirtualCamera
VirtualCamera { get; private set; }". We'll also have a property for our default values, so type
in "[field: SerializeField] public float
DefaultHorizontalWaitTime
{ get; private set; }" and a default value of "0f". Then, duplicate this property
and swap "Wait" with "Recentering" instead and default it to "4f". To get our "POV" reference, we'll create an
Initialization method, so type in "public void Initialize()". Inside, type in "cinemachinePOV =
VirtualCamera.GetCinemachineComponent();". All that's left now is to create a method
for Enabling and Disabling our "Recentering" option, so start by typing in "public void EnableRecentering()". Pass in "float waitTime = -1f" as the first parameter and "float recenteringTime = -1f" as the second parameter. Then, under the Enable method, create another one by typing
in "public void DisableRecentering()". To enable our option, we'll type in
"cinemachinePOV.m_HorizontalRecentering.m_enabled = true;" We'll then copy this line and paste it in
the "Disable" method, swapping "true" with "false" instead. That enables and disables our option,
but when enabling it, we also need to set our setting
values, as we'll need them if that's the case. The first thing we'll do is actually cancel
any existing recentering that the camera is doing by typing in
"cinemachinePOV.m_HorizontalRecentering.CancelRecentering();
". I'm not entirely sure if Genshin does this or not, but I'll leave it as I think it fixed some
weird Recentering that happened without this. For our values, we'll first need to check
if they're "-1f" and if they are, we'll set them to their default values. To do that, start by checking "if (waitTime == -1f)" and if that's the case, we type in
"waitTime = DefaultHorizontalWaitTime;". When that's done, duplicate this whole if statement and swap the "wait" parts with "recentering" instead. To set our "Horizontal Recentering" option values, we now type in "cinemachinePOV.m_HorizontalRecentering.m_WaitTime =
waitTime;". Then, duplicate this line
and swap "Wait" with "Recentering" instead. And that's all we need to enable or disable
our "Horizontal Recentering" option. We'll now add this utility as a property of
the "Player" Script, so open it up. In here, create a new property by typing in "[field: SerializeField] public PlayerCameraUtility
CameraUtility { get; private set; }". I'll also add in a "[field: Header("Camera")]". Then, in the "Awake" method, we need to "Initialize"
it by typing in "CameraUtility.Initialize();". This simply sets our "CinemachinePOV" Component. When that's done, save it all up and go to Unity. In here, select the "Player" Game Object and select or drag
the "PlayerCamera" to the "Virtual Camera" field. That's our Camera Utility done, so we can now start thinking
on actually Recentering our Camera. To do that, open up the "PlayerMovementState" Script. We'll create 2 methods here that will call
our "Enable" and "Disable Recentering" methods, just so that we do not call in a big line every time and also because we'll need to do something
else later on when Enabling it. To do that, go to our "Reusable Methods" region and type in "protected void EnableCameraRecentering(float
waitTime = -1f, float recenteringTime = -1f)". Inside, call in "stateMachine.Player.CameraUtility
.EnableRecentering(waitTime, recenteringTime);". Then, under this method, we'll create the
"Disable" method by typing in "protected void DisableCameraRecentering()". Inside, we call in
"stateMachine.Player.CameraUtility.DisableRecentering();". That's all that's needed for these 2 for now,
so lets now create a method to update our Recentering
values. Above, type in
"protected void UpdateCameraRecenteringState()" and accept a "Vector2" named "movementInput". We'll understand in a bit why we'll be passing in a variable
instead of using the "MovementInput" property right away. We'll have to disable or enable our
"Horizontal Recentering" option depending on both the Camera Angle and our Movement Input. There's a situation that disabled our Recentering
option which is when we stop moving. Instead of doing this here, we'll actually
do that in the "OnMovementCanceled" option. However, we only have one in our "Grounded
State" and none in our "Movement State", so we'll have to create another. To do that,
head over to the "AddInputActionsCallbacks" method. Inside, add in a new callback by typing in "stateMachine.Player.Input.PlayerActions.Movement.canceled
+= OnMovementCanceled;". Don't forget to remove it as well. I'll rename the parameter to "context"
and then add this method to the "Input Methods" region. Because we'll leave it as "private", it won't
conflict with the "Grounded State" method. Inside, call in "DisableCameraRecentering();". Back into our "UpdateCameraRecenteringState" method, we'll still "return" if there's no Movement Input, so type
in "if (movementInput == Vector2.zero)" and "return;"
if that's the case. Next, we'll need to disable it when moving forwards, as it sometimes seemed to give us a weird Recentering even though it's supposed not to Recenter in this situation, so type in "if (movementInput == Vector2.up)"
and call in "DisableCameraRecentering();", "return;"ing right after as well. Then, we'll do it for when moving "backwards" so type in "if (movementInput == Vector2.down)". In this case, we'll need to iterate through our "Backwards"
list and see if it's within the ranges, Disabling in case it
isn't. Of course, to do that, we need our camera
angle, so above, outside of the if statement, type in "float cameraVerticalAngle =
stateMachine.Player.MainCameraTransform.eulerAngles.x;". Note that "x" is the Camera Vertical Angle. Now, we'll need to know if the range is between "-90" and
"90", as looking up has the same effect as looking down. However, we're only checking for positive angles in our
lists, so we'll have to convert the negative values
to positive using Absolutes. To do that, type in
"cameraVerticalAngle = Mathf.Abs(cameraVerticalAngle);". The reason why we're doing this here instead of above is because we'll need to add something else before we do
this. Our "eulerAngles" only return positive values. That means that "-90" will be returned as "270" and so on. However, we want to make it "90" instead of "270" when making them positive. So, we'll simply make it negative again, by
typing in, above our second assignment, "if (cameraVerticalAngle >= 270f)". If that's the case, we subtract "360" degrees
to the angle by typing in "cameraVerticalAngle -= 360f;". This should now get us an angle between "-90"
and "90" and then make it positive so that we can compare it with our list angle ranges. We can now iterate through our settings list,
so inside of the if statement, type in "foreach (PlayerCameraRecenteringData recenteringData
in movementData.BackwardsCameraRecenteringData)". Inside, we'll check "if
(!recenteringData.IsWithinRange(cameraVerticalAngle))" and "continue;" to the next one if that's the case. Otherwise, we call in "EnableCameraRecentering(recenteringData.WaitTime,
recenteringData.RecenteringTime);". At the end, we "return;" to exit the loop
and method, as we've found the desired angle range already. If we did end up outside of the loop, then
it means that we didn't find any setting with this angle, so we call in "DisableCameraRecentering();"
and "return;" right after. That's all there is for setting our values. We'll need to do this exact thing with our "Sideways" list, so select the code we've just added except
the last "return;" statement and extract it to a new method. If you can't extract it, remove the "return;" of the
"foreach" loop for a bit while you extract it. I'll name it "SetCameraRecenteringState();". I'll also pass in the list of the backwards
recentering data and add it as a parameter. Then, I'll rename the parameter to "cameraRecenteringData"
and import the "List" namespace. When that's done, swap the "Backwards List"
usage with the "cameraRecenteringData" parameter. Don't forget to add the "return;" statement
back in our "foreach" loop as well. I'll make this method "protected", just in
case we ever need to reuse it somewhere. All that's left to do now is to add the setting
for when we're moving Sideways. Because we've tested all other directions
and also because diagonal directions seem to use the same settings as the Sideways directions,
we can just copy the method call we did for our "Backwards" direction
and paste it in right after the if statement without checking for anything. We'll then simply swap "Backwards" with "Sideways"
to use our "Sideways Data" instead. And that's really all we need to do for our
"UpdateCameraRecenteringState" Method. Of course, we still need to call this method somewhere. We'll call it both when our "Movement Input" has changed and whenever our mouse has moved,
as both of these mean that either our Camera Angle or our Player Direction have changed. So, in our "AddInputActionsCallbacks" method,
in the "Reusable Methods" region, we'll add 2 new calbacks: "stateMachine.Player.Input.PlayerActions.Look.started +=
OnMouseMovementStarted;". And then, "stateMachine.Player.Input.PlayerActions.Movement.performed
+= OnMovementPerformed;". Make sure you remove both callbacks. I'll rename both parameters to "context"
and then add both methods to the "Input Methods" region. Again, the difference between "started" and
"performed" in our "Movement" is that "started" would only be called on the first key
press, while "performed" means it will call on every
key press, like going from "W" to "WD". Our "mouse started" will be called quite a few times though, likely because of the way "Delta" or the mouse works. In the "OnMouseMovementStarted" method, we'll call in "UpdateCameraRecenteringState();" and pass
in "stateMachine.ReusableData.MovementInput". In our "OnMovementPerformed" method, we'll
call in "UpdateCameraRecenteringState();". This callback here is the reason why we needed
to add the "Movement Input" as a parameter instead of using it right away. We're currently on the callback of the "Movement" action. This means that the "Movement Input" we're
reading in the "Update" method isn't yet updated to the value that we have in this
callback, as this callback is called before setting the value. This of course means that using the "MovementInput" property in our Recentering methods, would update the
"Recentering State" using the old "MovementInput" value. Thankfully, we can easily get the updated
value here from the "context" variable. To do that, pass in "context.ReadValue();". With that done,
our Camera Recentering should now be working, so save it all up, go back to Unity and enter Play Mode. Moving the camera around in the vertical axis
or changing movement directions should now move and rotate the Player according
to the Settings List. However, we currently have a few problems
in our implementation. The first problem you might've noticed is
that our Camera Recentering Rotation Speed seems to depend in our current Player Speed. If we change between Walking, Running and Sprinting, we change how fast our Player or Camera rotates. We'll be fixing this by making our "Recentering
Time" be dependent in our Player Speed. The way we'll do that is quite simple. Let's say that for the Speed of "5", which
is our current Base Speed, we have a Recentering Time of
"4". This means that for a Speed of "10", we have "x". This is of course a simple rule of 3. To get the "x", which in this case it's pretty
easy for us, as it should be "8", we multiply "10" with "4" and then divide it all by "5". This gets us exactly what we need, which is "8". However, it seems that we actually want the opposite. The higher our Speed,
the lower our Recentering Time should be. In this case, we want the result to be "2". To do that, we simply use the Inverse rule of 3. The way we do that is by multiplying "5" by
"4" and then divide it all by "10". This gets us the result we want, which is "2". We'll be adding this formula to our Camera
Utility Enable Recentering method, so open up the "PlayerCameraUtility" Script. In here, in the "EnableRecentering" method,
we'll be adding two new parameters at the end: "float baseMovementSpeed = 1f"
and "float movementSpeed = 1f". Then, before we assign our values to the "Horizontal
Recentering" fields, we apply the inverse rule of 3 by typing in "recenteringTime = recenteringTime * baseMovementSpeed /
movementSpeed;". Note that we do not use the "*=" operator
as that would multiply the "recenteringTime" with the division of "baseMovementSpeed" by
"movementSpeed", which is not what we want. When that's done,
go to where we use our "EnableRecentering" method, which should be in our "PlayerMovementState" Script. In here, we'll create a new variable by typing in "float movementSpeed"
and assign it to "= GetMovementSpeed();". Then, we check "if (movementSpeed == 0f)"
and if that's the case, we set the "movementSpeed = movementData.BaseSpeed;". This will make it so that we don't divide
by 0, which shouldn't be possible and instead divide by the Base Speed, which
will make it so it returns the normal Recentering Time, as we're multiplying by Base Speed
and then dividing by Base Speed again. When that's done, pass in to the method call
"movementData.BaseSpeed" and "movementSpeed". This should take care of setting the correct
values for different Speeds, so save it all up and go back to Unity. Entering Play Mode, our Speed should no longer
make our Camera Rotation faster, although it might not be a completely perfect solution. This however brings us to our second problem: Changing States, like going from "Running" to "Walking", does not update the Recentering values until
we change to a new "Movement" Input Key. So moving to one side and pressing the "Walk Toggle" keeps the Recentering Time the exact same,
which means we still have our problem. The way we'll be fixing this is quite simple:
Every time we enter a "Grounded State", we'll update our "Camera Recentering State". To do that, open up the "PlayerGroundedState" Script. In here, in our "Enter" method, at the bottom, call in "UpdateCameraRecenteringState(stateMachine
.ReusableData.MovementInput);". This seems like it would be enough, but we
need to remember that this uses our Speed Modifier now. The problem with that is that we are currently
only setting our speed modifiers after we call the "base" method,
which means, we won't have them updated by the time we call in the
"UpdateCameraRecenteringState" method. To fix this, we'll go to every "Grounded State",
starting with the "Idling State" and in the "Enter" method, we'll place the
Speed Modifier line to be above our "base" method call. Repeat this process for every "Grounded State"
that sets a speed modifier. With this done, we should no longer have the problem we had. However, we also need to remember something else: We can "Enter" and "Exit" Slopes,
which update our Speed Modifier. This simply means that the moment the Ground angle changes, we should make sure we update the Recentering
values to our new Player Speed. To do that is quite simple, as we have a method
that returns us the new Slope Speed Modifier. So, start by opening up the "PlayerGroundedState" Script. In here, go to the "SetSlopeSpeedModifierOnAngle"
method in the "Main Methods" region. Inside, add in a new if statement by typing in "if (stateMachine.ReusableData
.MovementOnSlopesSpeedModifier != slopeSpeedModifier)". Make sure you place our current assignment in here. Then, we'll also call in "UpdateCameraRecenteringState(stateMachine.ReusableData
.MovementInput);". This makes sure our values are updated if
our slope speed modifier is changed. If we save and go back to Unity, entering Play Mode should
now update the Recentering values correctly. We do have two other problems to solve though. The first one is that our Rotation Stops when we cancel
Input, even if we currently are in the "Jumping State". So "Moving", "Jumping"
and then unpressing the "Movement" Key will disable our Camera Recentering. In Genshin, this is not what happens when
we're in the middle of the "Jump" and should instead keep it enabled and rotating. To fix this problem we'll simply need to "override"
our "OnMovementCanceled" method and leave it empty. Of course, we currently have a private method
in our "Movement State" and a "protected" method in our "Grounded State", which means that we can't currently override
it in our "Jump State". Thankfully, in the "Grounded State" one,
we no longer need to change to the "Idling State", as no State does that when cancelling Input anymore. So, we'll simply remove it and make our "Movement
State" method "protected virtual" instead. To do that, open up the "PlayerGroundedState" Script. In here, in the "Input Methods" region, remove
the "OnMovementCanceled" method. Make sure you also remove it from the "Add"
and "RemoveInputActionsCallbacks" methods as well. When that's done, open up the "PlayerMovementState" Script. In the "Input Methods" region, make our "OnMovementCanceled"
method a "protected virtual" method. We now need to make sure that our currently
"overridden" methods need to call the base method, or be removed if they were left empty
to not transition to the "Idling State". So, open up every "Grounded State" Script and do that. For the States that transition to other States in this
method, we need to make sure we call the "base" method
after we change States. This is because when changing States,
we now call in the "UpdateCameraRecenteringState" method in the "Enter" method,
so it would enable it back after disabling it. When the "Grounded States" are all updated,
open up the "PlayerJumpingState" Script. In here, add in a new region at the bottom
named "Input Methods". Inside, we'll "override" the "OnMovementCanceled"
method and leave it empty. This should be enough, so save and go back to Unity. Entering Play Mode, "Moving", "Jumping" and
releasing the "Movement" Key should no longer "Disable" our "Horizontal Recentering". There's now only one problem left, which is that in Genshin, the Walking State seems to have the default Recentering Time
when we're looking down and move "backwards". The "Idling State" seems to also have a "Wait Time" when
we're looking down and move "backwards". For the "Sideways" cases, both seem to still
rotate fast and without Waiting before Recentering. The way we'll be doing this is quite simple: We'll simply create a reusable property that
allows us to set the current "Backwards" and "Sideways
Lists". To do that, open up the "PlayerGroundedData" Script. In here, copy the "Backwards" list property. Then, open up the "PlayerWalkData" Script. In here, paste the copied property under our
"BaseSpeed" property and import the necessary namespace. We only need to override our "Backwards" Data
so we don't need to create a property for the "Sideways"
Data. For our "Idling" case, we'll need a new Data
class, so go back to Unity. In here, open up the Player Grounded "Data" folder and create a new C# Script to which I'll name
"PlayerIdleData". Open it up and remove the default methods and inheritance. We'll make the class "[Serializable]" as well. Then, paste the copied property here. When that's done, go back to the "PlayerGroundedData"
folder and duplicate the "Walk" property. Swap the "Walk" parts with "Idle" instead. When that's done,
open up the "PlayerStateReusableData" Script. We'll be creating a property to hold these rotation values, so under our Speeds and Forces properties,
paste the "Backwards" property we've copied before twice. Then, swap the second one to be "Sideways"
instead of "Backwards". This is just in case we ever need to "override"
the "Sideways" Data as well. Remove the "[SerializeField]" attribute and
give them a "public set". When that's done, go to the "PlayerMovementState" Script. In the constructor, after setting our other variables, we'll
type in "stateMachine.ReusableData.BackwardsCameraRecenteringData
= movementData.BackwardsCameraRecenteringData;". Then, duplicate this line
and swap "Backwards" with "Sideways". I'll extract both lines to a new method to
which I'll name "SetBaseCameraRecenteringData();". I'll make this method "protected" and add
it to the "Reusable Methods" region. When that's done, open up the "PlayerIdlingState" Script. In here, we'll create a new variable by typing
in "private PlayerIdleData idleData;". In the constructor, we then set the
"idleData = movementData.IdleData;". Then, in the "Enter" method, before we call
the "base" method, we type in "stateMachine.ReusableData.BackwardsCameraRecenteringData
= idleData.BackwardsCameraRecenteringData;". Note that we need to make sure we set this
before we call the "base" method as otherwise our "Enter" method
"UpdateCameraRecenteringState" call would update with our old "Data List". We now need to reset this Data whenever we
leave the "Idling State" by using our "SetBaseRotationData" method. However, we can't really do that
when we "Exit" the "Idling State". This is because if we do that, then the moment
we "Move", we'll set the values back to normal, meaning that our "Wait Time" will be set to
"0" again, so we'll start rotating fast instantaneously. If we take a look at Genshin, if we wait until
we're "Idle" and start "Moving backwards" while looking
Down, then we'll Wait for a while before our Recentering happens. However, if we start "Moving backwards" while
we're on a "Stopping State", it will be instantaneous and we won't Wait
for the Recentering to start happening. This means that we can simply reset the values
when we "Enter" our "Stopping States". To do that, open up the "PlayerStoppingState" Script. In the "Enter" method, before we call in the "base" method, call in "SetBaseCameraRecenteringData();". All that's left is our "Walking State", so
start by copying the line in our "Enter" method and then open up the "PlayerWalkingState" Script. In here, in the "Enter" method, paste the
line we've just copied before the "base" method call. Then, swap the "idleData" with "walkData" instead. Make sure you create the "walkData" variable
and set it in the constructor. When that's done, "override" the "Exit" method. We'll need to reset the Data back to its default values, which we can do by calling in
"SetBaseCameraRecenteringData();". That should set all the Data Lists correctly,
but we of course can't forget to actually use the reusable properties in our methods. To do that, head back to the "PlayerMovementState" Script. In our "UpdateCameraRecenteringState" method,
in the "Reusable Methods" region, swap our "movementData" usages with
"stateMachine.ReusableData" instead. This should be enough to get it working, so
save it all up and go back to Unity. In our second "Inspector" tab, for our "Idle Data", we'll only have one range, which will be from "80" to "90". For the "WaitTime", set it to "0.25" and for
the "RecenteringTime", set it to "0.5". For our "Walk Data", we'll also only have one range, which will be from "80" to "90", with a "WaitTime"
and "RecenteringTime" of "-1". If we now enter Play Mode, we should finally
have our Recentering working fine. We're now almost ready to start adding animations, but before we go ahead and do that,
we'll first fix a few existing Bugs. The first bug we have is due to Gravity. This bug happens when we are near an Edge and our Capsule Collider starts falling
due to its "Spherical" bottom. This basically makes our Player fall and
also adds a bit of velocity due to Physics. This is a problem because this can happen
in the States where we are standing still, like the "Idling State" or the "Landing States", as it makes our Player slide while
we were supposed to not be moving, due to the velocity it added
when falling off the Edge. The fix for this bug is quite simple: If the Player is moving in one of
these states, we reset its velocity. Thankfully for us, we already have 2
methods that allow us to do just that. We only need to fix this problem in 3 States, which are the "Idling State" and the
"Light" and "Hard Landing States". Knowing that, start by opening up
the "PlayerIdlingState" script. In here, "override" the "PhysicsUpdate" method. Then, type in "if (!IsMovingHorizontally())"
and if that's the case, we "return;". In the case that it "is Moving" though,
we simply call in "ResetVelocity();". And that's all we need to do, so now copy this whole "PhysicsUpdate" method and
go over to the "PlayerLightLandingState" Script. In here, paste the "PhysicsUpdate" we've
just copied into the "IState Methods" region. Then, do the same for our
"PlayerHardLandingState". We won't be doing this in our "PlayerLandingState"
because our "Rolling State" doesn't need it. That solves one bug, but we
still have a few more to fix. The next one is regarding our Dashing State. I thought I had fixed it but turns
out that there's still one bug, which happens when we "Move", Dash
and then Unpress the "Movement" Key. It doesn't always happen, but when it
does, we simply "Dash" at a low speed. I'm not entirely sure why but it's likely because
we are using the movement speed in this Dash and we just changed to a low speed modifier State. I've changed the code a bit and
hopefully it is enough to fix it, but in short: we'll simply always
add an initial force when "Dashing". To do that, open up the
"PlayerDashingState" Script. In here, rename our "AddForce"
method to "Dash" instead. Because we'll be adding a force in
case we have a movement input as well, place the current code that gets us the direction above our if statement
and remove the "return;" statement. Inside of our if statement, we'll now call in "UpdateTargetRotation(GetMovementInputDirection());". This will simply update our target rotation to
be relative to our "Movement" Input and Camera, which will allow us to add a
Force towards that direction. Then, we'll set the "characterRotationDirection = GetTargetRotationDirection
(stateMachine.ReusableData.CurrentTargetRotation.y)", which will get us the direction of our Input
relative to the Camera, as we've just updated it. Of course, our "characterRotationDirection"
variable name no longer makes sense, so I'll rename it to "dashDirection" instead. From what I've tried, it seemed to
work without this whole if statement, but I'll leave it as is,
as it makes more sense and to make sure it doesn't cause any rotation bug. With this, we're now always adding a force
to our "Dash" towards a certain direction, which depends on whether our "Movement"
Input is being pressed or not. This should be enough to fix it. However, we also need to take into
account the movement speed on slopes, as right now we'll be Dashing
slower when we're on a slope. So, go to the "GetMovementSpeed" method
in the "PlayerMovementState" Script. In here, add a new parameter by typing in
"bool shouldConsiderSlopes = true". Then, create a new variable by
typing in "float movementSpeed" and assign to it the first multiplication. Then, we'll check "if (shouldConsiderSlopes)",
and if that's true, we type in "movementSpeed *=
stateMachine.ReusableData.MovementOnSlopesSpeedModifier;". At the end, we update the whole "return" statement
to return this "movementSpeed" variable instead. When that's done, go back to
the "PlayerDashingState" Script and pass in "false" to the
"GetMovementSpeed()" method. And that's another Bug fixed, so for the next one,
I've previously added a "Mathf.Abs()" when Falling in case we fell into a Ground
that was above our current Ground. However, that's of course not valid as
if we ever "Fall" in such a situation, we don't want the Player to go to the "Hard
Landing State" or the "Rolling State", but only to the "Light Landing State",
as the Ground is above the Player. To fix that, open up the
"PlayerFallingState" Script. In here, in our "OnContactWithGround" method,
where we transition to the "Landing States", remove the "Mathf.Abs()" from
the "fallDistance" variable. We'll now always transition to the "Light
Landing State" in the cases we've just mentioned. That's all we need for this one, so for the next one,
I've also previously said that we didn't need to update the target
rotation when we "Jumped" and had "Input", but with the addition of our "Landing States"
into our System, that's no longer the case. This is because our "Light Landing
State" does not allow "Movement", which means that our target
rotation will never be updated. This makes it so that if we
start "Jumping" to one side and we press the opposite side
key in the middle of the "Jump" and then "Jump" while in
the "Light Landing State", we will keep "Jumping" towards the old direction. To fix that, open up the
"PlayerJumpingState" Script. In the "Jump" method, inside
of the first if statement, before assigning our
"jumpDirection" a new value, type in "UpdateTargetRotation(GetMovementInputDirection());". This updates our target rotation to be
relative to our Input and the Camera, so replicating the situation we've
mentioned before should now work correctly. That should be enough but we
now have one final bug to fix. This bug has to do with our "Falling State". You might've noticed sometimes
that when we fall from a "Ramp", we don't enter the "Falling State". The problem lies in that we're
using the "bounds.extents" variable to set the size of our "OverlapBox". Apparently, Unity changes this variable, which means its values can
become something we don't want. I'm not entirely sure why,
but according to this post, it's because the bounds are
cubes in Unity and therefore when we rotate, it will change the extents. Our "CapsuleCollider" "Vertical Extents" property
is fine because we're using a cached extents, so even if it changes at runtime, we are not
getting that one but the initial extents. For our "OverlapBox" "bounds.extents"
though,, we'll have to cache it. Thankfully, we already have a reference to
our "BoxCollider", so we can easily do that. To do it, open up the
"PlayerTriggerColliderData" Script. In here, add in a new property by typing in "public Vector3 GroundCheckColliderExtents
{ get; private set; }". Then, create an Initialization method by typing in
"public void Initialize()" and inside type in "GroundCheckColliderExtents =
GroundCheckCollider.bounds.extents;". Of course, we need a way to call this method,
so open up the "CapsuleColliderUtility" Script. We could override the "Initialize" method but we
return here if the collider is already initialized so we'll create another method
instead, which we'll call here. So, under our "Initialize" method,
create a new method by typing in "protected virtual void
OnInitialize()" and leave it empty. Then, call it at the end
of our "Initialize" method. Back to our "PlayerCapsuleColliderUtility" Script, now "override" the "OnInitialize" method
and call in "TriggerColliderData.Initialize();". If you use Composition, then you would
simply create an "Initialize" method here and call both properties
"Initialize" methods inside. We now need to use this new extents property,
so open up the "PlayerGroundedState" Script. In here, go to the "Main Methods" region
to our "IsThereGroundUnderneath" method. Inside, swap our
"groundCheckCollider.bounds.extents" with "stateMachine.Player.ColliderUtility
.TriggerColliderData.GroundCheckColliderExtents". Our "bounds.center" is alright because we want
the center at the time we check for the "Ground". This should be enough to fix our bugs, so
saving it all up and going back to Unity, all of our Bugs should be solved. Of course, some of them were only visible with
Animations due to the Animation Transition Events. Talking about Animations, it's finally time
for us to add them into our System. Not only will they make our system look better,
they will also make it possible for us to transition to the States that required animation events. We've already downloaded our Animations before,
so we're basically ready to use them. In case you haven't, make sure you do it first. In Unity, to control our animations, we need
to create an "Animator Controller" and also add an "Animator" Component to an Object. The "Animator Controller" allows us to add
transitions between multiple animations as well as set the conditions for those transitions. The "Animator" Component allows us to set
the necessary data for those conditions through code or even Play or Stop a specific animation. In other words, it allows us to control the
"Animator Controller" through code, among other things. It's also the Component that allows us to specify what's the
"Animator Controller" of a specific Game Object. We'll be adding our "Animator" Component to
our Character Model, so select it in our "Player" Game
Object. In here, simply click on "Add Component" and
search for "Animator". When that's done, simply add it by double
clicking on it or pressing "Enter". I'll set the "Animator" "Culling Mode"
to "Cull Update Transforms". Simply put, "Culling Mode" translates to "what
should animations do when they're not on camera?". With our option, it will stop playing the
animation when the Object exits the Camera, but still calculate where it should be, so
that the next time it enters the Camera, it will start playing the animation at the
right frame as if it never left in the first place. With that done, we now need an "Animator Controller"
so go to the Player "Animations" Folder and create a new folder named "Controllers". Inside, we'll create a new "Animator Controller"
by right clicking in the Project Window and going to "Create > Animator Controller",
at around the middle area. I'll name it "PlayerAnimatorController". When that's done, select our "Character Model"
and drag our "PlayerAnimatorController" to the "Animator" Component "Controller" field. Double clicking our Controller Asset should
open up the "Animator" Window. I'll leave it docked right next to our "Input Actions"
Window. Now, if this is your first time using an "Animator
Controller", this Window is likely quite strange to you. While we won't be going through
every available option or feature, we'll at least go through what we need to know. The Graph in the middle represents the area
where our Animations and Transitions will stay. You can move it around by using the mouse middle button. Go ahead and open up the
Player "Grounded" "Clips" Animations folder. Then, drag the "Idle" animation to this middle
area in the "Animator" Window. Unity automatically creates a rectangle for us. This rectangle is called a "State". If we click on it, on the right side,
we should see our "Idle" animation assigned to the "Motion" field. Unity also named our "State"
the same as our dragged animation. With that, we know that in the "Animator Controller", we can
have "States" that may have an animation attached to them. We can create a State without dragging an
animation by right clicking on the Graph area and going to "Create State > Empty". We can rename it using the top field
and we can attach a "Motion", or "Animation" if we want. So, "States" can represent "Animations"
if a "Motion" field is attached, and it is through them
that we'll add transitions to other "States". A "State" with no "Motion" attached to it
can be used as a transition intermediary, such as "enter this State without animating
and then check which State we should transition to next". To add a transition to another "State", simply
right click on the "State" we want to transition from and choose "Make Transition". Then, left click on the "State" we want to transition to. This will create a transition from one "State" to another. We can see all of the transitions from the
selected "State" to other "States" on the "Transitions"
field or we can click a specific "Transition" line. In this "Transition", there are a few options
that will be important for us to know, some which are within the "Settings" foldout. The first one is the "Has Exit Time" option. Simply put, this means that the "Transition" will only
happen once the "Animation" of the "State"
we're transitioning from has finished. This leads us to the "Exit Time" option,
which controls what percentage of the "Animation" needs to be finished
for the "Transition" to happen if we have the "Has Exit Time" option enabled. In this case, we aren't waiting for the whole
"Animation" to finish but instead "75%" of it. We'll be disabling the "Has Exit Time" option
for all of our "Animations", so this won't really matter too much for us. The next important option for our use cases
is the "Transition Duration" option. This is simply how long does it take
for this "Transition" to happen. The default "0.25" value means that it will
take "0.25" seconds to "Transition" from one "State" to the
other. A value of "0" would make it an instantaneous "Transition". Something to note here is that Unity seems to blend both
"States" "Animations" when there is a "Transition Duration". This is cool because it makes our transitions look smoother. Of course, a big "Duration" would also mean
it would take a long time to animate the Player with the second "State" "Animation". A value of "0" would also make it so there
is no blending whatsoever and would make it a snappy "Animation Transition". Later on, we'll need to be careful when using
"Animation Events" and setting up an "Animation Transition", but we'll understand why whenever we get there. The last important option for us is the "Conditions" field. Much like the name entitles, this specifies the "Condition"
that we need to fulfill for this "Transition" to happen. This is what we'll be using to know if we
should "Transition" from a "State" to another. Now, we do need something to "Condition" with. That "something" being a value,
like a "boolean" value or a "float" value, which will allow us to add "Conditions" such as "Transition
if we are walking" or "Transition if the speed is over 5
units". This is what the "Parameters" tab at the left
side of the Window is for. To add a "Parameter", we click on the plus (+) icon
and choose the type of value that we desire. Throughout all of this series we'll have one of our values
be a "Trigger", while the rest of them will all be
"Booleans". For our "Movement System" Part,
we'll only be needing "Booleans". Lets add one. In our "Conditions", if we now add a new "Condition",
our new "Boolean Parameter" should show there with a dropdown to choose
if the parameter should be "true" or "false" for this "Transition" to happen. That's mostly all of the basics we'll need
to know about the Animator Controller. However, right now, if we were to drag all
of our animations into this area and make the necessary transitions,
it would very likely end up being an horrendous mess, which people often call the "Animator Web". We'll be learning one of two things that can
help us organize things better, which are "Sub-State
Machines". We won't be taking a look at "Blend Trees", but we'll at
least learn how to make our "Sub-State Machines" reusable. Of course, while this will reduce the amount
of webs one area has, in a big game with a lot of
animations, this will likely still cause an "Animator Web". There are probably a few assets out there, both free and
paid, that can swap this "Animator" system
from Unity with a cleaner solution, but we won't be using any. In case you're wondering what "Blend Trees"
are, they are a nice way to blend between "Animations", often used in things such as "Movement" where you have a
different "Movement Animation" per direction. If your Project is one of those, it will likely
be worth it for you to go learn a bit more about them. Now, regarding about what are reusable "Sub-State
Machines", they are simply normal "Sub-State Machines" made in a way that we can copy them
into other "Animator Controllers" without caring about what "Transitions"
to that specific "Sub-State Machine" exist nor what "Transitions"
that "Sub-State Machine" has to the outside. It will likely become a bit more clear what
this means when we start doing them. To create a "Sub-State Machine", we simply right click in
the main area and select "Create Sub-State Machine". I'll name this one we've just created "Grounded". Feel free to remove the other "States" we've added. To go inside of this "Sub-State Machine",
we simply double click on it. To go back, we either double click on the
"(Up) Sub-State Machine", or we click once at the top in the "Base Layer" Tab. You might also have noticed that "Sub-State
Machines" are represented by a diamond-like rectangle shape compared to the rounded rectangles
that represent normal "States". Inside of this "Sub-State Machine", we can
create other "States" or even other "Sub-State Machines". We're going to be doing both, as our animations
will be organized much like our folders are. So, start by creating 3 other "Sub-State Machines":
"Moving", "Stopping" and "Landing". For our normal "States", we'll drag our "Idle"
and "Dash" animations to this Window area. Lets start by updating our "Moving Sub-State Machine". To do that, enter on it by double clicking it. Then, open up our Grounded "Moving" Animations
folder and drag all of the animations here. I'll organize them from the left to the right
following "Walk", "Run" and "Sprint". I'll also go ahead and organize the default
"States" and "Sub-State Machines" from top to the bottom, where the "Up State" and the "Entry" Node will be on top and
the "Any State" and "Exit" Nodes will be on the bottom. Because dragging our animations automatically
sets the "States" with all the Data we need, all that's left to do is to set their "Transitions". When we've dragged our "Animations" in, Unity
already set up a "Default Transition" to one of them, which is marked by an yellow arrow. A "Default Transition" means that when we
"Enter" this "Sub-State Machine" and no other "Conditions"
from the "Entry" Node are valid, or true, it will always enter the "Default Transition". This is why if we select that yellow "Transition",
there is no "Conditions" field, or any field at all. If we wanted to change this "Default Transition"
to some other "State", we would right click on the "Entry" Node
and select "Set StateMachine Default State" and choose the "State" we want to "Transition" to by
default. We'll set it to our "Run State", so if yours isn't, make
sure it is. Now, from our previous transition code, we
know that "Walking" can "Transition" to "Running" and "Running" can "Transition" to "Walking",
but none of them can "Transition" to "Sprinting", as only "Dashing" transitions to "Sprinting". We also know that while "Sprinting"
can go to both "Running" and "Walking", in Genshin it "Transitions" to "Running" and then the
"Running State" takes care of "Transitioning" to the
"Walking State". Knowing that, create a new "Transition"
from the "Walk" to the "Run State". Then, create one from the "Run" to the "Walk State". Finally, create another one from the "Sprint" to the "Run
State". We now need to set our "Conditions" for each
of these "Transitions", so we need to create "Parameters". In the "Parameters" Tab, create 3 new "Parameters":
"isWalking", "isRunning" and "isSprinting". If you haven't deleted the previous "Parameter"
we created, feel free to do it now. When that's done,
add these to the corresponding "Transitions", so: "Walk" to "Run" will have "isRunning is true". "Run" to "Walk" will have "isWalking is true". And finally "Sprint" to "Run" will have "isRunning is true". That's it for our "Transitions" between the 3 "States", but, why exactly did we create our "isSprinting"
"Parameter" if we're not using it? The reason why is because "States" outside
of this "Sub-State Machine" might want to "Transition" to a "State" that's
inside of this "Sub-State Machine". Such is the case of the "Dash State", which
can "Transition" to the "Sprint State". Whenever a "Transition" happens from the outside
of this "Sub-State Machine" to its inside, it will first arrive at the "Entry" Node,
which then takes care of "Transitioning" to the correct
"States". So, add a "Transition" from the "Entry" Node
to the "Walk State" and another one to the "Sprint State". Then, add the "Condition" of "isWalking is
true" and the "Condition" of "isSprinting is true",
accordingly. If none of these "Conditions" is valid, then
our "Sub-State Machine" will transition to our "Run State", as it has the "Default Transition" set to it. Note that this also means that we only need
the "isRunning Parameter" because we need it in the "Transitions Conditions"
between the 3 "States" themselves, as the "Default Transition" doesn't require it. There's now only one "Transition" left, which
is a "Transition" from our "States" to the outside "States". Now, normally, people would likely go ahead and add
"Transitions" from these "States" directly to the outside
"States". This can be done by creating a "Transition" to the "Up"
Node. This should bring up a menu that shows every
existing "State" and "Sub-State Machines" that are outside of this "Sub-State Machine". While this works fine, this is basically setting up a
dependency, which makes it so that this "Sub-State Machine"
is no longer reusable. The reason being that we're directly "Transitioning"
to a "State" or "Sub-State Machine" from the outside of this "Sub-State Machine". So, if we copied this "Sub-State Machine" elsewhere, we would also copy those dependent "Transitions". This isn't what we want. What we want is to know that we indeed can
"Transition" to a "State" from the outside, but we want to decide what that "State" is
outside of this "Sub-State Machine". When we copy a "Sub-State Machine" that defined
its "Transitions" outside, those "Transitions" won't be copied with it. However, we do need a way to tell our "Sub-State
Machine States" that they can "Transition" to the outside, without directly telling them
what "States" they can "Transition" to. The way we do that is by using the "Exit" Node. When a "Transition" goes to the "Exit" Node,
it will "Exit" this "Sub-State Machine" and stays in the "(Up) Sub-State Machine" checking for
whatever "Conditions" it has. This makes it so that we can add our "Transitions"
outside of the "Sub-State Machine", which means that if we want to add a new "Transition",
we don't need to update the insides of a "Sub-State Machine" and can simply add that "Transition" outside. Knowing that, create 3 new "Transitions",
one from each "State" to the "Exit" Node. For our "Condition", we'll create a new "Parameter"
of type "Bool" and name it "Moving". I'll move this "Parameter" up. Then, add it to every "Condition" of our new
"Transitions" and set it be "Moving is false". Note that we need to add it one by one, as
selecting all of them doesn't really edit them all. Now, this "Moving Parameter" is also how we're
going to be transitioning to this "Sub-State Machine" without caring about what "States" are inside. If from the outside, we create a "Transition"
from a "State" to this "Sub-State Machine" and make its "Condition" be "Moving is true",
we can easily add or remove "States" from the inside of the "Moving Sub-State Machine"
without breaking existing outside "Transitions", as those outside "Transitions" do not care
at all what "States" we have inside, but simply that they need to "Transition"
to that "Sub-State Machine" when we start "Moving". That's it for our "Moving Transitions", but
we should of course set each "Transition" setting correctly. So, one by one, disable the "Has Exit Time"
option, which renders the "Exit Time" field useless and leave the "Transition Duration" as "0.25". You might've noticed
that some of our "Transitions" are greyed out and don't allow us to set any "Settings". This happens when our "Transition" is an "Entry Transition". The settings of these "Transitions"
are set through our "Exit Transitions". This simply means that when we "Enter" a "Sub-State
Machine" through a "Transition" from the outside, its "Entry" Node "Transition" settings were
defined by that outside "Transition". So, for example, our "Moving Sub-State Machine
Exit Transitions" define the settings of our future "Stopping Sub-State Machine Entry
Transitions". This makes sense because our "Exit Transitions"
are the "States" we're transitioning from. For example, we'll be "Transitioning"
from the "Walk State" to an outside "State" and the "Walk State" defines that there should
be no "Exit Time" when we're "Transitioning" from it. Of course, this also means that the "Entry
Transitions" are the "States" we're transitioning to, so they shouldn't be the ones deciding the "Transition"
settings. That's it for our "Moving Sub-State Machine",
so in the "(Up) Sub-State Machine", lets add "Transitions" from the other "States" to this one. Before we do that though, I'll organize them a bit. I'll add the "Landing" to the "top-left",
the "Moving" to the "bottom-left", the "Stopping" to the "top-right", the "Dash" to the
"bottom-right" and the "Idle" to the "middle-up". I'll also make sure that the "Idle State" is the "Default
State". The "States" or "Sub-State Machines" that
can "Transition" to the "Moving Sub-State Machine" are: "Landing", "Stopping", "Idle" and "Dash". The condition for all of these 4 "Transitions"
will be "Moving is true". Now, you might've noticed that only our "States"
have a white transition. This is because in normal "States" there are
no "Exit Transitions" as there's no "Exit" Node, so our created "Transition" can be considered
their own "Exit Transition". This means
that we need to define their "Transition" settings here. Because of that, in both the "Idle" and "Dash Transitions",
we'll be disabling the "Has Exit Time" option. Make sure the "Transition Duration" is always
set to "0.25", which should be the default value. As a test, if we were to duplicate our "Moving Sub-State
Machine", it would come without the "Transitions", which basically makes it so that we can
copy it to other "Animator Controllers". As a side note, if you're wondering about
the "Any State" Node in the "Sub-State Machine", it means "Any State" in the whole "Animator
Controller" and not for that specific "Sub-State Machine". As far as I know, there is no built-in Node for the later. That's basically all we need to know regarding
our "Animator Controller", so all we need to do now is to add the remaining
"Animations" and their "Transitions". Lets start with our "Stopping Sub-State Machine",
so double click on it to open it up. Then, open the "Stopping" Clips Animations
Folder and drag the "Animations" into the "Sub-State
Machine". I'll order them as "Medium", "Light" and "HardStop"
as I like to keep the "Default Transition" in the middle, which will be our "LightStop State". I'll also organize them before we keep going. When that's done, I'll add a "Transition"
from the "Entry" Node to the other 2 "States", as all the "States" in this "Sub-State Machine"
can be "Transitioned" into from outside "States". Then, add a "Transition" from all "States" to the "Exit"
Node. We'll not add any "Transition" between the 3 "States" as they can't "Transition" between each other. In every white colored "Transition",
disable the "Has Exit Time" option. We'll only need 3 new "Parameters" here, so add in "Stopping", "isMediumStopping" and "isHardStopping". From the "Entry" Node to the "States",
add the "Condition" of "isMediumStopping is true" for one and "isHardStopping is true" for the other. From the "States" to the "Exit" Node,
add the "Condition" of "Stopping is false". We don't need a "Parameter" for our "Light Landing State" because it uses the "Default Transition",
which doesn't require a "Condition" and also because no other "State" inside of
this "Sub-State Machine" "Transitions" to it. That's all we need here
so go up to the "(Up) Sub-State Machine". The "States" that will be able to transition
to this "Sub-State Machine" are: "Moving" and "Dash". Make sure you're "Transitioning" to the "Sub-State
Machine" and not a specific "State". The "Transition Conditions" are of course "Stopping is
true". Again, select the white "Transitions"
and disable their "Has Exit Time" option. For our last Grounded "Sub-State Machine",
we have the "Landing Sub-State Machine", so open it up. Open up the "Landing" folder
and drag the existing "Landing Animations" here. I'll organize the area here as well. We'll make the "Light Landing" the default "Animation". Because it is the default one, we don't need a "Condition"
nor a "Parameter" so simply "Transition" it to the "Exit"
Node. Disable the "Has Exit Time" option from the "Transition" and
then create a new "Parameter" named "Landing". Add a "Condition" of "Landing is false" to the "Transition". Add a "Transition" from the "Entry" Node
to the other 2 "Landing Animations". Create 2 new "Parameters": "isRolling" and "isHardFalling". Add them in their respective "Conditions"
in the "Entry Transitions". Then, add another "Transition"
from the other 2 "Landing States" to the "Exit" Node and add the "Condition" of "Landing is false"
to both "Transitions". Untick the "Has Exit Time" option in both of them as well. That's really it so go to the "(Up) Sub-State Machine". The following "States" can "Transition" to
this "Sub-State Machine": "Jumping" and "Falling". Of course, we don't yet have our
"Jump" and "Falling States" here, but that doesn't really matter, as all that
we need to do is to add a "Transition" from the "Entry" Node to the "Landing Sub-State Machine"
with a "Condition" of "Landing is true". This is the good thing about reusable "Sub-State Machines", we don't need to care what "States"
from the outside transition to it. One thing you might've noticed is that inside
of our "Sub-State Machines" our "(Up) Sub-State Machine"
might transition to the "Entry" Node. These are the "Transitions" of the outside
"States" or "Sub-State Machines" to this one, but we don't need to worry about them as if
we duplicate the "Sub-State Machine", they won't be present in the new one. All that's left now for our "Grounded States"
are our "Idle" and "Dash States". Because these aren't "Sub-State Machines", we don't really
need to do anything else besides adding their "Transitions". Knowing that, we'll be able to "Transition"
to the "Idle State" from: "Landing" and "Stopping". The other 2 will go to the "Stopping State"
and not to the "Idle State". We need to create a "Parameter" for this "State"
so add a new "Boolean" and name it "isIdling". I'll drag the "Parameter" to the top. Then, add the "Condition" of "isIdling is true"
for both "Transitions". Because no "States" directly "Transition" to this one,
none of them are white colored "Transitions", meaning that their settings were set from the respective
"Sub-State Machine Transitions" to the "Exit" Node. For our "Dash State", we can "Transition" from: "Landing", "Stopping", "Moving" and "Idle". Create a new "Parameter" of type "Bool" named "isDashing". I'll move it under the "isIdling" "Parameter". Then, add the "Condition" of "isDashing is true"
to every "Transition". For the "Transition" between the "Idle State"
and the "Dash State", we'll also need to disable the "Has Exit Time" option. That's it for our "Grounded Sub-State Machine",
so we now need to do our "Airborne Sub-State Machine". Of course, every "Grounded State" can enter
the "Airborne State" known as "Jumping State" and also the "Falling State". This means that we need a "Transition" to the "Exit" Node
from every "State" to be able to "Transition" to them, so go ahead and add a "Transition"
from every "State" to the "Exit" Node. For the "Condition", add a new "Bool Parameter"
named "Grounded" above all others and set the "Condition" for each "Transition"
to be "Grounded is false". For the white "Transitions", make sure you
also disable the "Has Exit Time" option. Back into our "Base Layer", add a new "Sub-State Machine". I'll name it "Airborne". I'll organize them a bit. All Nodes can be above the "Entry" Node as we won't use
them. I'll also place the "Airborne Sub-State Machine" a bit to
the left as later on in the series we'll add the "Swimming"
one to the right. We'll need to "Transition" from and to the "Grounded
Sub-State Machine", so add a "Transition" from both sides. Then, create a new "Bool Parameter" named "Airborne". For the "Transition" from the "Grounded Sub-State
Machine" to the "Airborne Sub-State Machine", we'll add a "Condition" of "Airborne is true". For the opposite "Transition",
we'll set a "Condition" of "Grounded is true". In my original system
I also added the "Current State is false Condition" due to a bug that I don't really remember, but I couldn't seem to get any bug with the way we have
things right now, so I'll leave it as is. If you do end up finding any bug regarding
animations between different "Sub-State Machines", then you might want to try and add that extra "Condition". Unless my brain isn't thinking correctly though,
there should be no problems and it's possible that the bug was fixed through
finishing the System. Opening the "Airborne Sub-State Machine",
we need to drag our "Jump" and "Fall Animations" here so make sure you go to the right folder and drag them in. I'll organize them a bit. We'll make our "Jump Animation" the default "Animation". We'll be able to "Transition"
from the "Jump State" to the "Fall State", so add in a new "Transition" to it. Then, create a new parameter named "isFalling"
and add it as a "Condition" to the "Transition" we've just
created. Of course, it "Transitions" when it's "true". Make sure to untick the "Has Exit Time" option as well. Then, add another "Transition"
from the "Entry" Node to the "Fall State" with the "Condition" of "isFalling is true". When that's done, add a "Transition" from
both "Animations" towards the "Exit" Node with the "Condition" of "Airborne is false" in both of them. Again, make sure they have the "Has Exit Time" option
unticked. With that done, our "Animator Controller"
is finished and all we need to do now is to set the "Parameters" to either "true" or "false"
through code and also set up our "Animation Events". Lets start by setting our "Parameters" first. We'll be doing that using the "Animator.SetBool()" method. This method accepts a "string",
which is the name of the "Parameter", or an "int", which is the "ID" of the "Parameter". The "ID" here is an "Hash" that we can get
from the "StringToHash" method. If we pass in a "string" to the "SetBool"
method, it will call this "StringToHash" method to transform it into the "Hash" integer. That means that any "SetBool" call with a
"string" as the argument will need to do that conversion. Because of that, it would be way faster for
us if we instead passed in that "Hash" right away. Of course, we ourselves don't know that "Hash",
but the fix for that is quite simple: We simply need to cache that "Hash" once by
using the "StringToHash" method and then pass it in into the "SetBool" method. This ensures that the "StringToHash" method
is only called once per "Parameter", instead of being called once on every "SetBool"
call, which should lead to a better performance. Of course, this all assumes that the "StringToHash"
method doesn't cache the data into an "HashSet". If it does, then there shouldn't really be
much or any performance difference. However, regardless of it caching it or not,
it's still a great idea to cache it in as that makes it possible to change its name
through the "Inspector" whenever we want and the changes will be seen across all usages. To do this, in Unity, open up the Player "Data" Scripts
folder and create a new folder named "Animations". Inside, create a new C# Script named "PlayerAnimationData". Open it up and remove the default methods and inheritance and make the class "[Serializable]". We'll need two things for each "Parameter":
Its "name", which is the same as the one we've set in our "Animator Controller"
and a property to store our "Hash". Lets start with our names. I'll create an "[Header()]" with the text
of "State Group Parameter Names" and then, type in "[SerializeField] private string groundedParameterName;" and default it to "Grounded". Then, duplicate this line 4 more times and swap "grounded" with "moving", "stopping", "landing" and
"airborne". Make sure the actual strings have the exact
same name as the "Animator Controller Parameters", even the upper cased letters. I'll now create another "[Header()]" with
the text of "Grounded Parameter Names". Copy one of the lines above and paste them here. I'll paste it in a total of "10" times. When that's done, we'll swap the variable
names and default values with "idle" and "isIdling",
"dash" and "isDashing", "walk" and "isWalking",
"run" and "isRunning", "sprint" and "isSprinting",
"mediumStop" and "isMediumStopping", "hardStop" and "isHardStopping",
"roll" and "isRolling", "hardLand" and "isHardLanding"
and "fall" and "isFalling". Remember that some of our "States" don't have
a "Parameter" as they use "Default Transitions", so we don't need to add them here. If you ever intend to add "Parameters" for them in the
future, then you can also create a variable for them if you want to, but I think you need to make sure they also
exist in the "Animator Controller". That's it for our "Grounded strings", so we
now need to create our "Airborne strings". To do that, copy the first two lines, including the
"Header". Then, paste them in and swap "Grounded" with "Airborne" and set the variable name to be "fallParameterName"
and its value to be "isFalling". With that done, we now need to create properties
for our "Hashes". To do that, create a new public property by typing in "public int GroundedParameterHash { get; private set; }". We don't need the "[field: SerializeField]"
here as we don't need to set these through the "Inspector", as they are generated by the "StringToHash" method. Duplicate this line for each variable we have
above and rename them accordingly. That's done, so we now need to initialize them. To do that, create a new method by typing in "public void Initialize()" and inside type in "GroundedParameterHash =
Animator.StringToHash(groundedParameterName);". The "Animator" here is the "Animator" class itself and the "StringToHash" is simply a "static" method. Do the same thing for every single "Parameter". When that's done, make sure the default names
are correct and save. If you saved and they weren't correct,
make sure that you update them correctly in the "Inspector" as well
when we add it to the "Player" Script. To add it, open up the "Player" Script. In here, create a new "[field: SerializeField]
public PlayerAnimationData AnimationData { get; private set;
}". I'll also add in a "[field: Header("Animations")]". Now, in the "Awake" method, call in
"AnimationData.Initialize();". We now have a reference to our "Parameters Hashes". Of course, we'll need to use the "Animator.SetBool" method, which means we need a reference to our "Animator". To do that, create a new property by typing in "public Animator Animator { get; private set; }". Then, in the "Awake" method, type in
"Animator = GetComponentInChildren();". Note that "GetComponentInChildren"
first searches in the parent and only then in its "Children",
which doesn't really make sense given the name. If you wanted it to search only in the "Children",
then you would probably iterate through its children with the "GetChild" method
or through its transform, if that works. Because we really only call it once
and because we're searching for the "Animator" of the
Player, which there should likely only be one, I'll leave it as is. With that, we'll need to start setting our "Animator
Parameters". To do that, first open up the "PlayerMovementState" Script. We'll create a method here to not need to
call the "SetBool" method every time, so in our "Reusable Methods" region create
a new method by typing in "protected void StartAnimation(int animationHash)". Inside, call in
"stateMachine.Player.Animator.SetBool(animationHash,
true);". We'll also need one to set it to "false",
so duplicate this method and swap the "Start" with "Stop" and the "true" with
"false". With that done, we simply need to call these
methods in each State "Enter" and "Exit" methods, both to "Start" and "Stop", respectively. So, start by opening up the "PlayerGroundedState" Script. In the "Enter" method, type in "StartAnimation(stateMachine.Player.AnimationData
.GroundedParameterHash);". When that's done, copy this line. Then, "override" the "Exit" method
and paste the line we've just copied. In here, simply swap "StartAnimation" with
"StopAnimation" instead. That's all we need to set a "Parameter" value,
so all that's left is to do the exact same thing for the rest of the "States". So, open up every "State" and call both methods
for the "States" that have a "Parameter". That's all we need to do, so when you're done
doing that, save everything and go back to Unity. If we enter Play Mode, our "Animations" should
now be playing whenever a "State" changes. However, you might be noticing a problem when
doing something like "Running" and then "Jumping". This seems to happen because there's a prioritization
order for the "Transitions", likely because you can have 2 conditions being
"true" at the same time, so Unity needs to prioritize one of
them. In this case, if it happens to you, it's likely
that you're prioritizing a "Transition" to something like a "Stopping State" or "Idle
State" instead of prioritizing to the "Jump State" or in this case the "Exit Node". To fix that, exit Play Mode and go to the "Animator
Controller". Clicking any "State", we should see the "Transitions" it
has. We can drag them and set their order
by holding the 2 small bars icon. I'll be changing their order to be: "Exit",
"Moving", "Dash", "Stopping", "Landing" and "Idle", in that
order. Do it for every "Grounded State". For the "Airborne States",
we'll focus on the "Exit" one and then on the "Fall". This should now make it work more correctly. Of course, we still need to take care of our "Animation
Events". To do that, exit Play Mode if you entered
it and select the "Character Model". Then, in the "Project Window" tab,
right click to "Add a new Tab" and select "Animation" in case you don't have one open. This tab shows the animations that are in
the "Animator Controller" of our currently selected Game
Object. This is where we can add our "Animation Events". If we move to any frame that we desire and
click on the last small icon with the Tooltip of "Add Event", we'll add an event to the selected frame, which should show in the frame column. Clicking on it shows us the "Animation Event"
Window, where we can select a public method to call. We however have two problems here: The first problem is that
we have no way of calling our "State Events" as only methods that are part of a Script
in the same Game Object as the "Animator" Component show in this dropdown list. Because our Model is separate from our Player,
this means that we can't access our Player Script code. Furthermore, none of our "States" are "MonoBehaviours",
so it isn't possible for us to add them as Components, which means that
we have no way of directly calling their methods. The second problem is regarding
our "Animation Events" and the "Transition Duration", but we'll take a look at it after we fix the first problem. To fix it, we can create a public method in
our "Player" Script that calls the State Machine "Animation Events" methods. However, our "Animator" currently sits on
a child Game Object of the "Player" Game Object, which again, means that we have no access
to our Player Script methods. To fix that, we'll have to create a new class
that references the "Player" and calls its public methods. We can then attach that Script as a Component
of the "Character Model", which allows us to use it in our "Animation Events". So, start by removing the "Animation Event"
we've just added by right-clicking it and pressing "Delete". Then, open up the "Player" Script. In here, we'll need to create one method for each of the
"Animation Events" methods, so type in, at the bottom, "public void OnMovementStateAnimationEnterEvent()" and inside, call in
"movementStateMachine.OnAnimationEnterEvent();". Duplicate this method twice and swap "Enter"
with "Exit" for the first one and then "Enter" with "Transition" for the second one. When that's done, save it up and go back to Unity. In here, navigate to the Player "Utilities" Scripts folder and create a new folder named "Animations". Inside, we'll create a new C# Script named
"PlayerAnimationEventTrigger". Open it up and remove the default methods. Because we'll add this as a Component, we
need to keep the "MonoBehaviour" inheritance. In here, create a "private" variable of type
"Player" named "player". Then, in the "Awake" method, get it by typing in "player = transform.parent.GetComponent();". There's also the "GetComponentInParent()" method, but that one also calls the "GetComponent"
in the Game Object we call it in, much like the "GetComponentInChildren()"
method, so we'll use our current solution. When that's done, we'll create a new method
to call the events by typing in "public void TriggerOnMovementStateAnimationEnterEvent()"
and inside call in "player.OnMovementStateAnimationEnter();". Duplicate this method twice and swap "Enter"
with "Exit" for the first one and "Enter" with "Transition" for the second one. When that's done, save it up and go back into Unity. Then, in our "CharacterModel" Game Object,
add the "PlayerAnimationEventTrigger" as a new Component. Back to our "Animation" tab,
we can now add in our "Animation Events". Click on the dropdown on the left and select "Dash". We'll need to transition to our "Sprinting State"
here and we'll do it around half way into the "Animation",
so select a frame around the middle, to which I'll chose the "0:11" mark
and add a new "Animation Event". Then, select the "Transition Event" method
in the "Animation Event" Window. Note that you can add multiple "Animation Events"
to the same frame if you wish to. When that's done,
open up the dropdown again and select "LightStop". The "Stopping States" will simply "Transition"
to the "Idling State" at the end of their animation, so around the last frame or around the "1 second" mark, as the "Animation" is somewhat coming to a "Stop" here,
we'll add a new "Animation Event". Call in the "Transition Event" method. Do the same in our "MediumStop Animation". In our "HardStop Animation"
we'll add it a bit earlier, around the "0:20" mark. Then, in our "LightLanding Animation", we'll have an
"Animation Event" "Transitioning" to the "Idling State" at
the last frame. Next, in our "Rolling Animation",
add a new "Animation Event" around the last frame, I'll place it at the "1:02" mark, "Transitioning"
to the "Medium Stopping" or "Moving States". I added it at around the "1:02" mark as that
looked a bit better for me. If the "Transitions" don't look too good,
you can always add them earlier or later, as you desire. Next, for our "HardLanding Animation", we'll need 2
"Events". One will be for enabling our "Movement Input",
while the other one will be to go to the "Idling State". I'll add the first one at around the "1:11" mark,
which should be around when we're getting up and call in the "Exit Event" method. Then, I'll add the second one at around the "1:25" mark
and call in the "Transition Event" method. And that should be it for our "Animation Events". However, we still need to talk about our problem
regarding "Animation Events" and "Transition Durations". Lets enter Play Mode to see what the actual problem is. In here, lets "Dash", wait a bit and "Dash" again. We can see that our "Player" just stopped
"Dashing" right after our second "Dash". The reason why is because "Animation Events"
can still be called when we're in the middle of a
"Transition". This is possible because while we're "Transitioning",
our previous "Animation" is still happening, or running, until we fully transition to the other. An even bigger problem with this is that
we'll already be in our new "State", so the event that will be called
will be the one of that new "State". In our case, we entered the "Dashing State"
and then the "Hard Stopping State" and then "Dashed" near the "Animation Event"
of the "HardStop" animation. Because in code we have already swapped to
the "Dashing State", the "Animation Event" ended up calling the
"Dashing State Event" instead and entered the "Hard Stopping State" again, as that's what
we do in the "Dashing State Transition Event" method. Thankfully, we can easily fix this by not calling an
"Animation Event" if we're in the middle of a "Transition". To do that, open up our "PlayerAnimationEventTrigger"
Script. Then, create a new "private" method that returns
"bool" named "IsInAnimationTransition". We'll add a parameter of type "int" named
"layerIndex" and default it to "0". While we won't be using "Layers" here,
the method we'll call requires it so it's always nice to have the possibility of passing it
in. To be honest, I don't really know much about "Layers", but this "Index" is the index of the "Layer"
our "Animations" are in the "Animator Controller", which should be the first and only one, so it's the index of
"0". Inside of this method, simply type in
"return player.Animator.IsInTransition(layerIndex);". Then, in all of the other methods,
before we call our Player "Animation Events", check "if (IsInAnimationTransition())"
and "return;" if that's the case. Copy the if statement to the other 2 methods as well. When that's done, save it up and go back to Unity. Entering Play Mode, we should no longer have this problem and our "Animations" and "Transitions"
should all be working fine. If you did want to call the previous "State Animation
Event", you could do that by adding a new variable
to our "State Machine" that held the "previousState" and simply call
the "previousState Animation Event" method instead. Of course, you would need a new method in
our "Player" Script to do that. Also note that if our "Transition Duration" was "0 seconds", this problem wouldn't happen
as the "Transition" would be instantaneous. Someone also wondered if we were gonna add
the double Animation that Genshin offers, but I have decided when I started trying to
replicate this System that I would not do it. In case you're wondering what do I mean by "double
Animation", it's simply that Genshin seems to have 2 animations
in certain "States" like "Jumping" or "Stopping". If we take a look at it, we can see the arms
and legs that go to the front can be different. I'm not sure if they do this using IK or simply have 2
Animations, but the leg and arm that go to the front seem
to depend on the foot that was at the front as well. For example, if you "Jump" when your right foot is ahead, you seem to "Jump" with the "Left Knee" forward. It does seem to only happen after the feet
has touched the "Ground", as if you "Jump" before that, you'll "Jump"
with the same side "Knee" forward. According to a forum post, if you're using a "Generic"
Model, which we are, for no particular reason besides
me not really understanding very well how Models and Animations work, we can know which foot is at the
front by using "Animation Events". You would simply go to each step
in the "Moving States Animations", for example, and whenever the foot touches the "Ground" in the
"Animation", call in an event that sets a variable that
tells you what foot is currently at front. If you end up using an "Humanoid" Model, which
is probably what we should've been using, you can know it using the "animator.pivotWeight" variable. Although, it seems that this doesn't work
for some if they have the "Root Animation" option enabled and they had to use something else like the
"Animator.GetBoneTransform()" method. For the animations itself, it's possible that
they do it with IK by setting where the knee should go, or you could flip the animation and add both "Animations" to
an array or simply 2 variables, which you would then choose an "Animation" depending on
which foot is currently at the front. Of course, we won't be doing it ourselves
as that's what I've decided myself before. Regardless, our "Animations" should now be working fine and we should now be able to "Transition"
to every existing Player "State". With all that done, the first part of our
Genshin Impact Movement System is finalized. This took quite a lot longer than I expected
it would, but I hope you were able to understand most
of it without too much trouble. Of course, we still have 2 Systems left to
do, which are the "Gliding" and the "Swimming" Systems. Thankfully, both use the "Movement" System
as their base, so they're not really very hard to create. However, I will be placing this series into
an halt for a while. Both Systems do still need some fixing, as
I've left them unfixed to be able to start the first part, but that's not really the reason why. The simple reasons for why it's coming to
an halt is "Money" and "Motivation". A little over a year ago I got laid off from
work and decided to try and get into game dev and keep going if I was able to earn a somewhat good enough income within around a year. Unfortunately, the amount of money I'm making
right now is "0€", which is the currency my country uses. For those who are interested, it roughly translates
to "0$". This is of course not sustainable, which means
I can't really keep things the way they are, as otherwise I'll have to stop making videos
as I'll have to go find work and well, work. Unfortunately, I can't really record audio
after work hours, so doing both isn't very plausible, which means I would likely not do any more videos for a few
years. Before that happens though, I'd like to try
and earn some money through game dev so I'm thinking on trying to make a few small
games in hopes that it gets me some income. I do plan on finishing the series if everything
goes well, but if things don't go well then that'll depend
on how bad it went, but hopefully I'll be able to finish it up
before I leave to work. I did think about a Patreon but I know I won't
get enough income with it to keep going so I would still have to leave, which would
make me feel like I was kind-of scamming people, which I don't really enjoy the feeling, so
I removed that consideration. Games are also what I'm mostly interested
on and that's what I wanted to focus myself on
when I got into game dev, but in this a little over a year I've mostly focused on tutorials
and not on making games, which brings me to the second point of "Motivation". To be completely honest with you, I've been
feeling quite unmotivated to edit videos and I think I've felt that in the last videos
where I kinda just threw things around without caring too much
even if there were errors on the videos. I also started slacking quite a lot and haven't
slept very well for a while now, so I haven't really been enjoying these days. I will try to rest for a week or so and then
likely try and make some small games to get my motivation up and hopefully get
some income with it so that I can keep doing this. Because of that though, I won't be continuing
with this series for a while so our "Gliding" and "Swimming" Systems won't
be coming so soon. As a tip though, the main feature of the "Gliding"
is simply limiting the velocity much like we did for our "Falling State" but
with a lower value, while "Swimming" is mostly freezing your vertical
position when you enter the water. I also added a GitHub repository where you can check the
code we've done so far for our "Movement System" Of course, it does not contain the "Gliding"
and "Swimming" Systems code, as they aren't really fixed yet. I do apologize for this outcome and I do hope
that you can understand it. Regardless, I hope you've enjoyed the series
so far and learned something new with it. The full video with the first part should
also be coming somewhat soon. It's quite a long video to render so it will
very likely come with a lot of audio artifacts so I apologize for that right now and hope
that it doesn't come with any video artifact whatsoever. Until then though, see you in the next one.