U
tab 02Character & Combat9 lessons — tap to open
02 / 09 · 12 min
Character & Combat

Movement, jumping and landing on blocks

your project so far

Assets/

Scenes/

Arena.unity

KnightCharacterController

Scripts/

CharacterStats.csCharacterSwitcher.csSimpleMover.cs (on Knight)→PlayerMovement.cs

you already havethis lesson addsthis lesson retires

SimpleMover from tab 01 got the Knight walking, but it slides through walls, ignores gravity and always moves along the world axes. This lesson replaces it — on the Knight only.

Mage and Archer keep SimpleMover. That is the point: after this lesson you can press 1 and 2 and feel the difference between the two in the same scene.

Movement is always the same three steps: read input, turn it into a direction, apply it with Time.deltaTime.

animated diagram

Keys become a Vector3, the vector becomes metres per second.

The full movement script

PlayerMovement.cs — remove SimpleMover from Knight first
using UnityEngine;

[RequireComponent(typeof(CharacterController))]
public class PlayerMovement : MonoBehaviour
{
    [Header("Move")]
    [SerializeField] private float walkSpeed = 4f;
    [SerializeField] private float runSpeed = 8f;
    [SerializeField] private float turnSmooth = 0.08f;

    [Header("Jump & gravity")]
    [SerializeField] private float jumpHeight = 1.4f;
    [SerializeField] private float gravity = -20f;

    private CharacterController cc;
    private Camera cam;
    private float verticalVelocity;
    private float turnVelocity;

    void Awake()
    {
        cc = GetComponent<CharacterController>();
        cam = Camera.main;
    }

    void Update()
    {
        // ---- 1. read input
        float h = Input.GetAxisRaw("Horizontal");
        float v = Input.GetAxisRaw("Vertical");
        Vector3 input = new Vector3(h, 0f, v).normalized;

        // ---- 2. move relative to the camera
        Vector3 move = Vector3.zero;
        if (input.sqrMagnitude > 0.01f)
        {
            float targetAngle = Mathf.Atan2(input.x, input.z) * Mathf.Rad2Deg
                              + cam.transform.eulerAngles.y;

            float angle = Mathf.SmoothDampAngle(
                transform.eulerAngles.y, targetAngle, ref turnVelocity, turnSmooth);
            transform.rotation = Quaternion.Euler(0f, angle, 0f);

            move = Quaternion.Euler(0f, targetAngle, 0f) * Vector3.forward;
        }

        float speed = Input.GetKey(KeyCode.LeftShift) ? runSpeed : walkSpeed;

        // ---- 3. gravity and jump
        if (cc.isGrounded && verticalVelocity < 0f)
            verticalVelocity = -2f;                       // stick to the ground

        if (cc.isGrounded && Input.GetButtonDown("Jump"))
            verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);

        verticalVelocity += gravity * Time.deltaTime;

        // ---- 4. one single Move call
        Vector3 velocity = move * speed + Vector3.up * verticalVelocity;
        cc.Move(velocity * Time.deltaTime);
    }
}

Put it on the Knight

  1. 1

    Select Knight in the Hierarchy. On the SimpleMover component, click the three dots → Remove Component. The Knight is getting the new script instead; Mage and Archer keep theirs.

  2. 2

    In Project, inside Assets/Scripts: Assets▸Create▸C# Script, name it PlayerMovement — the file name must match the class name exactly, or Unity refuses to attach it.

  3. 3

    Open it, paste the script above over everything inside, save.

  4. 4

    Drag PlayerMovement from Project onto the Knight. A Character Controller appears by itself — that is what [RequireComponent] at the top of the script does.

  5. 5

    Fit that controller to the model, or the capsule sits half in the floor: Center 0, 1, 0 · Radius 0.3 · Height 2. The green capsule in the Scene view should wrap the Knight.

The four ideas inside it

  • Camera-relative movement: pressing W means 'away from the camera', not 'towards world +Z'. Add the camera's Y angle and the controls feel right at every camera angle.

  • SmoothDampAngle turns instantly-changing angles into a smooth spin, and it handles the 359° → 0° wrap correctly.

  • Gravity is accumulated into verticalVelocity every frame; that is what makes falling accelerate instead of being a constant slide.

  • Sqrt(h * -2 * g) is the physics formula for 'jump exactly h metres high' — so the Inspector value is in metres, which is a joy to tune.

Grounded keeps flickering

  • Set verticalVelocity to a small negative number (-2) while grounded so the controller stays pressed onto the floor.

  • Check Slope Limit and Step Offset on the CharacterController — a 0.3 step offset lets the character walk up small ledges.

  • Add a small coyote time (0.15 s after leaving the ground you can still jump). Players will not notice it, but the game feels fair.

Keep the switcher working

CharacterSwitcher enables and disables a SimpleMover. The Knight no longer has one, so the list needs to hold something both scripts share.

CharacterSwitcher.cs — one word changes
// before
[SerializeField] private List<SimpleMover> characters = new List<SimpleMover>();

// after — MonoBehaviour is the base class of both scripts
[SerializeField] private List<MonoBehaviour> characters = new List<MonoBehaviour>();

// everything below still works unchanged, because .enabled lives on MonoBehaviour
characters[i].enabled = (i == index);
coyote time
private float coyote;

// ...
if (cc.isGrounded) coyote = 0.15f;
else coyote -= Time.deltaTime;

if (coyote > 0f && Input.GetButtonDown("Jump"))
{
    verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);
    coyote = 0f;
}