quakeball/Assets/Scripts/Players/Player.cs

335 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;
using NeonTea.Quakeball.Animation;
using NeonTea.Quakeball.Audio;
using NeonTea.Quakeball.Items;
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;
public MoveStyle[] MoveStyles;
public SoldierProceduralAnimator SoldierProceduralAnimator;
[Header("Item related")]
public Transform ItemRoot;
public GameObject[] PossibleItems;
public int CurrentItemIdx;
public Item CurrentItem;
[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;
[Header("Visuals")]
public float LerpedDesyncDistance;
public DesyncLerper[] Lerpables;
public float Lean;
public ParticleSystem Splatter;
[Header("Audio")]
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 NextAllowedShot;
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) {
if (!IsDead) {
Pitch = packet.Pitch;
Yaw = packet.Yaw;
MoveDirection = packet.MoveDirection;
CurrentMoveStyle = packet.MoveStyle;
}
}
/// <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 = true;
if (!shouldApplyWithoutInspection) {
// TODO: Gaze into the crystal ball to determine the nefariousness level of the packet and update ShouldApply accordingly.
}
if (ShouldApply) {
Vector3 Delta = transform.position - syncPckt.Location;
if (Delta.magnitude < LerpedDesyncDistance) {
// 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;
}
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 next = NextAllowedShot;
if (Net.Singleton.Instance is Server) {
float ping = ((Server)Net.Singleton.Instance).Players[NetId].Ping;
next -= ping;
}
if (Time.time < next) {
return;
}
NextAllowedShot = Time.time + CurrentItem.Cooldown;
CurrentItem.Shoot(this);
}
public void Hit(ulong sourceUid) {
Terminal.Singleton.Println($"{NetId} hit sourceIid: {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) {
Terminal.Singleton.Println($"{killer} killed me: {NetId}, deadness currently: {IsDead}");
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;
}
SoldierProceduralAnimator.StartRagdoll();
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) {
Terminal.Singleton.Println($"Respawned (I am {NetId}), deadness before proper resurrection: {IsDead}");
if (Net.Singleton.Instance != null && Net.Singleton.Instance.LocalPlayer.Id == NetId && IsDead) {
GameObject.FindGameObjectWithTag("DeadScreen").GetComponent<DeadScreen>().Open = false;
Net.Singleton.Instance.LocalPlayer.Controlled.GetComponent<LocalPlayer>().DisableInput -= 1;
}
transform.position = location;
SoldierProceduralAnimator.StopRagdoll();
foreach (GameObject obj in DisabledOnDeath) {
obj.SetActive(true);
}
IsDead = false;
}
public bool IsGrounded() {
return CharacterController.isGrounded && Vector3.Dot(GravitationalVelocity, Vector3.down) >= 0;
}
public void SwitchItem(int index) {
if (CurrentItem != null) {
CurrentItem.SwitchedOut();
}
GameObject obj = GameObject.Instantiate(PossibleItems[index]);
Vector3 localPos = obj.transform.localPosition;
Quaternion localRot = obj.transform.localRotation;
obj.transform.parent = ItemRoot.transform;
obj.transform.localPosition = localPos;
obj.transform.localRotation = localRot;
CurrentItem = obj.GetComponent<Item>();
CurrentItem.Holder = this;
CurrentItem.Switched();
SoldierProceduralAnimator.OnItemSwitched();
}
/// <summary>The normal of the ground below the player. If there is no ground, it's <c>Vector3.up</c> by default.</summary>
private Vector3 GroundCast() {
RaycastHit hit;
Vector3 feetPosition = FeetPosition + transform.position + CharacterController.center + Vector3.down * CharacterController.height / 2;
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;
}
}
private void Start() {
SwitchItem(CurrentItemIdx);
}
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 - CameraRoot.parent.eulerAngles.x, Yaw - CameraRoot.parent.eulerAngles.y, Lean);
}
private void LateUpdate() {
UpdateMovement();
}
private void UpdateMovement() {
bool Grounded = IsGrounded();
bool FallingDown = Vector3.Dot(Vector3.down, GravitationalVelocity) > 0;
if (Grounded && FallingDown) {
GravitationalVelocity = GroundCastLength * Vector3.down;
} else if (!Grounded) {
GravitationalVelocity += Physics.gravity * Time.deltaTime;
}
Vector3 GroundNormal = GroundCast();
float FrictionVelocityFactor = Mathf.Max(GroundVelocity.magnitude, MoveStyle.StopVelocity);
float Deccel = FrictionVelocityFactor * Time.deltaTime;
if (Grounded || GroundNormal != Vector3.up) {
Deccel *= MoveStyle.Friction;
} else {
Deccel *= MoveStyle.AirFriction;
}
float FrictionedVelocity = Mathf.Max(0, GroundVelocity.magnitude - Deccel);
GroundVelocity = GroundVelocity.normalized * FrictionedVelocity;
Vector3 FixedHeading = Vector3.ProjectOnPlane(MoveDirection, GroundNormal).normalized;
float CurrentSpeed = Vector3.Dot(GroundVelocity, FixedHeading);
float Acceleration = MoveStyle.TargetVelocity * Time.deltaTime;
if (Grounded || GroundNormal != Vector3.up) {
Acceleration *= MoveStyle.Acceleration;
} else {
Acceleration *= MoveStyle.AirAcceleration;
}
Acceleration = Mathf.Min(Acceleration, MoveStyle.TargetVelocity - CurrentSpeed);
GroundVelocity += FixedHeading * Acceleration;
CharacterController.Move((GravitationalVelocity + GroundVelocity) * 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, reset falling velocity downwards (same as "static gravity" when grounded)
GravitationalVelocity.y = GroundCastLength;
}
if (ShowMoveVector) {
Debug.DrawLine(transform.position + CharacterController.center, transform.position + CharacterController.center + GravitationalVelocity, Color.yellow, 1.0f);
Debug.DrawLine(transform.position + CharacterController.center, transform.position + CharacterController.center + GroundVelocity, Color.green, 1.0f);
}
float TargetBobbiness = Grounded ? GroundVelocity.magnitude / MoveStyle.TargetVelocity : 0;
CurrentItem.Animator.SetLayerWeight(1, Mathf.Lerp(CurrentItem.Animator.GetLayerWeight(1), TargetBobbiness, 10f * Time.deltaTime));
}
}
}