Need something simpler? See the (basic version).
Part of these game systems:
intermediate Movement PRO

3D Player Controller Pro

Advanced 3D character controller with crouch, slide, ladder climbing, swimming, stamina system, and footstep audio.

Unity 2022.3+ · 7.0 KB · PlayerController3DPro.cs

How to Use

1

Attach to your player with a CharacterController

2

Set walk, sprint, and crouch speeds

3

Press Left Ctrl or C to crouch, Left Shift to sprint

4

Sprint + Crouch triggers a slide

5

Tag ladder triggers as 'Ladder' — character auto-enters climb mode

6

Tag water trigger volumes as 'Water' — Space rises, Ctrl sinks

7

Stamina drains while sprinting/swimming, regens when idle

8

Assign footstep AudioClips for walk and sprint

9

Hook OnStaminaChanged to your stamina UI bar

Source Code

PlayerController3DPro.cs
C#
using UnityEngine;
using UnityEngine.Events;

[RequireComponent(typeof(CharacterController))]
public class PlayerController3DPro : MonoBehaviour
{
    [Header("Movement")]
    [SerializeField] private float walkSpeed = 5f;
    [SerializeField] private float sprintSpeed = 9f;
    [SerializeField] private float crouchSpeed = 2.5f;
    [SerializeField] private float smoothing = 10f;

    [Header("Jumping")]
    [SerializeField] private float jumpForce = 8f;
    [SerializeField] private float gravity = -20f;
    [SerializeField] private float coyoteTime = 0.15f;

    [Header("Crouching")]
    [SerializeField] private float crouchHeight = 1f;
    [SerializeField] private float standHeight = 2f;
    [SerializeField] private float crouchTransitionSpeed = 8f;
    [SerializeField] private LayerMask ceilingMask;

    [Header("Sliding")]
    [SerializeField] private float slideSpeed = 14f;
    [SerializeField] private float slideDuration = 0.8f;
    [SerializeField] private float slideCooldown = 1.5f;
    [SerializeField] private float slideDeceleration = 12f;

    [Header("Ladders")]
    [SerializeField] private float climbSpeed = 4f;
    [SerializeField] private string ladderTag = "Ladder";

    [Header("Swimming")]
    [SerializeField] private float swimSpeed = 4f;
    [SerializeField] private float buoyancy = 3f;
    [SerializeField] private float waterDrag = 4f;
    [SerializeField] private string waterTag = "Water";

    [Header("Stamina")]
    [SerializeField] private float maxStamina = 100f;
    [SerializeField] private float sprintDrain = 15f;
    [SerializeField] private float swimDrain = 10f;
    [SerializeField] private float staminaRegen = 20f;
    [SerializeField] private float regenDelay = 1f;

    [Header("Footsteps")]
    [SerializeField] private AudioSource footstepSource;
    [SerializeField] private AudioClip[] walkClips;
    [SerializeField] private AudioClip[] sprintClips;
    [SerializeField] private float walkStepInterval = 0.5f;
    [SerializeField] private float sprintStepInterval = 0.3f;

    [Header("Events")]
    public UnityEvent<float, float> OnStaminaChanged;
    public UnityEvent OnSlideStarted;
    public UnityEvent OnLadderEntered;
    public UnityEvent OnWaterEntered;

    private CharacterController cc;
    private Vector3 velocity;
    private float currentSpeed;
    private float coyoteTimer;
    private float currentStamina;
    private float staminaRegenTimer;
    private float stepTimer;
    private float slideTimer;
    private float slideCooldownTimer;
    private float currentSlideSpeed;
    private Vector3 slideDirection;

    private bool isSprinting;
    private bool isCrouching;
    private bool isSliding;
    private bool isOnLadder;
    private bool isInWater;
    private float targetHeight;

    private void Awake()
    {
        cc = GetComponent<CharacterController>();
        currentStamina = maxStamina;
        targetHeight = standHeight;
    }

    private void Update()
    {
        if (isOnLadder) { HandleLadder(); return; }
        if (isInWater) { HandleSwimming(); return; }

        HandleGroundCheck();
        HandleMovement();
        HandleCrouch();
        HandleSlide();
        HandleJump();
        HandleGravity();
        HandleStamina();
        HandleFootsteps();

        cc.Move(velocity * Time.deltaTime);
        UpdateHeight();
    }

    private void HandleGroundCheck()
    {
        if (cc.isGrounded)
        {
            coyoteTimer = coyoteTime;
            if (velocity.y < 0f) velocity.y = -2f;
        }
        else
        {
            coyoteTimer -= Time.deltaTime;
        }
    }

    private void HandleMovement()
    {
        if (isSliding) return;

        float h = Input.GetAxisRaw("Horizontal");
        float v = Input.GetAxisRaw("Vertical");
        Vector3 move = (transform.right * h + transform.forward * v).normalized;

        isSprinting = Input.GetKey(KeyCode.LeftShift) && !isCrouching && currentStamina > 0f && v > 0;

        float targetSpeed = isCrouching ? crouchSpeed : (isSprinting ? sprintSpeed : walkSpeed);
        currentSpeed = Mathf.Lerp(currentSpeed, move.magnitude > 0 ? targetSpeed : 0f,
            smoothing * Time.deltaTime);

        Vector3 horizontal = move * currentSpeed;
        velocity.x = horizontal.x;
        velocity.z = horizontal.z;
    }

    private void HandleCrouch()
    {
        if (isSliding) return;

        if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.C))
        {
            if (!isCrouching)
            {
                isCrouching = true;
                targetHeight = crouchHeight;

                // Initiate slide if sprinting
                if (isSprinting && slideCooldownTimer <= 0f)
                    StartSlide();
            }
            else if (CanStandUp())
            {
                isCrouching = false;
                targetHeight = standHeight;
            }
        }
    }

    private bool CanStandUp()
    {
        Vector3 origin = transform.position + Vector3.up * crouchHeight;
        float checkDist = standHeight - crouchHeight;
        return !Physics.SphereCast(origin, cc.radius * 0.9f, Vector3.up, out _, checkDist, ceilingMask);
    }

    private void HandleSlide()
    {
        slideCooldownTimer -= Time.deltaTime;

        if (!isSliding) return;

        slideTimer -= Time.deltaTime;
        currentSlideSpeed = Mathf.Max(0f, currentSlideSpeed - slideDeceleration * Time.deltaTime);

        velocity.x = slideDirection.x * currentSlideSpeed;
        velocity.z = slideDirection.z * currentSlideSpeed;

        if (slideTimer <= 0f || currentSlideSpeed <= 1f)
            EndSlide();
    }

    private void StartSlide()
    {
        isSliding = true;
        slideTimer = slideDuration;
        slideCooldownTimer = slideCooldown;
        currentSlideSpeed = slideSpeed;
        slideDirection = transform.forward;
        targetHeight = crouchHeight;
        OnSlideStarted?.Invoke();
    }

    private void EndSlide()
    {
        isSliding = false;
        if (CanStandUp())
        {
            isCrouching = false;
            targetHeight = standHeight;
        }
    }

    private void HandleJump()
    {
        if (Input.GetButtonDown("Jump") && coyoteTimer > 0f)
        {
            // Auto-stand before jumping if crouching
            if (isCrouching)
            {
                if (!CanStandUp()) return;
                isCrouching = false;
                targetHeight = standHeight;
            }
            velocity.y = jumpForce;
            coyoteTimer = 0f;
        }
    }

    private void HandleGravity()
    {
        velocity.y += gravity * Time.deltaTime;
    }

    private void HandleLadder()
    {
        float v = Input.GetAxisRaw("Vertical");
        float h = Input.GetAxisRaw("Horizontal");

        velocity = new Vector3(0f, v * climbSpeed, 0f);
        cc.Move(velocity * Time.deltaTime);

        if (Input.GetButtonDown("Jump") || Mathf.Abs(h) > 0.8f)
        {
            isOnLadder = false;
            velocity = transform.forward * walkSpeed * 0.5f + Vector3.up * jumpForce * 0.5f;
            cc.Move(velocity * Time.deltaTime);
        }
    }

    private void HandleSwimming()
    {
        float h = Input.GetAxisRaw("Horizontal");
        float v = Input.GetAxisRaw("Vertical");
        Vector3 move = (transform.right * h + transform.forward * v).normalized;

        float vert = 0f;
        if (Input.GetKey(KeyCode.Space)) vert = buoyancy;
        else if (Input.GetKey(KeyCode.LeftControl)) vert = -buoyancy;

        Vector3 targetVelocity = move * swimSpeed + Vector3.up * vert;
        velocity = Vector3.Lerp(velocity, targetVelocity, waterDrag * Time.deltaTime);

        cc.Move(velocity * Time.deltaTime);

        // Drain stamina
        if (move.magnitude > 0.1f)
        {
            currentStamina -= swimDrain * Time.deltaTime;
            currentStamina = Mathf.Max(0f, currentStamina);
            OnStaminaChanged?.Invoke(currentStamina, maxStamina);
        }
    }

    private void HandleStamina()
    {
        if (isSprinting)
        {
            currentStamina -= sprintDrain * Time.deltaTime;
            currentStamina = Mathf.Max(0f, currentStamina);
            staminaRegenTimer = regenDelay;
            OnStaminaChanged?.Invoke(currentStamina, maxStamina);
        }
        else
        {
            staminaRegenTimer -= Time.deltaTime;
            if (staminaRegenTimer <= 0f && currentStamina < maxStamina)
            {
                currentStamina += staminaRegen * Time.deltaTime;
                currentStamina = Mathf.Min(maxStamina, currentStamina);
                OnStaminaChanged?.Invoke(currentStamina, maxStamina);
            }
        }
    }

    private void HandleFootsteps()
    {
        if (!cc.isGrounded || isSliding || footstepSource == null) return;

        float speed = new Vector2(velocity.x, velocity.z).magnitude;
        if (speed < 0.5f) return;

        float interval = isSprinting ? sprintStepInterval : walkStepInterval;
        stepTimer -= Time.deltaTime;

        if (stepTimer <= 0f)
        {
            stepTimer = interval;
            AudioClip[] clips = isSprinting ? sprintClips : walkClips;
            if (clips != null && clips.Length > 0)
            {
                AudioClip clip = clips[Random.Range(0, clips.Length)];
                footstepSource.pitch = Random.Range(0.9f, 1.1f);
                footstepSource.PlayOneShot(clip);
            }
        }
    }

    private void UpdateHeight()
    {
        float current = cc.height;
        if (Mathf.Abs(current - targetHeight) > 0.01f)
        {
            cc.height = Mathf.Lerp(current, targetHeight, crouchTransitionSpeed * Time.deltaTime);
            cc.center = Vector3.up * (cc.height * 0.5f);
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag(ladderTag))
        {
            isOnLadder = true;
            velocity = Vector3.zero;
            OnLadderEntered?.Invoke();
        }
        else if (other.CompareTag(waterTag))
        {
            isInWater = true;
            velocity *= 0.3f;
            OnWaterEntered?.Invoke();
        }
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.CompareTag(ladderTag)) isOnLadder = false;
        else if (other.CompareTag(waterTag))
        {
            isInWater = false;
            velocity.y = Mathf.Max(velocity.y, jumpForce * 0.4f);
        }
    }

    /// <summary>True if the character is sprinting.</summary>
    public bool IsSprinting => isSprinting;

    /// <summary>True if the character is crouching.</summary>
    public bool IsCrouching => isCrouching;

    /// <summary>True if the character is sliding.</summary>
    public bool IsSliding => isSliding;

    /// <summary>True if climbing a ladder.</summary>
    public bool IsOnLadder => isOnLadder;

    /// <summary>True if in water.</summary>
    public bool IsInWater => isInWater;

    /// <summary>Current stamina value (0 to maxStamina).</summary>
    public float CurrentStamina => currentStamina;
}