using UnityEngine; using UnityEngine.InputSystem; /// /// Capsule player: camera-relative movement, jump, gravity and mouse look. /// Input comes from the project asset Assets/InputSystem_Actions.inputactions /// ("Player" map: Move / Jump / Sprint). /// [RequireComponent(typeof(CharacterController))] public class PlayerController : MonoBehaviour { [Header("Input")] [Tooltip("Optional: the project's InputSystem_Actions asset. Actions are resolved by name from it.")] [SerializeField] private InputActionAsset actionsAsset; [Tooltip("Optional explicit references. Take priority over actionsAsset when set.")] [SerializeField] private InputActionReference moveAction; [SerializeField] private InputActionReference jumpAction; [SerializeField] private InputActionReference sprintAction; private InputAction _move; private InputAction _jump; private InputAction _sprint; [Header("Movement")] [SerializeField] private float walkSpeed = 5f; [SerializeField] private float sprintMultiplier = 1.8f; [SerializeField] private float acceleration = 14f; [SerializeField] private float rotationSpeed = 14f; [SerializeField] private float jumpHeight = 1.4f; [Header("Gravity")] [SerializeField] private float gravity = -22f; [SerializeField] private float groundedForce = -2f; [Tooltip("Extra ground probe at the feet, in addition to CharacterController.isGrounded.")] [SerializeField] private float groundCheckRadius = 0.12f; [Tooltip("Probe height relative to the transform origin. The capsule's feet are at y = 0.")] [SerializeField] private float groundCheckOffset = 0.12f; [SerializeField] private LayerMask groundMask = ~0; [Header("Camera")] [SerializeField] private Transform cameraPivot; [Tooltip("Create a third-person camera at runtime when the scene has no Main Camera.")] [SerializeField] private bool createCameraIfMissing = true; [SerializeField] private bool controlCursor = true; [SerializeField] private float lookSensitivity = 0.12f; [SerializeField] private float pitchMin = -35f; [SerializeField] private float pitchMax = 70f; [SerializeField] private float cameraDistance = 5f; [SerializeField] private float cameraHeight = 1.2f; [SerializeField] private float cameraSideOffset = 0.6f; private CharacterController _controller; private Vector3 _planarVelocity; private float _verticalVelocity; private bool _grounded; private float _yaw; private float _pitch = 15f; private Camera _camera; public bool IsGrounded { get { return _grounded; } } public float CurrentSpeed { get { return _planarVelocity.magnitude; } } private void Awake() { _controller = GetComponent(); _yaw = transform.eulerAngles.y; ResolveActions(); EnableActions(true); } /// /// Prefers explicit InputActionReference fields, otherwise resolves /// "Player/Move", "Player/Jump" and "Player/Sprint" from the actions asset. /// private void ResolveActions() { _move = moveAction != null ? moveAction.action : null; _jump = jumpAction != null ? jumpAction.action : null; _sprint = sprintAction != null ? sprintAction.action : null; if (actionsAsset == null) return; if (_move == null) _move = actionsAsset.FindAction("Player/Move", false); if (_jump == null) _jump = actionsAsset.FindAction("Player/Jump", false); if (_sprint == null) _sprint = actionsAsset.FindAction("Player/Sprint", false); } private void OnEnable() { EnableActions(true); if (controlCursor) { Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; } } private void OnDisable() { if (controlCursor) { Cursor.lockState = CursorLockMode.None; Cursor.visible = true; } } private void EnableActions(bool enable) { SetEnabled(_move, enable); SetEnabled(_jump, enable); SetEnabled(_sprint, enable); } private static void SetEnabled(InputAction action, bool enable) { if (action == null) return; if (enable) action.Enable(); else action.Disable(); } private void Update() { ReadLook(); ReadMove(); ApplyGravityAndJump(); Move(); UpdateCamera(); } private void ReadLook() { if (Mouse.current == null) return; Vector2 delta = Mouse.current.delta.ReadValue() * lookSensitivity; _yaw += delta.x; _pitch = Mathf.Clamp(_pitch - delta.y, pitchMin, pitchMax); } private void ReadMove() { Vector2 input = _move != null ? _move.ReadValue() : Vector2.zero; Quaternion yawRotation = Quaternion.Euler(0f, _yaw, 0f); Vector3 desired = yawRotation * new Vector3(input.x, 0f, input.y); if (desired.sqrMagnitude > 1f) desired.Normalize(); float speed = walkSpeed; if (_sprint != null && _sprint.IsPressed()) speed *= sprintMultiplier; _planarVelocity = Vector3.MoveTowards(_planarVelocity, desired * speed, acceleration * Time.deltaTime); } private void ApplyGravityAndJump() { // CharacterController.isGrounded is the authoritative check (its capsule // matches the visual): the extra sphere probe only helps on slopes/steps. Vector3 origin = transform.position + Vector3.up * groundCheckOffset; bool probeHit = Physics.CheckSphere(origin, groundCheckRadius, groundMask, QueryTriggerInteraction.Ignore); _grounded = _controller.isGrounded || probeHit; if (_grounded && _verticalVelocity < 0f) _verticalVelocity = groundedForce; bool wantsJump = _jump != null && _jump.WasPressedThisFrame(); if (wantsJump && _grounded) _verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity); _verticalVelocity += gravity * Time.deltaTime; } private void Move() { Vector3 motion = _planarVelocity + Vector3.up * _verticalVelocity; _controller.Move(motion * Time.deltaTime); if (_planarVelocity.sqrMagnitude > 0.05f) { Quaternion target = Quaternion.LookRotation(_planarVelocity.normalized, Vector3.up); transform.rotation = Quaternion.Slerp(transform.rotation, target, rotationSpeed * Time.deltaTime); } } private void UpdateCamera() { if (_camera == null) { _camera = Camera.main; if (_camera == null && createCameraIfMissing) _camera = CreateFallbackCamera(); if (_camera == null) return; } Transform pivot = cameraPivot != null ? cameraPivot : transform; Vector3 pivotPoint = pivot.position + Vector3.up * cameraHeight + pivot.right * cameraSideOffset; Quaternion rotation = Quaternion.Euler(_pitch, _yaw, 0f); Vector3 position = pivotPoint + rotation * new Vector3(0f, 0f, -cameraDistance); _camera.transform.SetPositionAndRotation(position, rotation); } /// /// Builds a third-person camera at runtime so the prefab is playable even in /// a scene that has no camera. Only used when not overridden by an existing Main Camera. /// private static Camera CreateFallbackCamera() { GameObject cameraObject = new GameObject("PlayerCamera"); cameraObject.tag = "MainCamera"; Camera camera = cameraObject.AddComponent(); camera.fieldOfView = 60f; camera.nearClipPlane = 0.1f; camera.farClipPlane = 1000f; cameraObject.AddComponent(); return camera; } private void OnDrawGizmosSelected() { Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(transform.position + Vector3.up * groundCheckOffset, groundCheckRadius); }}