Comitting lobby code without testing.
This commit is contained in:
parent
66f84652dc
commit
60fa58b07f
14 changed files with 1207 additions and 37 deletions
|
|
@ -1322,12 +1322,25 @@ namespace TD.UI
|
|||
matchEndTitle.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
panel.Add(matchEndTitle);
|
||||
|
||||
// Action row — Retry (back to lobby with everyone who retried)
|
||||
// and Return to Main Menu (this player only disconnects).
|
||||
var actionRow = new VisualElement();
|
||||
actionRow.style.flexDirection = FlexDirection.Row;
|
||||
actionRow.style.marginTop = 8;
|
||||
panel.Add(actionRow);
|
||||
|
||||
var retryBtn = new Button(OnRetryClicked) { text = "Retry" };
|
||||
retryBtn.style.minWidth = 120;
|
||||
retryBtn.style.minWidth = 140;
|
||||
retryBtn.style.height = 36;
|
||||
retryBtn.style.fontSize = 16;
|
||||
retryBtn.style.marginTop = 8;
|
||||
panel.Add(retryBtn);
|
||||
retryBtn.style.marginRight = 12;
|
||||
actionRow.Add(retryBtn);
|
||||
|
||||
var menuBtn = new Button(OnReturnToMainMenuClicked) { text = "Return to Main Menu" };
|
||||
menuBtn.style.minWidth = 200;
|
||||
menuBtn.style.height = 36;
|
||||
menuBtn.style.fontSize = 16;
|
||||
actionRow.Add(menuBtn);
|
||||
|
||||
matchEndOverlay.Add(panel);
|
||||
root.Add(matchEndOverlay);
|
||||
|
|
@ -1368,41 +1381,34 @@ namespace TD.UI
|
|||
// sceneLoaded callback survives the scene reload (HUDController dies),
|
||||
// re-arms StartHost once the fresh scene has finished loading, and
|
||||
// unsubscribes itself.
|
||||
// Retry: take everyone back to the Lobby scene via LobbyService. The
|
||||
// lobby preserves race picks, clears ready state. Anyone who clicked
|
||||
// Return to Main Menu instead has already disconnected — they don't
|
||||
// come along.
|
||||
private void OnRetryClicked()
|
||||
{
|
||||
var svc = LobbyService.Instance;
|
||||
if (svc != null)
|
||||
{
|
||||
svc.RequestReturnToLobbyRpc();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: LobbyService isn't spawned (e.g. testing the gameplay
|
||||
// scene standalone without the lobby flow). Hard-reload the scene.
|
||||
Debug.LogWarning("[HUDController] LobbyService not found — falling back to scene reload.");
|
||||
var nm = NetworkManager.Singleton;
|
||||
if (nm == null)
|
||||
{
|
||||
Debug.LogWarning("[HUDController] Retry clicked but NetworkManager is null.");
|
||||
return;
|
||||
}
|
||||
if (!nm.IsServer)
|
||||
{
|
||||
Debug.LogWarning("[HUDController] Retry only works on the host. " +
|
||||
"Clients should ask the host to retry.");
|
||||
return;
|
||||
}
|
||||
|
||||
Scene active = SceneManager.GetActiveScene();
|
||||
s_pendingHostRestartBuildIndex = active.buildIndex;
|
||||
SceneManager.sceneLoaded += OnSceneLoadedForRetry;
|
||||
|
||||
nm.Shutdown();
|
||||
SceneManager.LoadScene(active.buildIndex);
|
||||
if (nm != null && nm.IsServer && nm.SceneManager != null)
|
||||
nm.SceneManager.LoadScene(SceneManager.GetActiveScene().name, LoadSceneMode.Single);
|
||||
}
|
||||
|
||||
private static int s_pendingHostRestartBuildIndex = -1;
|
||||
|
||||
private static void OnSceneLoadedForRetry(Scene loaded, LoadSceneMode mode)
|
||||
// Return to Main Menu: disconnect only this player. SessionFlow's
|
||||
// OnClientDisconnect handler routes us back to MainMenu locally. Other
|
||||
// peers remain in the match (until the host quits, at which point
|
||||
// SessionFlow on each remaining client routes them out too).
|
||||
private void OnReturnToMainMenuClicked()
|
||||
{
|
||||
if (loaded.buildIndex != s_pendingHostRestartBuildIndex) return;
|
||||
|
||||
SceneManager.sceneLoaded -= OnSceneLoadedForRetry;
|
||||
s_pendingHostRestartBuildIndex = -1;
|
||||
|
||||
var nm = NetworkManager.Singleton;
|
||||
if (nm != null) nm.StartHost();
|
||||
else Debug.LogWarning("[HUDController] Retry: no NetworkManager in reloaded scene.");
|
||||
TD.Net.NetworkBootstrap.Disconnect();
|
||||
}
|
||||
|
||||
// ----- Helpers ----------------------------------------------------
|
||||
|
|
|
|||
270
Assets/_Project/Scripts/UI/LobbyController.cs
Normal file
270
Assets/_Project/Scripts/UI/LobbyController.cs
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
// Assets/_Project/Scripts/UI/LobbyController.cs
|
||||
using System.Linq;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using TD.Core;
|
||||
using TD.Gameplay;
|
||||
using TD.Net;
|
||||
|
||||
namespace TD.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Drives the lobby UI. Requires a <see cref="UIDocument"/> on the same
|
||||
/// GameObject. Programmatic UI for v1 — UXML can be added later if needed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>What's displayed.</b> A player list with one row per connected
|
||||
/// player showing slot, display name, race pick, and ready ✓. The local
|
||||
/// player's row has interactive controls: a race dropdown (currently a
|
||||
/// placeholder with just <see cref="RaceId.None"/> until Phase 1.8 fills
|
||||
/// the enum) and a Ready toggle button. The host sees a Start Match
|
||||
/// button at the bottom, enabled only when every player is ready.</para>
|
||||
///
|
||||
/// <para><b>Polling vs reactive.</b> The list is rebuilt every Update from
|
||||
/// <see cref="PlayerMatchState.AllPlayers"/>. PlayerMatchState's
|
||||
/// <c>NetworkVariable</c>s expose <c>OnValueChanged</c> events we could
|
||||
/// hook for reactive updates, but polling is simpler at lobby scale
|
||||
/// (max 9 players × a handful of fields each) and avoids tracking
|
||||
/// subscription lifecycles across spawn/despawn.</para>
|
||||
///
|
||||
/// <para><b>Race picker placeholder.</b> The <see cref="RaceId"/> enum
|
||||
/// currently only has <c>None</c>. The picker shows a single "Test Race"
|
||||
/// button that calls <c>SubmitRaceRpc(RaceId.None)</c> — temporary, so
|
||||
/// ready-up flow can be exercised end-to-end before Phase 1.8 lands real
|
||||
/// races. Replace with the actual race-selection UI when
|
||||
/// <see cref="RaceDefinition"/> assets exist.</para>
|
||||
/// </remarks>
|
||||
[RequireComponent(typeof(UIDocument))]
|
||||
public class LobbyController : MonoBehaviour
|
||||
{
|
||||
// ----- Cached UI elements -----------------------------------------
|
||||
|
||||
private VisualElement playerListContainer;
|
||||
private Button startMatchButton;
|
||||
private Button leaveButton;
|
||||
private Label statusLabel;
|
||||
|
||||
// ----- Lifecycle --------------------------------------------------
|
||||
|
||||
private void Start()
|
||||
{
|
||||
var doc = GetComponent<UIDocument>();
|
||||
var root = doc?.rootVisualElement;
|
||||
if (root == null)
|
||||
{
|
||||
Debug.LogError("[LobbyController] rootVisualElement is null. " +
|
||||
"Check the UIDocument's Panel Settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
BuildUI(root);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (playerListContainer == null) return;
|
||||
RefreshPlayerList();
|
||||
RefreshStartButton();
|
||||
}
|
||||
|
||||
// ----- UI construction --------------------------------------------
|
||||
|
||||
private void BuildUI(VisualElement root)
|
||||
{
|
||||
root.style.flexDirection = FlexDirection.Column;
|
||||
root.style.alignItems = Align.Center;
|
||||
root.style.justifyContent = Justify.FlexStart;
|
||||
root.style.backgroundColor = new Color(0.05f, 0.05f, 0.08f, 1f);
|
||||
root.style.width = Length.Percent(100);
|
||||
root.style.height = Length.Percent(100);
|
||||
root.style.paddingTop = 40;
|
||||
|
||||
// Title.
|
||||
var title = new Label("Lobby");
|
||||
title.style.fontSize = 36;
|
||||
title.style.color = Color.white;
|
||||
title.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
title.style.marginBottom = 24;
|
||||
root.Add(title);
|
||||
|
||||
// Player list container (rebuilt every Update from AllPlayers).
|
||||
playerListContainer = new VisualElement();
|
||||
playerListContainer.style.flexDirection = FlexDirection.Column;
|
||||
playerListContainer.style.minWidth = 520;
|
||||
playerListContainer.style.paddingTop = 12;
|
||||
playerListContainer.style.paddingBottom = 12;
|
||||
playerListContainer.style.paddingLeft = 16;
|
||||
playerListContainer.style.paddingRight = 16;
|
||||
playerListContainer.style.backgroundColor = new Color(0f, 0f, 0f, 0.4f);
|
||||
playerListContainer.style.marginBottom = 24;
|
||||
root.Add(playerListContainer);
|
||||
|
||||
// Action row: Start Match (host) + Leave (everyone).
|
||||
var actionRow = new VisualElement();
|
||||
actionRow.style.flexDirection = FlexDirection.Row;
|
||||
actionRow.style.marginTop = 8;
|
||||
root.Add(actionRow);
|
||||
|
||||
startMatchButton = new Button(OnStartMatchClicked) { text = "Start Match" };
|
||||
startMatchButton.style.minWidth = 200;
|
||||
startMatchButton.style.height = 44;
|
||||
startMatchButton.style.fontSize = 18;
|
||||
startMatchButton.style.marginRight = 16;
|
||||
startMatchButton.SetEnabled(false);
|
||||
actionRow.Add(startMatchButton);
|
||||
|
||||
leaveButton = new Button(OnLeaveClicked) { text = "Leave Lobby" };
|
||||
leaveButton.style.minWidth = 200;
|
||||
leaveButton.style.height = 44;
|
||||
leaveButton.style.fontSize = 18;
|
||||
actionRow.Add(leaveButton);
|
||||
|
||||
statusLabel = new Label(string.Empty);
|
||||
statusLabel.style.color = new Color(1f, 0.6f, 0.3f);
|
||||
statusLabel.style.marginTop = 16;
|
||||
statusLabel.style.minHeight = 20;
|
||||
root.Add(statusLabel);
|
||||
}
|
||||
|
||||
// ----- Per-frame refresh ------------------------------------------
|
||||
|
||||
private void RefreshPlayerList()
|
||||
{
|
||||
playerListContainer.Clear();
|
||||
|
||||
ulong localId = NetworkManager.Singleton != null
|
||||
? NetworkManager.Singleton.LocalClientId
|
||||
: ulong.MaxValue;
|
||||
|
||||
// Sort by slot for stable ordering. AllPlayers is keyed by clientId
|
||||
// which may not be slot-ordered.
|
||||
var players = PlayerMatchState.AllPlayers.OrderBy(p => (int)p.Slot).ToList();
|
||||
|
||||
foreach (var pms in players)
|
||||
{
|
||||
bool isLocal = pms.OwnerClientId == localId;
|
||||
playerListContainer.Add(BuildPlayerRow(pms, isLocal));
|
||||
}
|
||||
|
||||
if (players.Count == 0)
|
||||
playerListContainer.Add(new Label("(no players connected)") { style = { color = Color.gray } });
|
||||
}
|
||||
|
||||
private VisualElement BuildPlayerRow(PlayerMatchState pms, bool isLocal)
|
||||
{
|
||||
var row = new VisualElement();
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.alignItems = Align.Center;
|
||||
row.style.paddingTop = 6;
|
||||
row.style.paddingBottom = 6;
|
||||
row.style.paddingLeft = 8;
|
||||
row.style.paddingRight = 8;
|
||||
row.style.marginBottom = 4;
|
||||
row.style.backgroundColor = isLocal
|
||||
? new Color(0.18f, 0.22f, 0.30f)
|
||||
: new Color(0.10f, 0.10f, 0.12f);
|
||||
|
||||
// Slot label.
|
||||
var slotLabel = new Label($"P{(int)pms.Slot}");
|
||||
slotLabel.style.minWidth = 40;
|
||||
slotLabel.style.color = Color.white;
|
||||
slotLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
row.Add(slotLabel);
|
||||
|
||||
// Display name.
|
||||
var nameLabel = new Label(string.IsNullOrEmpty(pms.DisplayName)
|
||||
? $"Player {(int)pms.Slot}"
|
||||
: pms.DisplayName);
|
||||
nameLabel.style.minWidth = 140;
|
||||
nameLabel.style.color = Color.white;
|
||||
row.Add(nameLabel);
|
||||
|
||||
// Race indicator.
|
||||
var raceLabel = new Label($"Race: {pms.RaceSelection}");
|
||||
raceLabel.style.minWidth = 160;
|
||||
raceLabel.style.color = pms.RaceSelection == RaceId.None
|
||||
? new Color(0.7f, 0.5f, 0.5f)
|
||||
: new Color(0.85f, 0.85f, 0.85f);
|
||||
row.Add(raceLabel);
|
||||
|
||||
// Ready indicator.
|
||||
var readyLabel = new Label(pms.IsReady ? "READY ✓" : "not ready");
|
||||
readyLabel.style.minWidth = 100;
|
||||
readyLabel.style.color = pms.IsReady
|
||||
? new Color(0.3f, 0.85f, 0.3f)
|
||||
: new Color(0.6f, 0.6f, 0.6f);
|
||||
readyLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
row.Add(readyLabel);
|
||||
|
||||
// Local-only controls.
|
||||
if (isLocal)
|
||||
{
|
||||
// PLACEHOLDER race picker — the RaceId enum only has None right
|
||||
// now (Phase 1.8 will fill it). For now the single button submits
|
||||
// RaceId.None, which keeps the ready-up gate effectively a no-op
|
||||
// (the server requires RaceSelection != None to allow ready). To
|
||||
// exercise the flow end-to-end before races exist, comment out
|
||||
// the gate in PlayerMatchState.SubmitReadyRpc.
|
||||
//
|
||||
// TODO (Phase 1.8): replace with a dropdown of RaceDefinition
|
||||
// assets discovered at runtime, each option calling
|
||||
// pms.SubmitRaceRpc(definition.Id).
|
||||
var pickRaceBtn = new Button(() => pms.SubmitRaceRpc(RaceId.None))
|
||||
{
|
||||
text = "Pick Race (stub)"
|
||||
};
|
||||
pickRaceBtn.style.minWidth = 130;
|
||||
pickRaceBtn.style.height = 28;
|
||||
pickRaceBtn.style.marginLeft = 12;
|
||||
row.Add(pickRaceBtn);
|
||||
|
||||
var readyBtn = new Button(() => pms.SubmitReadyRpc(!pms.IsReady))
|
||||
{
|
||||
text = pms.IsReady ? "Unready" : "Ready"
|
||||
};
|
||||
readyBtn.style.minWidth = 100;
|
||||
readyBtn.style.height = 28;
|
||||
readyBtn.style.marginLeft = 8;
|
||||
row.Add(readyBtn);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
private void RefreshStartButton()
|
||||
{
|
||||
var nm = NetworkManager.Singleton;
|
||||
bool isHost = nm != null && nm.IsHost;
|
||||
|
||||
startMatchButton.style.display = isHost ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
if (!isHost) return;
|
||||
|
||||
bool canStart = LobbyService.AreAllPlayersReady(out string reason);
|
||||
startMatchButton.SetEnabled(canStart);
|
||||
statusLabel.text = canStart ? string.Empty : reason;
|
||||
}
|
||||
|
||||
// ----- Button handlers --------------------------------------------
|
||||
|
||||
private void OnStartMatchClicked()
|
||||
{
|
||||
var svc = LobbyService.Instance;
|
||||
if (svc == null)
|
||||
{
|
||||
Debug.LogError("[LobbyController] LobbyService.Instance is null. " +
|
||||
"Scene setup is incomplete.");
|
||||
return;
|
||||
}
|
||||
svc.RequestStartMatchRpc();
|
||||
}
|
||||
|
||||
private void OnLeaveClicked()
|
||||
{
|
||||
// Disconnect drops our connection. SessionFlow will route the
|
||||
// local peer back to the MainMenu scene when the disconnect
|
||||
// callback fires.
|
||||
NetworkBootstrap.Disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Project/Scripts/UI/LobbyController.cs.meta
Normal file
2
Assets/_Project/Scripts/UI/LobbyController.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8c40427d598ba944c82f3790429c5532
|
||||
233
Assets/_Project/Scripts/UI/MainMenuController.cs
Normal file
233
Assets/_Project/Scripts/UI/MainMenuController.cs
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
// Assets/_Project/Scripts/UI/MainMenuController.cs
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using TD.Net;
|
||||
|
||||
namespace TD.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Drives the main menu UI. Requires a <see cref="UIDocument"/> on the same
|
||||
/// GameObject. Builds the UI programmatically — no UXML needed for v1.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Buttons.</b>
|
||||
/// <list type="bullet">
|
||||
/// <item><b>Host</b> — calls <see cref="NetworkBootstrap.StartHost"/> on
|
||||
/// the default port, then NGO scene-loads the Lobby. Clients that
|
||||
/// join later get pulled into whatever scene the server is in.</item>
|
||||
/// <item><b>Join</b> — reveals IP + port fields, then calls
|
||||
/// <see cref="NetworkBootstrap.StartClient"/>. The server will pull
|
||||
/// the client into the current networked scene (Lobby or Match)
|
||||
/// once the connection completes.</item>
|
||||
/// <item><b>Quit</b> — Application.Quit (no-op in the editor).</item>
|
||||
/// </list></para>
|
||||
///
|
||||
/// <para><b>Future Steam integration.</b> A lobby-browser panel will be
|
||||
/// added here. The Host button will create a Steam lobby instead of a
|
||||
/// direct-IP host; Join becomes "browse public lobbies" + "accept friend
|
||||
/// invite". See Project_Roadmap.md §1.7-Future Steam Lobby Migration.</para>
|
||||
/// </remarks>
|
||||
[RequireComponent(typeof(UIDocument))]
|
||||
public class MainMenuController : MonoBehaviour
|
||||
{
|
||||
// ----- Inspector --------------------------------------------------
|
||||
|
||||
[Tooltip("Default port shown in the Join field and used by the Host button.")]
|
||||
[SerializeField] private ushort defaultPort = NetworkBootstrap.DefaultPort;
|
||||
|
||||
[Tooltip("Default address pre-filled in the Join field for solo testing.")]
|
||||
[SerializeField] private string defaultJoinAddress = NetworkBootstrap.DefaultConnectAddress;
|
||||
|
||||
// ----- Cached UI elements -----------------------------------------
|
||||
|
||||
private Button hostButton;
|
||||
private Button joinButton;
|
||||
private Button quitButton;
|
||||
private VisualElement joinPanel;
|
||||
private TextField joinAddressField;
|
||||
private TextField joinPortField;
|
||||
private Button joinConfirmButton;
|
||||
private Button joinCancelButton;
|
||||
private Label statusLabel;
|
||||
|
||||
// ----- Lifecycle --------------------------------------------------
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// UIDocument creates its panel in OnEnable, which runs after Awake.
|
||||
// Start is the safe time to access rootVisualElement.
|
||||
var doc = GetComponent<UIDocument>();
|
||||
var root = doc?.rootVisualElement;
|
||||
if (root == null)
|
||||
{
|
||||
Debug.LogError("[MainMenuController] rootVisualElement is null. " +
|
||||
"Check the UIDocument's Panel Settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
BuildUI(root);
|
||||
}
|
||||
|
||||
// ----- UI construction --------------------------------------------
|
||||
|
||||
private void BuildUI(VisualElement root)
|
||||
{
|
||||
// Centered vertical stack on a dark background.
|
||||
root.style.flexDirection = FlexDirection.Column;
|
||||
root.style.justifyContent = Justify.Center;
|
||||
root.style.alignItems = Align.Center;
|
||||
root.style.backgroundColor = new Color(0.05f, 0.05f, 0.08f, 1f);
|
||||
// The UIDocument root needs explicit width/height to fill the screen.
|
||||
root.style.width = Length.Percent(100);
|
||||
root.style.height = Length.Percent(100);
|
||||
|
||||
// Title.
|
||||
var title = new Label("Unity Tower Defense");
|
||||
title.style.fontSize = 48;
|
||||
title.style.color = Color.white;
|
||||
title.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
title.style.marginBottom = 60;
|
||||
root.Add(title);
|
||||
|
||||
// Primary button column.
|
||||
var buttonColumn = new VisualElement();
|
||||
buttonColumn.style.flexDirection = FlexDirection.Column;
|
||||
buttonColumn.style.alignItems = Align.Center;
|
||||
root.Add(buttonColumn);
|
||||
|
||||
hostButton = MakeMenuButton("Host Game", OnHostClicked);
|
||||
joinButton = MakeMenuButton("Join Game", OnJoinClicked);
|
||||
quitButton = MakeMenuButton("Quit", OnQuitClicked);
|
||||
buttonColumn.Add(hostButton);
|
||||
buttonColumn.Add(joinButton);
|
||||
buttonColumn.Add(quitButton);
|
||||
|
||||
// Join sub-panel — hidden until Join is clicked. Holds the IP+port
|
||||
// fields and the Connect / Cancel buttons.
|
||||
joinPanel = new VisualElement();
|
||||
joinPanel.style.flexDirection = FlexDirection.Column;
|
||||
joinPanel.style.alignItems = Align.Center;
|
||||
joinPanel.style.marginTop = 24;
|
||||
joinPanel.style.paddingTop = 16;
|
||||
joinPanel.style.paddingBottom = 16;
|
||||
joinPanel.style.paddingLeft = 24;
|
||||
joinPanel.style.paddingRight = 24;
|
||||
joinPanel.style.backgroundColor = new Color(0f, 0f, 0f, 0.4f);
|
||||
joinPanel.style.display = DisplayStyle.None;
|
||||
root.Add(joinPanel);
|
||||
|
||||
joinAddressField = new TextField("Host address");
|
||||
joinAddressField.value = defaultJoinAddress;
|
||||
joinAddressField.style.width = 280;
|
||||
joinAddressField.style.color = Color.white;
|
||||
joinPanel.Add(joinAddressField);
|
||||
|
||||
joinPortField = new TextField("Port");
|
||||
joinPortField.value = defaultPort.ToString();
|
||||
joinPortField.style.width = 280;
|
||||
joinPortField.style.marginTop = 8;
|
||||
joinPortField.style.color = Color.white;
|
||||
joinPanel.Add(joinPortField);
|
||||
|
||||
var joinButtons = new VisualElement();
|
||||
joinButtons.style.flexDirection = FlexDirection.Row;
|
||||
joinButtons.style.marginTop = 12;
|
||||
joinPanel.Add(joinButtons);
|
||||
|
||||
joinConfirmButton = new Button(OnJoinConfirmClicked) { text = "Connect" };
|
||||
joinConfirmButton.style.minWidth = 120;
|
||||
joinConfirmButton.style.height = 32;
|
||||
joinConfirmButton.style.marginRight = 8;
|
||||
joinButtons.Add(joinConfirmButton);
|
||||
|
||||
joinCancelButton = new Button(OnJoinCancelClicked) { text = "Cancel" };
|
||||
joinCancelButton.style.minWidth = 120;
|
||||
joinCancelButton.style.height = 32;
|
||||
joinButtons.Add(joinCancelButton);
|
||||
|
||||
// Status line at the bottom for connection feedback.
|
||||
statusLabel = new Label(string.Empty);
|
||||
statusLabel.style.color = new Color(1f, 0.6f, 0.3f);
|
||||
statusLabel.style.marginTop = 32;
|
||||
statusLabel.style.minHeight = 20;
|
||||
root.Add(statusLabel);
|
||||
}
|
||||
|
||||
private static Button MakeMenuButton(string text, System.Action onClick)
|
||||
{
|
||||
var btn = new Button(() => onClick?.Invoke()) { text = text };
|
||||
btn.style.minWidth = 240;
|
||||
btn.style.height = 48;
|
||||
btn.style.fontSize = 20;
|
||||
btn.style.marginBottom = 8;
|
||||
return btn;
|
||||
}
|
||||
|
||||
// ----- Button handlers --------------------------------------------
|
||||
|
||||
private void OnHostClicked()
|
||||
{
|
||||
statusLabel.text = "Starting host…";
|
||||
if (!NetworkBootstrap.StartHost(defaultPort))
|
||||
{
|
||||
statusLabel.text = "Failed to start host. Check the console.";
|
||||
return;
|
||||
}
|
||||
|
||||
// After StartHost the local peer is the server + client. Trigger
|
||||
// the networked scene load to take everyone to the Lobby. NGO
|
||||
// replicates this to any future-joining clients automatically.
|
||||
NetworkBootstrap.LoadSceneAsHost(SceneNames.Lobby);
|
||||
}
|
||||
|
||||
private void OnJoinClicked()
|
||||
{
|
||||
// Toggle the join sub-panel.
|
||||
joinPanel.style.display = joinPanel.style.display == DisplayStyle.None
|
||||
? DisplayStyle.Flex
|
||||
: DisplayStyle.None;
|
||||
}
|
||||
|
||||
private void OnJoinConfirmClicked()
|
||||
{
|
||||
string address = string.IsNullOrWhiteSpace(joinAddressField.value)
|
||||
? defaultJoinAddress
|
||||
: joinAddressField.value.Trim();
|
||||
|
||||
ushort port = defaultPort;
|
||||
if (!string.IsNullOrWhiteSpace(joinPortField.value)
|
||||
&& ushort.TryParse(joinPortField.value.Trim(), out var parsed))
|
||||
{
|
||||
port = parsed;
|
||||
}
|
||||
|
||||
statusLabel.text = $"Connecting to {address}:{port}…";
|
||||
if (!NetworkBootstrap.StartClient(address, port))
|
||||
{
|
||||
statusLabel.text = "Failed to start client. Check the console.";
|
||||
return;
|
||||
}
|
||||
|
||||
// The server's NGO SceneManager will pull this client into whatever
|
||||
// scene the server is in (Lobby or Match) once the connection
|
||||
// completes. SessionFlow handles disconnect recovery.
|
||||
joinPanel.style.display = DisplayStyle.None;
|
||||
}
|
||||
|
||||
private void OnJoinCancelClicked()
|
||||
{
|
||||
joinPanel.style.display = DisplayStyle.None;
|
||||
statusLabel.text = string.Empty;
|
||||
}
|
||||
|
||||
private void OnQuitClicked()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorApplication.isPlaying = false;
|
||||
#else
|
||||
Application.Quit();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Project/Scripts/UI/MainMenuController.cs.meta
Normal file
2
Assets/_Project/Scripts/UI/MainMenuController.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 01199add5a12f4a4bb9a94d1e44fbb4d
|
||||
Loading…
Add table
Add a link
Reference in a new issue