Comitting lobby code without testing.
This commit is contained in:
parent
66f84652dc
commit
60fa58b07f
14 changed files with 1207 additions and 37 deletions
165
Assets/_Project/Scripts/Gameplay/LobbyService.cs
Normal file
165
Assets/_Project/Scripts/Gameplay/LobbyService.cs
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
// Assets/_Project/Scripts/Gameplay/LobbyService.cs
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using TD.Core;
|
||||
using TD.Net;
|
||||
|
||||
namespace TD.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Scene singleton that owns lobby-wide actions: Start Match (host),
|
||||
/// Return to Lobby (after a match), and the "host disconnected" broadcast.
|
||||
/// Lives in the <c>Lobby</c> scene with a NetworkObject; spawns when the
|
||||
/// scene loads.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>What lives here vs. on PlayerMatchState.</b> Per-player state
|
||||
/// (race, ready, slot, display name) lives on <see cref="PlayerMatchState"/>
|
||||
/// because it carries across scene transitions with the player connection.
|
||||
/// Lobby-wide ACTIONS (Start Match, Return to Lobby) live here because they
|
||||
/// are scoped to the lobby scene and don't need to persist.</para>
|
||||
///
|
||||
/// <para><b>Start Match flow.</b>
|
||||
/// <list type="number">
|
||||
/// <item>Host clicks Start in the lobby UI.</item>
|
||||
/// <item>UI calls <see cref="RequestStartMatchRpc"/>; server validates:
|
||||
/// every PlayerMatchState has a race and IsReady.</item>
|
||||
/// <item>Server calls <c>NetworkBootstrap.LoadSceneAsHost(SceneNames.Match)</c>.
|
||||
/// NGO replicates the scene load to every client.</item>
|
||||
/// <item>Match scene loads; <see cref="MatchState"/> spawns there; gameplay
|
||||
/// begins as usual.</item>
|
||||
/// </list></para>
|
||||
///
|
||||
/// <para><b>Return to Lobby flow.</b> Triggered by the match-end overlay's
|
||||
/// "Retry" button. Server calls <see cref="RequestReturnToLobbyRpc"/>;
|
||||
/// resets <see cref="PlayerMatchState.IsReady"/> on every player; loads
|
||||
/// the Lobby scene. Race picks are preserved so players don't have to
|
||||
/// re-select.</para>
|
||||
///
|
||||
/// <para><b>Host-leaves behavior (Option A).</b> When the host quits, NGO
|
||||
/// shuts down the connection for every client. Clients detect the disconnect
|
||||
/// in <see cref="MainMenuRedirectOnDisconnect"/> (subscribed to
|
||||
/// <c>NetworkManager.OnClientDisconnectCallback</c> on the local client)
|
||||
/// and load the MainMenu scene locally. See roadmap §1.7-Future Steam Lobby
|
||||
/// Migration for the Option C upgrade (lobby persists, new host elected)
|
||||
/// once Steam SDK is in.</para>
|
||||
/// </remarks>
|
||||
[RequireComponent(typeof(NetworkObject))]
|
||||
public class LobbyService : NetworkBehaviour
|
||||
{
|
||||
// ----- Singleton --------------------------------------------------
|
||||
|
||||
public static LobbyService Instance { get; private set; }
|
||||
|
||||
// ----- Lifecycle --------------------------------------------------
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Debug.LogError("[LobbyService] Duplicate instance detected. " +
|
||||
"Only one LobbyService should exist per scene.");
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
|
||||
// Entering the lobby always starts everyone un-ready. Race picks
|
||||
// are preserved from the previous lobby visit, but ready state
|
||||
// resets so each match requires explicit re-readying.
|
||||
if (IsServer)
|
||||
ResetAllReady();
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
// ----- Public RPC API (client → server) --------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Host-only request to begin the match. Validates that every connected
|
||||
/// player has picked a race and is ready, then transitions every peer
|
||||
/// to the Match scene via NGO scene management.
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
public void RequestStartMatchRpc(RpcParams rpcParams = default)
|
||||
{
|
||||
// Only the host's local client may start a match. We compare the
|
||||
// sender's clientId to the server's clientId (which == the host's
|
||||
// local clientId in host mode).
|
||||
if (rpcParams.Receive.SenderClientId != NetworkManager.Singleton.LocalClientId)
|
||||
{
|
||||
Debug.LogWarning("[LobbyService] Non-host client attempted to start the match. Ignored.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AreAllPlayersReady(out string reason))
|
||||
{
|
||||
Debug.Log($"[LobbyService] Cannot start match: {reason}");
|
||||
return;
|
||||
}
|
||||
|
||||
NetworkBootstrap.LoadSceneAsHost(SceneNames.Match);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Anyone can request a return to the lobby (typically the host clicks
|
||||
/// "Retry" after a match). Server-side: resets ready state on every
|
||||
/// player and triggers the scene transition. Race picks are preserved.
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
public void RequestReturnToLobbyRpc()
|
||||
{
|
||||
// We don't gate this on host-only currently — any client retrying is
|
||||
// semantically equivalent (the host needs to be willing to keep
|
||||
// running). When dedicated server / Steam migration lands this
|
||||
// becomes host-only.
|
||||
ResetAllReady();
|
||||
NetworkBootstrap.LoadSceneAsHost(SceneNames.Lobby);
|
||||
}
|
||||
|
||||
// ----- Public queries --------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// True if every connected player has picked a race AND marked themselves
|
||||
/// ready. Returns the reason as <paramref name="reason"/> when false
|
||||
/// (for debug logs / future UI tooltips on the disabled Start button).
|
||||
/// </summary>
|
||||
public static bool AreAllPlayersReady(out string reason)
|
||||
{
|
||||
int count = 0;
|
||||
foreach (var pms in PlayerMatchState.AllPlayers)
|
||||
{
|
||||
count++;
|
||||
if (pms.RaceSelection == RaceId.None)
|
||||
{
|
||||
reason = $"Slot {pms.Slot} hasn't picked a race.";
|
||||
return false;
|
||||
}
|
||||
if (!pms.IsReady)
|
||||
{
|
||||
reason = $"Slot {pms.Slot} is not ready.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
reason = "No players connected.";
|
||||
return false;
|
||||
}
|
||||
|
||||
reason = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ----- Server helpers --------------------------------------------
|
||||
|
||||
private static void ResetAllReady()
|
||||
{
|
||||
foreach (var pms in PlayerMatchState.AllPlayers)
|
||||
pms.SetReady(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Project/Scripts/Gameplay/LobbyService.cs.meta
Normal file
2
Assets/_Project/Scripts/Gameplay/LobbyService.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 46ab03868ea4bd541bd6be3446c2bd3d
|
||||
|
|
@ -89,30 +89,91 @@ namespace TD.Gameplay
|
|||
writePerm: NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
// STUBBED — written by the race-pick system (Phase 1.8). None until a race is chosen.
|
||||
[Tooltip("STUBBED — not written until the race-pick system lands (Phase 1.8).")]
|
||||
// Race chosen by this player. Written by the lobby system in response to
|
||||
// SetRaceRpc; carries over scene transitions because PlayerMatchState is
|
||||
// the player prefab (owned by the connection, persists across scenes).
|
||||
private readonly NetworkVariable<RaceId> raceSelection = new NetworkVariable<RaceId>(
|
||||
value: RaceId.None,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
// Lobby ready-state. Cleared back to false on scene transitions out of
|
||||
// the lobby (the new lobby session starts everyone un-ready).
|
||||
private readonly NetworkVariable<bool> isReady = new NetworkVariable<bool>(
|
||||
value: false,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
/// <summary>This player's assigned slot in the match. Authoritative once spawned.</summary>
|
||||
public PlayerSlot Slot => slot.Value;
|
||||
|
||||
/// <summary>Display name. Stub until a lobby/name system provides it.</summary>
|
||||
public string DisplayName => displayName.Value.ToString();
|
||||
|
||||
/// <summary>STUBBED. Race chosen by this player. <see cref="RaceId.None"/> until Phase 1.8.</summary>
|
||||
/// <summary>Race chosen by this player. <see cref="RaceId.None"/> until selected in the lobby.</summary>
|
||||
public RaceId RaceSelection => raceSelection.Value;
|
||||
|
||||
/// <summary>STUBBED. Sets this player's race selection. Called by the race-pick system (Phase 1.8).</summary>
|
||||
/// <summary>True if this player has marked themselves ready in the lobby.</summary>
|
||||
public bool IsReady => isReady.Value;
|
||||
|
||||
/// <summary>Server-only. Sets this player's race selection.</summary>
|
||||
public void SetRaceSelection(RaceId race)
|
||||
{
|
||||
if (!IsServer) { Debug.LogWarning("[PlayerMatchState] SetRaceSelection called on a client — ignored."); return; }
|
||||
raceSelection.Value = race;
|
||||
}
|
||||
|
||||
/// <summary>Server-only. Sets this player's ready state.</summary>
|
||||
public void SetReady(bool ready)
|
||||
{
|
||||
if (!IsServer) { Debug.LogWarning("[PlayerMatchState] SetReady called on a client — ignored."); return; }
|
||||
isReady.Value = ready;
|
||||
}
|
||||
|
||||
// ----- Client → Server lobby RPCs --------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Client → server: submit a race selection. Owner-only — players can
|
||||
/// only change their own race. Validates that the caller is the owner
|
||||
/// of this PlayerMatchState; ignored otherwise.
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
|
||||
public void SubmitRaceRpc(RaceId race)
|
||||
{
|
||||
// Reset ready state when race changes — players shouldn't be "ready"
|
||||
// with a stale pick.
|
||||
if (raceSelection.Value != race)
|
||||
{
|
||||
raceSelection.Value = race;
|
||||
isReady.Value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client → server: submit ready state. Owner-only. A player can only
|
||||
/// ready up if they've picked a race; the lobby UI gates this already
|
||||
/// but the server enforces it as a safety net.
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
|
||||
public void SubmitReadyRpc(bool ready)
|
||||
{
|
||||
if (ready && raceSelection.Value == RaceId.None)
|
||||
{
|
||||
Debug.Log($"[PlayerMatchState] Slot {Slot} tried to ready up without a race. Ignored.");
|
||||
return;
|
||||
}
|
||||
isReady.Value = ready;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-side helper: enumerate every currently-spawned PlayerMatchState.
|
||||
/// Used by <c>LobbyService</c> to evaluate "are all players ready" and
|
||||
/// by <c>LobbyController</c> on every peer to render the player list.
|
||||
/// </summary>
|
||||
public static IEnumerable<PlayerMatchState> AllPlayers => s_byClientId.Values;
|
||||
|
||||
/// <summary>
|
||||
/// Server-only. Fires on the server immediately after the slot is assigned in
|
||||
/// <see cref="OnNetworkSpawn"/>. Sibling components (e.g. PlayerBuilderSpawner)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue