Introduction
Due to the nature of their physics, Rigidbodies can sometimes be a hassle to work with because you have to abide by the laws of Unity's physics system. However, there are safe and robust ways to handle certain things, like walking up and down slopes
Character Controller vs. Rigidbody
Character Controllers are useful for many cases. They have an easy-to-use API, and as long as the surrounding world is static, they handle accurate and performant collisions. The problems begin when the world around the player is no longer static (e.g., moving platforms, multiple players, etc.) because Character Controllers simply are not designed for those sorts of environments. Every solution to the Character Controller's pitfall is extremely hacky and only works some of the time. That's where making the switch to a Rigidbody might be a reasonable move. The physics collisions (both static and dynamic) are extremely accurate out-of-the-box and there's more support for them in multiplayer. Now, the only issue is that you have to build everything from the ground up, but that's what this article is for. Hopefully, this should give you a good base to work with to expand upon.
How it works
Once you see the script, it looks simple, but there's a lot of thought that goes into it, so we can break the functionality down into a few parts. We need a physics material on the player's collider that has zero friction, the actual movement code (which comes with staying on slopes out-of-the-box), landing on slopes, and moving up slopes.
Let's dive deeper into each of these.
NOTE: This script requires a ground check, but for the purposes of the scope of the tutorial, I am assuming that you already have a ground check implemented. You must ensure that your ground check returns a normal vector!
Physics material
When a Rigidbody stands on a slope, the amount of friction of the slope will determine how fast the Rigidbody will slide downwards, but sometimes, we don't want this functionality at all. In most first/third person games, the player should stay still on a slope. In order to achieve this, we need to unintuitively disable friction on the player completely. This is because our movement code gives the illusion of friction so that it can be fine-tuned to keep the player completely still.
Create a physics material, set the Dynamic Friction and Static Friction values to zero, and change the Friction Combine to Minimum. Then, attach it to the player's collider.
Movement & staying on a slope
Here's the movement code:
var result = this.acceleration * (desiredVelocity - velocity) - this.damping * velocity;
this.rigidbody.AddForce(result, ForceMode.Acceleration);
Here's a better breakdown of what's happening:
// How far off is our current velocity from our desired velocity
var velocityError = desiredVelocity - velocity;
// How quickly we want to reach our desired velocity
var driveForce = this.acceleration * velocityError;
// The `damping` variable allows us to fine-tune our motion and slightly push against it
var friction = this.damping * velocity;
var result = driveForce - friction;
The cool part is that, due to the driveForce, we are always fighting any velocity error (including gravity pulling us down), which cancels out any sliding force down a slope. If our desired velocity is zero (meaning that we are not touching any movement keys), our current velocity is now going to be cancelled out due to the -velocity term, so that makes our lives easier.
Moving up slopes
This step involves the normal vector of the ground determined by your ground check code. The ProjectOnPlane method rotates your desiredVelocity vector to be perpendicular to the ground normal so that if our ground normal rotates toward us (we are moving up an incline), our velocity will rotate up with it, staying perpendicular to the ground that we are standing on.
var direction = Vector3.ProjectOnPlane(desiredVelocity, groundNormal);
Landing on slopes
If you were to run the code as is, you would notice that jumping upwards on a slope, then landing back down on it causes you to slide down for a second and then come to a rest. This is because it takes a few frames for our driveForce to fully kick in. The way that we solve that is by directly counteracting the player's downward velocity on the very first frame that the player hits the slope:
if (firstFrame)
{
this.rigidbody.AddForce(Vector3.ProjectOnPlane(new Vector3(0.0f, -this.rigidbody.linearVelocity.y), groundNormal), ForceMode.VelocityChange);
}
Finally, putting it all together along with all the variables associated with it gets you this:
[Header("Movement Settings")]
[SerializeField] private float speed = 5.0f;
[SerializeField] private float acceleration = 10.0f;
[SerializeField] private float damping = 2.0f;
[Header("Component References")]
[SerializeField] private new Rigidbody rigidbody;
[SerializeField] private new Transform camera;
// Stores the WASD input from the player
// This can be retrieved in a variety of ways, but this article does not go over how to do so
// The X axis is left/right and the Y axis is forwards/backwards
private Vector2 inputDirection;
/// <summary>
/// Moves the player along any slope
/// </summary>
/// <param name="groundNormal">
/// The normal vector of the ground directly below the player
/// </param>
/// <param name="firstFrame">
/// Whether this is the frame that we landed on the ground.
/// </param>
private void GroundMove(Vector3 groundNormal, bool firstFrame)
{
if (firstFrame)
this.rigidbody.AddForce(Vector3.ProjectOnPlane(new Vector3(0.0f, -this.rigidbody.linearVelocity.y), groundNormal), ForceMode.VelocityChange);
this.rigidbody.AddForce(Vector3.ProjectOnPlane(-Physics.gravity, normal), ForceMode.Acceleration);
var velocity = this.rigidbody.linearVelocity;
var worldInputDirection = new Vector3(this.inputDirection.x, 0.0f, this.inputDirection.y);
var direction = this.camera.TransformDirection(worldInputDirection);
direction = Vector3.ProjectOnPlane(direction, groundNormal);
var desiredVelocity = direction.normalized * this.speed;
var result = this.acceleration * (desiredVelocity - velocity) - this.damping * velocity;
this.rigidbody.AddForce(result, ForceMode.Acceleration);
}
If you have any questions, please feel free to email support@criticalanglestudios.com. Happy coding!
