Hunter shot: replicated laser beam + X-ray hit flash; snapshot apply in-place reseed

- Laser.shader + HunterLaserBeam: additive HDR beam, fade in/out over opacity
  and thickness, code-driven LineRenderer, tunable emission multiplier.
- FirstPersonMode: TryShoot computes world endpoint, fires beam with lateral
  muzzle offset (right) so it isn't dead-center from hunter view.
- NetworkPlayerSetup: shot relay RPCs (owner->server->all) + hit-flash RPC
  (server->all), flash OR-branch in ComputeXRay.
- MatchManager: hit reveal broadcasts X-ray flash first, delays hider removal
  by hitRevealDelay so the glow is visible before the body vanishes.
- MeshPainter reseed: in-place fast path avoids full GPU backend teardown on
  snapshot apply (no ~70MB RT churn), fixes spawn/paint-load hitch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-07-04 14:18:55 +02:00
1 parent 5b95cb7f56
commit 9290ef650b
10 files changed
+403 -23

No files matched your search

+43 -4
View File
@@ -115,6 +115,12 @@ public class MatchManager : MonoBehaviour
[Min(0f)]
[SerializeField] private float shootCooldown = 0.4f;
[Tooltip("Seconds a shot hider stays lit with the X-ray glow before it is actually removed from play " +
"(Classic: hidden as found; Conversion: respawned as a hunter). Gives the just-shot flash time to " +
"be seen. The hider stays a live target during this window; 0 = remove instantly (no visible flash).")]
[Min(0f)]
[SerializeField] private float hitRevealDelay = 0.35f;
[Header("Concealment reveal")]
[Tooltip("A hider whose ConcealmentMeter EmbeddedPercent reaches this value (while a match is running) is " +
"revealed through walls with the material's X-ray glow, so nobody can hide by burying itself deep " +
@@ -192,6 +198,9 @@ public class MatchManager : MonoBehaviour
// empties. _phaseRoutine is the running Hide->Seek->Resolution coroutine (stopped if the match is ended
// early via the authority's Back-to-Lobby).
private readonly HashSet<ulong> _pendingHiders = new HashSet<ulong>();
// Server-only. Hiders that were hit and are in their X-ray flash window, not yet removed. Kept out of the
// seek-end check by staying in _pendingHiders; this set only dedupes a second shot during the flash.
private readonly HashSet<ulong> _revealingHiders = new HashSet<ulong>();
// Server-only. Live NGO clientId -> persistent per-install id, reported by each client on connect. The
// clientId is ephemeral (reused after a disconnect); the value is stable across that client's restarts.
private readonly Dictionary<ulong, string> _persistentIds = new Dictionary<ulong, string>();
@@ -398,6 +407,7 @@ public class MatchManager : MonoBehaviour
// A hider that leaves mid-match can never be found - drop it so "all hiders found" can still trigger.
_pendingHiders.Remove(clientId);
_revealingHiders.Remove(clientId); // and out of the in-flight flash set (its resolve coroutine no-ops)
// Free the ephemeral clientId -> identity mapping (the clientId will be reused by a later joiner).
_persistentIds.Remove(clientId);
@@ -517,6 +527,7 @@ public class MatchManager : MonoBehaviour
RespawnAll(id => hunterIds.Contains(id) ? hunterPrefab : hiderPrefab, matchSpawnCenter);
_pendingHiders.Clear();
_revealingHiders.Clear();
foreach (ulong id in _net.ConnectedClientsIds)
if (!hunterIds.Contains(id)) _pendingHiders.Add(id);
@@ -656,13 +667,40 @@ public class MatchManager : MonoBehaviour
MarkHiderFound(targetClientId);
}
// Server-only. Resolves a hit on a hider for the active mode, and drops it from the pending set; the seek
// loop ends the phase early once that empties (no hiders left to find/convert). Classic flags the avatar
// found (replicated by its NetworkPlayerSetup -> free-cam); Conversion respawns it as a Hunter.
// Server-only. Resolves a hit on a hider: flashes its X-ray glow on every client, then (after hitRevealDelay
// so the flash is actually seen) drops it from the pending set and applies the mode outcome - Classic flags
// the avatar found (replicated by its NetworkPlayerSetup -> free-cam); Conversion respawns it as a Hunter.
// The hider stays in _pendingHiders during the flash so seek doesn't end early on the last hit; the seek
// loop ends the phase once _pendingHiders empties (no hiders left to find/convert).
private void MarkHiderFound(ulong hiderClientId)
{
_pendingHiders.Remove(hiderClientId);
if (_revealingHiders.Contains(hiderClientId)) return; // already flashing / being resolved
if (_net.ConnectedClients.TryGetValue(hiderClientId, out var c) && c.PlayerObject)
{
var setup = c.PlayerObject.GetComponent<NetworkPlayerSetup>();
if (setup) setup.FlashHitReveal(hitRevealDelay); // light it up on every client, now
}
_revealingHiders.Add(hiderClientId);
StartCoroutine(ResolveHitAfterReveal(hiderClientId));
}
// Server-only. Waits out the reveal flash, then removes the hider and applies the mode outcome. Split from
// MarkHiderFound so the glow is visible before the avatar leaves play (Classic hide / Conversion despawn).
private IEnumerator ResolveHitAfterReveal(ulong hiderClientId)
{
if (hitRevealDelay > 0f) yield return new WaitForSeconds(hitRevealDelay);
_revealingHiders.Remove(hiderClientId);
if (_net == null || !_net.IsServer) yield break;
_pendingHiders.Remove(hiderClientId);
// A Back-to-Lobby (EndMatch) during the flash tears the match down and respawns everyone; don't then
// convert/found a stale avatar.
if (Phase == MatchPhase.InMatch &&
_net.ConnectedClients.TryGetValue(hiderClientId, out var c) && c.PlayerObject)
{
if (matchMode == MatchMode.Conversion)
ConvertToHunter(hiderClientId, c.PlayerObject);
@@ -672,6 +710,7 @@ public class MatchManager : MonoBehaviour
if (setup) setup.ServerMarkFound();
}
}
string verb = matchMode == MatchMode.Conversion ? "converted to hunter" : "found";
Debug.Log($"[MatchManager] Hider {hiderClientId} {verb} - {_pendingHiders.Count} hider(s) left.", this);
}
@@ -115,6 +115,8 @@ public class NetworkPlayerSetup : NetworkBehaviour
private bool _lastXRay;
private bool _frozenApplied;
private bool _lastFrozen;
private float _xrayFlashUntil; // Time.time deadline for the just-shot X-ray flash (local per client)
private HunterLaserBeam _laser; // shot beam visual, lazily attached on the hunter avatar
public override void OnNetworkSpawn()
{
@@ -361,6 +363,10 @@ public class NetworkPlayerSetup : NetworkBehaviour
// The X-ray reveal predicate for this avatar on the local machine.
private bool ComputeXRay()
{
// Just-shot flash: a hunter's hit lights this hider up for a brief window (server-broadcast), on top
// of the normal reveal triggers, so the moment of the hit reads before the hider leaves play.
if (Time.time < _xrayFlashUntil) return true;
// Buried-hider reveal: already gated to hider + running match on the owner before it is published.
if (_embeddedReveal.Value) return true;
@@ -369,6 +375,52 @@ public class NetworkPlayerSetup : NetworkBehaviour
return ComputeResolutionFreeze();
}
// --- Hunter shot beam ------------------------------------------------------------------------------
// Cosmetic and client-trusted (a co-op hide & seek, matching the ReportHit trust model). The local
// shooter draws its own beam instantly for responsiveness and relays the world-space segment to the
// server, which echoes it to every other client. The RPCs live on the hunter avatar (this
// NetworkObject), so they only run on this shooter's instances - other avatars never draw a beam.
/// <summary>Owner-only. Draws the shot beam locally now and replicates the world-space segment so every
/// other client sees the same shot.</summary>
public void FireShot(Vector3 start, Vector3 end)
{
if (!IsOwner) return;
EnsureLaser().Fire(start, end);
FireShotServerRpc(start, end);
}
[ServerRpc]
private void FireShotServerRpc(Vector3 start, Vector3 end) => FireShotClientRpc(start, end);
[ClientRpc]
private void FireShotClientRpc(Vector3 start, Vector3 end)
{
if (IsOwner) return; // the shooter already drew it locally
EnsureLaser().Fire(start, end);
}
private HunterLaserBeam EnsureLaser()
{
if (!_laser) _laser = GetComponentInChildren<HunterLaserBeam>(true);
if (!_laser) _laser = gameObject.AddComponent<HunterLaserBeam>();
return _laser;
}
// --- Hunter hit flash ------------------------------------------------------------------------------
/// <summary>Server-only. Flashes THIS hider's through-walls X-ray glow on every client for
/// <paramref name="seconds"/> (the just-shot feedback), overriding the normal reveal predicate for that
/// window. MatchManager delays the actual removal by the same span so the glow is seen before it leaves play.</summary>
public void FlashHitReveal(float seconds)
{
if (!IsServer) return;
FlashHitRevealClientRpc(Mathf.Max(0f, seconds));
}
[ClientRpc]
private void FlashHitRevealClientRpc(float seconds) => _xrayFlashUntil = Time.time + seconds;
// True if THIS machine's local player is a Hunter role variant (so it must not see hiders during hide).
// Shared with MatchHud. Called per frame per avatar during hide/seek, so the GetComponent result is
// cached per local PlayerObject; a respawn/conversion swaps the object and refreshes the cache.