3D Player Controller
First/third person character controller with smooth movement, jumping, and slope handling using CharacterController.
How to Use
Attach to your player GameObject
Add a CharacterController component
Create an empty child at the feet for Ground Check
Set Ground Mask to your terrain layer
Sprint with Left Shift
Features
- Camera-relative movement so the player moves where the camera faces
- Sprint toggle with Left Shift and separate walk/sprint speeds
- Smooth rotation via LerpAngle for natural turning
- Configurable gravity and jump height using physics formula
- Ground detection via Physics.CheckSphere with Gizmo visualization
- Automatic CharacterController requirement via RequireComponent
When to Use This
Ideal for third-person action games, RPGs, adventure games, and horror games that need solid ground movement with sprinting. Use this when you want CharacterController-based movement (no Rigidbody physics) with camera-relative input. Works great as the foundation for any 3D game with a controllable character.
Common Mistakes
Not assigning the cameraTransform field and having no Camera tagged MainCamera will cause a NullReferenceException on startup. The groundCheck Transform must be positioned at the character's feet, not the center — otherwise ground detection fails. Setting gravity to a positive value will launch the character upward instead of pulling them down.
Source Code
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class PlayerController3D : MonoBehaviour
{
[Header("Movement")]
[SerializeField] private float walkSpeed = 6f;
[SerializeField] private float sprintSpeed = 10f;
[SerializeField] private float rotationSpeed = 10f;
[Header("Jumping")]
[SerializeField] private float jumpHeight = 1.5f;
[SerializeField] private float gravity = -20f;
[Header("Ground Check")]
[SerializeField] private Transform groundCheck;
[SerializeField] private float groundDistance = 0.3f;
[SerializeField] private LayerMask groundMask;
[Header("Camera")]
[SerializeField] private Transform cameraTransform;
private CharacterController controller;
private Vector3 velocity;
private bool isGrounded;
private void Awake()
{
controller = GetComponent<CharacterController>();
if (cameraTransform == null)
cameraTransform = Camera.main.transform;
}
private void Update()
{
// Ground check
isGrounded = Physics.CheckSphere(groundCheck.position, groundDistance, groundMask);
if (isGrounded && velocity.y < 0f)
velocity.y = -2f;
// Input
float h = Input.GetAxisRaw("Horizontal");
float v = Input.GetAxisRaw("Vertical");
Vector3 inputDir = new Vector3(h, 0f, v).normalized;
// Movement relative to camera
if (inputDir.magnitude >= 0.1f)
{
float targetAngle = Mathf.Atan2(inputDir.x, inputDir.z) * Mathf.Rad2Deg + cameraTransform.eulerAngles.y;
float angle = Mathf.LerpAngle(transform.eulerAngles.y, targetAngle, rotationSpeed * Time.deltaTime);
transform.rotation = Quaternion.Euler(0f, angle, 0f);
Vector3 moveDir = Quaternion.Euler(0f, targetAngle, 0f) * Vector3.forward;
float speed = Input.GetKey(KeyCode.LeftShift) ? sprintSpeed : walkSpeed;
controller.Move(moveDir.normalized * speed * Time.deltaTime);
}
// Jump
if (Input.GetButtonDown("Jump") && isGrounded)
velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
// Apply gravity
velocity.y += gravity * Time.deltaTime;
controller.Move(velocity * Time.deltaTime);
}
private void OnDrawGizmosSelected()
{
if (groundCheck != null)
{
Gizmos.color = isGrounded ? Color.green : Color.red;
Gizmos.DrawWireSphere(groundCheck.position, groundDistance);
}
}
}