Dennis Hackethal’s Blog
My blog about philosophy, coding, and anything else that interests me.
Controlling a Character's Movements in Unity
Disclaimer: I'm a mere beginner at Unity and game development generally. I wrote this for myself as I am learning and figured others might find it useful as well. Exercise caution when running this code!
Using Unity version 2020.3.2f1 and based on this tutorial – with some changes – I want to show you how you can make your character do the following:
- Move forward/backward
- Move sideways and diagonally
- Sprint while holding left shift
- Rotate by moving the mouse
My code does not implement:
- Jumping
- Looking up or down
- Going up or down slopes
Here's the resulting code without comments (I will walk you through it step by step further down):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
public CharacterController controller;
void Start()
{
Cursor.lockState = CursorLockMode.Locked;
}
void Update()
{
Move();
Rotate();
}
private void Move() {
float moveZ = Input.GetAxis("Vertical");
float moveX = Input.GetAxis("Horizontal");
Vector3 moveDirection = new Vector3(moveX, 0, moveZ);
moveDirection = transform.TransformDirection(moveDirection);
if (Input.GetKey(KeyCode.LeftShift)) {
moveDirection *= 2;
}
controller.Move(moveDirection * 5 * Time.deltaTime);
}
private void Rotate() {
float mouseX = Input.GetAxis("Mouse X") * 250 * Time.deltaTime;
transform.Rotate(Vector3.up, mouseX);
}
}
As you can see, it's not that much. Let's go over it step by step – actually, there's a few things we need to do before we start coding:
- Create a terrain by right-clicking on your hierarchy in Unity, choosing '3D object', then 'Terrain'
- Create another 3D object that you'll place on top of the terrain. This could be a cylinder, for example: '3D object' > 'Cylinder'. Call it 'Player'.
- Drag the main camera into the player. This way, whenever the player moves, the camera follows him
- Add a Rigidbody component to the player and check its 'Is Kinematic' property
- Add a Character Controller component to the player. This component comes with built-in support you'll need to control your player. Likewise leave its settings as is
- Add a script called 'PlayerController' to the player
Now we can start coding. Open the script in your favorite text editor and add this public property:
public CharacterController controller;
Save your changes.
Back in Unity, drag the Character Controller into the
controller
property on your scriptIn your script, make the following change to the
Update()
method:// Update is called once per frame void Update() { Move(); }
Add the corresponding private method
Move()
:private void Move() { // When hitting 'w' or the up arrow, this will evaluate to 1. // When hitting 's' or the down arrow, it will evaluate to -1. // (To be precise, it will return a float which gradually // increases to 1 as you hold 'w' or gradually decreases to -1 // as you hold 's', respectively. This makes movement smoother. // To get only 1 and -1, you can use `Input.GetAxisRaw` instead.) float moveZ = Input.GetAxis("Vertical"); // Our controller will expect a vector telling it in which // directions to move. Here we prepare a new vector that only // moves along the z axis. Vector3 moveDirection = new Vector3(0, 0, moveZ); // We need to move along the *player's* z axis, not the world's. // Once rotated, your player's z axis may be different from // the world's. moveDirection = transform.TransformDirection(moveDirection); // Execute the move. We multiply by 5 since otherwise the move // speed is a bit slow. // We then multiply by `Time.deltaTime`, which is a float that // returns the number of seconds that have passed since the // previous frame was rendered. This is crucial, otherwise // the player would move faster on a monitor with a higher // frame rate as this runs once per frame! controller.Move(moveDirection * 5 * Time.deltaTime); }
Four lines of code without comments. Not bad, right? Save these changes and run the project in Unity. You should be able to move forward and backward using w and s or the up and down arrow keys.
What about moving sideways? Easy. First, add this line below the call to
Input.GetAxis
:float moveX = Input.GetAxis("Horizontal");
This works just like
Input.GetAxis("Vertical")
except it listens for keys a and d as well as the left and right arrow keys.Change the line in which we declare
moveDirection
to:Vector3 moveDirection = new Vector3(moveX, 0, moveZ);
Note that we've replaced the first
0
withmoveX
. You can now move sideways, and also diagonally.Next, let's implement running while holding left shift. Add the following before the call to
controller.Move
:// Run while holding shift if (Input.GetKey(KeyCode.LeftShift)) { moveDirection *= 2; }
Lastly, we'll need to rotate the player left and right whenever we move our mouse. For this, we will add a second method called
Rotate
, along with a couple other changes:void Start() { // Lock cursor to the screen's center point and hide cursor. // You can get your cursor back while playing by pressing // the escape key. Cursor.lockState = CursorLockMode.Locked; } void Update() { Move(); Rotate(); // <- added this line } private void Rotate() { // Determine how much we want to rotate based on the mouse's // movement. Multiply by 250 to make more sensitive – depending // on your hardware and settings you may wish to tweak this number. float mouseX = Input.GetAxis("Mouse X") * 250 * Time.deltaTime; // Rotate around the x axis transform.Rotate(Vector3.up, mouseX); }
That's it!
Known problems with this code (most of which I mentioned at the beginning of this post):
- Can't look up or down yet
- Can't jump yet
- Can 'straferun'*
- No gravity, so probably couldn't move up/down slopes
* You may notice that when you move diagonally, your character moves faster than if you move him only forward/backward or sideways. Why is that?
As I learned here, it has to do with the Pythagorean theorem. If you simultaneously go one step forward and one step to the right, we can calculate the length of the resulting diagonal like this: 1² + 1² = diagonal²
. Meaning diagonal
comes out to the square root of 2, which is ~1.414. So while you move one unit forward and one unit sideways, combining the two into a diagonal results in moving 1.414 units! As pointed out in the video linked above, this is called 'straferunning', which was used in Doom to move faster and jump farther, as explained here:
Straferunning is running forward and moving sideways (strafing) at the same time, which results in moving faster than is possible in either direction alone. One of the advantages is being able to jump farther than is otherwise possible.
Fixing this is easy by normalizing the movement vector:
Vector3 moveDirection = new Vector3(moveX, 0, moveZ).normalized;
^
A normalized vector (also called a unit vector) points in the same direction as its non-normalized counterpart but always has length 1, no matter the values of moveX
and moveZ
.
The problem with this fix is that it can add a delay to your movements. You will notice that your character doesn't always stop immediately upon letting go of whatever keys you held to move. I don't know yet how to fix that and will need to review the above-linked video in which I learned about normalization.
What people are saying