Making movement that feels good? Great. Ignore real physics, iterate and playtest until you get something that can stand alone on its own feet.
Wallrunning? Bunnyhopping? Air strafing? Feedback?
Where do we start?
First, we need to detect and determine when our player is in the ground. How do we do this? A simple raycast works fine, we check the distance to the ground and if it’s less than a certain threshold, we consider the player grounded.
Next, we need to handle player input. Okay, we want to move forward, what do we do? We take the input direction and apply a force in that direction. But wait, we need to consider the player’s current velocity. If the player is already moving, we don’t want to just add more force in the same direction, we want to adjust the velocity based on the input. So, what Unity function do we use? AddForce with ForceMode.Acceleration. This way, we can apply a consistent acceleration regardless of the player’s mass.
Okay, what about jumping then? When the player presses the jump button, we need to apply an upward force. But we also need to consider if the player is grounded or not. That is what the raycast is for. If the player is grounded, we can apply a jump force. But how do we allow for control while in the air? A typical approach is to allow for some air control, but not as much as when grounded. Air strafing or lateral steering can be achieved by applying a smaller force in the direction of the input while in the air.
Great, and how do we add more feature without this becoming an unmanageable mess? We can use a state machine to manage the different states of the player. So we can easily check which state the player is in (grounded, jumping, falling, etc.) and apply the appropriate logic for each state. Easy.
Great, what about walls? How do we detect those? Yes, raycast again. Where do we raycast now? If we have one raycast down to check for ground, we can have another raycast in the direction of movement to check for walls. But that would only detect walls directly in front of the player. What if we want to run to a wall from the side? We raycast to both sides of the player as well. Okay, we can detect an object, but how do we know it is a wall? Do we use tags, layers, or something else? We use surface normal classification. If the normal of the surface we hit is within a certain angle of vertical, we consider it a wall. This makes it simple and we do not have to care about tagging or ensuring things are properly marked. There is only 3 types of surfaces we care about: ground, wall, and ceiling.
Okay, we cast a ray and detect a wall. What now? The player runs to the wall and we want to allow them to run along it. We can do this by projecting the player’s velocity onto the wall’s surface. We can use Vector3.ProjectOnPlane for this. Okay, what does that do EXACTLY? It takes the player’s current velocity and projects it onto a plane defined by the wall’s normal. Okay, but that does not allow us to wallrun yet. An elegant solution is to nullify gravity while wallrunning. Now you might want to have gravity, if you want to limit how long the player can wallrun. But in the case of MWM, we want to allow the player to wallrun indefinitely. So we just set gravity to zero while wallrunning. This allows the player to stick to the wall and easily run along it.
Essentially, we do this
Vector3 normalVelocity = Vector3.Project(_rb.linearVelocity, _currentSurface.Normal);
if (Vector3.Dot(normalVelocity, _currentSurface.Normal) > 0)
{
_rb.linearVelocity -= normalVelocity;
}This code projects the player’s velocity onto the wall’s normal and removes any component of the velocity that is going into the wall. This allows the player to stick to the wall and run along it.
Climbing and more raycasting
Small quality of life improvements
Coyote time, mario jump, timer based checks, camera based checks… to help player feel in control. Things a regular player does not even notice, but just feel better.
To be continued…