Spawn converted hunters in place and smooth their reveal
Fix the in-place Conversion respawn snapping to the origin (0,0,0), which surfaced when a hider was tagged mid wall-climb. The owner holds authority over the NetworkTransform, so the server's Instantiate position is ignored on the owner. Deliver the tagged world position through a replicated _spawnOverride the server writes in OnNetworkSpawn, and have the owner re-assert it before PlayerCharacterController's first Update snapshots the kill-plane rescue target. This replaces the fragile SpawnedMidSeek phase check. Also hide a freshly converted hunter on other clients for a short settle window (conversionRevealDelay) so nobody sees it pop in or interpolate from the spawn frame; the shot hider is already despawned, then the hunter cleanly materializes in its place. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
24de7ef3da
commit
fe8f12b8bb
2 files changed
+92
-16
No files matched your search
@@ -682,6 +682,11 @@ public class MatchManager : MonoBehaviour
|
||||
current.Despawn(true);
|
||||
|
||||
var go = Instantiate(hunterPrefab, pos, rot);
|
||||
// Hand the tagged world position to the owner: the owner-authoritative transform ignores the server's
|
||||
// Instantiate pos, so without this the converted hunter snaps to the prefab origin (0,0,0). Must be set
|
||||
// BEFORE the spawn so OnNetworkSpawn can push it into the replicated _spawnOverride snapshot.
|
||||
var setup = go.GetComponent<NetworkPlayerSetup>();
|
||||
if (setup) setup.MarkConversionSpawn(pos);
|
||||
go.GetComponent<NetworkObject>().SpawnAsPlayerObject(clientId);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -45,6 +46,14 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
// The footprint is typically a chunk of its AABB, so a handful of tries hits the inside with near-certainty.
|
||||
private const int SpawnAreaSampleAttempts = 24;
|
||||
|
||||
[Header("Conversion (mode 2)")]
|
||||
[Tooltip("Seconds a freshly CONVERTED hunter (a hider shot during Seek in Conversion mode) stays hidden " +
|
||||
"on other clients after it spawns, so nobody sees it pop in or interpolate from the spawn frame. " +
|
||||
"The shot hider is despawned immediately, so there is a brief gap, then the hunter cleanly appears " +
|
||||
"in its place. Per-client and cosmetic; the owner is first-person so it is never gated.")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float conversionRevealDelay = 0.2f;
|
||||
|
||||
[Header("Role")]
|
||||
[Tooltip("Which side this avatar plays. Baked per prefab variant (Hider on the base/Hider prefab, " +
|
||||
"Hunter on the Hunter prefab); GameModeManager reads it on spawn to build this role's mode set.")]
|
||||
@@ -72,9 +81,28 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
/// target that is already out of play.</summary>
|
||||
public bool IsFound => _isFound.Value;
|
||||
|
||||
// Conversion-mode in-place respawn position. The owner holds authority over the NetworkTransform, so the
|
||||
// server's Instantiate position is NOT honoured on the owner (it would otherwise snap to the prefab origin,
|
||||
// 0,0,0). The server writes the tagged world position here in its OnNetworkSpawn so it rides the initial
|
||||
// spawn snapshot to the owner, which then restores it itself. The NaN sentinel means "no override" (a
|
||||
// normal lobby/match-start spawn) -> the owner runs its usual spawn-area/ring placement.
|
||||
private static readonly Vector3 NoSpawnOverride = new Vector3(float.NaN, float.NaN, float.NaN);
|
||||
private readonly NetworkVariable<Vector3> _spawnOverride =
|
||||
new NetworkVariable<Vector3>(NoSpawnOverride, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
|
||||
|
||||
// Server-only handoff from MatchManager.ConvertToHunter: the world position the converted hunter must keep.
|
||||
// Pushed into the replicated _spawnOverride during OnNetworkSpawn (where the variable is spawned and the
|
||||
// server is allowed to write it - a pre-spawn write is silently dropped); a plain field needs no replication.
|
||||
private Vector3? _conversionSpawnPos;
|
||||
|
||||
/// <summary>Server-only. Call on a freshly-instantiated Conversion hunter BEFORE spawning it so the owner
|
||||
/// restores the tagged position (the owner-authoritative transform ignores the server's Instantiate pos).</summary>
|
||||
public void MarkConversionSpawn(Vector3 worldPos) => _conversionSpawnPos = worldPos;
|
||||
|
||||
private Renderer[] _bodyRenderers; // skinned body mesh(es), toggled by the per-client visibility pass
|
||||
private bool _visibilityApplied;
|
||||
private bool _lastVisible = true;
|
||||
private bool _renderGate; // conversion: body force-hidden until the fresh hunter has settled into place
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
@@ -82,16 +110,24 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
_isFound.OnValueChanged += OnFoundChanged;
|
||||
if (_isFound.Value) ApplyFound(); // defensive: an avatar is never normally spawned already-found
|
||||
|
||||
// Server pushes a Conversion respawn's tagged position into the replicated override before the spawn
|
||||
// snapshot goes out, so the owner (which owns the transform) can restore it in its own OnNetworkSpawn.
|
||||
if (IsServer && _conversionSpawnPos.HasValue)
|
||||
_spawnOverride.Value = _conversionSpawnPos.Value;
|
||||
|
||||
var controller = GetComponent<PlayerCharacterController>();
|
||||
|
||||
if (IsOwner)
|
||||
{
|
||||
// A Conversion-mode tag respawns the hider as this Hunter variant mid-Seek; keep the server-set
|
||||
// position (it converts where it was tagged) by skipping the spawn-area/ring placement. Match-start
|
||||
// variants spawn while the sub-phase is still None, so they DO place; end-match lobby avatars spawn
|
||||
// after the sub-phase is cleared, so they place too. Only an in-place conversion lands here mid-Seek.
|
||||
if (!SpawnedMidSeek())
|
||||
// A Conversion-mode tag respawns the hider as this Hunter variant in place. The owner-authoritative
|
||||
// transform ignores the server's Instantiate position, so without help the owner snaps to the prefab
|
||||
// origin (0,0,0); the server delivers the tagged position via _spawnOverride, restored here. A normal
|
||||
// spawn (NaN override) runs the usual spawn-area/ring placement instead.
|
||||
Vector3 overridePos = _spawnOverride.Value;
|
||||
if (float.IsNaN(overridePos.x))
|
||||
PlaceAtSpawn();
|
||||
else
|
||||
ApplyOwnerPosition(overridePos);
|
||||
|
||||
var manager = FindFirstObjectByType<GameModeManager>(FindObjectsInactive.Include);
|
||||
if (manager)
|
||||
@@ -128,6 +164,19 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
// replicas only: an owner is never hidden from its own camera.
|
||||
foreach (var anim in GetComponentsInChildren<Animator>(true))
|
||||
anim.cullingMode = AnimatorCullingMode.AlwaysAnimate;
|
||||
|
||||
// Conversion mode: a hider shot during Seek is despawned and respawned in place as this Hunter
|
||||
// variant. Keep the fresh hunter hidden on this (remote) client for a short settle window so nobody
|
||||
// sees it appear or interpolate from the spawn frame - the shot hider is already gone, then the
|
||||
// hunter cleanly materializes in its place. Detected via the replicated override the server wrote
|
||||
// above; it rides the spawn snapshot, so it is already valid here. The owner returned early above
|
||||
// (first-person, own body self-hidden), so it is never gated.
|
||||
if (!float.IsNaN(_spawnOverride.Value.x))
|
||||
{
|
||||
_renderGate = true;
|
||||
SetBodyRenderers(false); // hide immediately, before the first UpdateVisibility runs
|
||||
StartCoroutine(RevealAfterSettle());
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
@@ -182,17 +231,30 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
bool found = _isFound.Value;
|
||||
if (IsOwner && !found) return;
|
||||
|
||||
bool visible = ComputeVisible(found);
|
||||
// While the conversion render gate is up, the body stays hidden no matter what ComputeVisible says.
|
||||
bool visible = !_renderGate && ComputeVisible(found);
|
||||
if (_visibilityApplied && visible == _lastVisible) return;
|
||||
SetBodyRenderers(visible);
|
||||
}
|
||||
|
||||
// Toggles every body renderer and records the applied state for the change-guard above.
|
||||
private void SetBodyRenderers(bool visible)
|
||||
{
|
||||
if (_bodyRenderers != null)
|
||||
foreach (var r in _bodyRenderers)
|
||||
if (r) r.enabled = visible;
|
||||
|
||||
_lastVisible = visible;
|
||||
_visibilityApplied = true;
|
||||
}
|
||||
|
||||
// Conversion: drop the render gate after the settle delay so the next visibility pass reveals the hunter
|
||||
// at its final, replicated position. Per-client and cosmetic (each client reveals on its own clock).
|
||||
private IEnumerator RevealAfterSettle()
|
||||
{
|
||||
yield return new WaitForSeconds(conversionRevealDelay);
|
||||
_renderGate = false;
|
||||
}
|
||||
|
||||
// Decides whether THIS avatar's body is rendered for the local viewer. Hunters and non-match avatars are
|
||||
// always visible; a Classic-found hider is always hidden; Resolution reveals everyone. The rest is the
|
||||
// hider-visibility matrix per mode:
|
||||
@@ -224,15 +286,6 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
return true;
|
||||
}
|
||||
|
||||
// True if this avatar is spawning during the SEEK sub-phase - i.e. a Conversion-mode tag respawning a
|
||||
// hider as the Hunter variant in place. Match-start and end-match respawns never happen during Seek
|
||||
// (start spawns before Hide, end spawns after the phase is cleared), so this uniquely flags a conversion.
|
||||
private static bool SpawnedMidSeek()
|
||||
{
|
||||
var match = MatchManager.Instance;
|
||||
return match && match.LocalSubPhase == MatchManager.MatchSubPhase.Seek;
|
||||
}
|
||||
|
||||
// True if THIS machine's local player is a Hunter role variant (so it must not see hiders during hide).
|
||||
private static bool LocalViewerIsHunter()
|
||||
{
|
||||
@@ -259,6 +312,24 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
PlaceOnSpawnRing();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-asserts the server-delivered Conversion position on the owner. The owner-authoritative
|
||||
/// NetworkTransform makes the owner's transform the source of truth, so the server's Instantiate
|
||||
/// position alone doesn't stick - the owner must set it here, BEFORE PlayerCharacterController's first
|
||||
/// Update snapshots the kill-plane rescue target (otherwise a hider tagged mid-wall-climb spawns, falls
|
||||
/// off the fresh hunter capsule below the kill plane, and is rescued to the still-zero default target =
|
||||
/// origin). The CharacterController is toggled around the write because it otherwise overrides direct
|
||||
/// transform writes (same pattern as PlaceOnSpawnRing/PlaceRandomlyInArea).
|
||||
/// </summary>
|
||||
private void ApplyOwnerPosition(Vector3 worldPos)
|
||||
{
|
||||
var cc = GetComponent<CharacterController>();
|
||||
bool reenable = cc && cc.enabled;
|
||||
if (cc) cc.enabled = false;
|
||||
transform.position = worldPos;
|
||||
if (cc) cc.enabled = reenable;
|
||||
}
|
||||
|
||||
// Resolves the spawn-area collider for this avatar: the Role-specific area (Hunter/Hider) takes priority,
|
||||
// then the shared spawnAreaTag area; a miss on both leaves it to the caller's client-id ring fallback.
|
||||
private bool TryGetSpawnArea(out Collider area)
|
||||
|
||||
Reference in new issue
Block a user