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
- Select your character in the Hierarchy
- Add a Capsule Collider (Component > Physics > Capsule Collider)
- Configure the collider:
- Height: 2.0
- Radius: 0.5
- Direction: Y-Axis
- Add a Rigidbody (Component > Physics > Rigidbody)
- 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
- Select your character in the Hierarchy
- Add Component > Scripts > Player Controller
- 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
- Select your Main Camera in the Hierarchy
- Add Component > Scripts > Camera Controller
- Set the Target to your character
- 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
- Edit → Project Settings → Input Manager
- 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
- Create a Plane (3D Object > Plane) for ground
- Scale it up (Scale: 10, 1, 10)
- Add some obstacles (cubes, spheres) to test movement
- 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.