using System.Collections; using TMPro.EditorUtilities; using UnityEngine; using UnityEngine.Events; using UnityEngine.InputSystem; using UnityEngine.UI; using static Player; using static UnityEditor.Experimental.GraphView.GraphView; public class Player : MonoBehaviour { public static Player Instance { get; private set; } public enum PlayerState { Idle, Moving, Jumping, JumpingDouble, Falling, Dashing, Stunned, Dead, Loser, Winner, } [System.Serializable] public class WallRun { [Tooltip("Can the player run along walls?")] public bool CanRunOnWalls = true; [Tooltip("Layers considered as walkable wall")] public LayerMask WallLayer = 0; [Tooltip("Stick duration to wall before fall")] public float StickDuration = 3; [Tooltip("Gravity applyed when sticked to wall")] public float StickedGravity = -5; } [System.Serializable] public class Dash { [Tooltip("Can the player dash?")] public bool CanDash = true; public float DashDuration = .3f; public float DashDelay = 1.5f; public float DashForce = 50; } [System.Serializable] public class DoubleJump { [Tooltip("Can the player duble jump?")] public bool CanDoubleJump = true; } [System.Serializable] public class Settings { [Header("Movements")] [Tooltip("Walk speed in km/h")] public float WalkSpeed = 15f; [Tooltip("Run speed in km/h")] public float RunSpeed = 30f; [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; [Tooltip("Layers considered as death zone")] public LayerMask DeathLayer = 0; [Header("Forces")] [Tooltip("Decay rate of extra forces (m/sē)")] public float ExtraForcesDrag = 8f; [Header("Debug")] [Tooltip("GUI logs of current state")] public bool StateLogs; [Header("Features")] public WallRun WallRun; public Dash Dash; public DoubleJump DoubleJump; } [System.Serializable] public class References { public InputActionAsset InputActions; public CharacterController Controller; public GameObject COG; public GameObject DiePrefab; public ParticleSystem[] DashEffects; public Transform[] DashJointsLevel; } [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 sticked to wall?")] public bool IsStickedToWall; [Tooltip("Is player dashing?")] public bool IsDashing = false; [Tooltip("Can player stick to walls?")] public bool CanStickToWalls; [Tooltip("Current running state")] [Range(0, 1)] public float Running; [Tooltip("Animation center of gravity")] [Range(-1, 1)] public float AnimationCenter; [Tooltip("Elapsed time since falling")] [Range(-1, 1)] public float FallingTime; [Tooltip("Elapsed time since sticked to wall")] [Range(-1, 1)] public float StickedTime; [Tooltip("Elapsed time since dash")] [Range(-1, 1)] public float DashingTime; [Tooltip("Current velocity in m/s")] public Vector3 Velocity; [Tooltip("Ground transform evaluated as parent")] public Transform Ground; public bool IsGrounded => 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, ReadOnly] private StateContainer _state; public StateContainer State => _state; #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 Public Fields public void Pause(bool pause = true) { _state.IsPaused = pause; } public void SetState(PlayerState state) { _state.CurrentState = state; //TriggerStateEvents(); } public void Lose() { if (_state.CurrentState == PlayerState.Loser) return; SetState(PlayerState.Loser); Pause(); } public void Die() { if (_state.CurrentState == PlayerState.Dead) return; SetState(PlayerState.Dead); Instantiate(_references.DiePrefab); Debug.Log("Player is dead"); } #endregion #region Private Fields // Inputs private InputAction _moveAction; private InputAction _runAction; private InputAction _jumpAction; private InputAction _targetAction; private InputAction _dashAction; // Camera private Camera _camera; // Ground private Vector3 _groundCheckRayOffset; private Vector3 _groundCheckSphereOffset; private float _groundCheckRadius; private Collider[] _overlapResults = new Collider[1]; private Vector3 _wallDirection; private Vector3 _wallNormal; private Vector3 _wallPosition; private Vector3 _lastPlatformPosition; private Quaternion _lastPlatformRotation; private Vector3 _platformVelocity; // Double Jump private bool _doubleJump; private float _doubleJumpTime; #endregion #region Unity Lifecycle /*void OnDrawGizmos() { Gizmos.matrix = transform.localToWorldMatrix; Gizmos.color = new Color(.2f, 1, .7f, .3f); Gizmos.DrawSphere(_groundCheckSphereOffset, _groundCheckRadius); }*/ void Awake() { if (!Instance) Instance = this; // Inputs _moveAction = _references.InputActions.FindActionMap("Player").FindAction("Move"); _runAction = _references.InputActions.FindActionMap("Player").FindAction("Sprint"); _jumpAction = _references.InputActions.FindActionMap("Player").FindAction("Jump"); _targetAction = _references.InputActions.FindActionMap("Player").FindAction("Target"); _dashAction = _references.InputActions.FindActionMap("Player").FindAction("Attack"); // Camera _camera = Camera.main; // Ground check geometry 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(); _runAction?.Enable(); _jumpAction?.Enable(); _targetAction?.Enable(); _dashAction?.Enable(); } void OnDisable() { _moveAction?.Disable(); _runAction?.Disable(); _jumpAction?.Disable(); _targetAction?.Disable(); _dashAction?.Disable(); } void Update() { float t = Time.deltaTime; CheckGround(t); SetGravity(t); SetVelocity(t); SetJump(); SetDash(); SetMovement(t); SetState(); } void OnControllerColliderHit(ControllerColliderHit hit) { if ((_settings.DeathLayer.value & (1 << hit.gameObject.layer)) != 0) { Die(); } } #endregion #region Player Logic private void CheckGround(float deltaTime) { // Raycast for center contact RaycastHit rayInfo; Vector3 rayOrigin = transform.position + _groundCheckRayOffset; bool rayHit = Physics.Raycast(rayOrigin, Vector3.down, out 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; bool wasGrounded = _state.IsGrounded; bool isGrounded = rayHit || sphereHit; // Double Jump if (isGrounded) { _doubleJump = false; _doubleJumpTime = 0; } // Wall run if (_settings.WallRun.CanRunOnWalls && !_state.CanStickToWalls) { _state.CanStickToWalls = _state.FallingTime > .3f; } bool isStickedToWall = false; if (!isGrounded && _settings.WallRun.CanRunOnWalls && _state.CanStickToWalls && _state.VerticalVelocity < 0) { int[] wallCheckAngles = new int[6] { 80, 70, 60, 50, 40, 30 }; int raySign = 1; for (int i = 0; i < wallCheckAngles.Length; i++) { if (isStickedToWall) break; for (int u = -1; u <= 1; u += 2) { Vector3 rayDir = Quaternion.Euler(0, wallCheckAngles[i] * u, 0) * transform.forward; float rayOffset = _references.Controller.radius * .8f; float raySize = _references.Controller.radius * .7f; isStickedToWall = Physics.Raycast(rayOrigin + rayDir * rayOffset, rayDir, out rayInfo, raySize, _settings.WallRun.WallLayer); Debug.DrawRay(rayOrigin + rayDir * rayOffset, rayDir * raySize, isStickedToWall ? Color.green : Color.red); if (isStickedToWall) { raySign = u; goto AfterRaycastWall; } } } AfterRaycastWall: if (!_state.IsStickedToWall && isStickedToWall) _state.StickedTime = 0; else if (_state.IsStickedToWall && !isStickedToWall) _state.CanStickToWalls = false; _state.IsStickedToWall = isStickedToWall; if (_state.IsStickedToWall) { _wallDirection = Vector3.ProjectOnPlane(Vector3.Cross(rayInfo.normal, Vector3.up), Vector3.up) * Mathf.Sign(Vector3.Cross(transform.forward, rayInfo.normal).y); _wallNormal = rayInfo.normal; _wallPosition = rayInfo.point; _state.AnimationCenter = raySign; _state.Running = 1; if (_state.StickedTime >= _settings.WallRun.StickDuration) { _state.IsStickedToWall = false; _state.CanStickToWalls = false; } } } else if (isGrounded) { _state.IsStickedToWall = false; _state.CanStickToWalls = true; } if (!isStickedToWall) _state.AnimationCenter = 0; if (isGrounded || isStickedToWall) { 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; _platformVelocity.y = 0; return; } // 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 Vector3 platformDelta = _state.Ground.position - _lastPlatformPosition; transform.position += platformDelta; // Store current state for next frame _lastPlatformPosition = _state.Ground.position; _lastPlatformRotation = _state.Ground.rotation; // Sync physics broadphase to prevents CC from seeing stale overlap Physics.SyncTransforms(); // Reset platform velocity _platformVelocity = Vector3.zero; _state.FallingTime = 0; } else { // Inherit platform velocity when player left the ground if (wasGrounded && _state.Ground != null) { _platformVelocity = (_state.Ground.position - _lastPlatformPosition) / Time.deltaTime; } // Decay velocity when player is in the air else { Vector3 platformVelocity = Vector3.MoveTowards(_platformVelocity, Vector3.zero, _settings.ExtraForcesDrag * deltaTime); platformVelocity.y = _platformVelocity.y; _platformVelocity = platformVelocity; } _state.FallingTime += deltaTime; _state.Ground = null; } } private void SetGravity(float deltaTime) { if (_state.IsStickedToWall) { _state.Velocity.y = _settings.WallRun.StickedGravity * Mathf.Pow(_state.StickedTime / _settings.WallRun.StickDuration, 2); } else if (_state.IsGrounded && _state.Velocity.y < 0) { _state.Velocity.y = STICK_FORCE; } else { if (_platformVelocity.y > 0) { _platformVelocity.y += GRAVITY * deltaTime; if (_platformVelocity.y < 0) _state.Velocity.y += _platformVelocity.y; } else { _state.Velocity.y += GRAVITY * deltaTime; } _state.Velocity.y = Mathf.Max(_state.Velocity.y, MAX_GRAVITY); } } private void SetVelocity(float deltaTime) { bool allowInput = !_state.IsPaused; switch (_state.CurrentState) { case PlayerState.Loser: case PlayerState.Dead: allowInput = false; _state.Running = 0; break; } Vector2 input = allowInput ? _moveAction.ReadValue() : Vector2.zero; bool run = _runAction.IsPressed(); _state.Running = Mathf.Clamp01(_state.Running + deltaTime * 4 * (run ? 1 : -1)); float speed = Mathf.Lerp(_settings.WalkSpeed, _settings.RunSpeed, _state.Running) * KMH_TO_MS; Vector3 moveInput = new Vector3(input.x, 0, input.y); // Convert input 2D to move 3D if (_state.IsStickedToWall) { _state.StickedTime += deltaTime * Mathf.Lerp(4, 1, moveInput.magnitude); moveInput = _wallDirection * Mathf.Sqrt(1 - _state.StickedTime / _settings.WallRun.StickDuration); } else { moveInput = Quaternion.Euler(0, _camera.transform.eulerAngles.y, 0) * moveInput; // Rotate move toward camera } moveInput *= speed; // Muliply move by speed float velovityTime = deltaTime * 8; switch (_state.CurrentState) { default: velovityTime *= 1; break; case PlayerState.Jumping: velovityTime *= .5f; break; case PlayerState.Falling: velovityTime *= .2f; break; } _state.Velocity.x = Mathf.Lerp(_state.Velocity.x, moveInput.x, velovityTime); _state.Velocity.z = Mathf.Lerp(_state.Velocity.z, moveInput.z, velovityTime); } private void SetJump() { if (_state.IsPaused) return; if (_jumpAction.triggered && (_state.IsGrounded || _state.IsStickedToWall || (!_doubleJump && _settings.DoubleJump.CanDoubleJump))) { if (_state.IsStickedToWall) { _state.Velocity = Vector3.Lerp(_wallNormal, Vector3.up, .4f).normalized * _settings.JumpForce * 1.6f; _doubleJump = false; _doubleJumpTime = 0; } else { _state.Velocity.y = _settings.JumpForce; if (!_doubleJump) _doubleJump = true; } _state.Ground = null; _state.IsStickedToWall = false; _state.CanStickToWalls = false; _groundCheckRadius = _references.Controller.radius + _settings.GroundTolerance; } if (_doubleJump) { _doubleJumpTime += Time.deltaTime; } } private void SetDash() { if (!_settings.Dash.CanDash || _state.IsPaused) return; _state.DashingTime = Mathf.Clamp(_state.DashingTime + Time.deltaTime, 0, _settings.Dash.DashDelay); float dashLoad = Mathf.Pow(Mathf.Clamp01((_state.DashingTime - _settings.Dash.DashDuration) / (_settings.Dash.DashDelay - _settings.Dash.DashDuration)), 2); foreach (Transform jnt in _references.DashJointsLevel) jnt.localScale = new Vector3(1, dashLoad, 1); if (!_state.IsDashing) { if (_dashAction.triggered && !_targetAction.IsPressed() && _state.DashingTime == _settings.Dash.DashDelay && !_state.IsStickedToWall) { _state.Velocity += transform.forward * _settings.Dash.DashForce; _state.IsDashing = true; _state.DashingTime = 0; foreach(ParticleSystem p in _references.DashEffects) p.Play(); } } else if(_state.DashingTime >= _settings.Dash.DashDuration) { _state.IsDashing = false; } } private void SetMovement(float deltaTime) { Vector3 motion = _state.Velocity + _platformVelocity; if (_state.IsStickedToWall) { Vector3 wallDelta = _wallPosition - transform.position; wallDelta -= wallDelta.normalized * _references.Controller.radius; motion += wallDelta; } _references.Controller.Move(motion * deltaTime); // Rotate Player if (_state.HorizontalVelocity.magnitude > .01f) { Vector3 velocity = _state.HorizontalVelocity; Quaternion controlerRot = Quaternion.LookRotation(velocity, Vector3.up); if (_state.IsStickedToWall) controlerRot = Quaternion.LookRotation(_wallDirection); float t = _settings.RotationSpeed * deltaTime; controlerRot = Quaternion.Slerp(transform.rotation, controlerRot, t); transform.rotation = controlerRot; } // Rotate COG float angle = Mathf.Lerp(0, 5, _state.Running); if (_doubleJump) angle = Mathf.Sqrt(Mathf.Clamp01(_doubleJumpTime * 3)) * 360; _references.COG.transform.localRotation = Quaternion.Euler(angle, 0, 0); } private void SetState() { switch (_state.CurrentState) { case PlayerState.Loser: case PlayerState.Dead: return; } if (_state.IsDashing) { State.CurrentState = PlayerState.Dashing; } else if (State.IsGrounded || State.IsStickedToWall) { if (State.HorizontalVelocity.sqrMagnitude > .1f) { State.CurrentState = PlayerState.Moving; } else { State.CurrentState = PlayerState.Idle; } } else { if (State.VerticalVelocity > 0) { if (!_doubleJump) State.CurrentState = PlayerState.Jumping; else State.CurrentState = PlayerState.JumpingDouble; } else { State.CurrentState = PlayerState.Falling; } } } #endregion }