using UnityEngine;
using UnityEngine.InputSystem;
/// <summary>
/// GAME 1303 Tier 7 — 3D first-person walking and looking. Store Simulator; also Apep's
/// Trial's 3D sections.
///
/// This is the only 3D script in the kit, so a few things differ from everything else:
/// CharacterController instead of Rigidbody2D, Vector3 instead of Vector2, and the camera
/// is a child that pitches while the body yaws.
///
/// SETUP
/// 1. Player: an empty GameObject with a CharacterController (Height 1.8, Radius 0.3,
/// Center Y 0.9) + this script + PlayerInput (Send Messages). Tag "Player".
/// 2. Child "CameraHolder" at Y 1.6 with the Main Camera inside it. Drag CameraHolder
/// into Camera Pivot.
/// 3. Input: the default actions asset already has Move and Look. Look binds to Mouse
/// Delta and the right stick.
/// 4. Cursor Lock hides and locks the mouse — press Escape to release it (handy in the
/// Editor; wire GameManager's pause to UnlockCursor).
/// 5. Mouse Sensitivity is per-player taste — expose it in an options menu.
///
/// Deliberately simple: walk, look, crouch, jump. No head bob, no sprint stamina, no
/// slope handling beyond what CharacterController gives free. Sanad's game needs a body
/// to carry boxes with, not a shooter.
/// </summary>
[RequireComponent(typeof(CharacterController))]
public class FirstPersonController : MonoBehaviour
{
[Header("Movement")]
public float walkSpeed = 3.5f;
public float sprintSpeed = 6f;
public float crouchSpeed = 1.8f;
public float acceleration = 12f;
public float gravity = -20f;
public float jumpHeight = 1.1f;
[Header("Look")]
public Transform cameraPivot;
public float mouseSensitivity = 0.12f;
public float stickSensitivity = 120f;
public float minPitch = -80f;
public float maxPitch = 80f;
public bool invertY = false;
[Header("Crouch")]
public float standHeight = 1.8f;
public float crouchHeight = 1.1f;
public float crouchSpeedChange = 8f;
[Header("Cursor")]
public bool lockCursor = true;
public bool InputEnabled { get; set; } = true;
public bool IsGrounded => controller.isGrounded;
private CharacterController controller;
private Vector2 moveInput, lookInput;
private Vector3 velocity;
private float pitch;
private bool sprinting, crouching, jumpQueued;
private bool usingMouse = true;
void Awake()
{
controller = GetComponent<CharacterController>();
if (cameraPivot == null && Camera.main != null) cameraPivot = Camera.main.transform;
}
void Start() { if (lockCursor) LockCursor(); }
// ---- Input System messages ----
void OnMove(InputValue v) { moveInput = InputEnabled ? v.Get<Vector2>() : Vector2.zero; }
void OnLook(InputValue v) { lookInput = v.Get<Vector2>(); usingMouse = Mouse.current != null && Mouse.current.delta.ReadValue().sqrMagnitude > 0.01f; }
void OnSprint(InputValue v) { sprinting = v.isPressed; }
void OnCrouch(InputValue v) { crouching = v.isPressed; }
void OnJump(InputValue v) { if (v.isPressed) jumpQueued = true; }
void Update()
{
Look();
Move();
Crouch();
if (Keyboard.current != null && Keyboard.current.escapeKey.wasPressedThisFrame) UnlockCursor();
}
void Look()
{
if (!InputEnabled || cameraPivot == null) return;
float sens = usingMouse ? mouseSensitivity : stickSensitivity * Time.deltaTime;
float yaw = lookInput.x * sens;
float pitchDelta = lookInput.y * sens * (invertY ? 1f : -1f);
transform.Rotate(Vector3.up * yaw);
pitch = Mathf.Clamp(pitch + pitchDelta, minPitch, maxPitch);
cameraPivot.localRotation = Quaternion.Euler(pitch, 0f, 0f);
}
void Move()
{
float speed = crouching ? crouchSpeed : (sprinting ? sprintSpeed : walkSpeed);
Vector3 wish = InputEnabled
? (transform.right * moveInput.x + transform.forward * moveInput.y).normalized * speed
: Vector3.zero;
Vector3 horizontal = new Vector3(velocity.x, 0f, velocity.z);
horizontal = Vector3.Lerp(horizontal, wish, acceleration * Time.deltaTime);
velocity.x = horizontal.x;
velocity.z = horizontal.z;
if (controller.isGrounded)
{
if (velocity.y < 0f) velocity.y = -2f; // keep it pinned to the floor
if (jumpQueued && InputEnabled && !crouching)
velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
}
jumpQueued = false;
velocity.y += gravity * Time.deltaTime;
controller.Move(velocity * Time.deltaTime);
}
void Crouch()
{
float target = crouching ? crouchHeight : standHeight;
if (Mathf.Approximately(controller.height, target)) return;
controller.height = Mathf.MoveTowards(controller.height, target, crouchSpeedChange * Time.deltaTime);
controller.center = new Vector3(0f, controller.height * 0.5f, 0f);
if (cameraPivot != null)
cameraPivot.localPosition = new Vector3(0f, controller.height - 0.2f, 0f);
}
public void LockCursor() { Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; }
public void UnlockCursor() { Cursor.lockState = CursorLockMode.None; Cursor.visible = true; }
}