using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
[Header("Components")]
[SerializeField] Rigidbody2D RB;
[SerializeField] CapsuleCollider2D Collider;
[Space]
[Header("Movement Settings")]
[SerializeField] float MoveSpeed;
[SerializeField] bool FacingRight = true;
bool CanMove = true;
[Space]
[Header("Jump Settings")]
[SerializeField] float JumpForce;
[SerializeField] float JumpCoyoteTime;
[SerializeField] float JumpBufferTime;
[SerializeField] float JumpResetTime;
bool IsJumping = false;
float TimeSinceLastJumpRequest = 0f;
float TimeSinceLastGrounded = 0f;
float TimeSinceLastJump = 0f;
[Space]
[Header("Physics Settings")]
[SerializeField] LayerMask GroundLayer;
[SerializeField] float WallCheckDistance;
[SerializeField] float GroundCheckDistance;
[SerializeField] Vector2 Gravity = Vector2.right;
[SerializeField] float GravityRotationSpeed;
[SerializeField] float MaxSlopeAngle;
List<Vector2> LocalDirections = new List<Vector2>();
Vector2 GroundNormal;
void Awake()
{
RB = GetComponent<Rigidbody2D>();
Collider = GetComponent<CapsuleCollider2D>();
LocalDirections = RetrieveLocalDirections();
}
void Update()
{
LocalDirections = RetrieveLocalDirections();
TryJump(JumpForce, LocalDirections[0]);
Timer();
}
void FixedUpdate()
{
if (CanMove)
{
Move();
}
RB.AddForce(Gravity * RB.mass * -Physics2D.gravity.y, ForceMode2D.Force);
float WallNormalThreshold = Mathf.Cos(MaxSlopeAngle * Mathf.Deg2Rad);
if (TrySweep(LocalDirections[2], GroundCheckDistance, out RaycastHit2D Hit, GroundLayer, WorldNormal => Vector2.Angle(WorldNormal, LocalDirections[0]) < MaxSlopeAngle) && TimeSinceLastJump <= 0)
{
TimeSinceLastGrounded = JumpCoyoteTime;
TimeSinceLastJump = 0f;
CanMove = true;
IsJumping = false;
GroundNormal = Hit.normal;
}
}
Vector2 Move()
{
float Angle = Vector2.SignedAngle(Vector2.down, Gravity.normalized);
float SmoothZ = Mathf.LerpAngle(transform.eulerAngles.z, Angle, GravityRotationSpeed * Time.deltaTime);
transform.eulerAngles = new Vector3(0, 0, SmoothZ);
Vector2 VelocityAlongGravity = Vector2.Dot(RB.linearVelocity, LocalDirections[0]) * LocalDirections[0];
Vector2 DesiredHorizontalVelocity = LocalDirections[1] * MoveSpeed * (FacingRight ? 1f : -1f);
Vector2 Direction = FacingRight ? LocalDirections[1] : LocalDirections[3];
float WallNormalThreshold = Mathf.Cos(MaxSlopeAngle * Mathf.Deg2Rad);
if (TrySweep(Direction, WallCheckDistance, out RaycastHit2D Hit, GroundLayer, WorldNormal => Mathf.Abs(WorldNormal.x) > (1f - WallNormalThreshold) && Mathf.Abs(WorldNormal.y) < WallNormalThreshold))
{
FacingRight = !FacingRight;
DesiredHorizontalVelocity = Vector2.zero;
}
RB.linearVelocity = VelocityAlongGravity + DesiredHorizontalVelocity;
return DesiredHorizontalVelocity;
}
bool TrySweep(Vector2 Direction, float Distance, out RaycastHit2D Hit, LayerMask Layer, System.Func<Vector2, bool> NormalTest)
{ // System.Func<Vector2, bool> NormalTest lets us create custom normal tests for different scenarios
var Hits = new RaycastHit2D[5];
int HitCount = Collider.Cast(Direction, new ContactFilter2D { layerMask = Layer, useLayerMask = true }, Hits, Distance);
for (int i = 0; i < HitCount; i++)
{
var H = Hits[i];
if (H.collider == null) continue;
if (NormalTest(H.normal))
{
Hit = H;
return true;
}
}
Hit = default;
return false;
}
public void TryJump(float JumpForce, Vector2 JumpDirection)
{
if (Input.GetKeyDown(KeyCode.Space))
{
TimeSinceLastJumpRequest = JumpBufferTime;
}
if (TimeSinceLastGrounded > 0 && TimeSinceLastJumpRequest > 0 && !IsJumping && TimeSinceLastJump <= 0)
{
IsJumping = true;
TimeSinceLastJumpRequest = 0f;
TimeSinceLastGrounded = 0f;
TimeSinceLastJump = JumpResetTime; // Reset the jump timer to prevent multiple jumps in quick succession
Vector2 LocalGravityVelocity = (Vector2)Vector3.Project(RB.linearVelocity, LocalDirections[2]);
Vector2 LateralVelocity = RB.linearVelocity - LocalGravityVelocity;
RB.linearVelocity = (JumpDirection * JumpForce) + LateralVelocity;
}
}
public void AddForce(Vector2 Force, ForceMode2D ForceMode = ForceMode2D.Impulse, bool AllowMovement = true)
{
TimeSinceLastJumpRequest = 0f;
TimeSinceLastGrounded = 0f;
TimeSinceLastJump = 0f;
CanMove = AllowMovement;
IsJumping = false;
float VelocityAlong = Vector2.Dot(RB.linearVelocity, Force.normalized);
RB.linearVelocity -= Force.normalized * VelocityAlong;
RB.AddForce(Force, ForceMode);
}
public void ChangeGravity(Vector2 NewGravity)
{
LocalDirections = RetrieveLocalDirections();
Gravity = NewGravity.normalized;
}
void Timer()
{
if (TimeSinceLastJumpRequest > 0)
{
TimeSinceLastJumpRequest -= Time.deltaTime;
}
if (TimeSinceLastGrounded > 0)
{
TimeSinceLastGrounded -= Time.deltaTime;
}
if (TimeSinceLastJump > 0)
{
TimeSinceLastJump -= Time.deltaTime;
}
}
List<Vector2> RetrieveLocalDirections()
{
float Angle = Vector2.SignedAngle(Vector2.down, Gravity.normalized);
return new List<Vector2>
{
Vector2.up.Rotate(Angle).normalized,
Vector2.right.Rotate(Angle).normalized,
Vector2.down.Rotate(Angle).normalized,
Vector2.left.Rotate(Angle).normalized
};
}
}
public static class Vector2Extensions
{
public static Vector2 Rotate(this Vector2 Vector, float Degrees)
{
float X = Vector.x * Mathf.Cos(Degrees * Mathf.Deg2Rad) - Vector.y * Mathf.Sin(Degrees * Mathf.Deg2Rad); // x cos phi - y sin phi
float Y = Vector.x * Mathf.Sin(Degrees * Mathf.Deg2Rad) + Vector.y * Mathf.Cos(Degrees * Mathf.Deg2Rad); // x sin phi + y cos phi
Vector = new Vector2(X, Y);
return Vector;
}
}