You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
330 lines
9.8 KiB
330 lines
9.8 KiB
7 months ago
|
using System.Collections;
|
||
|
using System.Collections.Generic;
|
||
|
using UnityEditor;
|
||
|
using UnityEngine;
|
||
|
using UnityEngine.InputSystem;
|
||
|
|
||
|
public class PlayerMovement : MonoBehaviour, PlayerInput.IMovementActions
|
||
|
{
|
||
|
// TYPES
|
||
|
public enum State
|
||
|
{
|
||
|
INVALID = 0,
|
||
|
|
||
|
GROUNDED = 1,
|
||
|
SLIDING = 2,
|
||
|
AIRBORNE = 3
|
||
|
}
|
||
|
|
||
|
|
||
|
// REFERENCES
|
||
|
public CapsuleCollider playerNavCollider;
|
||
|
public Rigidbody rgdBdy;
|
||
|
public PlayerCameraController camCntlr;
|
||
|
|
||
|
|
||
|
// PARAMETERS
|
||
|
public float smoothing;
|
||
|
|
||
|
public string[] groundingLayers;
|
||
|
public float moveForce = 30f;
|
||
|
public float maxSpeed = 10f;
|
||
|
public AnimationCurve brakingForceCurve = null;
|
||
|
public float slidingAngle = 45f;
|
||
|
|
||
|
public float jumpHeight = 3f;
|
||
|
public float jumpCooldown = 0.15f;
|
||
|
public float coyoteTime = 0.1f;
|
||
|
|
||
|
public float turnRate = 270f;
|
||
|
|
||
|
|
||
|
// PROPERTIES
|
||
|
public State CurState
|
||
|
{
|
||
|
get { return this._curState; }
|
||
|
set
|
||
|
{
|
||
|
// Guard; same state
|
||
|
if (value == this._curState)
|
||
|
return;
|
||
|
|
||
|
// TODO: Teardown
|
||
|
|
||
|
// Swap
|
||
|
this._curState = value;
|
||
|
|
||
|
// TODO: Setup
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// INTERNALS
|
||
|
private int _groundingMask = -1;
|
||
|
private PlayerInput _inpt = null;
|
||
|
|
||
|
private State _curState = State.AIRBORNE;
|
||
|
|
||
|
private Vector2 _curMvInpt = Vector2.zero;
|
||
|
private Vector2 _smoothMvInpt = Vector2.zero;
|
||
|
|
||
|
private bool _strafeToggleHeld = false;
|
||
|
|
||
|
private float _jumpHoldStartTime = -1f;
|
||
|
private float _lastJumpTime = -1f;
|
||
|
|
||
|
// Record the results of the grounding check
|
||
|
// as instance variables
|
||
|
private float _groundedTime = -1f;
|
||
|
private Vector3 _groundNormal = Vector3.up;
|
||
|
|
||
|
|
||
|
// LIFECYCLE
|
||
|
private void Awake()
|
||
|
{
|
||
|
this._groundingMask = LayerMask.GetMask(this.groundingLayers);
|
||
|
}
|
||
|
|
||
|
private void OnEnable()
|
||
|
{
|
||
|
if (this._inpt == null)
|
||
|
{
|
||
|
this._inpt = new PlayerInput();
|
||
|
this._inpt.Movement.SetCallbacks(this);
|
||
|
}
|
||
|
|
||
|
this._inpt.Enable();
|
||
|
}
|
||
|
|
||
|
private void OnDisable()
|
||
|
{
|
||
|
if (this._inpt != null)
|
||
|
{
|
||
|
this._inpt.Disable();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void FixedUpdate()
|
||
|
{
|
||
|
this.HandleInput();
|
||
|
this.GroundCheck();
|
||
|
|
||
|
switch(this._curState)
|
||
|
{
|
||
|
case State.GROUNDED:
|
||
|
this.HandleGrounded();
|
||
|
break;
|
||
|
case State.SLIDING:
|
||
|
this.HandleSliding();
|
||
|
break;
|
||
|
case State.AIRBORNE:
|
||
|
this.HandleAirborne();
|
||
|
break;
|
||
|
default:
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// BEHAVIORS
|
||
|
private void HandleInput()
|
||
|
{
|
||
|
// Smooth input
|
||
|
this._smoothMvInpt = MathExtras.LerpSmooth2(this._smoothMvInpt, Vector2.ClampMagnitude(this._curMvInpt, 1f), Time.fixedDeltaTime, this.smoothing);
|
||
|
|
||
|
// Clip Input
|
||
|
if (this._curMvInpt.sqrMagnitude < 1e-3f && this._smoothMvInpt.sqrMagnitude < 1e-3f)
|
||
|
this._smoothMvInpt = Vector2.zero;
|
||
|
}
|
||
|
|
||
|
// This gets run once per FixedUpdate
|
||
|
private void GroundCheck()
|
||
|
{
|
||
|
// Spherecast parameters determined from player's capsule collider
|
||
|
Vector3 start = (((this.playerNavCollider.height * 0.5f) - this.playerNavCollider.radius - 0.1f) * Vector3.down) + this.rgdBdy.position;
|
||
|
float dist = (Physics.gravity * Time.fixedDeltaTime * 3f).magnitude;
|
||
|
|
||
|
Ray ry = new Ray(start, Physics.gravity);
|
||
|
RaycastHit ht;
|
||
|
|
||
|
// _groundingMask determines which layers are checked for ground
|
||
|
// See LayerMask.GetMask(string[] layers) for more info. Can leave as -1 to check all layers.
|
||
|
if (Physics.SphereCast(ry, this.playerNavCollider.radius, out ht, dist, this._groundingMask))
|
||
|
{
|
||
|
// Hit ground, record
|
||
|
this._groundedTime = Time.time;
|
||
|
this._groundNormal = ht.normal;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Timeout ground normal
|
||
|
if (this._groundedTime > 0f && (Time.time - this._groundedTime) > this.coyoteTime)
|
||
|
{
|
||
|
this._groundedTime = -1f;
|
||
|
this._groundNormal = Vector3.up;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void ApplyGroundForces()
|
||
|
{
|
||
|
// Convert input direction to camera local
|
||
|
Vector3 input = this.camCntlr.cam.transform.TransformVector(new Vector3(this._smoothMvInpt.x, 0f, this._smoothMvInpt.y));
|
||
|
|
||
|
// Rotate to horizontal, then to match ground orientation
|
||
|
input = Quaternion.FromToRotation(Vector3.up, this._groundNormal) * Quaternion.FromToRotation(this.camCntlr.transform.up, Vector3.up) * input;
|
||
|
|
||
|
// Apply input force
|
||
|
this.rgdBdy.AddForce(input * this.moveForce, ForceMode.Force);
|
||
|
|
||
|
Debug.DrawLine(this.rgdBdy.position, this.rgdBdy.position + (input * this.moveForce), Color.white);
|
||
|
|
||
|
// Apply braking
|
||
|
Vector3 groundSpeed = Vector3.ProjectOnPlane(this.rgdBdy.velocity, this._groundNormal);
|
||
|
float brakingForce = this.brakingForceCurve.Evaluate(groundSpeed.magnitude / this.maxSpeed) * this.moveForce;
|
||
|
this.rgdBdy.AddForce(groundSpeed.normalized * -1f * brakingForce, ForceMode.Force);
|
||
|
|
||
|
Debug.DrawLine(this.rgdBdy.position, this.rgdBdy.position + (groundSpeed.normalized * -1f * brakingForce), Color.blue);
|
||
|
|
||
|
// Counteract gravity while grounded on slope
|
||
|
this.rgdBdy.AddForce(Vector3.ProjectOnPlane(Physics.gravity, this._groundNormal) * -1f);
|
||
|
|
||
|
// Stopping force (only when no input, only when grounded)
|
||
|
if (this._groundedTime == Time.time && this._smoothMvInpt == Vector2.zero)
|
||
|
this.rgdBdy.AddForce(MathExtras.ClipVec3(groundSpeed, 1f) * -1f * this.moveForce, ForceMode.Force);
|
||
|
}
|
||
|
|
||
|
private bool ApplyJumpForces()
|
||
|
{
|
||
|
// Guard; Jumped to recently
|
||
|
if ((Time.time - this._lastJumpTime) < this.jumpCooldown)
|
||
|
return false;
|
||
|
|
||
|
// Record
|
||
|
this._lastJumpTime = Time.time;
|
||
|
|
||
|
// Calculate jump height from gravity
|
||
|
Vector3 jumpFrc = Physics.gravity.normalized * -1f * Mathf.Sqrt(2f * Physics.gravity.magnitude * this.jumpHeight);
|
||
|
this.rgdBdy.AddForce(jumpFrc, ForceMode.VelocityChange);
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
private void HandleFacing()
|
||
|
{
|
||
|
// Get desired facing direction
|
||
|
Vector3 facingDir = this.transform.forward;
|
||
|
|
||
|
if (this._strafeToggleHeld)
|
||
|
{
|
||
|
// Face camera direction
|
||
|
facingDir = this.camCntlr.cam.transform.forward;
|
||
|
facingDir.y = 0f;
|
||
|
facingDir.Normalize();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Face direction of travel
|
||
|
Vector3 groundVel = Vector3.ProjectOnPlane(this.rgdBdy.velocity, this._groundNormal);
|
||
|
if (groundVel.sqrMagnitude > 1e-2f)
|
||
|
facingDir = groundVel.normalized;
|
||
|
}
|
||
|
|
||
|
// Get target rotation
|
||
|
Quaternion facingRot = Quaternion.FromToRotation(this.transform.forward, facingDir);
|
||
|
facingRot = Quaternion.Euler(Vector3.Scale(facingRot.eulerAngles, new Vector3(0f, 1f, 0f))) * this.rgdBdy.rotation;
|
||
|
|
||
|
// Yaw player towards facing direction
|
||
|
this.rgdBdy.rotation = Quaternion.RotateTowards(this.rgdBdy.rotation, facingRot, this.turnRate * Time.fixedDeltaTime);
|
||
|
}
|
||
|
|
||
|
// FSM
|
||
|
private void HandleGrounded()
|
||
|
{
|
||
|
// Watch for state change
|
||
|
if (Time.time != this._groundedTime)
|
||
|
{
|
||
|
this.CurState = State.AIRBORNE;
|
||
|
return;
|
||
|
}
|
||
|
else if (Vector3.Angle(Vector3.up, this._groundNormal) > this.slidingAngle)
|
||
|
{
|
||
|
this.CurState = State.SLIDING;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Check and execute jump
|
||
|
// The '&&' operator 'short-circuits', so ApplyJumpForces will only be run if the previous operands are true
|
||
|
if (this._jumpHoldStartTime > 0f && (Time.time - this._jumpHoldStartTime) < this.coyoteTime && this.ApplyJumpForces())
|
||
|
return;
|
||
|
|
||
|
this.ApplyGroundForces();
|
||
|
this.HandleFacing();
|
||
|
}
|
||
|
|
||
|
private void HandleSliding()
|
||
|
{
|
||
|
// Watch for state change
|
||
|
if (Time.time != this._groundedTime)
|
||
|
{
|
||
|
this.CurState = State.AIRBORNE;
|
||
|
return;
|
||
|
}
|
||
|
else if (Vector3.Angle(Vector3.up, this._groundNormal) < this.slidingAngle)
|
||
|
{
|
||
|
this.CurState = State.GROUNDED;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// TODO: Any behavior here? Braking force?
|
||
|
}
|
||
|
|
||
|
private void HandleAirborne()
|
||
|
{
|
||
|
// Watch for state change
|
||
|
if (Time.time == this._groundedTime)
|
||
|
{
|
||
|
if (Vector3.Angle(Vector3.up, this._groundNormal) > this.slidingAngle)
|
||
|
this.CurState = State.SLIDING;
|
||
|
else
|
||
|
this.CurState = State.GROUNDED;
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// TODO: Any behavior here? Air control?
|
||
|
}
|
||
|
|
||
|
|
||
|
// DEBUG
|
||
|
|
||
|
#if UNITY_EDITOR
|
||
|
private void OnDrawGizmos()
|
||
|
{
|
||
|
Handles.Label(this.transform.position + Vector3.up * 2f, $"State: {this._curState}\r\nSpeed: {this.rgdBdy.velocity.magnitude}\r\nAngle: {Vector3.Angle(Vector3.up, this._groundNormal)}");
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
// INPUT
|
||
|
public void OnMoveInput(InputAction.CallbackContext context)
|
||
|
{
|
||
|
this._curMvInpt = context.ReadValue<Vector2>();
|
||
|
}
|
||
|
|
||
|
public void OnStrafeToggle(InputAction.CallbackContext context)
|
||
|
{
|
||
|
if (context.phase == InputActionPhase.Started)
|
||
|
this._strafeToggleHeld = true;
|
||
|
else if (context.phase == InputActionPhase.Canceled)
|
||
|
this._strafeToggleHeld = false;
|
||
|
}
|
||
|
|
||
|
public void OnJump(InputAction.CallbackContext context)
|
||
|
{
|
||
|
if (context.phase == InputActionPhase.Started)
|
||
|
this._jumpHoldStartTime = Time.time;
|
||
|
else if (context.phase == InputActionPhase.Canceled)
|
||
|
this._jumpHoldStartTime = -1f;
|
||
|
}
|
||
|
}
|