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

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