Adding 9 Player level
This commit is contained in:
parent
fdada6f132
commit
a7be12fa9b
30 changed files with 45984 additions and 300 deletions
196
Assets/_Project/Scripts/Gameplay/MapRegistry.cs
Normal file
196
Assets/_Project/Scripts/Gameplay/MapRegistry.cs
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
// Assets/_Project/Scripts/Gameplay/MapRegistry.cs
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using TD.Levels;
|
||||
|
||||
namespace TD.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Persistent (DontDestroyOnLoad) singleton that holds every <see cref="LevelData"/> available
|
||||
/// to the lobby's map browser. Mirrors <see cref="RaceRegistry"/>'s pattern.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Inspector setup.</b> Place ONE <c>MapRegistry</c> GameObject in the <b>MainMenu
|
||||
/// scene</b> only. Drag every <see cref="LevelData"/> asset that should appear in the lobby's
|
||||
/// map browser into the <c>Maps</c> array. The first non-null entry becomes the
|
||||
/// <see cref="Default"/> map (auto-selected when a lobby first opens). Mark itself
|
||||
/// <c>DontDestroyOnLoad</c> on Awake so it survives MainMenu → Lobby → Match transitions and
|
||||
/// is reachable from any scene via <see cref="Instance"/>.</para>
|
||||
///
|
||||
/// <para><b>Why MainMenu-only?</b> Same reason as <see cref="RaceRegistry"/>: a single
|
||||
/// authoritative array prevents the "I updated one and forgot the other" designer trap.
|
||||
/// Duplicate instances dropped into Lobby or Match scenes self-destroy on Awake.</para>
|
||||
///
|
||||
/// <para><b>Editor-only standalone testing.</b> If you open the Lobby or Match scene directly
|
||||
/// from the editor without going through MainMenu, no MapRegistry exists. <see cref="Instance"/>
|
||||
/// is null; callers should handle that (the lobby UI shows an empty browser, Quick Start falls
|
||||
/// back to a hardcoded scene, etc.). For standalone testing, temporarily add a registry to
|
||||
/// whatever scene you're testing — but don't commit it.</para>
|
||||
///
|
||||
/// <para><b>Selectability.</b> A map is selectable in a lobby of N players iff
|
||||
/// <c>N <= map.PlayerCount</c>. The lobby UI still shows maps it can't currently use, just
|
||||
/// greyed out with an explanatory label — so players can see what other maps exist and how
|
||||
/// many players they'd need to play them.</para>
|
||||
///
|
||||
/// <para><b>Plain MonoBehaviour.</b> Not a NetworkBehaviour — every peer has the same LevelData
|
||||
/// assets in the build. Network state tracks only the selected map's index via
|
||||
/// <c>LobbyService.SelectedMapIndex</c>.</para>
|
||||
/// </remarks>
|
||||
public class MapRegistry : MonoBehaviour
|
||||
{
|
||||
// ----- Singleton -------------------------------------------------
|
||||
|
||||
public static MapRegistry Instance { get; private set; }
|
||||
|
||||
// ----- Inspector --------------------------------------------------
|
||||
|
||||
[Tooltip("All LevelData assets that should appear in the lobby's map browser. Drag each " +
|
||||
"asset into the array. The FIRST non-null entry becomes the default selection " +
|
||||
"when a lobby first opens; order subsequent entries however you want them sorted " +
|
||||
"in the UI. Null entries and assets with empty MapName are skipped with a warning.")]
|
||||
[SerializeField] private LevelData[] maps;
|
||||
|
||||
// ----- Public API -------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// All registered maps in inspector order. Never returns null entries; entries that failed
|
||||
/// validation in Awake are filtered out. Safe to iterate any time after Awake.
|
||||
/// </summary>
|
||||
public IReadOnlyList<LevelData> Maps => validatedMaps;
|
||||
|
||||
/// <summary>
|
||||
/// Total number of valid registered maps. Equivalent to <c>Maps.Count</c> but cheaper
|
||||
/// since it doesn't allocate an enumerator.
|
||||
/// </summary>
|
||||
public int Count => validatedMaps.Count;
|
||||
|
||||
/// <summary>
|
||||
/// The map auto-selected when a lobby first opens. Returns the first valid entry in the
|
||||
/// inspector array, or null if the registry has no valid maps (which would be a setup
|
||||
/// error — Quick Start and lobby will both log and degrade).
|
||||
/// </summary>
|
||||
public LevelData Default => validatedMaps.Count > 0 ? validatedMaps[0] : null;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the map at the given index, or null if the index is out of range. Tolerant of
|
||||
/// stale indices that might survive a registry edit (e.g. a NetworkVariable holding an
|
||||
/// index whose map was removed).
|
||||
/// </summary>
|
||||
public LevelData Get(int index)
|
||||
{
|
||||
if (index < 0 || index >= validatedMaps.Count) return null;
|
||||
return validatedMaps[index];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the index of the given map in <see cref="Maps"/>, or -1 if it isn't registered.
|
||||
/// Use this when you have a LevelData reference and need to sync the selection over the
|
||||
/// network as an integer.
|
||||
/// </summary>
|
||||
public int IndexOf(LevelData map)
|
||||
{
|
||||
if (map == null) return -1;
|
||||
for (int i = 0; i < validatedMaps.Count; i++)
|
||||
{
|
||||
if (validatedMaps[i] == map) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if a lobby with <paramref name="playerCount"/> players is allowed to start a match
|
||||
/// on <paramref name="map"/>. Currently the rule is just "lobby size must fit"; if minimum
|
||||
/// player counts become a thing later, gate them here.
|
||||
/// </summary>
|
||||
public static bool IsSelectableFor(LevelData map, int playerCount)
|
||||
{
|
||||
if (map == null) return false;
|
||||
if (playerCount < 1) return false;
|
||||
return playerCount <= map.PlayerCount;
|
||||
}
|
||||
|
||||
// ----- Lifecycle --------------------------------------------------
|
||||
|
||||
// Validated subset of the inspector array; built once in Awake and never mutated.
|
||||
// Holding a separate list lets us silently filter out null/invalid entries without
|
||||
// mutating the inspector array (which would surprise designers).
|
||||
private readonly List<LevelData> validatedMaps = new List<LevelData>();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Persistent-singleton pattern: first instance wins, duplicates self-destroy.
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[MapRegistry] Persistent instance already exists. " +
|
||||
$"Destroying duplicate in scene '{gameObject.scene.name}'. " +
|
||||
$"Keep MapRegistry in the MainMenu scene only.");
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
BuildLookup();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
// ----- Private ----------------------------------------------------
|
||||
|
||||
private void BuildLookup()
|
||||
{
|
||||
validatedMaps.Clear();
|
||||
|
||||
if (maps == null || maps.Length == 0)
|
||||
{
|
||||
Debug.LogWarning("[MapRegistry] No LevelData assets assigned. Drag assets into " +
|
||||
"the Maps array. Lobby map browser will be empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
var seen = new HashSet<LevelData>();
|
||||
foreach (var map in maps)
|
||||
{
|
||||
if (map == null) continue;
|
||||
|
||||
// Duplicates are likely an authoring mistake (designer dragged the same asset
|
||||
// twice); keep the first occurrence and warn.
|
||||
if (!seen.Add(map))
|
||||
{
|
||||
Debug.LogWarning($"[MapRegistry] Duplicate LevelData '{map.name}' in Maps " +
|
||||
$"array. Keeping first occurrence only.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(map.MapName))
|
||||
{
|
||||
Debug.LogWarning($"[MapRegistry] '{map.name}' has empty MapName — skipping. " +
|
||||
"Set MapName on the LevelData asset.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(map.ScenePath))
|
||||
{
|
||||
Debug.LogWarning($"[MapRegistry] '{map.name}' has empty ScenePath — skipping. " +
|
||||
"Bake the level so ScenePath gets populated.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (map.PlayerCount < 1)
|
||||
{
|
||||
Debug.LogWarning($"[MapRegistry] '{map.name}' has PlayerCount={map.PlayerCount} " +
|
||||
"(must be >= 1) — skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
validatedMaps.Add(map);
|
||||
}
|
||||
|
||||
Debug.Log($"[MapRegistry] Registered {validatedMaps.Count} map(s). " +
|
||||
$"Default = '{(Default != null ? Default.MapName : "<none>")}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue