using UnityEngine; using Unity.Netcode; /// /// First-person mode. The player keeps walking (WASD, camera-relative) but the camera moves to the /// player's eye (the CameraTarget) with free mouse-look, and the player's mesh is hidden so the body /// isn't seen from the inside. The orbit camera input is disabled while here so it doesn't fight for /// the cursor; the drives its own dedicated CinemachineCamera. /// public class FirstPersonMode : GameMode { public override string DisplayName => "First Person"; public override void Enter() { // Player stays fully controllable; only the view changes. if (Manager.Player) { Manager.Player.enabled = true; Manager.Player.ControlEnabled = true; } if (Manager.Painter) Manager.Painter.PaintingEnabled = false; // The orbit rig would otherwise keep grabbing the cursor and reading the mouse every frame. if (Manager.CameraInput) Manager.CameraInput.enabled = false; if (Manager.FirstPerson) Manager.FirstPerson.SetActive(true); } public override void Exit() { // Modes are Enter-only, so first-person must undo its own camera takeover (and unhide the mesh) // here; the next mode's Enter restores the player/painter/look-mode state. if (Manager.FirstPerson) Manager.FirstPerson.SetActive(false); if (Manager.CameraInput) { Manager.CameraInput.enabled = true; // Carry the first-person facing into the orbit camera so the next orbit mode (Paint / Player // Control) keeps the current view angle instead of snapping back to the orbit's last-used angle. // Camera.main still shows the first-person view this frame (the brain hasn't blended yet). Manager.CameraInput.SyncToView(); } } // --- Hunter shooting ------------------------------------------------------------------------------- // A hunter in first person shoots a single ray from the camera centre on left-click (with a cooldown), // but only during the SEEK sub-phase. A hit on an un-found hider is reported to the server, which // validates and marks the hider found. Gated on the local avatar actually being a hunter role variant, // so the legacy lobby first-person view never shoots. private float _nextShootTime; private static readonly RaycastHit[] _hits = new RaycastHit[16]; public override void Tick() { if (!CanShoot()) return; if (Time.time < _nextShootTime) return; if (!InputUtil.LeftClickedThisFrame()) return; _nextShootTime = Time.time + Mathf.Max(0f, MatchManager.Instance.ShootCooldown); TryShoot(); } private bool CanShoot() { var match = MatchManager.Instance; if (!match || !match.MatchActive || match.LocalSubPhase != MatchManager.MatchSubPhase.Seek) return false; if (!Manager.Player || !Manager.FirstPerson) return false; var setup = Manager.Player.GetComponent(); return setup && setup.ScopedModes && setup.Role == PlayerRole.Hunter; } private void TryShoot() { var match = MatchManager.Instance; Transform cam = Manager.FirstPerson.transform; // FP vcam sits at the eye, facing the look direction float range = Mathf.Max(0f, match.ShootRange); int count = Physics.RaycastNonAlloc(cam.position, cam.forward, _hits, range, ~0, QueryTriggerInteraction.Ignore); if (count == 0) return; // Nearest-first so the shot stops at the first solid thing it meets (a wall blocks; a nearer avatar // shields a farther one). The hunter's own body is skipped so looking down doesn't self-block. System.Array.Sort(_hits, 0, count, RaycastDistanceComparer.Instance); NetworkObject self = Manager.Player ? Manager.Player.GetComponent() : null; for (int i = 0; i < count; i++) { var netObj = _hits[i].collider.GetComponentInParent(); if (netObj && self && netObj == self) continue; // ignore our own collider if (!netObj || !netObj.IsPlayerObject) return; // first solid hit is world geometry -> blocked var setup = netObj.GetComponent(); if (setup && setup.Role == PlayerRole.Hider && !setup.IsFound) match.ReportHit(netObj.OwnerClientId); return; // the first non-self hit decides the shot } } private sealed class RaycastDistanceComparer : System.Collections.Generic.IComparer { public static readonly RaycastDistanceComparer Instance = new RaycastDistanceComparer(); public int Compare(RaycastHit a, RaycastHit b) => a.distance.CompareTo(b.distance); } }