Lesson 4: Character Controller & Movement Systems

Welcome to the heart of your RPG game! In this lesson, you'll create the foundation that makes your character feel responsive and fun to control. This is where your game transforms from a static scene into an interactive experience.

What You'll Build:

  • Smooth, responsive character movement
  • Camera controls that follow your character
  • Input handling for keyboard and gamepad
  • A movement system that feels great to play

Prerequisites

Before starting this lesson, make sure you have:

  • Completed Lesson 3: Art Pipeline & Asset Organization
  • A Unity project with your character model imported
  • Basic understanding of C# scripting
  • Unity 2022.3 LTS installed

Step 1: Setting Up Your Character

Step 1.1: Prepare Your Character GameObject

  1. Select your character in the Hierarchy
  2. Add a Capsule Collider (Component > Physics > Capsule Collider)
  3. Configure the collider:
    • Height: 2.0
    • Radius: 0.5
    • Direction: Y-Axis
  4. Add a Rigidbody (Component > Physics > Rigidbody)
  5. Configure the Rigidbody:
    • Mass: 1
    • Drag: 5
    • Angular Drag: 5
    • Freeze Rotation: X, Z (prevent character from tipping over)

Step 1.2: Create the Movement Script

Create a new C# script called PlayerController:

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [Header("Movement Settings")]
    public float moveSpeed = 5f;
    public float jumpForce = 7f;
    public float rotationSpeed = 10f;

    [Header("Ground Check")]
    public Transform groundCheck;
    public float groundDistance = 0.4f;
    public LayerMask groundMask;

    private Rigidbody rb;
    private bool isGrounded;
    private Vector3 moveDirection;

    void Start()
    {
        rb = GetComponent<Rigidbody>();

        // Create ground check object if it doesn't exist
        if (groundCheck == null)
        {
            GameObject groundCheckObj = new GameObject("GroundCheck");
            groundCheckObj.transform.SetParent(transform);
            groundCheckObj.transform.localPosition = new Vector3(0, -1f, 0);
            groundCheck = groundCheckObj.transform;
        }
    }

    void Update()
    {
        HandleInput();
        CheckGrounded();
    }

    void FixedUpdate()
    {
        Move();
    }

    void HandleInput()
    {
        // Get input
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        // Calculate movement direction
        moveDirection = new Vector3(horizontal, 0, vertical).normalized;

        // Jump input
        if (Input.GetButtonDown("Jump") && isGrounded)
        {
            Jump();
        }
    }

    void Move()
    {
        // Apply movement
        Vector3 move = moveDirection * moveSpeed;
        rb.velocity = new Vector3(move.x, rb.velocity.y, move.z);

        // Rotate character to face movement direction
        if (moveDirection.magnitude > 0.1f)
        {
            Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
            transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
        }
    }

    void Jump()
    {
        rb.velocity = new Vector3(rb.velocity.x, jumpForce, rb.velocity.z);
    }

    void CheckGrounded()
    {
        isGrounded = Physics.CheckSphere(groundCheck.position, groundDistance, groundMask);
    }

    void OnDrawGizmosSelected()
    {
        if (groundCheck != null)
        {
            Gizmos.color = isGrounded ? Color.green : Color.red;
            Gizmos.DrawWireSphere(groundCheck.position, groundDistance);
        }
    }
}

Step 1.3: Attach the Script

  1. Select your character in the Hierarchy
  2. Add Component > Scripts > Player Controller
  3. Configure the settings:
    • Move Speed: 5
    • Jump Force: 7
    • Rotation Speed: 10
    • Ground Distance: 0.4

Step 2: Setting Up the Camera

Step 2.1: Create a Camera Controller

Create a new C# script called CameraController:

using UnityEngine;

public class CameraController : MonoBehaviour
{
    [Header("Camera Settings")]
    public Transform target;
    public Vector3 offset = new Vector3(0, 5, -10);
    public float followSpeed = 10f;
    public float rotationSpeed = 5f;

    [Header("Mouse Look")]
    public float mouseSensitivity = 2f;
    public float maxLookAngle = 80f;

    private float currentX = 0f;
    private float currentY = 0f;

    void Start()
    {
        // Lock cursor to center of screen
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
    }

    void Update()
    {
        HandleMouseLook();
    }

    void LateUpdate()
    {
        FollowTarget();
    }

    void HandleMouseLook()
    {
        // Get mouse input
        currentX += Input.GetAxis("Mouse X") * mouseSensitivity;
        currentY -= Input.GetAxis("Mouse Y") * mouseSensitivity;

        // Clamp vertical rotation
        currentY = Mathf.Clamp(currentY, -maxLookAngle, maxLookAngle);

        // Apply rotation
        transform.rotation = Quaternion.Euler(currentY, currentX, 0);
    }

    void FollowTarget()
    {
        if (target == null) return;

        // Calculate desired position
        Vector3 desiredPosition = target.position + offset;

        // Smoothly move camera
        transform.position = Vector3.Lerp(transform.position, desiredPosition, followSpeed * Time.deltaTime);
    }
}

Step 2.2: Configure the Camera

  1. Select your Main Camera in the Hierarchy
  2. Add Component > Scripts > Camera Controller
  3. Set the Target to your character
  4. Configure the settings:
    • Offset: (0, 5, -10)
    • Follow Speed: 10
    • Rotation Speed: 5
    • Mouse Sensitivity: 2
    • Max Look Angle: 80

Step 3: Input System Setup

Step 3.1: Configure Input Manager

  1. EditProject SettingsInput Manager
  2. Verify these inputs exist:
    • Horizontal: A/D keys, Left/Right arrow keys
    • Vertical: W/S keys, Up/Down arrow keys
    • Jump: Space key
    • Mouse X: Mouse X axis
    • Mouse Y: Mouse Y axis

Step 3.2: Add Gamepad Support

Create a new C# script called InputManager:

using UnityEngine;

public class InputManager : MonoBehaviour
{
    [Header("Input Settings")]
    public float gamepadDeadzone = 0.2f;

    public Vector2 GetMovementInput()
    {
        Vector2 input = Vector2.zero;

        // Keyboard input
        input.x = Input.GetAxis("Horizontal");
        input.y = Input.GetAxis("Vertical");

        // Gamepad input (if connected)
        if (Input.GetJoystickNames().Length > 0)
        {
            float gamepadX = Input.GetAxis("Horizontal");
            float gamepadY = Input.GetAxis("Vertical");

            // Apply deadzone
            if (Mathf.Abs(gamepadX) < gamepadDeadzone) gamepadX = 0;
            if (Mathf.Abs(gamepadY) < gamepadDeadzone) gamepadY = 0;

            input.x = gamepadX;
            input.y = gamepadY;
        }

        return input;
    }

    public bool GetJumpInput()
    {
        return Input.GetButtonDown("Jump");
    }

    public Vector2 GetMouseInput()
    {
        return new Vector2(Input.GetAxis("Mouse X"), Input.GetAxis("Mouse Y"));
    }
}

Step 4: Advanced Movement Features

Step 4.1: Add Sprinting

Update your PlayerController script to include sprinting:

[Header("Sprint Settings")]
public float sprintSpeed = 8f;
public float sprintStamina = 100f;
public float staminaDrain = 20f;
public float staminaRegen = 15f;

private float currentStamina;
private bool isSprinting;

void Start()
{
    // ... existing code ...
    currentStamina = sprintStamina;
}

void HandleInput()
{
    // ... existing movement code ...

    // Sprint input
    isSprinting = Input.GetKey(KeyCode.LeftShift) && currentStamina > 0;

    if (isSprinting)
    {
        currentStamina -= staminaDrain * Time.deltaTime;
        currentStamina = Mathf.Clamp(currentStamina, 0, sprintStamina);
    }
    else
    {
        currentStamina += staminaRegen * Time.deltaTime;
        currentStamina = Mathf.Clamp(currentStamina, 0, sprintStamina);
    }
}

void Move()
{
    // Calculate speed based on sprinting
    float currentSpeed = isSprinting ? sprintSpeed : moveSpeed;

    // Apply movement
    Vector3 move = moveDirection * currentSpeed;
    rb.velocity = new Vector3(move.x, rb.velocity.y, move.z);

    // ... existing rotation code ...
}

Step 4.2: Add Crouching

Add crouching functionality:

[Header("Crouch Settings")]
public float crouchSpeed = 2f;
public float crouchHeight = 1f;
public float normalHeight = 2f;

private bool isCrouching;
private CapsuleCollider capsuleCollider;

void Start()
{
    // ... existing code ...
    capsuleCollider = GetComponent<CapsuleCollider>();
}

void HandleInput()
{
    // ... existing code ...

    // Crouch input
    isCrouching = Input.GetKey(KeyCode.LeftControl);

    // Adjust collider height
    if (isCrouching)
    {
        capsuleCollider.height = crouchHeight;
    }
    else
    {
        capsuleCollider.height = normalHeight;
    }
}

void Move()
{
    // Calculate speed based on crouching
    float currentSpeed = isCrouching ? crouchSpeed : (isSprinting ? sprintSpeed : moveSpeed);

    // ... rest of movement code ...
}

Step 5: Testing and Polish

Step 5.1: Create a Test Environment

  1. Create a Plane (3D Object > Plane) for ground
  2. Scale it up (Scale: 10, 1, 10)
  3. Add some obstacles (cubes, spheres) to test movement
  4. Create ramps to test jumping and movement

Step 5.2: Fine-tune Movement

Adjust these values for better feel:

  • Move Speed: 5-7 (good for RPG)
  • Jump Force: 6-8 (not too floaty)
  • Rotation Speed: 8-12 (responsive turning)
  • Follow Speed: 8-12 (smooth camera)

Step 5.3: Add Visual Feedback

Create a simple UI to show stamina:

using UnityEngine;
using UnityEngine.UI;

public class StaminaUI : MonoBehaviour
{
    public Slider staminaBar;
    public PlayerController player;

    void Update()
    {
        if (staminaBar != null && player != null)
        {
            staminaBar.value = player.currentStamina / player.sprintStamina;
        }
    }
}

Common Mistakes to Avoid

Mistake 1: Using Transform.Translate for Movement

// Wrong - doesn't work well with physics
transform.Translate(moveDirection * moveSpeed * Time.deltaTime);

// Correct - use Rigidbody for physics-based movement
rb.velocity = new Vector3(move.x, rb.velocity.y, move.z);

Mistake 2: Not Checking Ground Before Jumping

// Wrong - allows infinite jumping
if (Input.GetButtonDown("Jump"))
{
    Jump();
}

// Correct - check if grounded first
if (Input.GetButtonDown("Jump") && isGrounded)
{
    Jump();
}

Mistake 3: Camera Following Too Closely

// Wrong - jerky camera movement
transform.position = target.position + offset;

// Correct - smooth camera following
transform.position = Vector3.Lerp(transform.position, desiredPosition, followSpeed * Time.deltaTime);

Pro Tips for Success

Tip 1: Use Physics-Based Movement

  • Rigidbody movement feels more natural
  • Collision detection works automatically
  • Physics interactions are more realistic

Tip 2: Smooth Camera Following

  • Use LateUpdate for camera movement
  • Lerp between positions for smoothness
  • Separate rotation and position for better control

Tip 3: Input Responsiveness

  • Use GetAxis for smooth input
  • Apply deadzones for gamepad support
  • Separate input logic from movement logic

Troubleshooting

Problem: Character slides on slopes

Solution: Increase drag and angular drag on Rigidbody

Problem: Camera jitters when following

Solution: Use LateUpdate and increase follow speed

Problem: Character falls through ground

Solution: Check collider settings and ground layer mask

Problem: Movement feels sluggish

Solution: Increase move speed and rotation speed values

What's Next?

Congratulations! You've created a solid foundation for character movement. Your character now:

  • Moves smoothly with keyboard and gamepad input
  • Jumps and lands with proper physics
  • Has a responsive camera that follows smoothly
  • Includes advanced features like sprinting and crouching

Next Steps:

  • Learn about AI-powered NPC systems
  • Create interactive dialogue systems
  • Build quest and inventory systems
  • Add combat and magic systems

Ready to bring your world to life with intelligent NPCs? Let's move on to creating AI-powered characters that will make your RPG feel alive and engaging!


This lesson covers the essential movement systems for your RPG. For more advanced movement techniques, check our Unity Character Controller Guide and Camera Systems Tutorial.