233 lines
9.5 KiB
C#
233 lines
9.5 KiB
C#
// 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
|
|
}
|
|
}
|
|
}
|