334 lines
14 KiB
C#
334 lines
14 KiB
C#
using UnityEngine;
|
|
using NeonTea.Quakeball.Networking.Packets;
|
|
using NeonTea.Quakeball.Util;
|
|
using NeonTea.Quakeball.Networking;
|
|
using NeonTea.Quakeball.Networking.Instances;
|
|
using NeonTea.Quakeball.Combat;
|
|
using NeonTea.Quakeball.Interface;
|
|
|
|
namespace NeonTea.Quakeball.Players {
|
|
/// <summary>The central glue class for players (both local and remote).</summary>
|
|
/// <remarks>Other classes will handle netcode/inputs, and then speak to this class in order to make the little people run around.</remarks>
|
|
[RequireComponent(typeof(CharacterController))]
|
|
public class Player : MonoBehaviour {
|
|
|
|
/// <summary>The duration after running off a cliff, during which the player should still be considered grounded.</summary>
|
|
public float CoyoteTime;
|
|
public float PingBias;
|
|
|
|
/// <summary>How often should the player's movement be updated? Used by Local and Remote -Player classes.</summary>
|
|
public float UpdateFrequency = 1f;
|
|
|
|
public MoveStyle[] MoveStyles;
|
|
public Transform Gun;
|
|
public Animator GunBobber;
|
|
|
|
[Tooltip("GameObjects that are disabled on death and re-enabled on respawn.")]
|
|
public GameObject[] DisabledOnDeath;
|
|
|
|
[Header("Shooting")]
|
|
[Tooltip("For raycasting the shoot target.")]
|
|
public Transform CameraRoot;
|
|
public Transform BulletSourcePoint;
|
|
public LayerMask BulletHitLayer;
|
|
public float Cooldown;
|
|
|
|
[Header("Visuals")]
|
|
public DesyncLerper[] Lerpables;
|
|
public float Lean;
|
|
public GameObject LaserPrefab;
|
|
|
|
public ParticleSystem Splatter;
|
|
|
|
[Header("Audio")]
|
|
public AudioSource GunShotAudioSource;
|
|
public AudioClip RaygunClip;
|
|
public AudioSource HitAudioSource;
|
|
public AudioSource LocalHitAudioSource;
|
|
|
|
[Header("Player rotation status")]
|
|
/// <summary>The pitch of the player's head.</summary>
|
|
public float Pitch;
|
|
/// <summary>The total yaw of the player. Head yaw is Yaw - BodyYaw.</summary>
|
|
public float Yaw;
|
|
|
|
[Header("Player movement status")]
|
|
/// <summary>The direction the player is going.</summary>
|
|
public Vector3 MoveDirection;
|
|
|
|
/// <summary>The amount of movement the player wants to happen.</summary>
|
|
/// <remarks>Without analog controls, always 0 or 1.</remarks>
|
|
public float InputSpeed;
|
|
|
|
/// <summary>The way the player is moving.</summary>
|
|
public byte CurrentMoveStyle = 0;
|
|
public MoveStyle MoveStyle => MoveStyles[CurrentMoveStyle];
|
|
|
|
[Header("Runtime computed values")]
|
|
/// <summary>The speed at which the player is currently moving across the ground.</summary>
|
|
public Vector3 GroundVelocity;
|
|
|
|
/// <summary>The speed at which the player is rising or falling.</summary>
|
|
public Vector3 GravitationalVelocity;
|
|
|
|
/// <summary>The timestamp of when the player was last on the ground.</summary>
|
|
public float GroundedTime;
|
|
|
|
/// <summary> The possible networked Id of this Player instance </summary>
|
|
public ulong NetId;
|
|
|
|
/// <summary> Is the player dead</summary>
|
|
public bool IsDead;
|
|
|
|
public float LatestGroundedY;
|
|
|
|
[Header("Misc. technical knobs")]
|
|
public float GroundCastLength = 0.2f;
|
|
public LayerMask GroundLayer;
|
|
|
|
[Header("Debug settings")]
|
|
public bool ShowGroundCast;
|
|
public bool ShowMoveVector;
|
|
|
|
private CharacterController CharacterController;
|
|
private Vector3 FeetPosition;
|
|
|
|
private float LastShot;
|
|
private float TimeofDeath;
|
|
|
|
/// <summary>Creates a PlayerUpdatePckt representing this Player's current status, for sending to other peers.</summary>
|
|
public PlayerUpdatePckt CreateUpdatePacket(ulong id = 0) {
|
|
return new PlayerUpdatePckt(MoveDirection, CurrentMoveStyle, Pitch, Yaw, id);
|
|
}
|
|
|
|
/// <summary>Updates this Player with the given packet.</summary>
|
|
public void ProcessUpdatePacket(PlayerUpdatePckt packet) {
|
|
Pitch = packet.Pitch;
|
|
Yaw = packet.Yaw;
|
|
MoveDirection = packet.MoveDirection;
|
|
CurrentMoveStyle = packet.MoveStyle;
|
|
packet = null;
|
|
}
|
|
|
|
/// <summary>Creates a PlayerSyncPacket representing this Player's position and velocity, for sending to the server.</summary>
|
|
public PlayerSyncPacket CreateSyncPacket(ulong id = 0, bool unsynced = false) {
|
|
return new PlayerSyncPacket(id, unsynced, transform.position, GroundVelocity);
|
|
}
|
|
|
|
/// <summary>Applies the sync packet, checking it for cheatiness if shouldApply is false.</summary>
|
|
public bool ProcessSyncPacket(PlayerSyncPacket syncPckt, bool shouldApplyWithoutInspection = true) {
|
|
bool ShouldApply = shouldApplyWithoutInspection;
|
|
if (!shouldApplyWithoutInspection) {
|
|
// TODO: Gaze into the crystal ball to determine the nefariousness level of the packet and update ShouldApply accordingly.
|
|
ShouldApply = true;
|
|
}
|
|
if (ShouldApply) {
|
|
Vector3 Delta = transform.position - syncPckt.Location;
|
|
if (Delta.magnitude < 1) {
|
|
// Player is close enough to the sync packet, lerp them over.
|
|
foreach (DesyncLerper Lerper in Lerpables) {
|
|
Lerper.Offset(Delta);
|
|
}
|
|
}
|
|
transform.position = syncPckt.Location;
|
|
GroundVelocity = syncPckt.GroundVelocity;
|
|
}
|
|
return ShouldApply;
|
|
}
|
|
|
|
/// <summary>The normal of the ground below the player. If there is no ground, it's <c>Vector3.up</c> by default.</summary>
|
|
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 Jump() {
|
|
bool IsCoyoteTime = Time.time - GroundedTime <= CoyoteTime + PingBias;
|
|
if (IsCoyoteTime || IsGrounded()) {
|
|
GravitationalVelocity = Vector3.up * MoveStyle.JumpVelocity;
|
|
Vector3 Pos = transform.position;
|
|
Pos.y = LatestGroundedY;
|
|
transform.position = Pos;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public void Shoot() {
|
|
float delta = Time.time - LastShot;
|
|
if (Net.Singleton.Instance is Server) {
|
|
float ping = ((Server)Net.Singleton.Instance).Players[NetId].Ping;
|
|
delta += ping;
|
|
}
|
|
if (delta < Cooldown) {
|
|
return;
|
|
}
|
|
LastShot = Time.time;
|
|
Vector3 GunPoint = BulletSourcePoint.position;
|
|
Vector3 ShotDelta = CameraRoot.forward * 1000f;
|
|
|
|
Vector3 From = CameraRoot.position;
|
|
Vector3 Direction = CameraRoot.forward;
|
|
RaycastHit[] Hits = Physics.RaycastAll(From, Direction, 1000f, BulletHitLayer);
|
|
System.Array.Sort(Hits, (a, b) => { return a.distance.CompareTo(b.distance); });
|
|
foreach (RaycastHit Hit in Hits) {
|
|
ShotDelta = Hit.point - GunPoint;
|
|
Player Player = Hit.rigidbody != null ? Hit.rigidbody.GetComponent<Player>() : null;
|
|
if (Player == this) {
|
|
continue;
|
|
}
|
|
if (Player != null) {
|
|
if (Net.Singleton.Instance is Server) {
|
|
((Server)Net.Singleton.Instance).SendHit(NetId, Player.NetId);
|
|
Player.Hit(NetId);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
GameObject LaserEffect = Instantiate(LaserPrefab);
|
|
Laser Laser = LaserEffect.GetComponent<Laser>();
|
|
Laser.From = GunPoint;
|
|
Laser.To = GunPoint + ShotDelta;
|
|
|
|
GunShotAudioSource.PlayOneShot(RaygunClip);
|
|
}
|
|
|
|
public void Hit(ulong sourceUid) {
|
|
if (Net.Singleton.Instance is Server) {
|
|
((Server)Net.Singleton.Instance).HandlePlayerDeath(NetId, sourceUid);
|
|
}
|
|
bool IsLocal = true;
|
|
if (Net.Singleton.Instance != null) {
|
|
IsLocal = Net.Singleton.Instance.LocalPlayer.Id == sourceUid;
|
|
}
|
|
Splatter.Play();
|
|
HitAudioSource.Play();
|
|
if (IsLocal) {
|
|
Net.Singleton.Instance.LocalPlayer.Controlled.LocalHitAudioSource.Play();
|
|
}
|
|
}
|
|
|
|
/// <summary>Called when this Player is dead</summary>
|
|
public void Dead(ulong killer) {
|
|
if (IsDead) {
|
|
return;
|
|
}
|
|
if (Net.Singleton.Instance != null && Net.Singleton.Instance.LocalPlayer.Id == NetId) {
|
|
string name = $"Connection {killer}";
|
|
GameObject.FindGameObjectWithTag("DeadScreen").GetComponent<DeadScreen>().StartCountdown(name);
|
|
Net.Singleton.Instance.LocalPlayer.Controlled.GetComponent<LocalPlayer>().DisableInput += 1;
|
|
}
|
|
TimeofDeath = Time.time;
|
|
MoveDirection = Vector3.zero;
|
|
foreach (GameObject obj in DisabledOnDeath) {
|
|
obj.SetActive(false);
|
|
}
|
|
IsDead = true;
|
|
}
|
|
|
|
/// <summary>Called when this Player is respawned, after dying.</summary>
|
|
public void Respawn(Vector3 location) {
|
|
if (Net.Singleton.Instance != null && Net.Singleton.Instance.LocalPlayer.Id == NetId) {
|
|
GameObject.FindGameObjectWithTag("DeadScreen").GetComponent<DeadScreen>().Open = false;
|
|
Net.Singleton.Instance.LocalPlayer.Controlled.GetComponent<LocalPlayer>().DisableInput -= 1;
|
|
}
|
|
transform.position = location;
|
|
foreach (GameObject obj in DisabledOnDeath) {
|
|
obj.SetActive(true);
|
|
}
|
|
IsDead = false;
|
|
}
|
|
|
|
public bool IsGrounded() {
|
|
return CharacterController.isGrounded && Vector3.Dot(GravitationalVelocity, Vector3.down) >= 0;
|
|
}
|
|
|
|
private void Awake() {
|
|
CharacterController = GetComponent<CharacterController>();
|
|
FeetPosition = transform.position + CharacterController.center - Vector3.up * (CharacterController.height / 2 + CharacterController.skinWidth / 2);
|
|
if (GameObject.FindObjectOfType<Util.PhysicsSyncer>() == null) {
|
|
Debug.LogWarning("Player.Awake: There is no PhysicsSyncer in this scene! Some code will not work as expected.");
|
|
}
|
|
}
|
|
|
|
private void Update() {
|
|
if (Net.Singleton.Instance is Server) {
|
|
if (IsDead && (Time.time - TimeofDeath > 3)) {
|
|
((Server)Net.Singleton.Instance).HandlePlayerRespawn(NetId);
|
|
}
|
|
}
|
|
|
|
float TargetLean = -Vector3.Dot(GroundVelocity / MoveStyle.TargetVelocity, CameraRoot.right) * MoveStyle.LeanDegrees;
|
|
Lean = Mathf.Lerp(Lean, TargetLean, 30f * Time.deltaTime);
|
|
CameraRoot.localEulerAngles = new Vector3(Pitch, Yaw, Lean);
|
|
}
|
|
|
|
private void LateUpdate() {
|
|
UpdateMovement();
|
|
}
|
|
|
|
private void UpdateMovement() {
|
|
bool Grounded = IsGrounded();
|
|
|
|
bool FallingDown = Vector3.Dot(Vector3.down, GravitationalVelocity) > 0;
|
|
if (Grounded && FallingDown) {
|
|
GravitationalVelocity = Vector3.zero;
|
|
} else if (!Grounded) {
|
|
GravitationalVelocity += Physics.gravity * Time.deltaTime;
|
|
}
|
|
|
|
float FrictionVelocityFactor = Mathf.Max(GroundVelocity.magnitude, MoveStyle.StopVelocity);
|
|
float Deccel = FrictionVelocityFactor * Time.deltaTime;
|
|
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.deltaTime;
|
|
if (Grounded) {
|
|
Acceleration *= MoveStyle.Acceleration;
|
|
} else {
|
|
Acceleration *= MoveStyle.AirAcceleration;
|
|
}
|
|
Acceleration = Mathf.Min(Acceleration, MoveStyle.TargetVelocity - CurrentSpeed);
|
|
GroundVelocity += FixedHeading * Acceleration;
|
|
|
|
Vector3 FinalMoveVector = GroundVelocity + GravitationalVelocity;
|
|
CharacterController.Move(FinalMoveVector * Time.deltaTime);
|
|
if (CharacterController.isGrounded) {
|
|
GroundedTime = Time.time;
|
|
LatestGroundedY = transform.position.y;
|
|
}
|
|
if (GravitationalVelocity.y > 0.1 && Mathf.Abs(CharacterController.velocity.y) < 0.1) {
|
|
// Hit a roof while jumping
|
|
GravitationalVelocity.y = 0;
|
|
}
|
|
if (ShowMoveVector) {
|
|
Debug.DrawLine(
|
|
transform.position + CharacterController.center,
|
|
transform.position + CharacterController.center + FinalMoveVector,
|
|
Color.green, 1.0f
|
|
);
|
|
}
|
|
|
|
float TargetBobbiness = Grounded ? GroundVelocity.magnitude / MoveStyle.TargetVelocity : 0;
|
|
GunBobber.SetLayerWeight(1, Mathf.Lerp(GunBobber.GetLayerWeight(1), TargetBobbiness, 10f * Time.deltaTime));
|
|
}
|
|
}
|
|
}
|