Part of these game systems:
3D Player Controller Pro
Advanced 3D character controller with crouch, slide, ladder climbing, swimming, stamina system, and footstep audio.
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
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;
}