using UnityEngine; namespace NeonTea.Quakeball.Player { /// The central glue class for players (both local and remote). /// Other classes will handle netcode/inputs, and then speak to this class in order to make the little people run around. [RequireComponent(typeof(CharacterController))] public class Player : MonoBehaviour { /// How long after running off a cliff should the player be considered "on ground"? public float CoyoteTime; public MoveStyle RunningMoveStyle; public MoveStyle CrouchingMoveStyle; public Transform Head; [Header("Player head status")] /// The euler angle of the player's head. public Quaternion HeadRotation; [Header("Player movement status")] /// The direction the player is going. public Vector3 MoveDirection; /// The player's desire to jump currently. public bool Jumping; /// The amount of movement the player wants to happen. /// Without analog controls, always 0 or 1. public float InputSpeed; /// The way the player is moving. public MoveStyle MoveStyle; [Header("Runtime computed values")] /// The speed at which the player is currently moving across the ground. public Vector3 GroundVelocity; /// The speed at which the player is rising or falling. public Vector3 GravitationalVelocity; /// The timestamp of when the player was last on the ground. public float GroundedTime; [Header("Misc. technical knobs")] public float GroundCastLength = 0.2f; public LayerMask GroundLayer; [Header("Debug settings")] public bool ShowGroundCast; private CharacterController CharacterController; private Vector3 FeetPosition; /// The normal of the ground below the player. If there is no ground, it's Vector3.up by default. public Vector3 GroundCast() { RaycastHit Hit; if (ShowGroundCast) { Debug.DrawLine(FeetPosition, FeetPosition - Vector3.up, Color.red, 1f); } if (Physics.Raycast(FeetPosition, -Vector3.up, out Hit, GroundCastLength, GroundLayer)) { return Hit.normal; } else { return Vector3.up; } } public bool IsGrounded() { return Time.fixedTime - GroundedTime <= CoyoteTime; } /// Sets Heading via euler angles. public void SetRotation(float xAngle, float yAngle) { MoveDirection = Quaternion.Euler(xAngle, yAngle, 0) * Vector3.forward; } private void Awake() { CharacterController = GetComponent(); FeetPosition = transform.position + CharacterController.center - Vector3.up * (CharacterController.height / 2 + CharacterController.skinWidth / 2); } private void Update() { Head.localRotation = Quaternion.Lerp(Head.localRotation, HeadRotation, 10f * Time.deltaTime); } private void FixedUpdate() { bool Grounded = IsGrounded(); if (Grounded) { GravitationalVelocity = Vector3.zero; } else { GravitationalVelocity += Physics.gravity * Time.fixedDeltaTime; } float FrictionVelocityFactor = Mathf.Max(GroundVelocity.magnitude, MoveStyle.StopVelocity); float Deccel = FrictionVelocityFactor * Time.fixedDeltaTime; if (Grounded) { Deccel *= MoveStyle.Friction; } else { Deccel *= MoveStyle.AirFriction; } float FrictionedVelocity = Mathf.Max(0, GroundVelocity.magnitude - Deccel); GroundVelocity = GroundVelocity.normalized * FrictionedVelocity; Vector3 GroundNormal = GroundCast(); Vector3 FixedHeading = Vector3.ProjectOnPlane(MoveDirection, GroundNormal).normalized; float CurrentSpeed = Vector3.Dot(GroundVelocity, FixedHeading); float Acceleration = MoveStyle.TargetVelocity * Time.fixedDeltaTime; if (Grounded) { Acceleration *= MoveStyle.Acceleration; } else { Acceleration *= MoveStyle.AirAcceleration; } Acceleration = Mathf.Min(Acceleration, MoveStyle.TargetVelocity - CurrentSpeed); GroundVelocity += FixedHeading * Acceleration; CharacterController.Move((GroundVelocity + GravitationalVelocity) * Time.fixedDeltaTime); if (CharacterController.isGrounded) { GroundedTime = Time.fixedTime; } } } }