Add conversion statues shown in resolution phase
In Conversion mode, capture a snapshot (pose index + paint) of each hider at conversion time and materialize a frozen, painted statue at the found spot for the resolution phase, independent of the now-hunter avatar. - MatchStatue.cs (new): server-init pose/paint, network-driven pose then freeze, reveal-gate (hidden until paint applied), kills inherited sim. - PlayerPaintSync: _isStatue (sibling auto-detect) suppresses the three auto-behaviours so MatchStatue drives the paint flow deterministically. - MatchManager: statuePrefab; capture in ConvertToHunter; spawn on resolution enter; despawn + record clear on EndMatch/teardown/start. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
0a1e286a34
commit
0f108fce47
4 files changed
+232
-5
No files matched your search
@@ -0,0 +1,131 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using Chameleon.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A frozen, painted "statue" of a hider at the exact spot / stance / painting it held the moment it was
|
||||
/// CONVERTED into a hunter (Conversion mode only). <see cref="MatchManager"/> captures those on the
|
||||
/// conversion and spawns one statue per converted hider at the start of the Resolution phase, so every
|
||||
/// player sees where each hider was caught as a faithful representation (same skinned body, camo painting
|
||||
/// and fur) - independent of the now-hunter avatar, which has long since moved on.
|
||||
///
|
||||
/// Purely presentational: no controller, no collision, no gameplay role. It is a dedicated prefab (NOT a
|
||||
/// <see cref="NetworkPlayerSetup"/> avatar), so it never pollutes the scoreboard, per-client visibility
|
||||
/// matrix or resolution X-ray reveal - those all key off NetworkPlayerSetup, which the statue lacks.
|
||||
///
|
||||
/// Server-owned. Three replicated ingredients rebuild it identically on every client:
|
||||
/// <list type="bullet">
|
||||
/// <item><b>Transform</b> - the server Instantiates it at the captured pos/rot; the (server-authoritative)
|
||||
/// NetworkTransform replicates that.</item>
|
||||
/// <item><b>Painting</b> - rides the existing <see cref="PlayerPaintSync"/> snapshot transfer; this object
|
||||
/// drives the push explicitly (server loads the captured snapshot, then broadcasts it), and PlayerPaintSync
|
||||
/// suppresses its own auto pull/publish for a statue receiver so the two never race.</item>
|
||||
/// <item><b>Stance</b> - a single replicated pose index the pose controller reproduces (network-driven),
|
||||
/// then freezes.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class MatchStatue : NetworkBehaviour
|
||||
{
|
||||
[Tooltip("Seconds the captured stance is allowed to blend in (as the network-driven idle of that pose) " +
|
||||
"before the rig is frozen on that frame. Keeps the pose from snapping in mid-blend.")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float poseSettle = 0.35f;
|
||||
|
||||
// Server-only handoff from MatchManager.SpawnConversionStatues, consumed in OnNetworkSpawn / the first
|
||||
// ready frame. The paint may be null (capture failed) -> the statue stays on the blank seed look.
|
||||
private byte[] _serverPaint;
|
||||
private int _serverPoseIndex;
|
||||
|
||||
// The captured stance, replicated so every client poses the rig identically (server-written on spawn).
|
||||
private readonly NetworkVariable<int> _poseIndex =
|
||||
new NetworkVariable<int>(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
|
||||
|
||||
private MeshPainter _painter;
|
||||
private PlayerPaintSync _paintSync;
|
||||
private PlayerPoseController _pose;
|
||||
private SkinnedMeshRenderer[] _bodyRenderers;
|
||||
private FurShellRenderer[] _furRenderers;
|
||||
|
||||
private bool _hostPaintDone; // server: the captured snapshot has been loaded + pushed to peers (one-shot)
|
||||
private bool _bodyShown; // reveal-gate change guard
|
||||
private bool _frozen; // pose freeze applied
|
||||
private float _freezeAt; // Time.time deadline that pins the settled pose
|
||||
|
||||
/// <summary>Server-only. Call on a freshly-instantiated statue BEFORE spawning it: the captured painting
|
||||
/// snapshot (may be null) and the stance pose index to reproduce.</summary>
|
||||
public void ServerInit(int poseIndex, byte[] paint)
|
||||
{
|
||||
_serverPoseIndex = poseIndex;
|
||||
_serverPaint = paint;
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
_painter = GetComponentInChildren<MeshPainter>(true);
|
||||
_paintSync = GetComponentInChildren<PlayerPaintSync>(true);
|
||||
_pose = GetComponentInChildren<PlayerPoseController>(true);
|
||||
_bodyRenderers = GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
||||
_furRenderers = GetComponentsInChildren<FurShellRenderer>(true);
|
||||
|
||||
// Pure presentation: kill any inherited simulation so nothing moves/falls or fights the held pose.
|
||||
// Done before the disabled controllers' first Update (OnNetworkSpawn runs during the spawn), so no
|
||||
// gravity frame slips through.
|
||||
var controller = GetComponent<PlayerCharacterController>();
|
||||
if (controller) controller.enabled = false;
|
||||
var cc = GetComponent<CharacterController>();
|
||||
if (cc) cc.enabled = false;
|
||||
|
||||
if (IsServer) _poseIndex.Value = _serverPoseIndex;
|
||||
|
||||
// Hide the body until the painting has actually landed locally, so no client flashes the blank seed
|
||||
// material before its snapshot applies (mirrors the avatar's receiver-side reveal gate).
|
||||
SetBodyVisible(false);
|
||||
|
||||
// Reproduce the captured stance: network-driven so no local input / capsule / camera work runs; the
|
||||
// idle of that pose plays for a short settle, then Update freezes the rig on that frame.
|
||||
if (_pose)
|
||||
{
|
||||
_pose.EnableNetworkDrive();
|
||||
_pose.SetNetworkState(_poseIndex.Value, PlayerPoseController.Locomotion.Idle);
|
||||
}
|
||||
_freezeAt = Time.time + Mathf.Max(0f, poseSettle);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Server owner: once the painter is set up, load the captured snapshot and push it to every peer over
|
||||
// the existing PlayerPaintSync transfer. Always publish a baseline (even a null capture -> the blank
|
||||
// seed) so a client's receiver-side gate opens and the body can reveal. One-shot.
|
||||
if (IsServer && !_hostPaintDone && _painter && _painter.IsPaintReady && _paintSync)
|
||||
{
|
||||
_hostPaintDone = true;
|
||||
if (_serverPaint != null) _painter.LoadPaintFromSnapshot(_serverPaint);
|
||||
_paintSync.BroadcastLoadedPaint();
|
||||
_serverPaint = null;
|
||||
}
|
||||
|
||||
// Reveal once the painting is applied locally: the server right after its load/publish, a client once
|
||||
// its received snapshot has caught up to the published baseline.
|
||||
bool paintReady = IsServer ? _hostPaintDone : (_paintSync && _paintSync.PaintRevealReady);
|
||||
if (paintReady && !_bodyShown) SetBodyVisible(true);
|
||||
|
||||
// Re-assert the stance until it has settled (covers a pose index arriving a frame late), then freeze.
|
||||
if (_pose && !_frozen)
|
||||
{
|
||||
_pose.SetNetworkState(_poseIndex.Value, PlayerPoseController.Locomotion.Idle);
|
||||
if (Time.time >= _freezeAt) { _pose.Frozen = true; _frozen = true; }
|
||||
}
|
||||
}
|
||||
|
||||
// Toggles the skinned body AND the custom-pass fur together (the fur is not a Renderer, so the skinned
|
||||
// toggle misses it) - same atomic reveal the avatar uses, so a statue never shows stale pooled fur.
|
||||
private void SetBodyVisible(bool visible)
|
||||
{
|
||||
_bodyShown = visible;
|
||||
if (_bodyRenderers != null)
|
||||
foreach (var r in _bodyRenderers) if (r) r.enabled = visible;
|
||||
if (_furRenderers != null)
|
||||
foreach (var f in _furRenderers) if (f) f.enabled = visible;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user