Docs: reduce over-documentation across gameplay scripts
Trim restated narration, dead commented-out code, and oversized class headers; keep all why-comments (authority, timing, edge cases), tooltips, and serialization caveats. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
882d30797e
commit
066e308cde
21 files changed
+478
-902
No files matched your search
+54
-114
@@ -7,52 +7,22 @@ using UnityEngine.SceneManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Server-authoritative match flow. Lives on the persistent NetworkManager object in the boot scene
|
||||
/// (Main), which stays loaded the whole session (NetworkManager, cameras, HUD). The gameplay scenes
|
||||
/// (Lobby, Test Map) are loaded ADDITIVELY through NGO's scene manager so Main is never torn down.
|
||||
/// (Main); the gameplay scenes (Lobby, Test Map) are loaded ADDITIVELY through NGO's scene manager so
|
||||
/// Main (cameras/HUD/managers) is never torn down.
|
||||
///
|
||||
/// Two design pillars make the persistent-Main + additive-gameplay model actually work:
|
||||
/// Two design pillars make this model work:
|
||||
/// 1. Clients sync ADDITIVELY (<see cref="NetworkSceneManager.SetClientSynchronizationMode"/>). The NGO
|
||||
/// default (Single) would unload the connecting client's own Main and crash the player-object scene
|
||||
/// migration; additive keeps the client's Main and layers the server's scenes on top. Must be set on
|
||||
/// the server before any client connects (auto-propagated to clients).
|
||||
/// 2. Players are spawned by THIS manager, not NGO auto-spawn - that would spawn into whatever scene is
|
||||
/// active on connect (for the host: floor-less Main, while the additive Lobby load is in flight).
|
||||
/// <see cref="NetworkConfig.PlayerPrefab"/> is nulled and avatars are spawned manually once the
|
||||
/// gameplay scene is loaded. Main holds zero network objects.
|
||||
///
|
||||
/// 1. Clients sync ADDITIVELY (<see cref="NetworkSceneManager.SetClientSynchronizationMode"/> =
|
||||
/// <see cref="LoadSceneMode.Additive"/>). The NGO default (Single) would unload every scene the
|
||||
/// connecting client already has - including its own Main (cameras/HUD/managers) - and reload the
|
||||
/// server's active scene in single mode, which also crashes the player-object scene migration
|
||||
/// (MoveGameObjectToScene "Destination scene is not valid"). Additive keeps the client's Main and
|
||||
/// layers the server's scenes on top. Must be set on the server before any client connects; it is
|
||||
/// auto-propagated to clients via the synchronize event.
|
||||
///
|
||||
/// 2. Players are spawned by THIS manager, not by NGO auto-spawn. NGO would auto-spawn the player the
|
||||
/// instant a peer connects, into whatever scene is active right then - for the host that is Main
|
||||
/// (the additive Lobby load is still in flight), which has no floor, so the host falls into the
|
||||
/// void. We disable auto-spawn (null out <see cref="NetworkConfig.PlayerPrefab"/>) and instead
|
||||
/// spawn the neutral Player prefab into the gameplay scene only once it is loaded, so avatars always
|
||||
/// land on a floor. Main therefore holds zero network objects.
|
||||
///
|
||||
/// Flow: server start -> additive sync mode set -> Lobby loaded additively -> Player spawned for every
|
||||
/// connected peer (full mode set, including First Person). Late joiners spawn on connect into the
|
||||
/// already-loaded scene. The host walks into the Start Match trigger -> <see cref="StartMatch"/>: a
|
||||
/// configurable number of random connected clients are rolled Hunter (<see cref="hunterCount"/>,
|
||||
/// default 1), the rest Hider; the Lobby is unloaded and Test Map loaded
|
||||
/// (strictly serialized - an NGO constraint, only one scene op in flight); then every player object is
|
||||
/// despawned and respawned as its role's prefab variant.
|
||||
///
|
||||
/// Match-control AUTHORITY: the client allowed to start (walk into the trigger) and end the match.
|
||||
/// A host has its own player, so the host is the authority. A DEDICATED server (StartServer, no local
|
||||
/// player) hands authority to the FIRST client that connects, so a server nobody plays on is still
|
||||
/// controllable. The id is server-local; the designated remote client is told via a named message so it
|
||||
/// can ask the server (which validates the sender) to end. If the authority leaves, it is reassigned to
|
||||
/// another connected client.
|
||||
///
|
||||
/// End / join / teardown:
|
||||
/// - The match authority ends the match via <see cref="RequestEndMatch"/> -> <see cref="EndMatch"/>
|
||||
/// (driven by the online session panel's "Back to Lobby" button - Esc opens that panel rather than
|
||||
/// ending directly). Test Map is swapped back to Lobby and everyone is respawned as the neutral Player.
|
||||
/// The host/server ends directly; a remote authority client requests it over a named message that only
|
||||
/// the authority's id is honored for. A non-authority client's panel button is a plain disconnect.
|
||||
/// - A client that joins MID-MATCH spawns as a SPECTATOR: a dedicated EMPTY prefab (no body/collider)
|
||||
/// bound to free-cam-only, so it flies around the running map and watches but does not play. It
|
||||
/// respawns as a neutral lobby Player when the match ends.
|
||||
/// - On shutdown / disconnect (<see cref="ReturnToMainOnly"/>) every gameplay scene is unloaded so only
|
||||
/// Main is left loaded & active. NGO's Dispose does NOT unload additively-synced scenes, so we do it.
|
||||
/// Match-control AUTHORITY (may start/end the match): the host; on a DEDICATED server the first client
|
||||
/// to connect (told via named message, validated server-side on use, reassigned if it leaves).
|
||||
/// Mid-match joiners spawn as empty free-cam SPECTATOR avatars until the match ends.
|
||||
///
|
||||
/// Not a NetworkBehaviour by design: every state change rides existing NGO replication (additive scene
|
||||
/// sync + player-object spawn/despawn), so there is no in-scene NetworkObject hash matching or custom
|
||||
@@ -165,10 +135,8 @@ public class MatchManager : MonoBehaviour
|
||||
/// <summary>Server-local phase guard. Not replicated - see class summary.</summary>
|
||||
public MatchPhase Phase { get; private set; } = MatchPhase.Lobby;
|
||||
|
||||
// --- Replicated sub-phase mirror (every machine, set by the server's broadcast; the server sets its
|
||||
// own copy directly in SetPhaseServer). Cosmetic countdown runs off the local clock from the broadcast
|
||||
// duration; the authoritative transition is always server-driven, so any drift self-corrects on the
|
||||
// next phase message. ---
|
||||
// Replicated sub-phase mirror (set by the server's broadcast; the server sets its own copy directly).
|
||||
// The countdown is cosmetic/local; the authoritative transition is server-driven, so drift self-corrects.
|
||||
private MatchSubPhase _subPhase = MatchSubPhase.None;
|
||||
private float _phaseEndTime; // Time.time when the current sub-phase ends (for the HUD countdown)
|
||||
private float _phaseDuration;
|
||||
@@ -195,25 +163,21 @@ public class MatchManager : MonoBehaviour
|
||||
private bool _sceneReady; // server-only: a gameplay scene (lobby/map) is loaded & active, so it is safe to spawn avatars
|
||||
private bool _cleanedUp; // guards the shutdown scene-cleanup so a host (server+client stop) doesn't run it twice
|
||||
|
||||
// Role pick uses its OWN generator, seeded once from real entropy. UnityEngine.Random is a single
|
||||
// engine-wide generator whose state at match-start can sit at the same point every fresh play
|
||||
// session (nothing random consumed before it), which made the "random" hunter land on the same
|
||||
// client every time. An independent System.Random decouples the pick from that shared global state.
|
||||
// Own generator, seeded from real entropy: UnityEngine.Random's engine-wide state sits at the same
|
||||
// point every fresh session (nothing consumed before match start), which made the "random" hunter
|
||||
// land on the same client every time.
|
||||
private readonly System.Random _roleRng = new System.Random(System.Guid.NewGuid().GetHashCode());
|
||||
|
||||
// Painting carry across an avatar respawn (scene switch). Per-machine: the LOCAL owner's painting,
|
||||
// parked here by its despawning PlayerPaintSync (that component is gone before the replacement spawns)
|
||||
// and consumed once by the next owner avatar. This manager is persistent in Main, so it bridges the
|
||||
// despawn->spawn gap. Cleared on teardown (ReturnToMainOnly) so a fresh session never inherits a
|
||||
// previous one's painting; a normal respawn keeps Main loaded, so the parked copy survives it.
|
||||
// The LOCAL owner's painting, parked across an avatar respawn (scene switch) by its despawning
|
||||
// PlayerPaintSync and consumed once by the next owner avatar - this manager is persistent, so it
|
||||
// bridges the despawn->spawn gap. Cleared on teardown so a fresh session never inherits it.
|
||||
private byte[] _carriedPaint;
|
||||
|
||||
// Match-control authority. Server-local: which client may start/end the match. Host -> the host
|
||||
// itself (set at server start); dedicated server -> the first client to connect (set on connect).
|
||||
// Match-control authority. Server-local: which client may start/end the match. Host -> itself;
|
||||
// dedicated server -> the first client to connect.
|
||||
private bool _hasAuthorityClient;
|
||||
private ulong _authorityClientId;
|
||||
// Client-side mirror, set by the grant message below: this machine's local client is the authority,
|
||||
// so it reads Esc and asks the server to end the match.
|
||||
// Client-side mirror of the grant message: this machine may ask the server to end the match.
|
||||
private bool _isLocalAuthority;
|
||||
|
||||
// Named messages (CustomMessagingManager). One bridges server->authority-client ("you have authority"),
|
||||
@@ -239,17 +203,15 @@ public class MatchManager : MonoBehaviour
|
||||
_net = GetComponent<NetworkManager>();
|
||||
if (!_net) _net = FindFirstObjectByType<NetworkManager>();
|
||||
|
||||
// Disable NGO auto-spawn (decided purely by PlayerPrefab != null on both host and remote paths).
|
||||
// We keep the reference and spawn manually once a floored scene exists. Runtime-only mutation -
|
||||
// the scene asset still carries PlayerPrefab, so there is no asset diff.
|
||||
// Disable NGO auto-spawn (decided purely by PlayerPrefab != null); keep the reference and spawn
|
||||
// manually once a floored scene exists. Runtime-only mutation - no asset diff.
|
||||
if (_net != null)
|
||||
{
|
||||
_playerPrefab = _net.NetworkConfig.PlayerPrefab;
|
||||
_net.NetworkConfig.PlayerPrefab = null;
|
||||
}
|
||||
|
||||
// Placeholder on-screen match HUD (phase + countdown + hunter crosshair). Added at runtime so it
|
||||
// needs no scene/prefab authoring; trivially swapped for a proper Canvas later. Persistent like this.
|
||||
// Placeholder on-screen match HUD, added at runtime so it needs no scene/prefab authoring.
|
||||
if (!GetComponent<MatchHud>()) gameObject.AddComponent<MatchHud>();
|
||||
}
|
||||
|
||||
@@ -298,8 +260,7 @@ public class MatchManager : MonoBehaviour
|
||||
Phase = MatchPhase.Lobby;
|
||||
_sceneReady = false;
|
||||
|
||||
// Match-control authority. A host has its own player, so it is the authority right away. A
|
||||
// dedicated server has no local player; the first client to connect claims it (OnClientConnected).
|
||||
// Host -> authority right away; dedicated server -> the first client to connect claims it.
|
||||
_hasAuthorityClient = false;
|
||||
if (_net.IsHost) { _authorityClientId = _net.LocalClientId; _hasAuthorityClient = true; }
|
||||
|
||||
@@ -327,8 +288,7 @@ public class MatchManager : MonoBehaviour
|
||||
_isLocalAuthority = false;
|
||||
HookSceneManager();
|
||||
|
||||
// Listen for the server granting this machine match authority (dedicated-server case) and for the
|
||||
// sub-phase broadcasts. Registered here (client start) so the handlers exist before the server sends.
|
||||
// Registered at client start so the handlers exist before the server sends.
|
||||
if (_net.CustomMessagingManager != null)
|
||||
{
|
||||
_net.CustomMessagingManager.RegisterNamedMessageHandler(MsgAuthorityGranted, OnAuthorityGrantedMessage);
|
||||
@@ -391,22 +351,19 @@ public class MatchManager : MonoBehaviour
|
||||
yield return SceneOp(() => _net.SceneManager.LoadScene(lobbySceneName, LoadSceneMode.Additive),
|
||||
SceneEventType.LoadEventCompleted, lobbySceneName);
|
||||
|
||||
// Lobby is loaded and active on the server: now it is safe to spawn the already-connected
|
||||
// peers (the host, plus anyone who connected while the load was in flight).
|
||||
// Scene is up - safe to spawn the already-connected peers (host + anyone who joined mid-load).
|
||||
_sceneReady = true;
|
||||
foreach (ulong id in new List<ulong>(_net.ConnectedClientsIds)) SpawnFor(id, _playerPrefab, lobbySpawnCenter);
|
||||
}
|
||||
|
||||
// Server-side: a peer finished connecting. If a gameplay scene is already up, spawn it now;
|
||||
// otherwise LoadLobbyRoutine (lobby) or RespawnAll (mid-transition) covers it (the PlayerObject
|
||||
// guard in SpawnFor prevents a double spawn).
|
||||
// Server-side: a peer finished connecting. If a gameplay scene is already up, spawn it now; otherwise
|
||||
// the load/respawn routine covers it (the PlayerObject guard in SpawnFor prevents a double spawn).
|
||||
private void OnClientConnected(ulong clientId)
|
||||
{
|
||||
if (_net == null || !_net.IsServer) return;
|
||||
|
||||
// Dedicated-server case: the first client to connect becomes the match authority (a host already
|
||||
// claimed it at server start, so this only fires for a server with no local player). Tell the
|
||||
// client over a named message so it reads Esc locally; the trigger picks it up server-side.
|
||||
// claimed it at server start). Told via named message so it reads Esc locally.
|
||||
if (!_hasAuthorityClient)
|
||||
{
|
||||
_authorityClientId = clientId;
|
||||
@@ -418,9 +375,8 @@ public class MatchManager : MonoBehaviour
|
||||
// During a Lobby<->Test Map swap the active scene briefly falls back to Main (no floor); defer
|
||||
// and let the respawn loop spawn this joiner once the target scene is loaded.
|
||||
if (!_sceneReady || Phase == MatchPhase.Transitioning) return;
|
||||
// Mid-match joiner: spawn an empty SPECTATOR avatar (free-cam only) into the running map. They can
|
||||
// fly around and watch but don't play. When the host ends the match, EndMatchSequence's respawn
|
||||
// loop covers every connected client, so they turn into a neutral lobby Player with everyone else.
|
||||
// Mid-match joiner: empty SPECTATOR avatar (free-cam only); the end-of-match respawn loop turns
|
||||
// it into a neutral lobby Player with everyone else.
|
||||
if (Phase == MatchPhase.InMatch)
|
||||
{
|
||||
SpawnFor(clientId, spectatorPrefab, matchSpawnCenter);
|
||||
@@ -431,9 +387,8 @@ public class MatchManager : MonoBehaviour
|
||||
SpawnFor(clientId, _playerPrefab, lobbySpawnCenter);
|
||||
}
|
||||
|
||||
// Server-side: if the match authority leaves, hand authority to another connected client (lowest
|
||||
// remaining id) so a dedicated-server match stays controllable. No-op in the host case (the host
|
||||
// holds authority and only "leaves" on shutdown, which teardown handles).
|
||||
// Server-side: if the match authority leaves, hand authority to another connected client so a
|
||||
// dedicated-server match stays controllable.
|
||||
private void OnClientDisconnected(ulong clientId)
|
||||
{
|
||||
if (_net == null || !_net.IsServer) return;
|
||||
@@ -489,9 +444,8 @@ public class MatchManager : MonoBehaviour
|
||||
|
||||
Phase = MatchPhase.Transitioning;
|
||||
|
||||
// Roll N distinct random Hunters (clamped to the player count); everyone else is a Hider.
|
||||
// hunterCount 0 -> all Hiders, >= player count -> all Hunters. Partial Fisher-Yates: swap a
|
||||
// random not-yet-picked id into each leading slot, then take that slot as a Hunter.
|
||||
// Roll N distinct random Hunters via partial Fisher-Yates (swap a random not-yet-picked id into
|
||||
// each leading slot); hunterCount 0 -> all Hiders, >= player count -> all Hunters.
|
||||
int wanted = Mathf.Clamp(hunterCount, 0, ids.Count);
|
||||
var hunterIds = new HashSet<ulong>();
|
||||
for (int i = 0; i < wanted; i++)
|
||||
@@ -511,19 +465,16 @@ public class MatchManager : MonoBehaviour
|
||||
{
|
||||
var sm = _net.SceneManager;
|
||||
|
||||
// 1. Unload the lobby (strictly serialized: one scene op in flight at a time).
|
||||
// Strictly serialized: NGO allows only one scene op in flight at a time.
|
||||
Scene lobby = SceneManager.GetSceneByName(lobbySceneName);
|
||||
if (lobby.IsValid() && lobby.isLoaded)
|
||||
yield return SceneOp(() => sm.UnloadScene(lobby), SceneEventType.UnloadEventCompleted, lobbySceneName);
|
||||
|
||||
// 2. Load the match map.
|
||||
yield return SceneOp(() => sm.LoadScene(matchSceneName, LoadSceneMode.Additive),
|
||||
SceneEventType.LoadEventCompleted, matchSceneName);
|
||||
|
||||
// 3. Respawn every player as its rolled role's prefab variant.
|
||||
RespawnAll(id => hunterIds.Contains(id) ? hunterPrefab : hiderPrefab, matchSpawnCenter);
|
||||
|
||||
// The hiders the seek phase has to find: every connected client that wasn't rolled a hunter.
|
||||
_pendingHiders.Clear();
|
||||
foreach (ulong id in _net.ConnectedClientsIds)
|
||||
if (!hunterIds.Contains(id)) _pendingHiders.Add(id);
|
||||
@@ -588,10 +539,8 @@ public class MatchManager : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// Scene props tagged "PreMatch" exist only while players hide; the moment seek starts they must be gone
|
||||
// on every machine. ApplyPhaseLocal runs on all of them (server directly, clients via the phase message),
|
||||
// so each destroys its own local copies -- these are plain scene geometry, not NetworkObjects, so there
|
||||
// is nothing to replicate.
|
||||
// Runs on every machine (server directly, clients via the phase message); the tagged props are plain
|
||||
// scene geometry, not NetworkObjects, so each machine destroys its own local copies.
|
||||
private static void DestroyPreMatchObjects()
|
||||
{
|
||||
var props = GameObject.FindGameObjectsWithTag(PreMatchTag);
|
||||
@@ -599,9 +548,8 @@ public class MatchManager : MonoBehaviour
|
||||
Destroy(props[i]);
|
||||
}
|
||||
|
||||
// Sends a sub-phase (byte) + its remaining duration (float) + the match mode (byte) to one client, or to
|
||||
// all clients except the server itself (which applied it directly in SetPhaseServer, so it must not
|
||||
// double-apply via loopback).
|
||||
// Sends the sub-phase + duration + mode to one client, or to all clients except the server itself
|
||||
// (which applied it directly in SetPhaseServer, so it must not double-apply via loopback).
|
||||
private void BroadcastPhase(MatchSubPhase phase, float duration, MatchMode mode, ulong targetClient, bool toAll)
|
||||
{
|
||||
var cmm = _net != null ? _net.CustomMessagingManager : null;
|
||||
@@ -613,8 +561,6 @@ public class MatchManager : MonoBehaviour
|
||||
writer.WriteValueSafe((byte)mode);
|
||||
if (toAll)
|
||||
{
|
||||
// Every client except the server itself (the host already applied it directly in
|
||||
// SetPhaseServer, so it must not double-apply via loopback). One send, no writer reuse.
|
||||
_phaseRecipients.Clear();
|
||||
foreach (ulong id in _net.ConnectedClientsIds)
|
||||
if (id != _net.LocalClientId) _phaseRecipients.Add(id);
|
||||
@@ -694,12 +640,10 @@ public class MatchManager : MonoBehaviour
|
||||
Debug.Log($"[MatchManager] Hider {hiderClientId} {verb} - {_pendingHiders.Count} hider(s) left.", this);
|
||||
}
|
||||
|
||||
// Server-only (Conversion mode). Despawns the tagged hider's avatar and respawns it as the Hunter prefab
|
||||
// variant for the same client, AT THE SAME position/rotation - it converts where it was tagged, no teleport.
|
||||
// The fresh Hunter's owner skips its spawn-placement because the match is mid-Seek (see NetworkPlayerSetup),
|
||||
// so the server-set position sticks. Same despawn-then-spawn pattern as RespawnAll; the owner's painting
|
||||
// carries across via the per-machine paint carry. After this the client plays as a full hunter (size,
|
||||
// sprint, first-person, shooting) with no special-case runtime state.
|
||||
// Server-only (Conversion mode). Despawns the tagged hider and respawns it as the Hunter variant for
|
||||
// the same client AT THE SAME position/rotation - no teleport. The owner skips its spawn placement
|
||||
// mid-Seek (see NetworkPlayerSetup) so the server-set position sticks; painting carries across via
|
||||
// the paint carry. Afterwards the client is a full hunter with no special-case runtime state.
|
||||
private void ConvertToHunter(ulong clientId, NetworkObject current)
|
||||
{
|
||||
if (!hunterPrefab)
|
||||
@@ -712,9 +656,8 @@ 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.
|
||||
// The owner-authoritative transform ignores the server's Instantiate pos - hand the tagged position
|
||||
// to the owner, BEFORE the spawn, so it rides the replicated _spawnOverride snapshot.
|
||||
var setup = go.GetComponent<NetworkPlayerSetup>();
|
||||
if (setup) setup.MarkConversionSpawn(pos);
|
||||
go.GetComponent<NetworkObject>().SpawnAsPlayerObject(clientId);
|
||||
@@ -750,16 +693,14 @@ public class MatchManager : MonoBehaviour
|
||||
{
|
||||
var sm = _net.SceneManager;
|
||||
|
||||
// 1. Unload the match map (strictly serialized: one scene op in flight at a time).
|
||||
// Strictly serialized: NGO allows only one scene op in flight at a time.
|
||||
Scene map = SceneManager.GetSceneByName(matchSceneName);
|
||||
if (map.IsValid() && map.isLoaded)
|
||||
yield return SceneOp(() => sm.UnloadScene(map), SceneEventType.UnloadEventCompleted, matchSceneName);
|
||||
|
||||
// 2. Reload the lobby.
|
||||
yield return SceneOp(() => sm.LoadScene(lobbySceneName, LoadSceneMode.Additive),
|
||||
SceneEventType.LoadEventCompleted, lobbySceneName);
|
||||
|
||||
// 3. Respawn everyone as the neutral lobby Player (covers held mid-match joiners too).
|
||||
RespawnAll(_ => _playerPrefab, lobbySpawnCenter);
|
||||
|
||||
Phase = MatchPhase.Lobby;
|
||||
@@ -826,9 +767,8 @@ public class MatchManager : MonoBehaviour
|
||||
UnloadLocalIfLoaded(lobbySceneName);
|
||||
UnloadLocalIfLoaded(matchSceneName);
|
||||
|
||||
// The local avatar is gone; drop the mode cycle back to a player-less state so the persistent
|
||||
// orbit input stops re-locking the cursor (and any FreeCam/First-Person takeover is undone). Without
|
||||
// this the OS cursor stays captured back in the empty Main scene.
|
||||
// Drop the mode cycle back to a player-less state so the persistent orbit input stops re-locking
|
||||
// the cursor (and any FreeCam/First-Person takeover is undone).
|
||||
var modes = FindFirstObjectByType<GameModeManager>(FindObjectsInactive.Include);
|
||||
if (modes) modes.ReleaseToNoPlayer();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user