Radar-Sichtbarkeitsgates, Hider-Pings im Seek + Ping-Countdown
Radar: nur sichtbar bei >=1 Target, in First-Person und in der Seek-Phase (Overlay-Kamera getoggelt, Blip-Skripte laufen weiter). Singleton fuer Server-Zugriff. GameModeManager: Kamera + Tab-Moduswechsel erst ab gebundenem Lobby-Player (Inert-Zustand ohne Player, Orbit-Input geparkt). MatchManager: Server tickt im Seek alle radarPingInterval (30s) und broadcastet noch nicht gefundene Hider (NetworkObjectId + radarPingLifetime 3s) nur an Hunter-Clients; jeder Hunter loest lokal auf und spawnt Blips. Ping-Loop in die Seek-Coroutine gefaltet (EndMatch stoppt ihn mit). MatchHud: Ping-Countdown lokal aus Seek-Start + Kadenz berechnet, Format m:ss (0:30..0:01), ausserhalb Seek / bei deaktivierten Pings 0:00. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
23b51d07a4
commit
de82ce25de
4 files changed
+240
-12
No files matched your search
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
@@ -5,24 +6,53 @@ using UnityEngine;
|
||||
/// Spawns "Target" blips - clones of the child "Target Template" - each of which continuously points from
|
||||
/// the reference camera toward a world-space transform (visualising the bearing camera -> target) and then
|
||||
/// destroys itself after a given lifetime. See <see cref="RadarTarget"/> for the per-blip behaviour.
|
||||
///
|
||||
/// The whole radar overlay is only shown while it is useful: at least one live target, first-person is active,
|
||||
/// AND the match is in its Seek phase. It is hidden by switching the overlay camera's rendering off (not by
|
||||
/// deactivating the GameObjects), so hidden blips keep counting down their lifetime and self-destruct on schedule.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class Radar : MonoBehaviour
|
||||
{
|
||||
/// <summary>The scene's single radar overlay (lives in the persistent Main scene). Hunter clients spawn
|
||||
/// hider blips onto it from MatchManager's radar-ping sweep.</summary>
|
||||
public static Radar Instance { get; private set; }
|
||||
|
||||
[Tooltip("Template each spawned Target is cloned from. Auto-resolves to the child \"Target Template\".")]
|
||||
[SerializeField] private Transform targetTemplate;
|
||||
|
||||
[Tooltip("Camera the direction is measured FROM (camera -> target). Auto-resolves to the Camera this radar sits under.")]
|
||||
[SerializeField] private Camera referenceCamera;
|
||||
|
||||
[Tooltip("Overlay camera that renders the radar. Its rendering is toggled off unless the radar should show " +
|
||||
"(at least one live target AND first-person active). Auto-resolves to the Camera this radar sits under.")]
|
||||
[SerializeField] private Camera radarCamera;
|
||||
|
||||
[Tooltip("First-person rig. The radar is only shown while first-person is active. Empty = found automatically.")]
|
||||
[SerializeField] private FirstPersonController firstPerson;
|
||||
|
||||
// Live blips, pruned as they self-destruct; the radar shows only while at least one is alive.
|
||||
private readonly List<RadarTarget> _blips = new List<RadarTarget>();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
|
||||
if (!targetTemplate)
|
||||
{
|
||||
var t = transform.Find("Target Template");
|
||||
if (t) targetTemplate = t;
|
||||
}
|
||||
if (!referenceCamera) referenceCamera = GetComponentInParent<Camera>();
|
||||
if (!radarCamera) radarCamera = GetComponentInParent<Camera>();
|
||||
if (!firstPerson) firstPerson = FindFirstObjectByType<FirstPersonController>(FindObjectsInactive.Include);
|
||||
|
||||
ApplyVisibility(false); // hidden until a target exists, first-person is live AND the match is seeking
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -51,6 +81,35 @@ public sealed class Radar : MonoBehaviour
|
||||
|
||||
var blip = clone.GetComponent<RadarTarget>();
|
||||
blip.Init(target, referenceCamera, lifetime);
|
||||
_blips.Add(blip);
|
||||
return blip;
|
||||
}
|
||||
|
||||
// LateUpdate so the count reflects any blip that self-destructed this frame, in step with RadarTarget's aim.
|
||||
private void LateUpdate()
|
||||
{
|
||||
// Drop self-destructed blips (Destroy leaves a fake-null entry behind).
|
||||
for (int i = _blips.Count - 1; i >= 0; i--)
|
||||
if (!_blips[i]) _blips.RemoveAt(i);
|
||||
|
||||
bool visible = _blips.Count > 0 && firstPerson && firstPerson.enabled && InSeekPhase();
|
||||
ApplyVisibility(visible);
|
||||
}
|
||||
|
||||
// The radar only makes sense during the seek phase (hunters chasing hiders). Blips only spawn then anyway,
|
||||
// but a blip's lifetime can outlast the phase edge, so gating here hides the overlay the instant Seek ends.
|
||||
private static bool InSeekPhase()
|
||||
{
|
||||
var m = MatchManager.Instance;
|
||||
return m && m.MatchActive && m.LocalSubPhase == MatchManager.MatchSubPhase.Seek;
|
||||
}
|
||||
|
||||
// Shows/hides the whole radar overlay in one toggle (disc + blips) by switching the overlay camera's
|
||||
// rendering. The GameObjects stay active, so blips keep ticking down their lifetime while hidden. The
|
||||
// overlay renders only the Radar layer, so nothing else is affected.
|
||||
private void ApplyVisibility(bool visible)
|
||||
{
|
||||
if (radarCamera && radarCamera.enabled != visible)
|
||||
radarCamera.enabled = visible;
|
||||
}
|
||||
}
|
||||
@@ -80,8 +80,14 @@ public class GameModeManager : MonoBehaviour
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (_modes.Count > 0)
|
||||
// Go live only when this scene already holds a player to control (a scene-placed avatar). In the
|
||||
// networked Main scene the avatar spawns on connect, so the manager stays inert - no camera control,
|
||||
// no mode switching - until BindLocalPlayer promotes it once the lobby avatar exists. Without this,
|
||||
// the pre-spawn menu already lets you orbit the camera and Tab-cycle modes with no player.
|
||||
if (player && _modes.Count > 0)
|
||||
SwitchTo(Mathf.Clamp(startModeIndex, 0, _modes.Count - 1));
|
||||
else
|
||||
EnterNoPlayerState();
|
||||
}
|
||||
|
||||
/// <summary>Registers the modes for the current context. Without a role (no networked avatar yet,
|
||||
@@ -228,22 +234,35 @@ public class GameModeManager : MonoBehaviour
|
||||
/// </summary>
|
||||
public void ReleaseToNoPlayer()
|
||||
{
|
||||
_paused = false;
|
||||
_resumeIndex = -1;
|
||||
|
||||
TearDownActiveMode();
|
||||
_modes.Clear();
|
||||
ClearPlayerBindings();
|
||||
EnterNoPlayerState();
|
||||
}
|
||||
|
||||
// The cycle is empty (no SwitchTo runs), so notify mode-following UI explicitly - otherwise the
|
||||
// paint HUD (hides only via ModeChanged) would stay open after a shutdown during PaintMode.
|
||||
ModeChanged?.Invoke(null);
|
||||
/// <summary>
|
||||
/// Puts the manager in the inert "no local player" state: no active mode, the persistent orbit input
|
||||
/// parked in Disabled (it otherwise re-locks the cursor every frame while AlwaysActive, so a one-off
|
||||
/// unlock would be reverted next frame) and the cursor freed. Entered at startup in a networked scene
|
||||
/// before the lobby avatar spawns, and again after it is gone (see <see cref="ReleaseToNoPlayer"/>).
|
||||
/// <see cref="BindLocalPlayer"/> / <see cref="BindSpectator"/> promote out of it. With no active mode
|
||||
/// (_activeIndex < 0) Update ignores the switch key, so Tab cannot bootstrap a mode from here.
|
||||
/// </summary>
|
||||
private void EnterNoPlayerState()
|
||||
{
|
||||
_paused = false;
|
||||
_resumeIndex = -1;
|
||||
_activeIndex = -1;
|
||||
|
||||
if (cameraInput)
|
||||
cameraInput.LookMode = ThirdPersonOrbitInput.OrbitLookMode.Disabled;
|
||||
|
||||
// Belt-and-braces explicit release (covers the orbit input being absent/disabled).
|
||||
InputUtil.SetCursorLocked(false);
|
||||
|
||||
// The cycle is inert (no SwitchTo runs), so notify mode-following UI explicitly - otherwise the
|
||||
// paint HUD (hides only via ModeChanged) would stay open after a shutdown during PaintMode.
|
||||
ModeChanged?.Invoke(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -299,6 +318,9 @@ public class GameModeManager : MonoBehaviour
|
||||
{
|
||||
if (_paused) return;
|
||||
if (_modes.Count == 0) return;
|
||||
// No active mode = the inert no-player state (pre-lobby, or after ReleaseToNoPlayer). Ignore the
|
||||
// switch key and Tick so Tab can't bootstrap a mode and no camera rig is driven until a bind.
|
||||
if (_activeIndex < 0) return;
|
||||
|
||||
// Resolution (the reveal window) is watch-only: force out of Paint and keep the switch key from
|
||||
// re-entering. No restore needed - the end-of-match respawn rebuilds the cycle at index 0.
|
||||
|
||||
@@ -128,6 +128,18 @@ public class MatchManager : MonoBehaviour
|
||||
[Range(0f, 100f)]
|
||||
[SerializeField] private float embeddedRevealPercent = 80f;
|
||||
|
||||
[Header("Radar pings")]
|
||||
[Tooltip("Seconds between radar sweeps during Seek. Every interval each still-un-found hider is registered " +
|
||||
"as a blip on every HUNTER's radar (hiders and spectators never receive them). Fires only in the " +
|
||||
"Seek phase. 0 disables radar pings.")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float radarPingInterval = 30f;
|
||||
|
||||
[Tooltip("How long (seconds) each hider radar blip stays alive before it fades. Kept short relative to " +
|
||||
"radarPingInterval so hunters get a periodic sweep, not a constant track.")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float radarPingLifetime = 3f;
|
||||
|
||||
/// <summary>Range of the hunter's shoot raycast (read by <see cref="FirstPersonMode"/>).</summary>
|
||||
public float ShootRange => shootRange;
|
||||
/// <summary>Hunter shoot cooldown in seconds (read by <see cref="FirstPersonMode"/>).</summary>
|
||||
@@ -145,6 +157,7 @@ public class MatchManager : MonoBehaviour
|
||||
// The countdown is cosmetic/local; the authoritative transition is server-driven, so drift self-corrects.
|
||||
private MatchSubPhase _subPhase = MatchSubPhase.None;
|
||||
private float _phaseEndTime; // Time.time when the current sub-phase ends (for the HUD countdown)
|
||||
private float _seekStartTime; // Time.time Seek began locally (anchors the ping-countdown cadence, START-aligned)
|
||||
private MatchMode _localMode = MatchMode.Classic; // mirror of the server's matchMode, carried in the phase message
|
||||
|
||||
private const string PreMatchTag = "PreMatch"; // scene props destroyed when seek starts
|
||||
@@ -158,6 +171,25 @@ public class MatchManager : MonoBehaviour
|
||||
/// <summary>Seconds left in the current sub-phase for the HUD (clamped to 0; cosmetic).</summary>
|
||||
public float LocalPhaseRemaining => Mathf.Max(0f, _phaseEndTime - Time.time);
|
||||
|
||||
/// <summary>Seconds between radar ping sweeps (0 = pings disabled). Baked identically on every client
|
||||
/// (persistent MatchManager, no replication - like <see cref="EmbeddedRevealPercent"/>), so the HUD can
|
||||
/// drive the ping countdown locally.</summary>
|
||||
public float RadarPingInterval => radarPingInterval;
|
||||
|
||||
/// <summary>Seconds until the next radar ping sweep for the HUD (counts down from <see cref="RadarPingInterval"/>
|
||||
/// and repeats). 0 outside Seek or when pings are disabled. Computed locally from the local Seek-start +
|
||||
/// fixed interval - cosmetic, like <see cref="LocalPhaseRemaining"/> (a mid-match joiner's is offset by the
|
||||
/// time it missed; the server's sweep timer is START-aligned the same way).</summary>
|
||||
public float LocalNextPingIn
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_subPhase != MatchSubPhase.Seek || radarPingInterval <= 0f) return 0f;
|
||||
float intoInterval = (Time.time - _seekStartTime) % radarPingInterval;
|
||||
return radarPingInterval - intoInterval;
|
||||
}
|
||||
}
|
||||
|
||||
private NetworkManager _net;
|
||||
private GameObject _playerPrefab; // captured from NetworkConfig.PlayerPrefab; auto-spawn is then disabled so we spawn manually into a loaded scene
|
||||
private bool _hooked;
|
||||
@@ -193,6 +225,9 @@ public class MatchManager : MonoBehaviour
|
||||
// app restarts). Lets the server map the ephemeral NGO clientId to a stable identity - the reconnect
|
||||
// foundation. See PersistentPlayerId.
|
||||
private const string MsgIdentity = "Match_Identity";
|
||||
// Server -> HUNTER clients only, on each Seek radar sweep: the still-un-found hiders' NetworkObjectIds plus
|
||||
// the blip lifetime. Each hunter resolves the ids locally and drops a Radar blip pointing at the hider.
|
||||
private const string MsgRadarPing = "Match_RadarPing";
|
||||
|
||||
// Server-only sub-phase machine. _pendingHiders holds the hiders not yet found; seek ends the instant it
|
||||
// empties. _phaseRoutine is the running Hide->Seek->Resolution coroutine (stopped if the match is ended
|
||||
@@ -206,6 +241,8 @@ public class MatchManager : MonoBehaviour
|
||||
private readonly Dictionary<ulong, string> _persistentIds = new Dictionary<ulong, string>();
|
||||
private Coroutine _phaseRoutine;
|
||||
private readonly List<ulong> _phaseRecipients = new List<ulong>(); // reused scratch for the phase broadcast
|
||||
private readonly List<ulong> _radarPingIds = new List<ulong>(); // reused scratch: hider NetworkObjectIds for a sweep
|
||||
private readonly List<ulong> _radarRecipients = new List<ulong>(); // reused scratch: hunter client recipients for a sweep
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -258,6 +295,7 @@ public class MatchManager : MonoBehaviour
|
||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgAuthorityGranted);
|
||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgRequestEndMatch);
|
||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgPhase);
|
||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgRadarPing);
|
||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgReportHit);
|
||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgIdentity);
|
||||
}
|
||||
@@ -311,6 +349,7 @@ public class MatchManager : MonoBehaviour
|
||||
{
|
||||
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgAuthorityGranted, OnAuthorityGrantedMessage);
|
||||
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgPhase, OnPhaseMessage);
|
||||
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgRadarPing, OnRadarPingMessage);
|
||||
}
|
||||
|
||||
// Report our persistent id to the server the moment our own connection completes (host records its own
|
||||
@@ -546,7 +585,7 @@ public class MatchManager : MonoBehaviour
|
||||
yield return WaitPhase(hideDuration, null);
|
||||
|
||||
SetPhaseServer(MatchSubPhase.Seek, seekDuration);
|
||||
yield return WaitPhase(seekDuration, () => _pendingHiders.Count == 0);
|
||||
yield return RunSeekPhase(seekDuration);
|
||||
|
||||
SetPhaseServer(MatchSubPhase.Resolution, resolveDuration);
|
||||
yield return WaitPhase(resolveDuration, null);
|
||||
@@ -555,6 +594,32 @@ public class MatchManager : MonoBehaviour
|
||||
EndMatch();
|
||||
}
|
||||
|
||||
// Server-only. The seek wait with the radar-ping sweep folded in, so pinging lives inside _phaseRoutine
|
||||
// (an early Back-to-Lobby stops that routine and so stops pinging too). Ends the instant the last hider is
|
||||
// found - identical early-out to the plain WaitPhase(_pendingHiders.Count == 0) it replaces for Seek.
|
||||
private IEnumerator RunSeekPhase(float seconds)
|
||||
{
|
||||
float t = seconds;
|
||||
float pingTimer = radarPingInterval; // first sweep one interval into Seek
|
||||
while (t > 0f)
|
||||
{
|
||||
if (_pendingHiders.Count == 0) yield break;
|
||||
|
||||
if (radarPingInterval > 0f)
|
||||
{
|
||||
pingTimer -= Time.deltaTime;
|
||||
if (pingTimer <= 0f)
|
||||
{
|
||||
BroadcastRadarPings();
|
||||
pingTimer = radarPingInterval;
|
||||
}
|
||||
}
|
||||
|
||||
t -= Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Waits up to 'seconds' of server real time, returning early the moment 'until' (if supplied) is true.
|
||||
private IEnumerator WaitPhase(float seconds, System.Func<bool> until)
|
||||
{
|
||||
@@ -582,8 +647,13 @@ public class MatchManager : MonoBehaviour
|
||||
_subPhase = phase;
|
||||
_phaseEndTime = Time.time + duration;
|
||||
_localMode = mode;
|
||||
// Props staged only for the hide phase are gone the instant seek begins.
|
||||
if (changed && phase == MatchSubPhase.Seek) DestroyPreMatchObjects();
|
||||
// Props staged only for the hide phase are gone the instant seek begins; anchor the local ping-countdown
|
||||
// cadence to the same moment (START-aligned, matching the server's sweep timer).
|
||||
if (changed && phase == MatchSubPhase.Seek)
|
||||
{
|
||||
_seekStartTime = Time.time;
|
||||
DestroyPreMatchObjects();
|
||||
}
|
||||
}
|
||||
|
||||
// Runs on every machine (server directly, clients via the phase message); the tagged props are plain
|
||||
@@ -630,6 +700,69 @@ public class MatchManager : MonoBehaviour
|
||||
ApplyPhaseLocal((MatchSubPhase)phase, duration, (MatchMode)mode);
|
||||
}
|
||||
|
||||
// Server-only. One radar sweep during Seek: registers every still-un-found hider as a blip on each HUNTER's
|
||||
// radar. Sends the hiders' NetworkObjectIds + blip lifetime to the hunter clients; a host-hunter applies
|
||||
// locally (no loopback message, mirroring SetPhaseServer). Hiders and spectators never receive a ping.
|
||||
private void BroadcastRadarPings()
|
||||
{
|
||||
if (_net == null || !_net.IsServer || _pendingHiders.Count == 0) return;
|
||||
|
||||
_radarPingIds.Clear();
|
||||
foreach (ulong hiderClientId in _pendingHiders)
|
||||
if (_net.ConnectedClients.TryGetValue(hiderClientId, out var c) && c.PlayerObject)
|
||||
_radarPingIds.Add(c.PlayerObject.NetworkObjectId);
|
||||
if (_radarPingIds.Count == 0) return;
|
||||
|
||||
// Host-as-hunter: spawn on its own radar directly (it is excluded from the recipient list below).
|
||||
if (IsHunter(_net.LocalClientId)) SpawnRadarBlipsLocal();
|
||||
|
||||
_radarRecipients.Clear();
|
||||
foreach (ulong clientId in _net.ConnectedClientsIds)
|
||||
if (clientId != _net.LocalClientId && IsHunter(clientId)) _radarRecipients.Add(clientId);
|
||||
if (_radarRecipients.Count == 0) return;
|
||||
|
||||
var cmm = _net.CustomMessagingManager;
|
||||
if (cmm == null) return;
|
||||
int size = sizeof(float) + sizeof(int) + _radarPingIds.Count * sizeof(ulong);
|
||||
using (var writer = new FastBufferWriter(size, Allocator.Temp))
|
||||
{
|
||||
writer.WriteValueSafe(radarPingLifetime);
|
||||
writer.WriteValueSafe(_radarPingIds.Count);
|
||||
for (int i = 0; i < _radarPingIds.Count; i++)
|
||||
writer.WriteValueSafe(_radarPingIds[i]);
|
||||
cmm.SendNamedMessage(MsgRadarPing, _radarRecipients, writer);
|
||||
}
|
||||
}
|
||||
|
||||
// Host/server local path: resolve the swept hider NetworkObjectIds (in _radarPingIds) to their live
|
||||
// transforms and drop a blip on the local radar. Mirrors OnRadarPingMessage for the host's own hunter view.
|
||||
private void SpawnRadarBlipsLocal()
|
||||
{
|
||||
var radar = Radar.Instance;
|
||||
var spawnMgr = _net != null ? _net.SpawnManager : null;
|
||||
if (!radar || spawnMgr == null) return;
|
||||
for (int i = 0; i < _radarPingIds.Count; i++)
|
||||
if (spawnMgr.SpawnedObjects.TryGetValue(_radarPingIds[i], out var no) && no)
|
||||
radar.Spawn(no.transform, radarPingLifetime);
|
||||
}
|
||||
|
||||
// Hunter-client handler: a radar sweep arrived. Resolve each hider NetworkObjectId to its live transform and
|
||||
// drop a blip on the local radar. Only hunter clients are sent this message (filtered server-side).
|
||||
private void OnRadarPingMessage(ulong senderClientId, FastBufferReader reader)
|
||||
{
|
||||
reader.ReadValueSafe(out float lifetime);
|
||||
reader.ReadValueSafe(out int count);
|
||||
var radar = Radar.Instance;
|
||||
var spawnMgr = _net != null ? _net.SpawnManager : null;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
reader.ReadValueSafe(out ulong netObjId);
|
||||
if (radar && spawnMgr != null &&
|
||||
spawnMgr.SpawnedObjects.TryGetValue(netObjId, out var no) && no)
|
||||
radar.Spawn(no.transform, lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Client-side: the local hunter's shoot raycast hit the hider owned by <paramref name="targetClientId"/>.
|
||||
/// Routes to the server, which validates phase/role/found-state. On a host this IS the server, so it
|
||||
/// applies directly. No-op outside the seek phase.</summary>
|
||||
|
||||
@@ -24,6 +24,10 @@ public class MatchHud : MonoBehaviour
|
||||
[Tooltip("Countdown label in the top bar.")]
|
||||
[SerializeField] private TMP_Text _timeText;
|
||||
|
||||
[Tooltip("Seconds until the next radar ping sweep; counts down from the interval and repeats. Shown only " +
|
||||
"during the seek phase (blank otherwise).")]
|
||||
[SerializeField] private TMP_Text _pingTimeText;
|
||||
|
||||
[SerializeField] private TMP_Text _hunterCount;
|
||||
[SerializeField] private TMP_Text _hiderCount;
|
||||
|
||||
@@ -31,6 +35,7 @@ public class MatchHud : MonoBehaviour
|
||||
[SerializeField] private GameObject _crosshair;
|
||||
|
||||
private int _lastSecond = -1;
|
||||
private int _lastPingSecond = -1;
|
||||
private int _lastHunters = -1;
|
||||
private int _lastHiders = -1;
|
||||
private MatchManager.MatchSubPhase _lastPhase = (MatchManager.MatchSubPhase)(-1);
|
||||
@@ -38,9 +43,9 @@ public class MatchHud : MonoBehaviour
|
||||
private void Awake()
|
||||
{
|
||||
if (!_canvas) _canvas = GetComponent<Canvas>();
|
||||
if (!_canvas || !_phaseText || !_timeText || !_crosshair || !_hunterCount || !_hiderCount)
|
||||
if (!_canvas || !_phaseText || !_timeText || !_pingTimeText || !_crosshair || !_hunterCount || !_hiderCount)
|
||||
{
|
||||
Debug.LogError("[MatchHud] Unwired reference(s) - assign Canvas/Phase/Time/Crosshair/HunterCount/HiderCount in the Inspector.", this);
|
||||
Debug.LogError("[MatchHud] Unwired reference(s) - assign Canvas/Phase/Time/PingTime/Crosshair/HunterCount/HiderCount in the Inspector.", this);
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
@@ -70,6 +75,15 @@ public class MatchHud : MonoBehaviour
|
||||
_timeText.text = $"{s / 60:0}:{s % 60:00}";
|
||||
}
|
||||
|
||||
// Next-radar-sweep countdown: shown for everyone during Seek (pings only fire then), 0:00 otherwise.
|
||||
bool showPing = phase == MatchManager.MatchSubPhase.Seek && match.RadarPingInterval > 0f;
|
||||
int p = showPing ? Mathf.CeilToInt(Mathf.Max(0f, match.LocalNextPingIn)) : 0;
|
||||
if (p != _lastPingSecond)
|
||||
{
|
||||
_lastPingSecond = p;
|
||||
_pingTimeText.text = $"{p / 60:0}:{p % 60:00}";
|
||||
}
|
||||
|
||||
NetworkPlayerSetup.CountRoles(out int hunters, out int hiders);
|
||||
if (hunters != _lastHunters)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user