using System.Collections.Generic;
using NUnit.Framework;
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;
[Space]
[Header("Jump Settings")]
[SerializeField] float JumpForce;
[SerializeField] float JumpCoyoteTime;
[SerializeField] float JumpBufferTime;
bool IsJumping = false;
float TimeSinceLastJumpRequest = 0f;
float TimeSinceLastGrounded = 0f;
[Space]
[Header("Physics Settings")]
[SerializeField] LayerMask GroundLayer;
[SerializeField] float WallCheckDistance;
[SerializeField] float GroundCheckDistance;
[SerializeField] Vector2 Gravity = Vector2.right;
[SerializeField] bool IsGrounded = false;
[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();
Timer();
}
void FixedUpdate()
{
Vector2 DesiredHorizontalVelocity = Move();
TryJump(JumpForce, LocalDirections[0], DesiredHorizontalVelocity);
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, WorldNormal => Vector2.Angle(WorldNormal, LocalDirections[0]) < MaxSlopeAngle))
{
TimeSinceLastGrounded = JumpCoyoteTime;
IsJumping = false;
GroundNormal = Hit.normal;
IsGrounded = true;
}
else
{
IsGrounded = false;
}
Vector2 slopeTangent = Vector2.Perpendicular(GroundNormal).normalized;
Vector2 slopeVel = Vector2.Dot(RB.linearVelocity, slopeTangent) * slopeTangent;
Vector2 nonSlopeVel = RB.linearVelocity - slopeVel;
if (IsGrounded)
{
Debug.Log(nonSlopeVel);
}
}
bool TrySweep(Vector2 Direction, float Distance, out RaycastHit2D Hit, 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 = GroundLayer, 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;
}
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
};
}
Vector2 Move()
{
float Angle = Vector2.SignedAngle(Vector2.down, Gravity.normalized);
RB.rotation = Angle;
transform.rotation = Quaternion.Euler(0f, 0f, Angle);
Vector2 GravityAxis = LocalDirections[0];
Vector2 VelocityAlongGravity = Vector2.Dot(RB.linearVelocity, GravityAxis) * GravityAxis;
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, WorldNormal => Mathf.Abs(WorldNormal.x) > (1f - WallNormalThreshold) && Mathf.Abs(WorldNormal.y) < WallNormalThreshold))
{
FacingRight = !FacingRight;
DesiredHorizontalVelocity = Vector2.zero;
}
RB.linearVelocity = VelocityAlongGravity + DesiredHorizontalVelocity;
return DesiredHorizontalVelocity;
}
public void TryJump(float JumpForce, Vector2 JumpDirection, Vector2 DesiredHorizontalVelocity)
{
if (Input.GetKeyDown(KeyCode.Space))
{
TimeSinceLastJumpRequest = JumpBufferTime;
}
if (TimeSinceLastGrounded > 0 && TimeSinceLastJumpRequest > 0 && !IsJumping)
{
IsJumping = true;
TimeSinceLastJumpRequest = 0f;
TimeSinceLastGrounded = 0f;
RB.AddForce(JumpDirection.normalized * JumpForce, ForceMode2D.Impulse);
}
}
void Timer()
{
if (TimeSinceLastJumpRequest > 0)
{
TimeSinceLastJumpRequest -= Time.deltaTime;
}
if (TimeSinceLastGrounded > 0)
{
TimeSinceLastGrounded -= Time.deltaTime;
}
}
}
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;
}
}