[Header("Movement")]
public float moveSpeed = 5f;
public float jumpForce = 10f;
public float airControl = 0.4f;
[Header("Ground Check")]
public float groundCheckDistance = 0.3f;
public LayerMask groundLayer;
[Header("Jump Grace")]
public float coyoteTime = 0.15f;
private float coyoteTimer;
[Header("Rotation")]
public float rotationSpeed = 10f;
private Rigidbody2D rb;
private GrapplingHook grapple;
private bool isGrounded;
private Vector2 groundNormal;
private float moveInput;
private bool jumpPressed;
private bool jumpedThisFrame;
void Awake()
{
rb = GetComponent<Rigidbody2D>();
grapple = GetComponent<GrapplingHook>();
}
void Update()
{
moveInput = Input.GetAxis("Horizontal");
if (Input.GetButtonDown("Jump"))
jumpPressed = true;
}
void FixedUpdate()
{
GroundCheck();
HandleRotation();
jumpedThisFrame = false;
if (jumpPressed && coyoteTimer > 0f)
{
Jump();
coyoteTimer = 0f;
jumpedThisFrame = true;
}
if (!jumpedThisFrame && (grapple == null || !grapple.isGrappling))
HandleMovement();
jumpPressed = false;
}
// ================= CORE =================
void GroundCheck()
{
Vector2 gravityDir = -transform.up;
RaycastHit2D hit = Physics2D.Raycast(
rb.position,
gravityDir,
groundCheckDistance,
groundLayer
);
if (hit.collider != null)
{
isGrounded = true;
groundNormal = hit.normal;
coyoteTimer = coyoteTime; // reset grace timer
}
else
{
isGrounded = false;
coyoteTimer -= Time.fixedDeltaTime;
groundNormal = -gravityDir;
}
}
void HandleMovement()
{
Vector2 gravityDir = -transform.up;
Vector2 tangent = new Vector2(-gravityDir.y, gravityDir.x);
float control = isGrounded ? 1f : airControl;
Vector2 tangentVelocity = tangent * moveInput * moveSpeed * control;
// Preserve gravity-direction velocity
Vector2 gravityVelocity =
(Vector2)Vector3.Project(rb.linearVelocity, gravityDir);
rb.linearVelocity = tangentVelocity + gravityVelocity;
}
void Jump()
{
Vector2 jumpDir = -(-transform.up); // same as transform.up
rb.linearVelocity = Vector2.zero;
rb.AddForce(jumpDir * jumpForce, ForceMode2D.Impulse);
}
void HandleRotation()
{
Vector2 upDir = -(-transform.up); // face away from gravity
float targetAngle =
Mathf.Atan2(upDir.y, upDir.x) * Mathf.Rad2Deg - 90f;
float newAngle = Mathf.LerpAngle(
rb.rotation,
targetAngle,
rotationSpeed * Time.fixedDeltaTime
);
rb.MoveRotation(newAngle);
}