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

+37 -15
View File
@@ -78,36 +78,58 @@ public class FirstPersonMode : GameMode
return _setup && _setup.ScopedModes && _setup.Role == PlayerRole.Hunter;
}
// Beam starts a touch ahead of the eye so the FP near plane doesn't clip it and it reads as coming from
// the player rather than the exact camera pivot.
private const float MuzzleForwardOffset = 0.2f;
// Lateral muzzle offset (world right of the eye) so the beam originates off-centre instead of dead ahead
// - a dead-centre beam looks bad in first person. Purely visual; the shot raycast stays centred (aim
// unchanged), only the beam's drawn origin shifts, and the world-space endpoints replicate to peers.
private const float MuzzleRightOffset = 0.15f;
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);
Vector3 origin = cam.position;
Vector3 dir = cam.forward;
int count = Physics.RaycastNonAlloc(cam.position, cam.forward, _hits, range, ~0, QueryTriggerInteraction.Ignore);
int count = Physics.RaycastNonAlloc(origin, dir, _hits, range, ~0, QueryTriggerInteraction.Ignore);
if (count == _hits.Length && !_hitsSaturated)
{
_hitsSaturated = true;
Debug.LogWarning($"FirstPersonMode: shot raycast buffer saturated ({count}); the nearest blocker may be missed.");
}
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<NetworkObject>() : null;
for (int i = 0; i < count; i++)
Vector3 end = origin + dir * range; // nothing hit -> the beam runs to max range
if (count > 0)
{
var netObj = _hits[i].collider.GetComponentInParent<NetworkObject>();
if (netObj && self && netObj == self) continue;
if (!netObj || !netObj.IsPlayerObject) return; // first solid hit is world geometry -> blocked
// 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);
var setup = netObj.GetComponent<NetworkPlayerSetup>();
if (setup && setup.Role == PlayerRole.Hider && !setup.IsFound)
match.ReportHit(netObj.OwnerClientId);
return; // the first non-self hit decides the shot
NetworkObject self = Manager.Player ? Manager.Player.GetComponent<NetworkObject>() : null;
for (int i = 0; i < count; i++)
{
var netObj = _hits[i].collider.GetComponentInParent<NetworkObject>();
if (netObj && self && netObj == self) continue; // skip own body
end = _hits[i].point; // first non-self solid hit stops the beam here (wall or avatar)
if (netObj && netObj.IsPlayerObject)
{
var setup = netObj.GetComponent<NetworkPlayerSetup>();
if (setup && setup.Role == PlayerRole.Hider && !setup.IsFound)
match.ReportHit(netObj.OwnerClientId);
}
break; // the first non-self hit decides the shot
}
}
// Fire the beam on every shot (hit or miss), drawn locally and replicated to the other clients.
// The muzzle sits slightly forward + right of the eye so the beam is off-centre (see MuzzleRightOffset).
Vector3 muzzle = origin + dir * MuzzleForwardOffset + cam.right * MuzzleRightOffset;
if (_setup) _setup.FireShot(muzzle, end);
}
private sealed class RaycastDistanceComparer : System.Collections.Generic.IComparer<RaycastHit>