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;
}
}
}