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; namespace NeonTea.Quakeball.Players { /// 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 { /// The duration after running off a cliff, during which the player should still be considered grounded. public float CoyoteTime; public float PingBias; public MoveStyle[] MoveStyles; public Transform Gun; public Animator GunAnimator; public SoldierProceduralAnimator SoldierProceduralAnimator; [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 LayerMask BulletPassLayer; 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")] /// The pitch of the player's head. public float Pitch; /// The total yaw of the player. Head yaw is Yaw - BodyYaw. public float Yaw; [Header("Player movement status")] /// The direction the player is going. public Vector3 MoveDirection; /// 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 byte CurrentMoveStyle = 0; public MoveStyle MoveStyle => MoveStyles[CurrentMoveStyle]; [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; /// The possible networked Id of this Player instance public ulong NetId; /// Is the player dead 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; /// Creates a PlayerUpdatePckt representing this Player's current status, for sending to other peers. public PlayerUpdatePckt CreateUpdatePacket(ulong id = 0) { return new PlayerUpdatePckt(MoveDirection, CurrentMoveStyle, Pitch, Yaw, id); } /// Updates this Player with the given packet. public void ProcessUpdatePacket(PlayerUpdatePckt packet) { if (!IsDead) { Pitch = packet.Pitch; Yaw = packet.Yaw; MoveDirection = packet.MoveDirection; CurrentMoveStyle = packet.MoveStyle; } } /// Creates a PlayerSyncPacket representing this Player's position and velocity, for sending to the server. public PlayerSyncPacket CreateSyncPacket(ulong id = 0, bool unsynced = false) { return new PlayerSyncPacket(id, unsynced, transform.position, GroundVelocity); } /// Applies the sync packet, checking it for cheatiness if shouldApply is false. 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; } 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 | BulletPassLayer); 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() : null; if (Player == this) { continue; } if (((1 << Hit.collider.gameObject.layer) & BulletPassLayer) != 0) { ImpactSound ImpactSound = Hit.collider.GetComponent(); if (ImpactSound != null) { ImpactSound.PlayAt(Hit.point); } continue; } if (Player != null) { if (Net.Singleton.Instance is Server) { ((Server)Net.Singleton.Instance).SendHit(NetId, Player.NetId); Player.Hit(NetId); } } break; } GunAnimator.SetBool("Shot", true); GameObject LaserEffect = Instantiate(LaserPrefab); Laser Laser = LaserEffect.GetComponent(); Laser.From = GunPoint; Laser.To = GunPoint + ShotDelta; GunShotAudioSource.PlayOneShot(RaygunClip); } 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(); } } /// Called when this Player is dead 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().StartCountdown(name); Net.Singleton.Instance.LocalPlayer.Controlled.GetComponent().DisableInput += 1; } SoldierProceduralAnimator.StartRagdoll(); TimeofDeath = Time.time; MoveDirection = Vector3.zero; foreach (GameObject obj in DisabledOnDeath) { obj.SetActive(false); } IsDead = true; } /// Called when this Player is respawned, after dying. 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().Open = false; Net.Singleton.Instance.LocalPlayer.Controlled.GetComponent().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; } /// The normal of the ground below the player. If there is no ground, it's Vector3.up by default. 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 Awake() { CharacterController = GetComponent(); FeetPosition = transform.position + CharacterController.center - Vector3.up * (CharacterController.height / 2 + CharacterController.skinWidth / 2); if (GameObject.FindObjectOfType() == null) { Debug.LogWarning("Player.Awake: There is no PhysicsSyncer in this scene! Some code will not work as expected."); } LastShot = Time.time - Cooldown; } 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() { GunAnimator.SetBool("Shot", false); 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; GunAnimator.SetLayerWeight(1, Mathf.Lerp(GunAnimator.GetLayerWeight(1), TargetBobbiness, 10f * Time.deltaTime)); } } }