Gamasutra is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Gamasutra: The Art & Business of Making Gamesspacer
arrowPress Releases








If you enjoy reading this site, you might also want to check out these UBM Tech sites:


 

Unity: CHARACTER CONTROLLER vs RIGIDBODY

by Niels Tiercelin on 08/07/17 10:32:00 am   Featured Blogs

The following blog post, unless otherwise noted, was written by a member of Gamasutra’s community.
The thoughts and opinions expressed are those of the writer and not Gamasutra or its parent company.

 

When you’re creating a new project with Unity, one of the first things you have to do is code your avatar’s controller. It’s very important and you can’t rush it or your gamefeel will probably be bad. There are a lot of different things to do, ranging from the inputs acquisition to the movements and feedbacks. 
One of the first question I often ask myself is : “Should I use a Character Controller or a Rigidbody? ”.
Today, through the example of a simple moving, jumping and dashing avatar, we will explore the two approaches.

Here is a github of the complete project:
 
https://github.com/valgoun/CharacterController


The Character Controller approach

Setup

Before anything let’s just setup the project and a basic test scene. As written in our 8 Essential Gamedev Tipswe must be organized. Create these folders:

 

 

 

 

 

 

Then create a very simple scene with a ground, a capsule (our Player) and a stair to play with. 
Adding some verticality with the stairs will allow us to experiment with a lot of different and important things when coding our character: The way it collides with objects, the gravity when falling from a distance, and also to test the jumping mechanic if we want one.

I also put a simple box as a child of our player so we can see its orientation when looking around).

I also put a simple box as a child of our player so we can see its orientation when looking around).

Keep your scene clean, use parents.

 

The Character Controller

The Character Controller is a component you can add to your player. Its function is to move the player according to the environment (the colliders).
It doesn’t respond nor uses physics in any way. 
On top of that, the Character Controller comes with a Capsule Collider. Before seeing how it works, I recommend you to take a look at the manual and the scripting API, it’s always a good thing to do.

For this example, I used the default parameters but feel free to play with them to understand how they work.

 

The core concept behind the Character Controller is that it provides basic collider responses without any physics. Basically, you will move your player like you would do with a Transform, but you can’t go through colliders. 
The main advantage of using this technique is the amount of control we’ll have on how your player behaves, but the downfall is that you’ll have to code practically everything.

The Character Controller includes 2 methods used to move the character: SimpleMove and Move.

SimpleMove takes the speed as parameter and will move the character accordingly. On top of that, the character will respond to gravity. That’s the only physic you’ll get with the Character Controller. The downside is that the Y axis velocity is ignored by this method.

Move requires a little bit more work but is less limited. It takes in parameters the absolute movement. Therefore, it’s framerate dependent and you have to implement gravity on your own.

Even if it’s the more complicated method, it’s the one I prefer because SimpleMove becomes very quickly limiting by the fact that you have no effect on the Y axis velocity.

 

Basic Movements

Let’s implement a very basic movement. For this, let’s create a new C# script named “Character” that we add to our Player/capsule. Then, we’ll need a public variable so we can tweak the speed directly from the editor and a private variable to store a reference to our Character Controller.
(don’t forget to get the Character Controller in the Start method, see code below)

In the Update function, we get the Inputs, that we then store into a Vector3. After, we call the Move method from our CharacterController passing it the Input vector multiplied by the speed and the DeltaTime to be framerate independent and “voilà”, we have our basic movements:

public class Character : MonoBehaviour
{ 
private CharacterController _controller;

void Start()
    {
        _controller = GetComponent<CharacterController>();
    }
    
void Update()
    {
    Vector3 move = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
    _controller.Move(move * Time.deltaTime * Speed);
    }
    
}

 

Our character moves but doesn’t steer according to its movement. It’s easy to change the forward vector of the transform to be the movement vector:

 Vector3 move = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
    _controller.Move(move * Time.deltaTime * Speed);
        if (move != Vector3.zero)
            transform.forward = move;

 

Gravity

Let’s add gravity. For that, we’ll need a… gravity variable (or you can use the global gravity with Physics.gravity.y) and private variable to store the velocity of the character. 
Then, we just have to add the gravity to our player’s velocity at each update and to apply the velocity with the Move method:


 _velocity.y += Gravity * Time.deltaTime;
 _controller.Move(_velocity * Time.deltaTime);

If you try, you will probably feel the gravity as a bit weird. It’s because even when the player is grounded, the velocity is still increasing following the gravity.

To resolve this, we can reset the y velocity to 0 when the player is grounded. 
The CharacterController already has a variable to know if the character is grounded but I found it buggy and I tend to determine myself if the player is grounded. I Like to use the CheckSphere method from the Physics Class. It returns true if any colliders intersect the sphere defined by the parameters. I like to use an empty gameObject child of my player as center of the sphere and then I use a variable for the radius and the layer. This way I can control in editor the way I define the grounded status of my character. (If this part seems hard to understand, take a look at my project and recreate it on yours).

_isGrounded = Physics.CheckSphere(_groundChecker.position, GroundDistance, Ground, QueryTriggerInteraction.Ignore);
        if (_isGrounded && _velocity.y < 0)
            _velocity.y = 0f;

Here, _isGrounded is a bool variable created in the class, _groundChecker is a reference to the child of the player (the center of the sphere), GroundDistance is the radius of the sphere and Ground is the layer where ground objects are.

 

Jump

Adding a jump is pretty easy. When the jump button is pressed and the player grounded, we change the y velocity. To know which value we choose to set our velocity we can use this formula:

“velocity = JumpHeight * -2 * Gravity”

if (Input.GetButtonDown("Jump") && _isGrounded)
    _velocity.y += Mathf.Sqrt(JumpHeight * -2f * Gravity);

Here I chose to use some fancy math so I have a more convenient variable to control my jump (JumpHeight). Other would have chosen to use a direct variable (JumpForce) to control the jump : easier to code, trickier to control.

if (Input.GetButtonDown("Jump") && _isGrounded)
    _velocity.y += JumpForce;

 

Dash and Drag

One last example of movement you can implement : a dash. When you press the dash button your velocity will increase towards the direction you are going. Like the jump you could choose a direct implementation or use math and find a formula to make it simple to control. The maths here are a bit more complicated and I found this to works well.

 if (Input.GetButtonDown("Dash"))
        {
            Debug.Log("Dash");
            _velocity += Vector3.Scale(transform.forward, 
                                       DashDistance * new Vector3((Mathf.Log(1f / (Time.deltaTime * Drag.x + 1)) / -Time.deltaTime), 
                                                                  0, 
                                                                  (Mathf.Log(1f / (Time.deltaTime * Drag.z + 1)) / -Time.deltaTime)));
        }

What we’re doing here is scaling the forward vector (our direction) following a dash vector. The dash vector depends on 2 variables, the dash distance and the drag. We need to have a drag or otherwise, the player would never stop. the idea behind the drag is to “simulate” the friction forces (with air, ground etc…). Drag is an arbitrary value between 0 and Infinity with 0 meaning no drag at all. Here I chose to use a Vector3 to represent the drag so I can specify different drag values along the axis. Then to apply the drag we just need to add this at the end of the update function:

_velocity.x /= 1 + Drag.x * Time.deltaTime;
_velocity.y /= 1 + Drag.y * Time.deltaTime;
_velocity.z /= 1 + Drag.z * Time.deltaTime;

 

Character Controller Conclusion

What did we learn with this ? Well, when we’re using the character controller we have a lot of freedom but at the same time we have to code a lot of stuff ourselves even for simple actions like jumping or gravity.


The Rigidbody approach

Now that we have made our controller with the character lets see how to make the same with a Rigidbody, and what are the differences between the two approaches.

 

Setup

The scene setup is practically the same. The main difference is that we don’t have a Character Controller component attached to the Player GameObject but a Capsule Collider and a Rigidbody
On the Rigidbody, set the X and Z axis rotations to be locked. Also note that I changed the Drag, we’ll see why later. The other options are default values. We can leave the Capsule Collider to its default values.

Be sure to lock the X and Z axis rotations

 

 

Rigidbody Controller

This time, I won’t explain every step of the process because it’s very similar to what we’ve done in the Character Controller. I’ll focus on the differences between the two. Here’s the full script to give you an overview:

public class RigidbodyCharacter : MonoBehaviour
{

    public float Speed = 5f;
    public float JumpHeight = 2f;
    public float GroundDistance = 0.2f;
    public float DashDistance = 5f;
    public LayerMask Ground;

    private Rigidbody _body;
    private Vector3 _inputs = Vector3.zero;
    private bool _isGrounded = true;
    private Transform _groundChecker;

    void Start()
    {
        _body = GetComponent<Rigidbody>();
        _groundChecker = transform.GetChild(0);
    }

    void Update()
    {
        _isGrounded = Physics.CheckSphere(_groundChecker.position, GroundDistance, Ground, QueryTriggerInteraction.Ignore);


        _inputs = Vector3.zero;
        _inputs.x = Input.GetAxis("Horizontal");
        _inputs.z = Input.GetAxis("Vertical");
        if (_inputs != Vector3.zero)
            transform.forward = _inputs;

        if (Input.GetButtonDown("Jump") && _isGrounded)
        {
            _body.AddForce(Vector3.up * Mathf.Sqrt(JumpHeight * -2f * Physics.gravity.y), ForceMode.VelocityChange);
        }
        if (Input.GetButtonDown("Dash"))
        {
            Vector3 dashVelocity = Vector3.Scale(transform.forward, DashDistance * new Vector3((Mathf.Log(1f / (Time.deltaTime * _body.drag + 1)) / -Time.deltaTime), 0, (Mathf.Log(1f / (Time.deltaTime * _body.drag + 1)) / -Time.deltaTime)));
            _body.AddForce(dashVelocity, ForceMode.VelocityChange);
        }
    }


    void FixedUpdate()
    {
        _body.MovePosition(_body.position + _inputs * Speed * Time.fixedDeltaTime);
    }
}

 

FixedUpdate

The first notable difference is the FixedUpdate function. This function is called by Unity before every “physic update”. Indeed, physic updates and classic updates are not synced. To achieve a convincing physic simulation, we need to calculate it smoothly. Unity decided to pull apart the physic update from the classic update. This way if the frame rate is too low or too fast, it won’t impact the simulation.

Quick tip: you can change the physic frame rate (called “Fixed Timestep”) in the Time Manager in the project settings.

The idea behind the FixedUpdate function is that you put the physic code here. But as you can see, I still have a classic Update function. Indeed, the input system of Unity isn’t synced with the FixedUpdate so we have to retrieve the inputs in Update, stock them into a variable (here _inputs) and use it in FixedUpdate. You could manage the inputs directly in FixedUpdate but you would most likely have inconvenient behaviors.

 

Jumping & Dashing

if (Input.GetButtonDown("Jump") && _isGrounded)
{
_body.AddForce(Vector3.up * Mathf.Sqrt(JumpHeight * -2f * Physics.gravity.y), ForceMode.VelocityChange);
}
if (Input.GetButtonDown("Dash"))
{
Vector3 dashVelocity = Vector3.Scale(transform.forward, DashDistance * new Vector3((Mathf.Log(1f / (Time.deltaTime * _body.drag + 1)) / -Time.deltaTime), 0, (Mathf.Log(1f / (Time.deltaTime * _body.drag + 1)) / -Time.deltaTime)));
_body.AddForce(dashVelocity, ForceMode.VelocityChange);
}

Why didn’t you put this into Fixed Update ? It’s physic related.

Yes, but here I used these function in a discrete way : these AddForce calls are instantaneous, therefore it’s not frame dependent. AddForce is a function used to apply a force to Rigidbody. There are 4 different ways to apply a force:

  • Force: continuous and mass dependent
  • Acceleration: continuous and mass independent
  • Impulse: instant and mass dependent
  • VelocityChange: instant and mass independent

Here you can see I’m using velocityChange because I want to have an instant reaction when jumping or dashing, and I don’t care about the player’s mass.

void FixedUpdate()
{
_body.MovePosition(_body.position + _inputs * Speed * Time.fixedDeltaTime);
}

To move the character according to the player’s inputs, we use theMovePosition function. This function tries to move the player to a given position while respecting the collisions rules. It also doesn’t change the velocity of the Rigidbody. This way, the player’s movement are “separated” from the other physic interaction.

You can see we don’t bother ourselves with gravity or drag. That’s because the Rigidbody already does it for us. You can change the drag on the Rigidbody and the gravity in the project settings.

(Be careful with the drag, unlike the one we implemented with the Character Controller, this one isn’t split into different axes, so it’ll affect all three axes at the same time!)

 

Main differences

Even if we want to achieve the same goals with both techniques, they won’t behave exactly the same.

 

Collisions

They both react with colliders but there are some slight differences. While the Rigidbody will react very precisely and even use the physics material property to calculate the reaction, the Character Controller will be more permissive : It will automatically climb slopes and steps (according to its parameters).

Change these parameters to get closer to the feeling you are looking for.

 

Extensibility

In this example we didn’t have a lot of features to code and the Character Controller solution was the easiest one to do, but if we were to implement more mechanics, the Rigidbody would probably be the best way to go. It offers a lot more functions to interact with physic whereas the Character Controller doesn’t, therefore we’d need to code more for the same feature using the Character Controller.

 

In Conclusion

As you can see, we can achieve the same things using both techniques but the way we work will change according the option we choose.
That’s why I strongly advise you to always take some time to think about before starting to code any player controller script!


We hope this article was of some use to you!
If you want more details on this tutorial, or would like to request another one, please get in touch with us, on social media, the comments, etc. We’ll see it.

Follow us on Twitter and Facebook!

 

Read our 8 CRUCIAL POINTS TO DEFINE BEFORE STARTING A COMMERCIAL GAME
Or check out our LATEST WEEKLY DEVBLOG

Related Jobs

Dream Harvest
Dream Harvest — Brighton, England, United Kingdom
[05.25.19]

Technical Game Designer
Pixar Animation Studios
Pixar Animation Studios — Emeryville, California, United States
[05.24.19]

Animation Tools Software Engineer
Disbelief
Disbelief — Chicago, Illinois, United States
[05.24.19]

Senior Programmer, Chicago
Disbelief
Disbelief — Chicago, Illinois, United States
[05.24.19]

Junior Programmer, Chicago





Loading Comments

loader image