Add manual radar ping, blip refresh/fade, and resolution statue barrier
- Manual hider ping on "E": server-authoritative RequestManualPing gated to Seek + living hider; shared EmitRadarPings path so interval and manual pings produce identical blips. Ping key read client-side in GameModeManager, role- gated and excluded in FreeCam (E is fly-up there). - Radar refreshes an existing blip for the same transform target instead of duplicating it (lifetime resets to the configured value) via RadarTarget.Refresh. - Blips fade in/out independently of the disc fade: per-blip MoveTowards factor multiplied on top of the radar fade, refresh-safe, with separate in/out speeds. - Resolution countdown waits for conversion statues to load: generalized the match-start loading barrier (RunReadinessBarrier over _barrierTargets, hold phase carried in MsgBeginLoad) and run it a second time over statue IDs before starting the real resolveDuration. Only holds for Conversion with real statues. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
8f31c07bef
commit
bfcd84afe4
4 files changed
+199
-64
No files matched your search
@@ -117,6 +117,18 @@ public sealed class Radar : MonoBehaviour
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Already tracking this exact transform? Refresh its countdown instead of stacking a duplicate blip,
|
||||||
|
// so a fresh ping (interval or manual) just resets the existing target's lifetime.
|
||||||
|
for (int i = 0; i < _blips.Count; i++)
|
||||||
|
{
|
||||||
|
var existing = _blips[i];
|
||||||
|
if (existing && existing.Target == target)
|
||||||
|
{
|
||||||
|
existing.Refresh(lifetime);
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Transform clone = Instantiate(targetTemplate, targetTemplate.parent);
|
Transform clone = Instantiate(targetTemplate, targetTemplate.parent);
|
||||||
clone.name = "Target";
|
clone.name = "Target";
|
||||||
clone.localPosition = targetTemplate.localPosition;
|
clone.localPosition = targetTemplate.localPosition;
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ using UnityEngine;
|
|||||||
/// once its lifetime elapses - or earlier if its target is destroyed. The point's base colour and size encode
|
/// once its lifetime elapses - or earlier if its target is destroyed. The point's base colour and size encode
|
||||||
/// the two things the flat radar disc can't show via position alone: <see cref="forwardGradient"/> tints it by
|
/// the two things the flat radar disc can't show via position alone: <see cref="forwardGradient"/> tints it by
|
||||||
/// how far the target sits ahead of / behind the camera, and its scale shrinks with camera -> target distance.
|
/// how far the target sits ahead of / behind the camera, and its scale shrinks with camera -> target distance.
|
||||||
|
///
|
||||||
|
/// On top of the radar-wide fade, each blip fades in when it spawns and fades out at the end of its life (or when
|
||||||
|
/// its world target vanishes) via its own <see cref="_fade"/> factor, so blips appear/disappear smoothly instead
|
||||||
|
/// of popping. Fade-in and fade-out speeds are independent; the factor persists across <see cref="Refresh"/> so a
|
||||||
|
/// refreshed blip is pulled back to full opacity rather than flickering to zero first.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DisallowMultipleComponent]
|
[DisallowMultipleComponent]
|
||||||
public sealed class RadarTarget : MonoBehaviour
|
public sealed class RadarTarget : MonoBehaviour
|
||||||
@@ -20,11 +25,18 @@ public sealed class RadarTarget : MonoBehaviour
|
|||||||
[Tooltip("camera -> target distance mapped onto pointDistanceScale: x = near distance, y = far distance.")]
|
[Tooltip("camera -> target distance mapped onto pointDistanceScale: x = near distance, y = far distance.")]
|
||||||
[SerializeField] Vector2 pointDistanceRange;
|
[SerializeField] Vector2 pointDistanceRange;
|
||||||
|
|
||||||
|
[Header("Fade")]
|
||||||
|
[Tooltip("Per-blip fade-in speed in alpha units per second when the blip appears. e.g. 6 = ~0.17s. 0 = appear instantly.")]
|
||||||
|
[SerializeField] float fadeInSpeed = 6f;
|
||||||
|
[Tooltip("Per-blip fade-out speed in alpha units per second at the end of life / when the target vanishes. e.g. 4 = ~0.25s. 0 = vanish instantly.")]
|
||||||
|
[SerializeField] float fadeOutSpeed = 4f;
|
||||||
|
|
||||||
private Transform _target;
|
private Transform _target;
|
||||||
private Camera _camera;
|
private Camera _camera;
|
||||||
private float _lifetime;
|
private float _lifetime;
|
||||||
private float _age;
|
private float _age;
|
||||||
private bool _initialised;
|
private bool _initialised;
|
||||||
|
private float _fade; // per-blip 0..1 ramp, multiplied on top of the radar-wide fade; persists across Refresh
|
||||||
|
|
||||||
private MaterialPropertyBlock _mpb;
|
private MaterialPropertyBlock _mpb;
|
||||||
private Color _pointColor = Color.white; // last gradient-evaluated point colour; its alpha is scaled by the radar fade
|
private Color _pointColor = Color.white; // last gradient-evaluated point colour; its alpha is scaled by the radar fade
|
||||||
@@ -44,27 +56,39 @@ public sealed class RadarTarget : MonoBehaviour
|
|||||||
PushPointColor();
|
PushPointColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Restarts this blip's countdown to <paramref name="lifetime"/> seconds. Used when a ping arrives
|
||||||
|
/// for a target this radar already tracks: the existing blip is refreshed instead of stacking a duplicate.</summary>
|
||||||
|
public void Refresh(float lifetime)
|
||||||
|
{
|
||||||
|
_lifetime = lifetime;
|
||||||
|
_age = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
// LateUpdate so the aim uses the camera's final position/rotation for this frame.
|
// LateUpdate so the aim uses the camera's final position/rotation for this frame.
|
||||||
private void LateUpdate()
|
private void LateUpdate()
|
||||||
{
|
{
|
||||||
if (!_initialised) return;
|
if (!_initialised) return;
|
||||||
|
|
||||||
_age += Time.deltaTime;
|
_age += Time.deltaTime;
|
||||||
if (_age >= _lifetime)
|
|
||||||
|
// Fade out at the very end of life, or immediately if the world target vanished (e.g. a caught hider)
|
||||||
|
// instead of dangling. The fade-out window equals the time to ramp 1 -> 0 at fadeOutSpeed, so the point
|
||||||
|
// reaches full transparency right as the lifetime elapses; then the blip destroys itself.
|
||||||
|
float fadeOutWindow = fadeOutSpeed > 0f ? 1f / fadeOutSpeed : 0f;
|
||||||
|
bool fadingOut = !_target || RemainingLifetime <= fadeOutWindow;
|
||||||
|
|
||||||
|
float goal = fadingOut ? 0f : 1f;
|
||||||
|
float speed = Mathf.Max(0f, fadingOut ? fadeOutSpeed : fadeInSpeed);
|
||||||
|
_fade = Mathf.MoveTowards(_fade, goal, speed * Time.deltaTime);
|
||||||
|
|
||||||
|
if (fadingOut && (speed <= 0f || _fade <= 0.0001f))
|
||||||
{
|
{
|
||||||
Destroy(gameObject);
|
Destroy(gameObject);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The world target may despawn (e.g. a caught hider) before this blip expires - drop it, don't dangle.
|
Aim(); // keeps last orientation/colour if the target is gone (Aim early-returns)
|
||||||
if (!_target)
|
PushPointColor(); // every frame so the alpha tracks both the radar fade and this blip's own fade
|
||||||
{
|
|
||||||
Destroy(gameObject);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Aim();
|
|
||||||
PushPointColor(); // every frame so the blip's alpha tracks the radar fade even when Aim early-returns
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Aim()
|
private void Aim()
|
||||||
@@ -100,13 +124,13 @@ public sealed class RadarTarget : MonoBehaviour
|
|||||||
_pointColor = forwardGradient.Evaluate(t);
|
_pointColor = forwardGradient.Evaluate(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes the point's _BaseColor with its alpha scaled by the radar's shared fade factor, so blips fade in/out
|
// Writes the point's _BaseColor with its alpha scaled by both the radar's shared fade factor (so blips fade in
|
||||||
// in step with the disc instead of the overlay snapping on/off.
|
// step with the disc) and this blip's own fade (so it eases in on spawn and out at the end of its life).
|
||||||
private void PushPointColor()
|
private void PushPointColor()
|
||||||
{
|
{
|
||||||
if (!pointRenderer) return;
|
if (!pointRenderer) return;
|
||||||
Color c = _pointColor;
|
Color c = _pointColor;
|
||||||
c.a *= Radar.Instance ? Radar.Instance.CurrentFade : 1f;
|
c.a *= (Radar.Instance ? Radar.Instance.CurrentFade : 1f) * _fade;
|
||||||
_mpb ??= new MaterialPropertyBlock();
|
_mpb ??= new MaterialPropertyBlock();
|
||||||
pointRenderer.GetPropertyBlock(_mpb);
|
pointRenderer.GetPropertyBlock(_mpb);
|
||||||
_mpb.SetColor(BaseColorId, c);
|
_mpb.SetColor(BaseColorId, c);
|
||||||
|
|||||||
@@ -42,6 +42,16 @@ public class GameModeManager : MonoBehaviour
|
|||||||
[Tooltip("Mode active at startup (0 = Player Control).")]
|
[Tooltip("Mode active at startup (0 = Player Control).")]
|
||||||
public int startModeIndex = 0;
|
public int startModeIndex = 0;
|
||||||
|
|
||||||
|
[Header("Hider radar ping")]
|
||||||
|
[Tooltip("Key a HIDER presses to manually ping itself on every hunter's radar during Seek - the same blip " +
|
||||||
|
"as the periodic sweep, on demand. Ignored for hunters, outside Seek, and in Free Cam (whose fly " +
|
||||||
|
"controls already use this key).")]
|
||||||
|
#if ENABLE_INPUT_SYSTEM
|
||||||
|
public Key pingKey = Key.E;
|
||||||
|
#else
|
||||||
|
public KeyCode pingKey = KeyCode.E;
|
||||||
|
#endif
|
||||||
|
|
||||||
[Header("Player collision")]
|
[Header("Player collision")]
|
||||||
[Tooltip("ON = players physically block each other (default). OFF = the local player passes through every " +
|
[Tooltip("ON = players physically block each other (default). OFF = the local player passes through every " +
|
||||||
"other player - their CharacterController stops colliding with other players' body colliders. " +
|
"other player - their CharacterController stops colliding with other players' body colliders. " +
|
||||||
@@ -76,6 +86,7 @@ public class GameModeManager : MonoBehaviour
|
|||||||
private bool _paused;
|
private bool _paused;
|
||||||
private int _resumeIndex = -1;
|
private int _resumeIndex = -1;
|
||||||
private bool _loadFrozen; // true while the pre-match Loading barrier is holding the local player still
|
private bool _loadFrozen; // true while the pre-match Loading barrier is holding the local player still
|
||||||
|
private PlayerRole? _localRole; // bound local avatar's role (null = pre-spawn / spectator / unscoped lobby)
|
||||||
|
|
||||||
// Player-collision reconciler state (see ReconcilePlayerCollision). Non-serialized: reset each play session.
|
// Player-collision reconciler state (see ReconcilePlayerCollision). Non-serialized: reset each play session.
|
||||||
private bool _playerCollisionDirty = true; // force a re-apply (startup, toggle flip, inspector edit)
|
private bool _playerCollisionDirty = true; // force a re-apply (startup, toggle flip, inspector edit)
|
||||||
@@ -180,6 +191,7 @@ public class GameModeManager : MonoBehaviour
|
|||||||
{
|
{
|
||||||
player = null;
|
player = null;
|
||||||
painter = null;
|
painter = null;
|
||||||
|
_localRole = null; // spectator / released: no hider ping
|
||||||
PainterChanged?.Invoke(null);
|
PainterChanged?.Invoke(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,9 +249,15 @@ public class GameModeManager : MonoBehaviour
|
|||||||
// Lobby avatar -> full set; match variant -> its role's modes. Scope is baked per prefab
|
// Lobby avatar -> full set; match variant -> its role's modes. Scope is baked per prefab
|
||||||
// variant, so it follows the spawned avatar with no replicated-phase timing race.
|
// variant, so it follows the spawned avatar with no replicated-phase timing race.
|
||||||
if (setup && setup.ScopedModes)
|
if (setup && setup.ScopedModes)
|
||||||
|
{
|
||||||
|
_localRole = setup.Role;
|
||||||
BuildModes(setup.Role);
|
BuildModes(setup.Role);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
_localRole = null; // unscoped lobby avatar: no hider ping
|
||||||
BuildModes(null);
|
BuildModes(null);
|
||||||
|
}
|
||||||
|
|
||||||
// Index 0 is each set's intended start mode.
|
// Index 0 is each set's intended start mode.
|
||||||
SwitchTo(0);
|
SwitchTo(0);
|
||||||
@@ -394,6 +412,13 @@ public class GameModeManager : MonoBehaviour
|
|||||||
SwitchTo(next);
|
SwitchTo(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A hider manually pings itself onto every hunter's radar (same blip as the periodic sweep), on demand.
|
||||||
|
// Excluded in Free Cam whose fly controls already use this key; the phase/role gate lives in MatchManager
|
||||||
|
// (RequestManualPing no-ops outside Seek / for a found hider), so a stray press is harmless.
|
||||||
|
if (_localRole == PlayerRole.Hider && !(ActiveMode is FreeCamMode) &&
|
||||||
|
InputUtil.GameplayKeyPressed(pingKey) && MatchManager.Instance)
|
||||||
|
MatchManager.Instance.RequestManualPing();
|
||||||
|
|
||||||
ActiveMode?.Tick();
|
ActiveMode?.Tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -251,6 +251,10 @@ public class MatchManager : MonoBehaviour
|
|||||||
private const string MsgReady = "Match_Ready";
|
private const string MsgReady = "Match_Ready";
|
||||||
// Client -> server (authority only): DEBUG request to skip the rest of the current sub-phase.
|
// Client -> server (authority only): DEBUG request to skip the rest of the current sub-phase.
|
||||||
private const string MsgDebugSkip = "Match_DebugSkip";
|
private const string MsgDebugSkip = "Match_DebugSkip";
|
||||||
|
// Client -> server: a still-un-found hider manually requests a radar ping on itself (E key), so it blips
|
||||||
|
// on every hunter's radar exactly like an interval sweep. The server gates the Seek phase and that the
|
||||||
|
// sender is a live hider before emitting; the sender id is the trusted routing (no payload).
|
||||||
|
private const string MsgManualPing = "Match_ManualPing";
|
||||||
|
|
||||||
// Server-only sub-phase machine. _pendingHiders holds the hiders not yet found; seek ends the instant it
|
// 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
|
// empties. _phaseRoutine is the running Hide->Seek->Resolution coroutine (stopped if the match is ended
|
||||||
@@ -270,18 +274,20 @@ public class MatchManager : MonoBehaviour
|
|||||||
private readonly List<ulong> _radarPingIds = new List<ulong>(); // reused scratch: hider NetworkObjectIds for a sweep
|
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 readonly List<ulong> _radarRecipients = new List<ulong>(); // reused scratch: hunter client recipients for a sweep
|
||||||
|
|
||||||
// Server-only pre-match loading barrier. _matchAvatarIds = the freshly respawned avatars every client must
|
// Server-only readiness barrier. _barrierTargets = the objects every client must have spawned & paint-applied
|
||||||
// have spawned & paint-applied; _expectedReady = the clients present when the barrier opened (a mid-load
|
// this round (the freshly respawned avatars pre-match, or the conversion statues at Resolution); _expectedReady
|
||||||
// joiner isn't required, so it can't stall the start); _readyClients = those that have reported ready.
|
// = the clients present when the barrier opened (a mid-load joiner isn't required, so it can't stall it);
|
||||||
// _loadRecipients is reused scratch for the broadcast. _loadGeneration tags the round so a late ack from a
|
// _readyClients = those that have reported ready. _loadRecipients is reused scratch for the broadcast.
|
||||||
// previous barrier is ignored.
|
// _loadGeneration tags the round so a late ack from a previous barrier is ignored.
|
||||||
private readonly List<ulong> _matchAvatarIds = new List<ulong>();
|
private readonly List<ulong> _barrierTargets = new List<ulong>();
|
||||||
private readonly List<ulong> _loadRecipients = new List<ulong>();
|
private readonly List<ulong> _loadRecipients = new List<ulong>();
|
||||||
private readonly HashSet<ulong> _expectedReady = new HashSet<ulong>();
|
private readonly HashSet<ulong> _expectedReady = new HashSet<ulong>();
|
||||||
private readonly HashSet<ulong> _readyClients = new HashSet<ulong>();
|
private readonly HashSet<ulong> _readyClients = new HashSet<ulong>();
|
||||||
private int _loadGeneration;
|
private int _loadGeneration;
|
||||||
// Client-side barrier state: the target avatar ids to wait for, the generation to echo back, and the running poll.
|
// Client-side barrier state: the target ids to wait for, the holding phase to poll during, the generation to
|
||||||
|
// echo back, and the running poll.
|
||||||
private readonly List<ulong> _loadTargetIds = new List<ulong>();
|
private readonly List<ulong> _loadTargetIds = new List<ulong>();
|
||||||
|
private MatchSubPhase _loadActivePhaseClient;
|
||||||
private int _loadGenerationClient;
|
private int _loadGenerationClient;
|
||||||
private Coroutine _clientLoadPoll;
|
private Coroutine _clientLoadPoll;
|
||||||
|
|
||||||
@@ -345,6 +351,7 @@ public class MatchManager : MonoBehaviour
|
|||||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgPhase);
|
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgPhase);
|
||||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgRadarPing);
|
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgRadarPing);
|
||||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgReportHit);
|
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgReportHit);
|
||||||
|
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgManualPing);
|
||||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgIdentity);
|
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgIdentity);
|
||||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgBeginLoad);
|
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgBeginLoad);
|
||||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgReady);
|
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgReady);
|
||||||
@@ -373,6 +380,7 @@ public class MatchManager : MonoBehaviour
|
|||||||
{
|
{
|
||||||
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgRequestEndMatch, OnRequestEndMatchMessage);
|
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgRequestEndMatch, OnRequestEndMatchMessage);
|
||||||
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgReportHit, OnReportHitMessage);
|
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgReportHit, OnReportHitMessage);
|
||||||
|
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgManualPing, OnManualPingMessage);
|
||||||
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgIdentity, OnIdentityMessage);
|
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgIdentity, OnIdentityMessage);
|
||||||
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgReady, OnReadyMessage);
|
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgReady, OnReadyMessage);
|
||||||
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgDebugSkip, OnDebugSkipMessage);
|
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgDebugSkip, OnDebugSkipMessage);
|
||||||
@@ -434,7 +442,7 @@ public class MatchManager : MonoBehaviour
|
|||||||
// Drop any in-flight loading-barrier state (StopAllCoroutines above already killed the polls).
|
// Drop any in-flight loading-barrier state (StopAllCoroutines above already killed the polls).
|
||||||
_expectedReady.Clear();
|
_expectedReady.Clear();
|
||||||
_readyClients.Clear();
|
_readyClients.Clear();
|
||||||
_matchAvatarIds.Clear();
|
_barrierTargets.Clear();
|
||||||
_loadTargetIds.Clear();
|
_loadTargetIds.Clear();
|
||||||
_clientLoadPoll = null;
|
_clientLoadPoll = null;
|
||||||
// Teardown unloads the gameplay scenes (destroys the statue objects); just drop our references.
|
// Teardown unloads the gameplay scenes (destroys the statue objects); just drop our references.
|
||||||
@@ -634,8 +642,8 @@ public class MatchManager : MonoBehaviour
|
|||||||
// Freeze everyone in Loading BEFORE unloading the lobby: the lobby floor vanishes a moment before the
|
// Freeze everyone in Loading BEFORE unloading the lobby: the lobby floor vanishes a moment before the
|
||||||
// map's is loaded, so the still-bound lobby avatars would otherwise drop into the void during the swap.
|
// map's is loaded, so the still-bound lobby avatars would otherwise drop into the void during the swap.
|
||||||
// Loading gates movement, gravity AND the local own-body mesh on every client (see GameModeManager); the
|
// Loading gates movement, gravity AND the local own-body mesh on every client (see GameModeManager); the
|
||||||
// readiness barrier below (BeginLoadingBarrierServer re-asserts Loading) holds that hold until every
|
// readiness barrier below (RunLoadingBarrier re-asserts Loading) holds that hold until every client has
|
||||||
// client has the map + all match avatars ready, then Hide releases it.
|
// the map + all match avatars ready, then Hide releases it.
|
||||||
SetPhaseServer(MatchSubPhase.Loading, 0f);
|
SetPhaseServer(MatchSubPhase.Loading, 0f);
|
||||||
|
|
||||||
yield return SwapScenes(lobbySceneName, matchSceneName);
|
yield return SwapScenes(lobbySceneName, matchSceneName);
|
||||||
@@ -670,10 +678,19 @@ public class MatchManager : MonoBehaviour
|
|||||||
SetPhaseServer(MatchSubPhase.Seek, seekDuration);
|
SetPhaseServer(MatchSubPhase.Seek, seekDuration);
|
||||||
yield return RunSeekPhase(seekDuration);
|
yield return RunSeekPhase(seekDuration);
|
||||||
|
|
||||||
SetPhaseServer(MatchSubPhase.Resolution, resolveDuration);
|
// Conversion mode: materialize a frozen statue of every converted hider at its capture spot. The last
|
||||||
// Conversion mode: materialize a frozen statue of every converted hider at its capture spot, now that
|
// hider is often still converting (its statue's painting hasn't replicated to every client yet), so
|
||||||
// Resolution (which reveals everyone) has begun. No-op with no records / no statue prefab / in Classic.
|
// hold the Resolution countdown - frozen at 0:00, exactly like the pre-match loading barrier - until
|
||||||
|
// every client has each statue spawned & painted. Then release the timer. No statue prefab / no
|
||||||
|
// conversions / Classic -> no hold, the countdown starts immediately (unchanged behaviour).
|
||||||
|
bool holdForStatues = statuePrefab && _conversionRecords.Count > 0;
|
||||||
|
SetPhaseServer(MatchSubPhase.Resolution, holdForStatues ? 0f : resolveDuration);
|
||||||
SpawnConversionStatues();
|
SpawnConversionStatues();
|
||||||
|
if (holdForStatues)
|
||||||
|
{
|
||||||
|
yield return RunStatueBarrier();
|
||||||
|
SetPhaseServer(MatchSubPhase.Resolution, resolveDuration); // statues loaded -> start the countdown
|
||||||
|
}
|
||||||
yield return WaitPhase(resolveDuration, null);
|
yield return WaitPhase(resolveDuration, null);
|
||||||
|
|
||||||
_phaseRoutine = null;
|
_phaseRoutine = null;
|
||||||
@@ -760,29 +777,61 @@ public class MatchManager : MonoBehaviour
|
|||||||
DebugSkipServer();
|
DebugSkipServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Pre-match loading barrier ---------------------------------------------------------------------
|
// --- Readiness barriers ----------------------------------------------------------------------------
|
||||||
// Server-only. Opens the barrier (snapshot the just-respawned avatars + the clients that must load them,
|
// Server-only. Pre-match: hold everyone in Loading (frozen, no countdown) until every client has spawned &
|
||||||
// switch everyone to Loading, tell each client which avatars to wait for), then spins until every still-
|
// paint-applied the freshly respawned avatars, or matchLoadTimeout elapses. Loading was already asserted
|
||||||
// connected expected client has reported ready - or matchLoadTimeout elapses.
|
// before the scene swap; re-assert it here after the respawn, then run the generic barrier over the avatars.
|
||||||
private IEnumerator RunLoadingBarrier()
|
private IEnumerator RunLoadingBarrier()
|
||||||
{
|
{
|
||||||
_skipPhase = false; // drop any debug-skip left over from the previous phase/match; only this phase may set it
|
_barrierTargets.Clear();
|
||||||
BeginLoadingBarrierServer();
|
foreach (ulong id in _net.ConnectedClientsIds)
|
||||||
|
if (_net.ConnectedClients.TryGetValue(id, out var c) && c.PlayerObject)
|
||||||
|
_barrierTargets.Add(c.PlayerObject.NetworkObjectId);
|
||||||
|
|
||||||
|
SetPhaseServer(MatchSubPhase.Loading, 0f);
|
||||||
|
yield return RunReadinessBarrier(MatchSubPhase.Loading);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server-only (Conversion). Hold the (already-entered) Resolution phase until every client has each conversion
|
||||||
|
// statue spawned & painted, so a just-caught hider's still-loading statue doesn't burn resolve time. The
|
||||||
|
// statues carry the same PlayerPaintSync the avatar barrier keys off, so "painted" == the statue's body reveal.
|
||||||
|
private IEnumerator RunStatueBarrier()
|
||||||
|
{
|
||||||
|
_barrierTargets.Clear();
|
||||||
|
for (int i = 0; i < _statues.Count; i++)
|
||||||
|
if (_statues[i]) _barrierTargets.Add(_statues[i].NetworkObjectId);
|
||||||
|
|
||||||
|
yield return RunReadinessBarrier(MatchSubPhase.Resolution);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server-only. Generic readiness barrier over the already-populated _barrierTargets: snapshot the expected
|
||||||
|
// clients, bump the round generation, broadcast the targets + holding phase, then spin until every still-
|
||||||
|
// connected expected client has reported ready - or matchLoadTimeout elapses. `activePhase` is the frozen
|
||||||
|
// holding phase (Loading pre-match, Resolution for statues) that clients poll during.
|
||||||
|
private IEnumerator RunReadinessBarrier(MatchSubPhase activePhase)
|
||||||
|
{
|
||||||
|
_skipPhase = false; // drop any debug-skip left over from the previous phase/match; only this wait may set it
|
||||||
|
_expectedReady.Clear();
|
||||||
|
_readyClients.Clear();
|
||||||
|
foreach (ulong id in _net.ConnectedClientsIds)
|
||||||
|
_expectedReady.Add(id);
|
||||||
|
_loadGeneration++;
|
||||||
|
BroadcastBeginLoad(activePhase);
|
||||||
|
|
||||||
float t = matchLoadTimeout;
|
float t = matchLoadTimeout;
|
||||||
while (!AllClientsReady())
|
while (!AllClientsReady())
|
||||||
{
|
{
|
||||||
if (ConsumeSkip()) break; // DEBUG: force the match to start now, without waiting for every client
|
if (ConsumeSkip()) break; // DEBUG: proceed now, without waiting for every client
|
||||||
|
|
||||||
// The host is a client too and never messages itself: fold its own local readiness in directly.
|
// The host is a client too and never messages itself: fold its own local readiness in directly.
|
||||||
if (LocalClientReady(_matchAvatarIds)) _readyClients.Add(_net.LocalClientId);
|
if (LocalClientReady(_barrierTargets)) _readyClients.Add(_net.LocalClientId);
|
||||||
|
|
||||||
if (matchLoadTimeout > 0f)
|
if (matchLoadTimeout > 0f)
|
||||||
{
|
{
|
||||||
t -= Time.deltaTime;
|
t -= Time.deltaTime;
|
||||||
if (t <= 0f)
|
if (t <= 0f)
|
||||||
{
|
{
|
||||||
Debug.LogWarning("[MatchManager] Load barrier timed out - starting the match anyway.", this);
|
Debug.LogWarning("[MatchManager] Readiness barrier timed out - proceeding anyway.", this);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -790,26 +839,6 @@ public class MatchManager : MonoBehaviour
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server-only. Captures the barrier target (the avatars RespawnAll just created + the clients that must load
|
|
||||||
// them), bumps the generation, switches everyone to Loading (duration 0 = no countdown) and broadcasts the
|
|
||||||
// target avatar ids so each client can start waiting.
|
|
||||||
private void BeginLoadingBarrierServer()
|
|
||||||
{
|
|
||||||
_matchAvatarIds.Clear();
|
|
||||||
_expectedReady.Clear();
|
|
||||||
_readyClients.Clear();
|
|
||||||
foreach (ulong id in _net.ConnectedClientsIds)
|
|
||||||
{
|
|
||||||
_expectedReady.Add(id);
|
|
||||||
if (_net.ConnectedClients.TryGetValue(id, out var c) && c.PlayerObject)
|
|
||||||
_matchAvatarIds.Add(c.PlayerObject.NetworkObjectId);
|
|
||||||
}
|
|
||||||
_loadGeneration++;
|
|
||||||
|
|
||||||
SetPhaseServer(MatchSubPhase.Loading, 0f);
|
|
||||||
BroadcastBeginLoad();
|
|
||||||
}
|
|
||||||
|
|
||||||
// True once every expected client that is STILL connected has reported ready. A client that disconnected
|
// True once every expected client that is STILL connected has reported ready. A client that disconnected
|
||||||
// mid-load drops out of the requirement; a mid-load joiner was never in _expectedReady, so it can't stall it.
|
// mid-load drops out of the requirement; a mid-load joiner was never in _expectedReady, so it can't stall it.
|
||||||
private bool AllClientsReady()
|
private bool AllClientsReady()
|
||||||
@@ -836,8 +865,8 @@ public class MatchManager : MonoBehaviour
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server -> every other client: the target avatar ids + generation for this barrier round.
|
// Server -> every other client: the target ids + holding phase + generation for this barrier round.
|
||||||
private void BroadcastBeginLoad()
|
private void BroadcastBeginLoad(MatchSubPhase activePhase)
|
||||||
{
|
{
|
||||||
var cmm = _net != null ? _net.CustomMessagingManager : null;
|
var cmm = _net != null ? _net.CustomMessagingManager : null;
|
||||||
if (cmm == null) return;
|
if (cmm == null) return;
|
||||||
@@ -847,13 +876,14 @@ public class MatchManager : MonoBehaviour
|
|||||||
if (id != _net.LocalClientId) _loadRecipients.Add(id);
|
if (id != _net.LocalClientId) _loadRecipients.Add(id);
|
||||||
if (_loadRecipients.Count == 0) return;
|
if (_loadRecipients.Count == 0) return;
|
||||||
|
|
||||||
int size = sizeof(int) + sizeof(int) + _matchAvatarIds.Count * sizeof(ulong);
|
int size = sizeof(int) + sizeof(byte) + sizeof(int) + _barrierTargets.Count * sizeof(ulong);
|
||||||
using (var writer = new FastBufferWriter(size, Allocator.Temp))
|
using (var writer = new FastBufferWriter(size, Allocator.Temp))
|
||||||
{
|
{
|
||||||
writer.WriteValueSafe(_loadGeneration);
|
writer.WriteValueSafe(_loadGeneration);
|
||||||
writer.WriteValueSafe(_matchAvatarIds.Count);
|
writer.WriteValueSafe((byte)activePhase);
|
||||||
for (int i = 0; i < _matchAvatarIds.Count; i++)
|
writer.WriteValueSafe(_barrierTargets.Count);
|
||||||
writer.WriteValueSafe(_matchAvatarIds[i]);
|
for (int i = 0; i < _barrierTargets.Count; i++)
|
||||||
|
writer.WriteValueSafe(_barrierTargets[i]);
|
||||||
cmm.SendNamedMessage(MsgBeginLoad, _loadRecipients, writer);
|
cmm.SendNamedMessage(MsgBeginLoad, _loadRecipients, writer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -863,6 +893,7 @@ public class MatchManager : MonoBehaviour
|
|||||||
private void OnBeginLoadMessage(ulong senderClientId, FastBufferReader reader)
|
private void OnBeginLoadMessage(ulong senderClientId, FastBufferReader reader)
|
||||||
{
|
{
|
||||||
reader.ReadValueSafe(out int generation);
|
reader.ReadValueSafe(out int generation);
|
||||||
|
reader.ReadValueSafe(out byte activePhase);
|
||||||
reader.ReadValueSafe(out int count);
|
reader.ReadValueSafe(out int count);
|
||||||
_loadTargetIds.Clear();
|
_loadTargetIds.Clear();
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
@@ -871,16 +902,17 @@ public class MatchManager : MonoBehaviour
|
|||||||
_loadTargetIds.Add(id);
|
_loadTargetIds.Add(id);
|
||||||
}
|
}
|
||||||
_loadGenerationClient = generation;
|
_loadGenerationClient = generation;
|
||||||
|
_loadActivePhaseClient = (MatchSubPhase)activePhase;
|
||||||
|
|
||||||
if (_clientLoadPoll != null) StopCoroutine(_clientLoadPoll);
|
if (_clientLoadPoll != null) StopCoroutine(_clientLoadPoll);
|
||||||
_clientLoadPoll = StartCoroutine(ClientLoadingPoll());
|
_clientLoadPoll = StartCoroutine(ClientLoadingPoll());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client-only. Polls until this machine has spawned & paint-applied every target avatar, then reports ready.
|
// Client-only. Polls until this machine has spawned & paint-applied every target, then reports ready. Bails
|
||||||
// Bails without a reply if the phase leaves Loading first (match ended, or the server already moved to Hide).
|
// without a reply if the phase leaves the holding phase first (match ended, or the server already moved on).
|
||||||
private IEnumerator ClientLoadingPoll()
|
private IEnumerator ClientLoadingPoll()
|
||||||
{
|
{
|
||||||
while (_subPhase == MatchSubPhase.Loading)
|
while (_subPhase == _loadActivePhaseClient)
|
||||||
{
|
{
|
||||||
if (LocalClientReady(_loadTargetIds))
|
if (LocalClientReady(_loadTargetIds))
|
||||||
{
|
{
|
||||||
@@ -992,6 +1024,15 @@ public class MatchManager : MonoBehaviour
|
|||||||
foreach (ulong hiderClientId in _pendingHiders)
|
foreach (ulong hiderClientId in _pendingHiders)
|
||||||
if (_net.ConnectedClients.TryGetValue(hiderClientId, out var c) && c.PlayerObject)
|
if (_net.ConnectedClients.TryGetValue(hiderClientId, out var c) && c.PlayerObject)
|
||||||
_radarPingIds.Add(c.PlayerObject.NetworkObjectId);
|
_radarPingIds.Add(c.PlayerObject.NetworkObjectId);
|
||||||
|
EmitRadarPings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server-only. Sends whatever hider NetworkObjectIds are staged in _radarPingIds to every HUNTER as radar
|
||||||
|
// blips (a host-hunter applies locally, no loopback - mirroring SetPhaseServer). Shared by the interval
|
||||||
|
// sweep and the manual single-hider ping so both drop the identical blip (same lifetime, same path).
|
||||||
|
// No-op if nothing is staged or there is no hunter to receive it.
|
||||||
|
private void EmitRadarPings()
|
||||||
|
{
|
||||||
if (_radarPingIds.Count == 0) return;
|
if (_radarPingIds.Count == 0) return;
|
||||||
|
|
||||||
// Host-as-hunter: spawn on its own radar directly (it is excluded from the recipient list below).
|
// Host-as-hunter: spawn on its own radar directly (it is excluded from the recipient list below).
|
||||||
@@ -1015,6 +1056,39 @@ public class MatchManager : MonoBehaviour
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Client-side: the local hider manually requests a radar ping on itself (the E key), making it
|
||||||
|
/// blip on every hunter's radar exactly like an interval sweep. Routes to the server, which gates the Seek
|
||||||
|
/// phase and that this client is still a live (un-found) hider. On a host this IS the server, so it applies
|
||||||
|
/// directly. No-op outside the Seek phase.</summary>
|
||||||
|
public void RequestManualPing()
|
||||||
|
{
|
||||||
|
if (_net == null || _subPhase != MatchSubPhase.Seek) return;
|
||||||
|
|
||||||
|
if (_net.IsServer) { ServerManualPing(_net.LocalClientId); return; }
|
||||||
|
SendEmptyNamedMessage(MsgManualPing, NetworkManager.ServerClientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server handler: a client asked for a manual self-ping. The sender id is the trusted routing;
|
||||||
|
// ServerManualPing validates it is a live hider during Seek.
|
||||||
|
private void OnManualPingMessage(ulong senderClientId, FastBufferReader reader)
|
||||||
|
{
|
||||||
|
if (_net == null || !_net.IsServer) return;
|
||||||
|
ServerManualPing(senderClientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server-only. Emits a single-hider radar ping for hiderClientId to every hunter - if it is a still-un-found
|
||||||
|
// hider during Seek. Same blip (and lifetime) as an interval sweep, just one target and on demand.
|
||||||
|
private void ServerManualPing(ulong hiderClientId)
|
||||||
|
{
|
||||||
|
if (_subPhase != MatchSubPhase.Seek) return;
|
||||||
|
if (!_pendingHiders.Contains(hiderClientId)) return; // not a live (un-found) hider
|
||||||
|
if (!_net.ConnectedClients.TryGetValue(hiderClientId, out var c) || !c.PlayerObject) return;
|
||||||
|
|
||||||
|
_radarPingIds.Clear();
|
||||||
|
_radarPingIds.Add(c.PlayerObject.NetworkObjectId);
|
||||||
|
EmitRadarPings();
|
||||||
|
}
|
||||||
|
|
||||||
// Host/server local path: resolve the swept hider NetworkObjectIds (in _radarPingIds) to their live
|
// 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.
|
// transforms and drop a blip on the local radar. Mirrors OnRadarPingMessage for the host's own hunter view.
|
||||||
private void SpawnRadarBlipsLocal()
|
private void SpawnRadarBlipsLocal()
|
||||||
|
|||||||
Reference in new issue
Block a user