Adding new Races, Main Menu -> Lobby flow, and what's needed to set up multiplayer testing.
This commit is contained in:
parent
60fa58b07f
commit
fdada6f132
29 changed files with 2581 additions and 176 deletions
|
|
@ -1,15 +1,18 @@
|
|||
// Assets/_Project/Scripts/Gameplay/PlayerBuilderSpawner.cs
|
||||
using System.Collections.Generic;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using TD.Core;
|
||||
using TD.Levels;
|
||||
using TD.Net;
|
||||
|
||||
namespace TD.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Lives on the Player Prefab. On the server, when the player NetworkObject spawns,
|
||||
/// instantiates and spawns a separate <see cref="Builder"/> NetworkObject owned by that
|
||||
/// player. The builder is positioned at the centroid of the player's zone before spawn.
|
||||
/// Lives on the Player Prefab. On the server, spawns and re-spawns a separate
|
||||
/// <see cref="Builder"/> NetworkObject owned by this player each time the Match
|
||||
/// scene loads. The builder is positioned at the centroid of the player's zone.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Why a separate NetworkObject?</b> Multi-builder races (Path E) become "spawn
|
||||
|
|
@ -20,31 +23,79 @@ namespace TD.Gameplay
|
|||
/// no-ops; they just receive the resulting Builder NetworkObject like any other
|
||||
/// replicated spawn.</para>
|
||||
///
|
||||
/// <para><b>Lifetime.</b> The spawned builder is destroyed when the player NetworkObject
|
||||
/// despawns (e.g., disconnect). NGO does this automatically because we set
|
||||
/// <c>destroyWithScene</c> and store no other references — the builder's despawn cleans
|
||||
/// up the static registry in <see cref="Builder.OnNetworkDespawn"/>.</para>
|
||||
/// <para><b>Lifetime & scene flow.</b> The player NetworkObject persists across
|
||||
/// scenes (MainMenu → Lobby → Match → Lobby …) because it's the
|
||||
/// <c>NetworkManager.PlayerPrefab</c>. The Builder is spawned with
|
||||
/// <c>destroyWithScene: true</c> so it's torn down on every scene unload — we
|
||||
/// only want a builder in the Match scene. <see cref="HandleSceneLoadCompleted"/>
|
||||
/// re-spawns when entering Match; <see cref="OnNetworkDespawn"/> handles
|
||||
/// disconnect cleanup.</para>
|
||||
///
|
||||
/// <para><b>Race-driven builder prefab.</b> If a <see cref="RaceRegistry"/> exists
|
||||
/// in the Match scene AND the player's selected <see cref="RaceDefinition"/> has
|
||||
/// a <c>BuilderPrefab</c> assigned, that prefab is used. Otherwise this falls back
|
||||
/// to the inspector-assigned <see cref="builderPrefab"/> (the universal default).
|
||||
/// Phase 1.8 races just need to fill in their <c>BuilderPrefab</c> field; no code
|
||||
/// change required.</para>
|
||||
/// </remarks>
|
||||
public class PlayerBuilderSpawner : NetworkBehaviour
|
||||
{
|
||||
[Tooltip("Builder prefab to instantiate. Must be registered with the NetworkManager " +
|
||||
"as a network prefab.")]
|
||||
[Tooltip("Default Builder prefab. Used when the player's race has no BuilderPrefab " +
|
||||
"assigned (or when no RaceRegistry is present in the scene). Must be " +
|
||||
"registered with the NetworkManager as a network prefab.")]
|
||||
[SerializeField] private GameObject builderPrefab;
|
||||
|
||||
// Cached reference so we can despawn the builder if needed (e.g., player disconnects).
|
||||
private NetworkObject spawnedBuilder;
|
||||
|
||||
// Track our scene-load subscription state so we can clean up correctly.
|
||||
private bool sceneSubscribed;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
if (builderPrefab == null)
|
||||
{
|
||||
Debug.LogError("[PlayerBuilderSpawner] No Builder prefab assigned. " +
|
||||
Debug.LogError("[PlayerBuilderSpawner] No default Builder prefab assigned. " +
|
||||
"Cannot spawn builder for client " + OwnerClientId + ".");
|
||||
return;
|
||||
}
|
||||
|
||||
// Subscribe to NGO's scene-load completion so we re-spawn the builder
|
||||
// every time the Match scene comes up (initial match start, Retry, etc.).
|
||||
if (NetworkManager != null && NetworkManager.SceneManager != null)
|
||||
{
|
||||
NetworkManager.SceneManager.OnLoadEventCompleted += HandleSceneLoadCompleted;
|
||||
sceneSubscribed = true;
|
||||
}
|
||||
|
||||
// Edge case: the player connected while a match is already in progress.
|
||||
// The Match scene is already loaded, so OnLoadEventCompleted won't fire
|
||||
// again until the next transition. Spawn now.
|
||||
if (SceneManager.GetActiveScene().name == SceneNames.Match)
|
||||
TrySpawnBuilder();
|
||||
}
|
||||
|
||||
// NGO fires this on the server once a scene load is acknowledged complete
|
||||
// by every connected client (or timed out). We only act when the Match
|
||||
// scene loads; Lobby / MainMenu loads are no-ops here.
|
||||
private void HandleSceneLoadCompleted(string sceneName,
|
||||
LoadSceneMode loadSceneMode,
|
||||
List<ulong> clientsCompleted,
|
||||
List<ulong> clientsTimedOut)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
if (sceneName != SceneNames.Match) return;
|
||||
TrySpawnBuilder();
|
||||
}
|
||||
|
||||
// Spawns the builder if it doesn't already exist. Defers to a SlotReady
|
||||
// event if PlayerMatchState hasn't finished assigning the slot yet.
|
||||
private void TrySpawnBuilder()
|
||||
{
|
||||
if (spawnedBuilder != null && spawnedBuilder.IsSpawned) return;
|
||||
|
||||
var pms = GetComponent<PlayerMatchState>();
|
||||
if (pms == null)
|
||||
{
|
||||
|
|
@ -53,8 +104,6 @@ namespace TD.Gameplay
|
|||
return;
|
||||
}
|
||||
|
||||
// PlayerMatchState.OnNetworkSpawn may have already fired (component order: it first)
|
||||
// or may fire after us (component order: we first). Handle both cases.
|
||||
if (pms.Slot != PlayerSlot.None)
|
||||
SpawnBuilderForOwner(pms.Slot);
|
||||
else
|
||||
|
|
@ -65,11 +114,21 @@ namespace TD.Gameplay
|
|||
{
|
||||
var pms = GetComponent<PlayerMatchState>();
|
||||
if (pms != null) pms.SlotReady -= OnOwnerSlotReady;
|
||||
SpawnBuilderForOwner(slot);
|
||||
|
||||
// Only spawn if we're in the Match scene. SlotReady can fire in MainMenu
|
||||
// (during initial connection) — we don't want a builder there.
|
||||
if (SceneManager.GetActiveScene().name == SceneNames.Match)
|
||||
SpawnBuilderForOwner(slot);
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
if (sceneSubscribed && NetworkManager != null && NetworkManager.SceneManager != null)
|
||||
{
|
||||
NetworkManager.SceneManager.OnLoadEventCompleted -= HandleSceneLoadCompleted;
|
||||
sceneSubscribed = false;
|
||||
}
|
||||
|
||||
// When the player despawns (disconnect), also despawn their builder if it still exists.
|
||||
if (IsServer && spawnedBuilder != null && spawnedBuilder.IsSpawned)
|
||||
{
|
||||
|
|
@ -84,7 +143,19 @@ namespace TD.Gameplay
|
|||
// Falls back to origin if loader/zone data isn't available.
|
||||
Vector3 spawnPos = ComputeZoneCentroid(slot);
|
||||
|
||||
var go = Instantiate(builderPrefab, spawnPos, Quaternion.identity);
|
||||
// Pick the prefab: race-specific takes priority, default falls back.
|
||||
// Falling back is what lets all races share the default builder
|
||||
// during Phase 1.7 — each RaceDefinition just needs the same default
|
||||
// assigned, and the spawner picks it up automatically.
|
||||
GameObject prefab = ResolveBuilderPrefab();
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogError("[PlayerBuilderSpawner] No builder prefab available. " +
|
||||
"Set the default in the inspector or assign one to the player's race.");
|
||||
return;
|
||||
}
|
||||
|
||||
var go = Instantiate(prefab, spawnPos, Quaternion.identity);
|
||||
var netObj = go.GetComponent<NetworkObject>();
|
||||
if (netObj == null)
|
||||
{
|
||||
|
|
@ -114,6 +185,22 @@ namespace TD.Gameplay
|
|||
|
||||
// ----- Helpers ----------------------------------------------------
|
||||
|
||||
// Picks the builder prefab to spawn for this player. Race-specific takes
|
||||
// priority when (a) RaceRegistry is in the scene, (b) the player picked
|
||||
// a race, and (c) that race's RaceDefinition has a BuilderPrefab assigned.
|
||||
// Otherwise falls back to the inspector-assigned default.
|
||||
private GameObject ResolveBuilderPrefab()
|
||||
{
|
||||
var pms = GetComponent<PlayerMatchState>();
|
||||
if (pms != null && pms.RaceSelection != RaceId.None && RaceRegistry.Instance != null)
|
||||
{
|
||||
var raceDef = RaceRegistry.Instance.Get(pms.RaceSelection);
|
||||
if (raceDef != null && raceDef.BuilderPrefab != null)
|
||||
return raceDef.BuilderPrefab;
|
||||
}
|
||||
return builderPrefab;
|
||||
}
|
||||
|
||||
private static Vector3 ComputeZoneCentroid(PlayerSlot slot)
|
||||
{
|
||||
var loader = LevelLoader.Instance;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue