using UnityEngine; using UnityEngine.InputSystem; using System; /// /// Player controller. /// [RequireComponent(typeof(CharacterController))] [DefaultExecutionOrder(1000)] public class Player : MonoBehaviour { public static Player Instance { get; private set; } public enum PlayerState { Idle, Moving, Jumping, Falling, Stunned, Eliminated, Loser, Winner, } [System.Serializable] public class Settings { [Header("Movements")] [Tooltip("Movement speed in km/h")] public float Speed = 18f; [Tooltip("Jump force in m/s")] public float JumpForce = 8f; [Tooltip("Player rotation speed towards movement direction")] public float RotationSpeed = 10f; [Tooltip("Ground detection tolerance")] public float GroundTolerance = 0.2f; [Tooltip("Layers considered as ground")] public LayerMask GroundLayer = 1; [Header("Debug")] [Tooltip("GUI logs of current state")] public bool StateLogs; } [System.Serializable] public class References { public CharacterController Controller; public InputActionAsset InputActions; } [System.Serializable] public class StateContainer { [Tooltip("Current player state")] public PlayerState CurrentState = PlayerState.Idle; [Tooltip("Is player paused?")] public bool IsPaused = false; [Tooltip("Is player grounded?")] public bool IsGrounded; [Tooltip("Current velocity in m/s")] public Vector3 Velocity; [Tooltip("Ground transform evaluated as parent")] public Transform Ground; public float VerticalVelocity => Velocity.y; public Vector3 HorizontalVelocity => new Vector3(Velocity.x, 0, Velocity.z); } [SerializeField] private Settings _settings; [SerializeField] private References _references; [SerializeField] private StateContainer _state; public StateContainer State => _state; #region Private Fields private InputAction _moveAction; private InputAction _jumpAction; private Camera _camera; // Ground check geometry private Vector3 _groundCheckRayOffset; private Vector3 _groundCheckSphereOffset; private float _groundCheckRadius; // OverlapSphere results buffer (non-alloc) private Collider[] _overlapResults = new Collider[1]; // Platform tracking private Vector3 _lastPlatformPosition; private Quaternion _lastPlatformRotation; // Events public event Action OnStateChanged; #endregion #region Constants private const float KMH_TO_MS = 1 / 3.6f; private const float STICK_FORCE = -5f; private const float GRAVITY = -20f; private const float MAX_GRAVITY = -50f; #endregion #region Unity Debug void OnGUI() { if (_settings.StateLogs) { GUIStyle style = new GUIStyle(); style.fontSize = 15; style.normal.textColor = Color.white; style.alignment = TextAnchor.UpperLeft; GUILayout.BeginArea(new Rect(10, 10, 400, 200)); GUILayout.Label($"State: {_state.CurrentState}", style); GUILayout.Label($"Grounded: {_state.IsGrounded}", style); GUILayout.Label($"Velocity: {_state.Velocity.magnitude:F2} m/s", style); GUILayout.EndArea(); } } void OnDrawGizmos() { Color c = _state.IsGrounded ? Color.green : new Color(1, .5f, 0); Gizmos.color = c; Gizmos.DrawRay(transform.position + _groundCheckRayOffset, Vector3.down * (_settings.GroundTolerance * 2f)); Gizmos.DrawWireSphere(transform.position + _groundCheckSphereOffset, _groundCheckRadius); } #endregion #region Unity Lifecycle void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; if (_references.Controller == null) _references.Controller = GetComponent(); _camera = Camera.main; _moveAction = _references.InputActions.FindActionMap("Player").FindAction("Move"); _jumpAction = _references.InputActions.FindActionMap("Player").FindAction("Jump"); _references.Controller.skinWidth = 0.08f; _references.Controller.minMoveDistance = 0f; _references.Controller.stepOffset = 0.4f; CharacterController cc = _references.Controller; _groundCheckRayOffset = cc.center + Vector3.up * (-cc.height * .5f - cc.skinWidth + _settings.GroundTolerance); _groundCheckSphereOffset = cc.center + Vector3.up * (-cc.height * .5f + cc.radius - cc.skinWidth - _settings.GroundTolerance); _groundCheckRadius = cc.radius; } void OnEnable() { _moveAction?.Enable(); _jumpAction?.Enable(); } void OnDisable() { _moveAction?.Disable(); _jumpAction?.Disable(); } void Update() { if (_state.IsPaused) return; float deltaTime = Time.deltaTime; SetPlatformOffset(); SetGravity(deltaTime); SetVelocity(deltaTime); SetJump(); SetMovement(deltaTime); SetState(); } #endregion #region Physics #endregion #region Public Methods /// /// Stuns for a specified duration (0 = infinite). /// public void Stun(float duration = 0) { if (_state.CurrentState == PlayerState.Eliminated) return; _state.CurrentState = PlayerState.Stunned; _state.Velocity = Vector3.zero; if (duration > 0) Invoke(nameof(RecoverFromStun), duration); } /// /// Recover from stun. /// public void RecoverFromStun() { if (_state.CurrentState == PlayerState.Stunned) _state.CurrentState = PlayerState.Idle; } /// /// Eliminates the player from the game. /// public void Eliminate() { _state.CurrentState = PlayerState.Eliminated; _state.Velocity = Vector3.zero; _references.Controller.enabled = false; } /// /// Sets the pause state of the player. /// public void Pause(bool paused) { _state.IsPaused = paused; } #endregion #region Player Logic private void SetPlatformOffset() { // Raycast for center contact Vector3 rayOrigin = transform.position + _groundCheckRayOffset; bool rayHit = Physics.Raycast(rayOrigin, Vector3.down, out RaycastHit rayInfo, _settings.GroundTolerance * 2f, _settings.GroundLayer); // OverlapSphere for edge contact Vector3 sphereOrigin = transform.position + _groundCheckSphereOffset; int overlapCount = Physics.OverlapSphereNonAlloc(sphereOrigin, _groundCheckRadius, _overlapResults, _settings.GroundLayer); bool sphereHit = overlapCount > 0; _state.IsGrounded = rayHit || sphereHit; if (_state.IsGrounded) { Transform currentGround = rayHit ? rayInfo.collider.transform : _overlapResults[0].transform; // Initialize references when landing on a new surface to prevent teleporting if (currentGround != _state.Ground) { _state.Ground = currentGround; _lastPlatformPosition = _state.Ground.position; _lastPlatformRotation = _state.Ground.rotation; return; } // Yaw delta: rotate player around platform pivot Quaternion rotationDelta = _state.Ground.rotation * Quaternion.Inverse(_lastPlatformRotation); float platformYaw = rotationDelta.eulerAngles.y; if (Mathf.Abs(platformYaw) > .001f) { Vector3 dir = transform.position - _state.Ground.position; dir = Quaternion.Euler(0, platformYaw, 0) * dir; transform.position = _state.Ground.position + dir; transform.Rotate(0, platformYaw, 0); } // Translation delta transform.position += _state.Ground.position - _lastPlatformPosition; // Store current state for next frame _lastPlatformPosition = _state.Ground.position; _lastPlatformRotation = _state.Ground.rotation; // Sync physics broadphase to the new transform, the CharacterController // doesn't see a stale overlap and generate a corrective push Physics.SyncTransforms(); } else { _state.Ground = null; } } private void SetGravity(float deltaTime) { if (_state.IsGrounded && _state.Velocity.y < 0) _state.Velocity.y = STICK_FORCE; else _state.Velocity.y = Mathf.Max(_state.Velocity.y + GRAVITY * deltaTime, MAX_GRAVITY); } private void SetVelocity(float deltaTime) { Vector2 input = _moveAction.ReadValue(); Vector3 forward = Vector3.ProjectOnPlane(_camera.transform.forward, Vector3.up).normalized; Vector3 right = Vector3.ProjectOnPlane(_camera.transform.right, Vector3.up).normalized; float speed = _settings.Speed * KMH_TO_MS; Vector3 moveInput = (forward * input.y + right * input.x) * speed; _state.Velocity.x = moveInput.x; _state.Velocity.z = moveInput.z; if (moveInput.sqrMagnitude > 0.001f) { Quaternion targetRot = Quaternion.LookRotation(moveInput); float step = _settings.RotationSpeed * deltaTime; Vector3 euler = Quaternion.Slerp(transform.rotation, targetRot, step).eulerAngles; transform.rotation = Quaternion.Euler(0, euler.y, 0); } } private void SetJump() { if (_jumpAction.triggered && _state.IsGrounded) { _state.Velocity.y = _settings.JumpForce; _state.Ground = null; } } private void SetMovement(float deltaTime) { _references.Controller.Move(_state.Velocity * deltaTime); } private void SetState() { if (_state.CurrentState == PlayerState.Stunned || _state.CurrentState == PlayerState.Eliminated || _state.CurrentState == PlayerState.Winner || _state.CurrentState == PlayerState.Loser) { return; } PlayerState previousState = _state.CurrentState; if (!_state.IsGrounded) { _state.CurrentState = _state.VerticalVelocity > 0 ? PlayerState.Jumping : PlayerState.Falling; } else { _state.CurrentState = _state.HorizontalVelocity.sqrMagnitude > 0.1f ? PlayerState.Moving : PlayerState.Idle; } if (previousState != _state.CurrentState) OnStateChanged?.Invoke(previousState, _state.CurrentState); } #endregion }