Add the hide/seek/resolution match state machine
Drive the match through three server-authoritative phases (hide, seek, resolution) with per-phase durations, broadcasting the active phase and countdown to clients over the existing named-message infrastructure so MatchManager stays a plain MonoBehaviour and Main keeps zero in-scene network objects. During hide, hider meshes are culled on hunter clients only (each client computes visibility locally from role, phase and found state). During seek, hunters shoot a single raycast from the first-person camera centre (left click, cooldown-gated); a validated hit marks the hider found, which hides its mesh for everyone and binds the owner to spectator freecam. Seek ends early once every hider is found, then resolution runs before returning to the lobby. Force AlwaysAnimate on remote replicas so the animator keeps writing bone transforms while the body is hidden or off-screen. The Paintman animator is authored CullUpdateTransforms, so a culled renderer froze the pose on the rest pose and the skinned collider baked that frozen pose, landing replicated paint stamps on the wrong body location and leaving the painting desynced after hiders were shown again. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
95effb91da
commit
5b3fe1d3d1
5 files changed
+555
-3
No files matched your search
@@ -62,8 +62,26 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
/// neutral lobby Player prefab (full mode set). Baked per prefab variant.</summary>
|
||||
public bool ScopedModes => scopedModes;
|
||||
|
||||
// Server-authoritative "this hider has been shot/found". Server-write (a hunter's hit is validated on
|
||||
// the server, not by the hider's owner); read by everyone. When it flips true the body is hidden on
|
||||
// every client and the owning client drops into free-cam-only spectating. Stays false on hunters/lobby.
|
||||
private readonly NetworkVariable<bool> _isFound =
|
||||
new NetworkVariable<bool>(false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
|
||||
|
||||
/// <summary>True once a hunter has found this hider (replicated). Read by the shoot raycast to skip a
|
||||
/// target that is already out of play.</summary>
|
||||
public bool IsFound => _isFound.Value;
|
||||
|
||||
private Renderer[] _bodyRenderers; // skinned body mesh(es), toggled by the per-client visibility pass
|
||||
private bool _visibilityApplied;
|
||||
private bool _lastVisible = true;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
_bodyRenderers = GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
||||
_isFound.OnValueChanged += OnFoundChanged;
|
||||
if (_isFound.Value) ApplyFound(); // defensive: an avatar is never normally spawned already-found
|
||||
|
||||
var controller = GetComponent<PlayerCharacterController>();
|
||||
|
||||
if (IsOwner)
|
||||
@@ -92,6 +110,106 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
// (ApplyNetworkStamp -> DrawBrushGpu) rasterizes the baker's live, per-frame BakedMesh to map a
|
||||
// 3D brush placement into UV space; freezing the bake would mis-map replicated paint onto any
|
||||
// non-rest pose. Its bake is the necessary cost of correct remote paint, not wasted work.
|
||||
|
||||
// The rig must keep evaluating even while this body is HIDDEN on the local client. During the hide
|
||||
// phase a hider's body is hidden on hunter clients (UpdateVisibility sets renderer.enabled = false),
|
||||
// and a remote can also sit off-screen. The avatar's Animator is authored as CullUpdateTransforms,
|
||||
// which STOPS writing the bone transforms whenever its renderer isn't visible. PlayerPoseController
|
||||
// drives the rig through a PlayableGraph on this Animator and resets un-keyed bones to rest every
|
||||
// frame, so a culled Animator leaves the rig frozen at the rest/T-pose. SkinnedColliderBaker then
|
||||
// bakes that wrong pose, and any paint stamp replicated during the hidden window lands on the wrong
|
||||
// body part - a desync that persists once the body is shown again. Forcing AlwaysAnimate keeps the
|
||||
// pose (and thus the baked mesh paint replay rasterizes) correct regardless of visibility. Remote
|
||||
// replicas only: an owner is never hidden from its own camera.
|
||||
foreach (var anim in GetComponentsInChildren<Animator>(true))
|
||||
anim.cullingMode = AnimatorCullingMode.AlwaysAnimate;
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
_isFound.OnValueChanged -= OnFoundChanged;
|
||||
}
|
||||
|
||||
/// <summary>Server-only: flag this hider as found by a hunter. Replicates to every client (the body is
|
||||
/// hidden for all; the owner drops into free-cam-only spectating). Idempotent.</summary>
|
||||
public void ServerMarkFound()
|
||||
{
|
||||
if (!IsServer || _isFound.Value) return;
|
||||
_isFound.Value = true; // OnValueChanged -> ApplyFound on every machine, including this server/host
|
||||
}
|
||||
|
||||
private void OnFoundChanged(bool previous, bool current)
|
||||
{
|
||||
if (current) ApplyFound();
|
||||
}
|
||||
|
||||
// A found hider leaves play: the per-client visibility pass hides its body, its baked collider is dropped
|
||||
// (so the invisible body neither blocks nor can be hit again), and on the owning client it switches to
|
||||
// free-cam-only so the player can still watch the rest of the match. Idempotent.
|
||||
private void ApplyFound()
|
||||
{
|
||||
var baker = GetComponentInChildren<SkinnedColliderBaker>(true);
|
||||
if (baker) baker.enabled = false;
|
||||
var meshCol = GetComponentInChildren<MeshCollider>(true);
|
||||
if (meshCol) meshCol.enabled = false;
|
||||
|
||||
if (IsOwner)
|
||||
{
|
||||
var controller = GetComponent<PlayerCharacterController>();
|
||||
if (controller) controller.enabled = false;
|
||||
var cc = GetComponent<CharacterController>();
|
||||
if (cc) cc.enabled = false;
|
||||
|
||||
var manager = FindFirstObjectByType<GameModeManager>(FindObjectsInactive.Include);
|
||||
if (manager) manager.BindSpectator();
|
||||
}
|
||||
}
|
||||
|
||||
private void Update() => UpdateVisibility();
|
||||
|
||||
// Per-client visibility: each machine decides whether to render THIS avatar's body from the LOCAL
|
||||
// viewer's perspective. A found hider is hidden for everyone; during HIDE, hiders are hidden on hunter
|
||||
// clients only (the spec - hunters can't see hiders yet). The local owner's own body is left alone unless
|
||||
// found, so this never fights the FirstPersonController's own-body self-hide.
|
||||
private void UpdateVisibility()
|
||||
{
|
||||
bool found = _isFound.Value;
|
||||
if (IsOwner && !found) return;
|
||||
|
||||
bool visible = ComputeVisible(found);
|
||||
if (_visibilityApplied && visible == _lastVisible) return;
|
||||
|
||||
if (_bodyRenderers != null)
|
||||
foreach (var r in _bodyRenderers)
|
||||
if (r) r.enabled = visible;
|
||||
|
||||
_lastVisible = visible;
|
||||
_visibilityApplied = true;
|
||||
}
|
||||
|
||||
private bool ComputeVisible(bool found)
|
||||
{
|
||||
if (found) return false;
|
||||
|
||||
var match = MatchManager.Instance;
|
||||
if (match && match.MatchActive
|
||||
&& match.LocalSubPhase == MatchManager.MatchSubPhase.Hide
|
||||
&& role == PlayerRole.Hider
|
||||
&& LocalViewerIsHunter())
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// True if THIS machine's local player is a Hunter role variant (so it must not see hiders during hide).
|
||||
private static bool LocalViewerIsHunter()
|
||||
{
|
||||
var nm = NetworkManager.Singleton;
|
||||
if (!nm || nm.LocalClient == null) return false;
|
||||
var po = nm.LocalClient.PlayerObject;
|
||||
if (!po) return false;
|
||||
var setup = po.GetComponent<NetworkPlayerSetup>();
|
||||
return setup && setup.ScopedModes && setup.Role == PlayerRole.Hunter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in new issue
Block a user