using System.Collections; using TMPro; using Unity.IO.LowLevel.Unsafe; using UnityEngine; using Game; using Music; using Player; using UnityEngine.InputSystem; namespace Player { [RequireComponent(typeof(Rigidbody2D))] [RequireComponent(typeof(BoxCollider2D))] [RequireComponent(typeof(PlayerInput))] [RequireComponent(typeof(AnimationPlayer))] [RequireComponent(typeof(Punch))] public class PlayerMovement : MonoBehaviour { /// /// Layers considered as ground for the player. /// [Header("Ground Layers")] public LayerMask ground; /// /// Reference to the player's UI text displaying player index. /// public TextMeshProUGUI playerText; /// /// Base walk speed of the player. /// [Header("Movement")] public float walkSpeed; /// /// Multiplier applied to walk speed. /// public float walkSpeedFactor = 1f; /// /// Maximum allowed horizontal speed for the player. /// public float maxSpeed = 5f; /// /// Runtime override for the maximum speed. /// public float maxSpeedOverride; /// /// Multiplier for slowing down the player when exceeding max speed. /// public float slowdownMultiplier = 10f; /// /// Current value of the horizontal movement axis. /// public float virtualAxisX; /// /// Current value of the jump button (pressed or not). /// public float virtualButtonJump; /// /// Value of the jump button in the previous frame. /// public float virtualButtonJumpLastFrame; /// /// Multiplier applied when turning around to adjust speed. /// public float turnaroundMultiplier = 2; /// /// Smoothing factor for walking movement. /// public float walkSmooth; /// /// Time in seconds to reach full speed from rest. /// public float secondsToFullSpeed; /// /// Force applied when jumping. /// public float jumpSpeed; /// /// Time window after leaving ground where jump is still allowed (coyote time). /// public float coyoteTime; /// /// Time window after pressing jump where jump is still buffered. /// public float jumpLenience; /// /// Minimum time before the player can be declared as not jumping. /// public float timeUnableToBeDeclaredNotJumping = 0.1f; /// /// Distance to check below the player for ground detection. /// public float groundCheckDistance; /// /// Reference to the Rigidbody2D component. /// private Rigidbody2D body; /// /// Reference to the BoxCollider2D component. /// private BoxCollider2D collide; /// /// Reference to the PlayerInput component. /// private PlayerInput input; /// /// Reference to the AnimationPlayer component. /// private AnimationPlayer animationPlayer; /// /// Reference to the Punch component. /// private Punch punch; /// /// Reference to the Damageable component. /// private Damageable damageable; /// /// Indicates if the jump input is still valid for buffered jumps. /// private bool jumpInputStillValid = false; /// /// Indicates if the player can be declared as not jumping. /// private bool canBeDeclaredNotJumping = true; /// /// Indicates if jump physics should be applied this frame. /// private bool jumpPhysics; /// /// Indicates if the player is currently jumping. /// private bool jumping; /// /// The last time the jump button was pressed. /// private float lastTimeJumpPressed; /// /// The last time the player was on the ground. /// private float lastTimeOnGround; /// /// The player's position in the previous frame. /// private Vector3 positionLastFrame; /// /// Initializes player components and sets up initial values. /// void Start() { maxSpeedOverride = maxSpeed; GetComponent().spawnPoint = transform.position; body = GetComponent(); collide = GetComponent(); input = GetComponent(); animationPlayer = GetComponent(); punch = GetComponent(); damageable = GetComponent(); playerText.text = input.playerIndex.ToString(); } /// /// Handles per-frame updates for player movement and jump input. /// private void Update() { if (GameManager.Instance != null && GameManager.Instance.gameOver) maxSpeed = 1f; UpdateVirtualAxis(); if (damageable.dying) return; Jump(); } /// /// Handles physics-based updates for jumping and horizontal movement. /// private void FixedUpdate() { JumpPhysics(); HorizontalMovement(); Land(); } /// /// Handles late frame updates, such as animation. /// private void LateUpdate() { Animate(); } /// /// Updates the player's animation state based on movement and grounded status. /// private void Animate() { if (!IsPhysicallyGrounded()) animationPlayer.SetState(AnimationPlayer.AnimationState.Jump); else { if (Mathf.Abs(virtualAxisX) >= 0.05f) animationPlayer.SetState(GameManager.Instance.gameOver ? AnimationPlayer.AnimationState.Walk : AnimationPlayer.AnimationState.Run); else animationPlayer.SetState(AnimationPlayer.AnimationState.Idle); } if (true) { if (virtualAxisX < -0.01f) animationPlayer.backwards = true; else if (virtualAxisX > 0.01f) animationPlayer.backwards = false; } else { if (body.linearVelocityX < -0.1f) animationPlayer.backwards = true; else if (body.linearVelocityX > 0.1f) animationPlayer.backwards = false; } } /// /// Handles logic for landing and stopping the jump state when grounded. /// private void Land() { if (body.linearVelocity.y >= 0f) return; if (IsPhysicallyGrounded()) { if (canBeDeclaredNotJumping) { jumping = false; } } } /// /// Handles jump input and determines if a jump should be triggered. /// private void Jump() { if (virtualButtonJumpLastFrame == 1f) { jumpInputStillValid = true; lastTimeJumpPressed = Time.time; } bool isBasicallyGrounded = IsBasicallyGrounded(); if ((virtualButtonJumpLastFrame == 1f && isBasicallyGrounded && jumping == false) || (jumpInputStillValid && Time.time - lastTimeJumpPressed <= jumpLenience && IsPhysicallyGrounded())) { AudioManager.Instance.PlaySound("Jump"); jumpPhysics = true; jumping = true; jumpInputStillValid = false; StartCoroutine(NotJumpingDelay()); } } /// /// Applies jump physics and forces to the player. /// private void JumpPhysics() { if (jumpPhysics) { if (!GetComponent().blocking) { if (body.linearVelocity.y < 0 || !IsPhysicallyGrounded()) body.linearVelocity = new Vector2(body.linearVelocity.x, 0); body.AddForce(Vector2.up * jumpSpeed, ForceMode2D.Impulse); if (Mathf.Abs(body.linearVelocityX) > maxSpeed) { body.linearVelocity = new Vector2(Mathf.Sign(body.linearVelocityX) * maxSpeed, body.linearVelocity.y); } } jumpPhysics = false; } if (!IsPhysicallyGrounded() && !(virtualButtonJump == 1f)) body.AddForce(Vector2.down * jumpSpeed); } /// /// Coroutine that delays the ability to declare the player as not jumping. /// private IEnumerator NotJumpingDelay() { canBeDeclaredNotJumping = false; yield return new WaitUntil(() => !IsBasicallyGrounded()); canBeDeclaredNotJumping = true; } /// /// Handles horizontal movement, including acceleration, deceleration, and blocking. /// private void HorizontalMovement() { float temporaryMax = IsPhysicallyGrounded() ? maxSpeedOverride : Mathf.Infinity; float temporarySlowdown = IsPhysicallyGrounded() ? slowdownMultiplier : 1; if (!GetComponent().blocking && (Mathf.Abs(body.linearVelocityX) <= maxSpeed || Mathf.Sign(body.linearVelocityX) != Mathf.Sign(virtualAxisX))) { body.AddForce(new Vector2(virtualAxisX * walkSpeed * walkSpeedFactor, 0), ForceMode2D.Force); } if (Mathf.Abs(body.linearVelocityX) >= temporaryMax) { body.AddForce(new Vector2(-Mathf.Sign(body.linearVelocityX) * (Mathf.Abs(body.linearVelocityX) - temporaryMax) * temporarySlowdown, 0)); } if (transform.position == positionLastFrame && (input.actions.FindAction("Move").ReadValue().x == 0)) { virtualAxisX = 0; } if (GetComponent().blocking) { body.AddForce(new Vector2(-body.linearVelocityX * 0.8f, 0), ForceMode2D.Force); } positionLastFrame = transform.position; } /// /// Updates the virtual axis and button values from input actions. /// private void UpdateVirtualAxis() { virtualButtonJump = input.actions.FindAction("Action").ReadValue(); virtualButtonJumpLastFrame = input.actions.FindAction("Action").WasPressedThisFrame() ? 1 : 0; virtualAxisX = input.actions.FindAction("Move").ReadValue().x; return; } /// /// Checks if the player is considered grounded, including coyote time. /// /// True if the player is basically grounded, otherwise false. public bool IsBasicallyGrounded() { if (IsPhysicallyGrounded()) { lastTimeOnGround = Time.time; } if (Time.time - lastTimeOnGround < coyoteTime) { return true; } return false; } /// /// Checks if the player is physically touching the ground using raycasts. /// /// True if the player is physically grounded, otherwise false. public bool IsPhysicallyGrounded() { RaycastHit2D leftCheck = Physics2D.Raycast(GetPointInBoxCollider(collide, -1, -1), Vector2.down, groundCheckDistance, ground); RaycastHit2D rightCheck = Physics2D.Raycast(GetPointInBoxCollider(collide, 1, -1), Vector2.down, groundCheckDistance, ground); RaycastHit2D midCheck = Physics2D.Raycast(GetPointInBoxCollider(collide, 0, -1), Vector2.down, groundCheckDistance, ground); if (leftCheck || rightCheck || midCheck) { return true; } return false; } /// /// Gets a point on the BoxCollider2D based on horizontal and vertical multipliers. /// /// The BoxCollider2D to use. /// Horizontal offset (-1 for left, 1 for right, 0 for center). /// Vertical offset (-1 for bottom, 1 for top, 0 for center). /// The calculated point in world space. public Vector2 GetPointInBoxCollider(BoxCollider2D boxCollider2D, float horizontal, float vertical) { return new Vector2 ( boxCollider2D.bounds.center.x + (horizontal * boxCollider2D.bounds.extents.x), boxCollider2D.bounds.center.y + (vertical * boxCollider2D.bounds.extents.y) ); } /// /// Stops the player's velocity if grounded, removing inertia. /// public void StopVelocity() { if (IsPhysicallyGrounded()) body.linearVelocity = Vector2.zero; } } }