HOW TO BUILD SMOOTH 3D PLAYER MOVEMENT IN UNITY USING C#

Create a responsive, robust 3D character controller with analog directional movement, sprint acceleration, ground raycast detection, and gravity.

1. Why Use CharacterController for Humanoids?

While vehicles and spaceships thrive with Rigidbody physics, humanoid characters often feel floaty or clumsy when driven purely by physical forces. Unity's built-in CharacterController component provides:

  • Instant stopping power with zero unwanted sliding momentum.
  • Automatic step-offset climb (smoothly walks up stairs and curbs).
  • Slope limit angles (prevents walking up steep cliffs).
  • Direct integration with follow cameras without physics interpolation stutter.

2. Production 3D Player Controller Script

Attach this script to your Player GameObject alongside a CharacterController:

using UnityEngine;

[RequireComponent(typeof(CharacterController))]
public class PlayerLocomotion : MonoBehaviour
{
    [Header("Movement Speeds")]
    [SerializeField] private float walkSpeed = 5f;
    [SerializeField] private float sprintSpeed = 8.5f;
    [SerializeField] private float jumpHeight = 1.8f;
    [SerializeField] private float gravity = -19.62f; // Snappier game feel than -9.81

    [Header("Ground Detection")]
    [SerializeField] private Transform groundCheck;
    [SerializeField] private float groundDistance = 0.3f;
    [SerializeField] private LayerMask groundMask;

    [Header("Camera Relative Direction")]
    [SerializeField] private Transform cameraTransform;

    private CharacterController controller;
    private Vector3 velocity;
    private bool isGrounded;

    private void Awake()
    {
        controller = GetComponent<CharacterController>();
        if (cameraTransform == null && Camera.main != null)
        {
            cameraTransform = Camera.main.transform;
        }
    }

    private void Update()
    {
        // 1. Ground Check with OverlapSphere
        isGrounded = Physics.CheckSphere(groundCheck.position, groundDistance, groundMask);

        if (isGrounded && velocity.y < 0)
        {
            // Small downward clamp keeps player glued on slopes
            velocity.y = -2f;
        }

        // 2. Read Inputs
        float horizontal = Input.GetAxisRaw("Horizontal");
        float vertical = Input.GetAxisRaw("Vertical");
        Vector3 inputDir = new Vector3(horizontal, 0f, vertical).normalized;

        // 3. Sprinting
        bool isSprinting = Input.GetKey(KeyCode.LeftShift);
        float currentSpeed = isSprinting ? sprintSpeed : walkSpeed;

        // 4. Move Relative to Camera Orientation
        if (inputDir.magnitude >= 0.1f)
        {
            // Calculate target angle based on camera heading
            float targetAngle = Mathf.Atan2(inputDir.x, inputDir.z) * Mathf.Rad2Deg + cameraTransform.eulerAngles.y;
            Vector3 moveDirection = Quaternion.Euler(0f, targetAngle, 0f) * Vector3.forward;

            controller.Move(moveDirection.normalized * currentSpeed * Time.deltaTime);

            // Smoothly rotate character toward movement heading
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.Euler(0f, targetAngle, 0f), 12f * Time.deltaTime);
        }

        // 5. Jump Execution
        if (Input.GetButtonDown("Jump") && isGrounded)
        {
            // Standard kinematic physics formula: v = sqrt(h * -2 * g)
            velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
        }

        // 6. Apply Gravity
        velocity.y += gravity * Time.deltaTime;
        controller.Move(velocity * Time.deltaTime);
    }

    // Visual Gizmo for Ground Check Radius
    private void OnDrawGizmosSelected()
    {
        if (groundCheck != null)
        {
            Gizmos.color = Color.green;
            Gizmos.DrawWireSphere(groundCheck.position, groundDistance);
        }
    }
}

3. Step-by-Step Inspector Setup

  1. Create a 3D Capsule GameObject named Player.
  2. Attach the CharacterController component (Height = 2.0, Center = (0, 1, 0), Radius = 0.5).
  3. Create an empty child GameObject named GroundCheck placed at the base of the player: Position (0, 0, 0).
  4. Assign all floor terrain and static meshes to a custom Layer named Ground.
  5. Attach PlayerLocomotion.cs and assign the GroundCheck transform and Ground LayerMask.

Related Movement Guides & Tools

Frequently Asked Questions

Unity's built-in controller.isGrounded property often loses contact when walking downhill or over bumpy terrain. Using a dedicated Physics.CheckSphere with a small downward velocity clamp (velocity.y = -2f) provides far more reliable ground detection without missed jumps.
You can project your movement vector onto the slope's surface normal using Vector3.ProjectOnPlane(moveDirection, slopeHit.normal). This directs forward velocity parallel to the ramp's incline, preventing gravity from dragging the character down the slope.