1209 lines
47 KiB
C#
1209 lines
47 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using UnityEngine.EventSystems;
|
|
using MegaKoop.Steam;
|
|
using MegaKoop.Networking;
|
|
using MegaKoop.Game.Networking;
|
|
using TMPro;
|
|
|
|
namespace MegaKoop.UI
|
|
{
|
|
/// <summary>
|
|
/// UGUI Multiplayer Lobby Controller integrated with SteamLobbyService.
|
|
/// Builds players list dynamically, handles host/join/invite/kick/ready/start.
|
|
/// </summary>
|
|
public class UGUIMultiplayerLobbyController : MonoBehaviour
|
|
{
|
|
// Panels
|
|
[Header("Optional Root Ref (assigned by builder)")]
|
|
[SerializeField] private GameObject panelLobbyRef;
|
|
private GameObject panelLobby;
|
|
private GameObject groupJoin;
|
|
private GameObject groupHost;
|
|
|
|
[Header("Manual Wiring - Header & Code")]
|
|
[SerializeField] private TMP_Text textStatus;
|
|
[SerializeField] private TMP_Text textLobbyCodeValue;
|
|
[SerializeField] private Button btnCopyCode;
|
|
|
|
[Header("Manual Wiring - Tabs")]
|
|
[SerializeField] private Button btnHostTab;
|
|
[SerializeField] private Button btnJoinTab;
|
|
|
|
[Header("Manual Wiring - Join Section")]
|
|
[SerializeField] private TMP_InputField inputLobbyCode;
|
|
[SerializeField] private Button btnConnect;
|
|
|
|
[Header("Manual Wiring - Host Section")]
|
|
[SerializeField] private TMP_Dropdown ddMaxPlayers;
|
|
[SerializeField] private Toggle tgPublicLobby;
|
|
[SerializeField] private Button btnCreateLobby;
|
|
|
|
[Header("Manual Wiring - Players List")]
|
|
[SerializeField] private TMP_Text textPlayerCount;
|
|
[SerializeField] private ScrollRect scrollPlayers;
|
|
[SerializeField] private Transform contentPlayers;
|
|
[SerializeField] private GameObject playerItemTemplate;
|
|
[SerializeField] private GameObject emptyPlayers;
|
|
|
|
[Header("Manual Wiring - Friends Picker")]
|
|
[SerializeField] private GameObject panelFriends;
|
|
[SerializeField] private Transform contentFriends;
|
|
[SerializeField] private GameObject emptyFriends;
|
|
[SerializeField] private Button btnBackFromFriends;
|
|
[SerializeField] private Button btnCloseFriendsOverlay;
|
|
|
|
[Header("Manual Wiring - Host Controls")]
|
|
[SerializeField] private Button btnInviteFriends;
|
|
[SerializeField] private Button btnKickSelected;
|
|
|
|
[Header("Manual Wiring - Footer")]
|
|
[SerializeField] private Button btnToggleReady;
|
|
[SerializeField] private Button btnStartGame;
|
|
[SerializeField] private Button btnLeaveLobby;
|
|
[SerializeField] private Button btnBackFromLobby;
|
|
|
|
[Header("Player Ready Styling")]
|
|
[SerializeField] private Color readyBorderColor = new Color(0.2f, 0.82f, 0.35f, 1f);
|
|
[SerializeField] private Color notReadyBorderColor = new Color(0.85f, 0.25f, 0.25f, 1f);
|
|
[SerializeField] private float avatarBorderThickness = 12f;
|
|
|
|
// Selection
|
|
private string selectedPlayerSteamId = string.Empty;
|
|
|
|
// Cached readiness state per member
|
|
private readonly Dictionary<string, bool> memberReadyCache = new Dictionary<string, bool>();
|
|
|
|
// Steam service
|
|
private SteamLobbyService steam;
|
|
private LobbyGameSceneCoordinator lobbyGameCoordinator;
|
|
private SteamCoopNetworkManager coopNetworkManager;
|
|
|
|
// Local state cache
|
|
private bool IsInLobby => steam != null && steam.IsInLobby;
|
|
private bool IsHost => steam != null && steam.IsHost;
|
|
private string LobbyCode => steam != null ? steam.LobbyCode : string.Empty;
|
|
private bool clientStartedFromSignal = false;
|
|
private bool leftDueToKick = false;
|
|
private bool inviteOverlayRequested = false;
|
|
|
|
private void Awake()
|
|
{
|
|
// Find UI (including inactive)
|
|
panelLobby = EnsureGO(panelLobby, "Panel_Lobby");
|
|
groupJoin = EnsureGO(groupJoin, "Group_Join");
|
|
groupHost = EnsureGO(groupHost, "Group_Host");
|
|
|
|
textStatus = EnsureText(textStatus, "Text_Status");
|
|
textLobbyCodeValue = EnsureText(textLobbyCodeValue, "Text_LobbyCodeValue");
|
|
btnCopyCode = EnsureButton(btnCopyCode, "Button_CopyCode");
|
|
|
|
btnHostTab = EnsureButton(btnHostTab, "Button_HostTab");
|
|
btnJoinTab = EnsureButton(btnJoinTab, "Button_JoinTab");
|
|
|
|
inputLobbyCode = EnsureInput(inputLobbyCode, "Input_LobbyCode");
|
|
btnConnect = EnsureButton(btnConnect, "Button_Connect");
|
|
|
|
ddMaxPlayers = EnsureDropdown(ddMaxPlayers, "Dropdown_MaxPlayers");
|
|
tgPublicLobby = EnsureToggle(tgPublicLobby, "Toggle_PublicLobby");
|
|
btnCreateLobby = EnsureButton(btnCreateLobby, "Button_CreateLobby");
|
|
|
|
textPlayerCount = EnsureText(textPlayerCount, "Text_PlayerCount");
|
|
scrollPlayers = EnsureScroll(scrollPlayers, "Scroll_Players");
|
|
contentPlayers = EnsureTransform(contentPlayers, "Content_PlayersList");
|
|
emptyPlayers = EnsureGO(emptyPlayers, "Empty_Players");
|
|
playerItemTemplate = EnsureGO(playerItemTemplate, "PlayerItemTemplate");
|
|
|
|
btnInviteFriends = EnsureButton(btnInviteFriends, "Button_InviteFriends");
|
|
btnKickSelected = EnsureButton(btnKickSelected, "Button_KickSelected");
|
|
|
|
btnToggleReady = EnsureButton(btnToggleReady, "Button_ToggleReady");
|
|
btnStartGame = EnsureButton(btnStartGame, "Button_StartGame");
|
|
btnLeaveLobby = EnsureButton(btnLeaveLobby, "Button_LeaveLobby");
|
|
btnBackFromLobby = EnsureButton(btnBackFromLobby, "Button_BackFromLobby");
|
|
|
|
// Friends picker
|
|
panelFriends = EnsureGO(panelFriends, "Panel_Friends");
|
|
contentFriends = EnsureTransform(contentFriends, "Content_FriendsList");
|
|
emptyFriends = EnsureGO(emptyFriends, "Empty_Friends");
|
|
btnBackFromFriends = EnsureButton(btnBackFromFriends, "Button_BackFromFriends");
|
|
btnCloseFriendsOverlay = EnsureButton(btnCloseFriendsOverlay, "Button_CloseFriendsOverlay");
|
|
|
|
EnsureSteamServices();
|
|
RegisterSteamEvents();
|
|
WireButtonEvents();
|
|
ValidateUI();
|
|
|
|
// Initial view: Host tab
|
|
ShowHostTab();
|
|
UpdateUI();
|
|
}
|
|
|
|
private void ShowFriendsPanel()
|
|
{
|
|
if (panelFriends) panelFriends.SetActive(true);
|
|
EnsureFriendsGridSetup();
|
|
RebuildFriendsList();
|
|
}
|
|
|
|
private void HideFriendsPanel()
|
|
{
|
|
if (panelFriends) panelFriends.SetActive(false);
|
|
}
|
|
|
|
private void RebuildFriendsList()
|
|
{
|
|
if (contentFriends == null || steam == null) return;
|
|
// Clear existing
|
|
var toDestroy = new List<GameObject>();
|
|
foreach (Transform child in contentFriends) toDestroy.Add(child.gameObject);
|
|
foreach (var go in toDestroy) DestroyImmediate(go);
|
|
|
|
var friends = steam.GetFriends();
|
|
if (friends.Count == 0)
|
|
{
|
|
if (emptyFriends) emptyFriends.SetActive(true);
|
|
return;
|
|
}
|
|
if (emptyFriends) emptyFriends.SetActive(false);
|
|
|
|
foreach (var f in friends)
|
|
{
|
|
// Icon button cell only
|
|
var cell = new GameObject($"Friend_{f.steamId}", typeof(RectTransform), typeof(Image), typeof(Button));
|
|
cell.transform.SetParent(contentFriends, false);
|
|
var img = cell.GetComponent<Image>();
|
|
if (steam.TryGetAvatarSprite(f.steamId, out var spr, false)) { img.sprite = spr; img.color = Color.white; img.preserveAspect = true; }
|
|
else { img.sprite = null; img.color = new Color(0.3f,0.6f,0.3f,1f); }
|
|
var rt = (RectTransform)cell.transform; rt.sizeDelta = new Vector2(72,72);
|
|
var btn = cell.GetComponent<Button>();
|
|
string sid = f.steamId;
|
|
btn.onClick.AddListener(() => { steam.InviteFriendBySteamId(sid); });
|
|
}
|
|
}
|
|
|
|
private void RefreshFriendsIcons()
|
|
{
|
|
foreach (Transform child in contentFriends)
|
|
{
|
|
if (child == null) continue;
|
|
var img = child.GetComponent<Image>();
|
|
if (img == null || img.sprite != null) continue;
|
|
// Name format: Friend_<steamId>
|
|
var name = child.name;
|
|
const string prefix = "Friend_";
|
|
if (!name.StartsWith(prefix)) continue;
|
|
var sid = name.Substring(prefix.Length);
|
|
if (steam.TryGetAvatarSprite(sid, out var spr, false))
|
|
{
|
|
img.sprite = spr; img.color = Color.white; img.preserveAspect = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void EnsureFriendsGridSetup()
|
|
{
|
|
if (contentFriends == null) return;
|
|
var grid = contentFriends.GetComponent<GridLayoutGroup>();
|
|
if (grid == null)
|
|
{
|
|
var oldV = contentFriends.GetComponent<VerticalLayoutGroup>(); if (oldV) DestroyImmediate(oldV);
|
|
grid = contentFriends.gameObject.AddComponent<GridLayoutGroup>();
|
|
grid.cellSize = new Vector2(72,72);
|
|
grid.spacing = new Vector2(10,10);
|
|
grid.startAxis = GridLayoutGroup.Axis.Horizontal;
|
|
var csf = contentFriends.GetComponent<ContentSizeFitter>(); if (csf == null) csf = contentFriends.gameObject.AddComponent<ContentSizeFitter>();
|
|
csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|
}
|
|
}
|
|
|
|
// Find an implementation of ISteamNGOAdapter in the scene (e.g., on NetworkManager)
|
|
private ISteamNGOAdapter GetSteamAdapter()
|
|
{
|
|
#if UNITY_2023_1_OR_NEWER
|
|
var monos = Object.FindObjectsByType<MonoBehaviour>(FindObjectsSortMode.None);
|
|
#else
|
|
var monos = Object.FindObjectsOfType<MonoBehaviour>();
|
|
#endif
|
|
foreach (var m in monos)
|
|
{
|
|
if (m is ISteamNGOAdapter adapter) return adapter;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (panelFriends && panelFriends.activeSelf)
|
|
{
|
|
if (Input.GetKeyDown(KeyCode.Escape)) HideFriendsPanel();
|
|
}
|
|
// While friends panel visible, keep trying to resolve avatars that were not ready yet
|
|
if (panelFriends && panelFriends.activeSelf && contentFriends && steam != null)
|
|
{
|
|
RefreshFriendsIcons();
|
|
}
|
|
// Auto-start client when host signals start via lobby data
|
|
if (!IsHost && IsInLobby && steam != null)
|
|
{
|
|
if (steam.IsStartSignaled() && !clientStartedFromSignal)
|
|
{
|
|
clientStartedFromSignal = true;
|
|
lobbyGameCoordinator?.BeginGame(steam);
|
|
#if UNITY_NETCODE || NETCODE_PRESENT
|
|
// Configure transport with host SteamID before starting client
|
|
var adapter = GetSteamAdapter();
|
|
if (adapter != null)
|
|
{
|
|
if (ulong.TryParse(steam.HostSteamIdString, out var hostSid))
|
|
adapter.ConfigureClient(hostSid);
|
|
}
|
|
NetworkBootstrap.StartClient("GameScene");
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// If host flagged us as kicked, leave lobby
|
|
if (steam != null && IsInLobby && !leftDueToKick && steam.IsKickedLocal())
|
|
{
|
|
leftDueToKick = true;
|
|
steam.LeaveLobby();
|
|
}
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
UnregisterSteamEvents();
|
|
}
|
|
|
|
private GameObject FindAnyGO(string name)
|
|
{
|
|
// Prefer searching under provided root (more robust if scene has duplicates)
|
|
if (panelLobbyRef)
|
|
{
|
|
var found = FindChildRecursive(panelLobbyRef.transform, name);
|
|
if (found) return found.gameObject;
|
|
}
|
|
// Fallback: search globally (including inactive)
|
|
var all = Resources.FindObjectsOfTypeAll<Transform>();
|
|
foreach (var t in all)
|
|
{
|
|
if (t == null) continue;
|
|
var go = t.gameObject;
|
|
if (!go.scene.IsValid() || !go.scene.isLoaded) continue;
|
|
if (go.name == name) return go;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private Transform FindChildRecursive(Transform root, string name)
|
|
{
|
|
if (root.name == name) return root;
|
|
for (int i = 0; i < root.childCount; i++)
|
|
{
|
|
var c = root.GetChild(i);
|
|
var r = FindChildRecursive(c, name);
|
|
if (r != null) return r;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private T FindAnyComponent<T>(string name) where T : Component
|
|
{
|
|
var go = FindAnyGO(name);
|
|
return go ? go.GetComponent<T>() : null;
|
|
}
|
|
|
|
// Allow builder to inject the root panel reference
|
|
public void SetLobbyRoot(GameObject root)
|
|
{
|
|
panelLobbyRef = root;
|
|
}
|
|
|
|
public void SetHostJoinGroups(GameObject hostGroup, GameObject joinGroup)
|
|
{
|
|
groupHost = hostGroup;
|
|
groupJoin = joinGroup;
|
|
}
|
|
|
|
public void SetHeaderControls(TMP_Text status, TMP_Text lobbyCode, Button copyCode)
|
|
{
|
|
textStatus = status;
|
|
textLobbyCodeValue = lobbyCode;
|
|
btnCopyCode = copyCode;
|
|
}
|
|
|
|
public void SetTabButtons(Button hostTab, Button joinTab)
|
|
{
|
|
btnHostTab = hostTab;
|
|
btnJoinTab = joinTab;
|
|
}
|
|
|
|
public void SetJoinSection(TMP_InputField lobbyCodeInput, Button connectButton)
|
|
{
|
|
inputLobbyCode = lobbyCodeInput;
|
|
btnConnect = connectButton;
|
|
}
|
|
|
|
public void SetHostSection(TMP_Dropdown maxPlayersDropdown, Toggle publicToggle, Button createLobbyButton)
|
|
{
|
|
ddMaxPlayers = maxPlayersDropdown;
|
|
tgPublicLobby = publicToggle;
|
|
btnCreateLobby = createLobbyButton;
|
|
}
|
|
|
|
public void SetPlayersList(TMP_Text playerCountText, ScrollRect playersScroll, Transform playersContent, GameObject itemTemplate, GameObject emptyState)
|
|
{
|
|
textPlayerCount = playerCountText;
|
|
scrollPlayers = playersScroll;
|
|
contentPlayers = playersContent;
|
|
playerItemTemplate = itemTemplate;
|
|
emptyPlayers = emptyState;
|
|
}
|
|
|
|
public void SetFriendsPicker(GameObject friendsPanel, Transform friendsContent, GameObject friendsEmpty, Button backButton, Button closeOverlayButton)
|
|
{
|
|
panelFriends = friendsPanel;
|
|
contentFriends = friendsContent;
|
|
emptyFriends = friendsEmpty;
|
|
btnBackFromFriends = backButton;
|
|
btnCloseFriendsOverlay = closeOverlayButton;
|
|
}
|
|
|
|
public void SetHostControls(Button inviteFriendsButton, Button kickSelectedButton)
|
|
{
|
|
btnInviteFriends = inviteFriendsButton;
|
|
btnKickSelected = kickSelectedButton;
|
|
}
|
|
|
|
public void SetFooterControls(Button toggleReadyButton, Button startGameButton, Button leaveLobbyButton, Button backButton)
|
|
{
|
|
btnToggleReady = toggleReadyButton;
|
|
btnStartGame = startGameButton;
|
|
btnLeaveLobby = leaveLobbyButton;
|
|
btnBackFromLobby = backButton;
|
|
}
|
|
|
|
private TMP_Text EnsureText(TMP_Text existing, string name) => existing ? existing : FindAnyComponent<TMP_Text>(name);
|
|
private Button EnsureButton(Button existing, string name) => existing ? existing : FindAnyComponent<Button>(name);
|
|
private Toggle EnsureToggle(Toggle existing, string name) => existing ? existing : FindAnyComponent<Toggle>(name);
|
|
private TMP_Dropdown EnsureDropdown(TMP_Dropdown existing, string name) => existing ? existing : FindAnyComponent<TMP_Dropdown>(name);
|
|
private TMP_InputField EnsureInput(TMP_InputField existing, string name) => existing ? existing : FindAnyComponent<TMP_InputField>(name);
|
|
private ScrollRect EnsureScroll(ScrollRect existing, string name) => existing ? existing : FindAnyComponent<ScrollRect>(name);
|
|
private Transform EnsureTransform(Transform existing, string name) => existing ? existing : FindAnyGO(name)?.transform;
|
|
private GameObject EnsureGO(GameObject existing, string name) => existing ? existing : FindAnyGO(name);
|
|
|
|
#region Steam
|
|
private void EnsureSteamServices()
|
|
{
|
|
GameObject servicesRoot = null;
|
|
|
|
if (steam == null)
|
|
{
|
|
// Unity 2023+: use FindFirstObjectByType; older: FindObjectOfType
|
|
#if UNITY_2023_1_OR_NEWER
|
|
steam = Object.FindFirstObjectByType<SteamLobbyService>();
|
|
#else
|
|
steam = Object.FindObjectOfType<SteamLobbyService>();
|
|
#endif
|
|
if (steam != null)
|
|
{
|
|
servicesRoot = steam.gameObject;
|
|
}
|
|
|
|
if (steam == null)
|
|
{
|
|
servicesRoot = GameObject.Find("SteamServices") ?? new GameObject("SteamServices");
|
|
if (servicesRoot.GetComponent<SteamManager>() == null)
|
|
{
|
|
servicesRoot.AddComponent<SteamManager>();
|
|
}
|
|
steam = servicesRoot.GetComponent<SteamLobbyService>() ?? servicesRoot.AddComponent<SteamLobbyService>();
|
|
DontDestroyOnLoad(servicesRoot);
|
|
}
|
|
}
|
|
|
|
if (steam != null)
|
|
{
|
|
servicesRoot ??= steam.gameObject;
|
|
|
|
if (servicesRoot.GetComponent<SteamManager>() == null)
|
|
{
|
|
servicesRoot.AddComponent<SteamManager>();
|
|
}
|
|
|
|
T EnsureComponent<T>() where T : Component
|
|
{
|
|
if (servicesRoot.TryGetComponent<T>(out var existing))
|
|
{
|
|
return existing;
|
|
}
|
|
#if UNITY_2023_1_OR_NEWER
|
|
var found = Object.FindFirstObjectByType<T>(FindObjectsInactive.Include);
|
|
#else
|
|
var found = Object.FindObjectOfType<T>();
|
|
#endif
|
|
if (found != null)
|
|
{
|
|
return found;
|
|
}
|
|
return servicesRoot.AddComponent<T>();
|
|
}
|
|
|
|
EnsureComponent<SteamLobbyManager>();
|
|
EnsureComponent<SteamP2PTransport>();
|
|
coopNetworkManager = EnsureComponent<SteamCoopNetworkManager>();
|
|
coopNetworkManager.SynchronizeWithLobby(steam);
|
|
lobbyGameCoordinator = servicesRoot.GetComponent<LobbyGameSceneCoordinator>() ?? servicesRoot.AddComponent<LobbyGameSceneCoordinator>();
|
|
|
|
DontDestroyOnLoad(servicesRoot);
|
|
}
|
|
|
|
SyncCoopNetwork();
|
|
}
|
|
|
|
private void SyncCoopNetwork()
|
|
{
|
|
if (steam == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (coopNetworkManager == null)
|
|
{
|
|
#if UNITY_2023_1_OR_NEWER
|
|
coopNetworkManager = Object.FindFirstObjectByType<SteamCoopNetworkManager>(FindObjectsInactive.Include);
|
|
#else
|
|
coopNetworkManager = Object.FindObjectOfType<SteamCoopNetworkManager>();
|
|
#endif
|
|
}
|
|
|
|
coopNetworkManager?.SynchronizeWithLobby(steam);
|
|
}
|
|
|
|
private void RegisterSteamEvents()
|
|
{
|
|
if (steam == null) return;
|
|
steam.OnLobbyCreated += OnLobbyCreated;
|
|
steam.OnLobbyEntered += OnLobbyEntered;
|
|
steam.OnLobbyLeft += OnLobbyLeft;
|
|
steam.OnMembersChanged += OnMembersChanged;
|
|
steam.OnLobbyDataUpdated += OnLobbyDataUpdated;
|
|
steam.OnJoinFailed += OnJoinFailed;
|
|
steam.OnAvatarUpdated += OnAvatarUpdated;
|
|
steam.OnLobbyCreated += HandleLobbyCreatedSync;
|
|
steam.OnLobbyEntered += HandleLobbyEnteredSync;
|
|
steam.OnLobbyLeft += HandleLobbyLeftSync;
|
|
steam.OnMembersChanged += HandleMembersChangedSync;
|
|
steam.OnLobbyDataUpdated += HandleLobbyDataUpdatedSync;
|
|
}
|
|
|
|
private void UnregisterSteamEvents()
|
|
{
|
|
if (steam == null) return;
|
|
steam.OnLobbyCreated -= OnLobbyCreated;
|
|
steam.OnLobbyEntered -= OnLobbyEntered;
|
|
steam.OnLobbyLeft -= OnLobbyLeft;
|
|
steam.OnMembersChanged -= OnMembersChanged;
|
|
steam.OnLobbyDataUpdated -= OnLobbyDataUpdated;
|
|
steam.OnJoinFailed -= OnJoinFailed;
|
|
steam.OnAvatarUpdated -= OnAvatarUpdated;
|
|
steam.OnLobbyCreated -= HandleLobbyCreatedSync;
|
|
steam.OnLobbyEntered -= HandleLobbyEnteredSync;
|
|
steam.OnLobbyLeft -= HandleLobbyLeftSync;
|
|
steam.OnMembersChanged -= HandleMembersChangedSync;
|
|
steam.OnLobbyDataUpdated -= HandleLobbyDataUpdatedSync;
|
|
}
|
|
|
|
private void OnLobbyCreated()
|
|
{
|
|
selectedPlayerSteamId = string.Empty;
|
|
memberReadyCache.Clear();
|
|
UpdateUIFromSteam();
|
|
// Auto-open invite overlay for the host
|
|
if (inviteOverlayRequested && steam != null && steam.IsInLobby && steam.IsHost)
|
|
{
|
|
inviteOverlayRequested = false;
|
|
steam.InviteFriends();
|
|
if (!steam.IsOverlayEnabled()) ShowFriendsPanel();
|
|
}
|
|
}
|
|
|
|
private void OnLobbyEntered()
|
|
{
|
|
selectedPlayerSteamId = string.Empty;
|
|
memberReadyCache.Clear();
|
|
UpdateUIFromSteam();
|
|
// Auto-open invite overlay if we are the host entering our lobby
|
|
if (inviteOverlayRequested && steam != null && steam.IsInLobby && steam.IsHost)
|
|
{
|
|
inviteOverlayRequested = false;
|
|
steam.InviteFriends();
|
|
if (!steam.IsOverlayEnabled()) ShowFriendsPanel();
|
|
}
|
|
}
|
|
|
|
private void OnLobbyLeft()
|
|
{
|
|
selectedPlayerSteamId = string.Empty;
|
|
memberReadyCache.Clear();
|
|
UpdateUIFromSteam();
|
|
inviteOverlayRequested = false;
|
|
}
|
|
|
|
private void HandleLobbyCreatedSync()
|
|
{
|
|
SyncCoopNetwork();
|
|
}
|
|
|
|
private void HandleLobbyEnteredSync()
|
|
{
|
|
SyncCoopNetwork();
|
|
}
|
|
|
|
private void HandleLobbyLeftSync()
|
|
{
|
|
SyncCoopNetwork();
|
|
}
|
|
|
|
private void HandleMembersChangedSync()
|
|
{
|
|
SyncCoopNetwork();
|
|
}
|
|
|
|
private void HandleLobbyDataUpdatedSync()
|
|
{
|
|
SyncCoopNetwork();
|
|
}
|
|
|
|
private void OnMembersChanged() => UpdateUIFromSteam();
|
|
private void OnLobbyDataUpdated() => UpdateUIFromSteam();
|
|
private void OnJoinFailed(string reason) { Debug.LogWarning($"[Lobby] Join failed: {reason}"); UpdateUIFromSteam(); }
|
|
#endregion
|
|
|
|
private void OnAvatarUpdated(string steamId)
|
|
{
|
|
UpdateAvatarUIForSteamId(steamId);
|
|
}
|
|
|
|
private void UpdateAvatarUIForSteamId(string steamId)
|
|
{
|
|
// Update friends grid cell
|
|
if (contentFriends)
|
|
{
|
|
var cell = FindChildRecursive(contentFriends, $"Friend_{steamId}");
|
|
if (cell)
|
|
{
|
|
var img = cell.GetComponent<Image>();
|
|
if (img && steam.TryGetAvatarSprite(steamId, out var spr, true))
|
|
{
|
|
img.sprite = spr; img.color = Color.white; img.preserveAspect = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update player list item avatar
|
|
if (contentPlayers)
|
|
{
|
|
var item = FindChildRecursive(contentPlayers, $"PlayerItem_{steamId}");
|
|
if (item)
|
|
{
|
|
var av = FindChildRecursive(item, "Image_Avatar");
|
|
if (av)
|
|
{
|
|
var img = av.GetComponent<Image>();
|
|
if (img)
|
|
{
|
|
if (steam.TryGetAvatarSprite(steamId, out var spr2, true))
|
|
{
|
|
img.sprite = spr2;
|
|
img.color = Color.white;
|
|
img.preserveAspect = true;
|
|
}
|
|
else
|
|
{
|
|
img.sprite = null;
|
|
img.color = new Color(0.3f, 0.6f, 0.3f, 1f);
|
|
}
|
|
|
|
if (TryGetMemberReadyState(steamId, out var isReady))
|
|
{
|
|
ApplyReadyBorder(img, isReady);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void WireButtonEvents()
|
|
{
|
|
if (btnHostTab) btnHostTab.onClick.AddListener(ShowHostTab);
|
|
if (btnJoinTab) btnJoinTab.onClick.AddListener(ShowJoinTab);
|
|
if (btnCreateLobby) btnCreateLobby.onClick.AddListener(CreateLobby);
|
|
if (btnConnect) btnConnect.onClick.AddListener(JoinLobby);
|
|
if (btnCopyCode) btnCopyCode.onClick.AddListener(CopyCode);
|
|
if (btnInviteFriends) btnInviteFriends.onClick.AddListener(() => {
|
|
if (!IsInLobby)
|
|
{
|
|
// Create a lobby first; overlay will auto-open in callbacks
|
|
inviteOverlayRequested = true;
|
|
CreateLobby();
|
|
}
|
|
else
|
|
{
|
|
steam.InviteFriends();
|
|
if (!steam.IsOverlayEnabled())
|
|
{
|
|
ShowFriendsPanel();
|
|
}
|
|
}
|
|
});
|
|
if (btnKickSelected) btnKickSelected.onClick.AddListener(() => { if (IsInLobby && IsHost && !string.IsNullOrEmpty(selectedPlayerSteamId)) steam.Kick(selectedPlayerSteamId); });
|
|
if (btnToggleReady) btnToggleReady.onClick.AddListener(ToggleReady);
|
|
if (btnStartGame) btnStartGame.onClick.AddListener(StartGame);
|
|
if (btnLeaveLobby) btnLeaveLobby.onClick.AddListener(() => { if (IsInLobby) steam.LeaveLobby(); });
|
|
if (btnBackFromLobby) btnBackFromLobby.onClick.AddListener(() => {
|
|
#if UNITY_2023_1_OR_NEWER
|
|
var mm = Object.FindFirstObjectByType<UGUIMainMenuController>();
|
|
#else
|
|
var mm = Object.FindObjectOfType<UGUIMainMenuController>();
|
|
#endif
|
|
if (mm) { mm.SendMessage("ShowMainMenu", SendMessageOptions.DontRequireReceiver); }
|
|
});
|
|
if (btnBackFromFriends) btnBackFromFriends.onClick.AddListener(HideFriendsPanel);
|
|
if (btnCloseFriendsOverlay) btnCloseFriendsOverlay.onClick.AddListener(HideFriendsPanel);
|
|
}
|
|
|
|
private void ShowHostTab()
|
|
{
|
|
if (groupHost) groupHost.SetActive(true);
|
|
if (groupJoin) groupJoin.SetActive(false);
|
|
HighlightTab(btnHostTab, true);
|
|
HighlightTab(btnJoinTab, false);
|
|
}
|
|
|
|
private void ShowJoinTab()
|
|
{
|
|
if (groupHost) groupHost.SetActive(false);
|
|
if (groupJoin) groupJoin.SetActive(true);
|
|
HighlightTab(btnHostTab, false);
|
|
HighlightTab(btnJoinTab, true);
|
|
}
|
|
|
|
// Public helpers for main menu
|
|
public void QuickHost(int maxPlayers = 4, bool isPublic = true)
|
|
{
|
|
if (panelLobby) panelLobby.SetActive(true);
|
|
ShowHostTab();
|
|
if (ddMaxPlayers)
|
|
{
|
|
var idx = ddMaxPlayers.options.FindIndex(o => o.text == maxPlayers.ToString());
|
|
if (idx >= 0) ddMaxPlayers.value = idx;
|
|
}
|
|
if (tgPublicLobby) tgPublicLobby.isOn = isPublic;
|
|
CreateLobby();
|
|
}
|
|
|
|
public void ShowJoinTabPublic()
|
|
{
|
|
if (panelLobby) panelLobby.SetActive(true);
|
|
ShowJoinTab();
|
|
if (inputLobbyCode) inputLobbyCode.Select();
|
|
}
|
|
|
|
private void HighlightTab(Button btn, bool active)
|
|
{
|
|
if (!btn) return;
|
|
var img = btn.GetComponent<Image>();
|
|
if (img) img.color = active ? new Color(0.3f,0.6f,0.3f,0.9f) : new Color(0.2f,0.2f,0.2f,0.9f);
|
|
}
|
|
|
|
private void CreateLobby()
|
|
{
|
|
int maxPlayers = 4;
|
|
if (ddMaxPlayers && int.TryParse(ddMaxPlayers.options[ddMaxPlayers.value].text, out var mp)) maxPlayers = mp;
|
|
bool isPublic = tgPublicLobby ? tgPublicLobby.isOn : true;
|
|
steam?.CreateLobby(maxPlayers, isPublic);
|
|
}
|
|
|
|
private void JoinLobby()
|
|
{
|
|
var code = inputLobbyCode ? inputLobbyCode.text : string.Empty;
|
|
if (!string.IsNullOrEmpty(code) && code.Length == 6)
|
|
{
|
|
steam?.JoinLobbyByCode(code);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning("[Lobby] Invalid code. Must be 6 characters.");
|
|
}
|
|
}
|
|
|
|
private void CopyCode()
|
|
{
|
|
var code = LobbyCode;
|
|
if (!string.IsNullOrEmpty(code))
|
|
{
|
|
GUIUtility.systemCopyBuffer = code;
|
|
if (btnCopyCode)
|
|
{
|
|
var text = btnCopyCode.GetComponentInChildren<TMP_Text>();
|
|
if (text)
|
|
{
|
|
text.text = "COPIED!";
|
|
Invoke(nameof(ResetCopyText), 1.2f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
private void ResetCopyText()
|
|
{
|
|
var text = btnCopyCode ? btnCopyCode.GetComponentInChildren<TMP_Text>() : null;
|
|
if (text) text.text = "COPY";
|
|
}
|
|
|
|
private void ResetInviteText()
|
|
{
|
|
var text = btnInviteFriends ? btnInviteFriends.GetComponentInChildren<TMP_Text>() : null;
|
|
if (text) text.text = "INVITE FRIENDS";
|
|
}
|
|
|
|
private void ToggleReady()
|
|
{
|
|
if (IsHost || !IsInLobby || steam == null) return;
|
|
var members = GetSteamMembers();
|
|
CacheMemberReadyStates(members);
|
|
var localId = steam.LocalSteamIdString;
|
|
bool current = false;
|
|
var me = members.FirstOrDefault(m => m.steamId == localId);
|
|
// When tuple is default, steamId will be null; guard
|
|
if (me.steamId == localId) current = me.isReady;
|
|
bool newReadyState = !current;
|
|
steam.SetReady(newReadyState);
|
|
if (!string.IsNullOrEmpty(localId))
|
|
{
|
|
memberReadyCache[localId] = newReadyState;
|
|
UpdateAvatarUIForSteamId(localId);
|
|
}
|
|
UpdateUIFromSteam();
|
|
}
|
|
|
|
private void StartGame()
|
|
{
|
|
if (!IsHost || !IsInLobby) return;
|
|
// Ensure all ready
|
|
var allReady = GetSteamMembers().All(m => m.isReady);
|
|
if (!allReady)
|
|
{
|
|
Debug.LogWarning("[Lobby] Not all players are ready.");
|
|
return;
|
|
}
|
|
steam?.StartGameSignal();
|
|
lobbyGameCoordinator?.BeginGame(steam);
|
|
#if UNITY_NETCODE || NETCODE_PRESENT
|
|
// Optionally start host and load scene
|
|
var adapter = GetSteamAdapter();
|
|
if (adapter != null && steam != null)
|
|
{
|
|
if (ulong.TryParse(steam.LocalSteamIdString, out var hostSid))
|
|
adapter.ConfigureHost(hostSid);
|
|
}
|
|
NetworkBootstrap.StartHost("GameScene");
|
|
#endif
|
|
}
|
|
|
|
private void UpdateUIFromSteam()
|
|
{
|
|
UpdateStatus();
|
|
UpdateCode();
|
|
RebuildPlayers();
|
|
UpdateVisibility();
|
|
}
|
|
|
|
private void UpdateUI()
|
|
{
|
|
UpdateStatus();
|
|
UpdateCode();
|
|
UpdateVisibility();
|
|
}
|
|
|
|
private void ValidateUI()
|
|
{
|
|
// Log any missing critical references to help scene setup
|
|
void Check(object obj, string name)
|
|
{
|
|
if (obj == null) Debug.LogError($"[Lobby UI] Missing UI element: {name}. Ensure Tools > MegaKoop > Generate UGUI Main Menu and names match.");
|
|
}
|
|
Check(panelLobby, "Panel_Lobby");
|
|
Check(groupHost, "Group_Host");
|
|
Check(groupJoin, "Group_Join");
|
|
Check(panelFriends, "Panel_Friends");
|
|
Check(contentFriends, "Content_FriendsList");
|
|
Check(emptyFriends, "Empty_Friends");
|
|
Check(btnHostTab, "Button_HostTab");
|
|
Check(btnJoinTab, "Button_JoinTab");
|
|
Check(btnCreateLobby, "Button_CreateLobby");
|
|
Check(btnConnect, "Button_Connect");
|
|
Check(inputLobbyCode, "Input_LobbyCode");
|
|
Check(ddMaxPlayers, "Dropdown_MaxPlayers");
|
|
Check(tgPublicLobby, "Toggle_PublicLobby");
|
|
Check(btnInviteFriends, "Button_InviteFriends");
|
|
Check(btnBackFromFriends, "Button_BackFromFriends");
|
|
Check(btnStartGame, "Button_StartGame");
|
|
Check(btnLeaveLobby, "Button_LeaveLobby");
|
|
}
|
|
|
|
private void UpdateStatus()
|
|
{
|
|
if (textStatus)
|
|
{
|
|
textStatus.text = IsInLobby ? (IsHost ? "HOSTING" : "CONNECTED") : "OFFLINE";
|
|
textStatus.color = IsInLobby ? new Color(0.7f,1f,0.7f) : new Color(0.8f,0.8f,0.8f);
|
|
}
|
|
}
|
|
|
|
private void UpdateCode()
|
|
{
|
|
if (textLobbyCodeValue)
|
|
{
|
|
textLobbyCodeValue.text = (!string.IsNullOrEmpty(LobbyCode) && IsInLobby) ? LobbyCode : "------";
|
|
}
|
|
}
|
|
|
|
private void UpdateVisibility()
|
|
{
|
|
if (btnStartGame)
|
|
{
|
|
bool canHostStart = IsInLobby && IsHost;
|
|
btnStartGame.gameObject.SetActive(canHostStart);
|
|
if (btnStartGame.gameObject.activeSelf)
|
|
{
|
|
btnStartGame.interactable = AreAllPlayersReady();
|
|
}
|
|
}
|
|
if (btnLeaveLobby) btnLeaveLobby.gameObject.SetActive(IsInLobby);
|
|
if (btnToggleReady) btnToggleReady.gameObject.SetActive(IsInLobby && !IsHost);
|
|
if (btnInviteFriends) btnInviteFriends.gameObject.SetActive(IsInLobby && IsHost);
|
|
if (btnKickSelected) btnKickSelected.gameObject.SetActive(IsInLobby && IsHost);
|
|
|
|
if (groupHost) groupHost.SetActive(!IsInLobby); // hide setup groups once in lobby
|
|
if (groupJoin) groupJoin.SetActive(!IsInLobby);
|
|
|
|
if (emptyPlayers) emptyPlayers.SetActive(IsInLobby && contentPlayers && contentPlayers.childCount == 0);
|
|
}
|
|
|
|
private List<(string steamId, string name, bool isReady, bool isHost)> GetSteamMembers()
|
|
{
|
|
return steam != null ? steam.GetMembers().ToList() : new List<(string, string, bool, bool)>();
|
|
}
|
|
|
|
private void CacheMemberReadyStates(IEnumerable<(string steamId, string name, bool isReady, bool isHost)> members)
|
|
{
|
|
memberReadyCache.Clear();
|
|
if (members == null) return;
|
|
foreach (var member in members)
|
|
{
|
|
if (string.IsNullOrEmpty(member.steamId)) continue;
|
|
memberReadyCache[member.steamId] = member.isReady;
|
|
}
|
|
}
|
|
|
|
private bool TryGetMemberReadyState(string steamId, out bool isReady)
|
|
{
|
|
isReady = false;
|
|
if (string.IsNullOrEmpty(steamId)) return false;
|
|
|
|
if (memberReadyCache.TryGetValue(steamId, out var cachedReady))
|
|
{
|
|
isReady = cachedReady;
|
|
return true;
|
|
}
|
|
|
|
var members = GetSteamMembers();
|
|
CacheMemberReadyStates(members);
|
|
foreach (var member in members)
|
|
{
|
|
if (member.steamId == steamId)
|
|
{
|
|
isReady = member.isReady;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private bool AreAllPlayersReady()
|
|
{
|
|
if (!IsInLobby) return false;
|
|
if (memberReadyCache.Count == 0)
|
|
{
|
|
CacheMemberReadyStates(GetSteamMembers());
|
|
}
|
|
if (memberReadyCache.Count == 0) return false;
|
|
foreach (var kvp in memberReadyCache)
|
|
{
|
|
if (!kvp.Value) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void ApplyReadyBorder(Image avatarImage, bool isReady)
|
|
{
|
|
if (avatarImage == null) return;
|
|
|
|
var legacyOutline = avatarImage.GetComponent<Outline>();
|
|
if (legacyOutline != null) legacyOutline.enabled = false;
|
|
|
|
var borderImage = EnsureAvatarBorderImage(avatarImage);
|
|
if (borderImage == null) return;
|
|
|
|
if (avatarBorderThickness <= 0f)
|
|
{
|
|
borderImage.enabled = false;
|
|
return;
|
|
}
|
|
|
|
borderImage.enabled = true;
|
|
borderImage.color = isReady ? readyBorderColor : notReadyBorderColor;
|
|
|
|
var avatarRect = avatarImage.rectTransform;
|
|
var borderRect = borderImage.rectTransform;
|
|
borderRect.anchorMin = avatarRect.anchorMin;
|
|
borderRect.anchorMax = avatarRect.anchorMax;
|
|
borderRect.pivot = avatarRect.pivot;
|
|
borderRect.anchoredPosition = avatarRect.anchoredPosition;
|
|
borderRect.localPosition = avatarRect.localPosition;
|
|
borderRect.localRotation = avatarRect.localRotation;
|
|
borderRect.localScale = avatarRect.localScale;
|
|
var padding = avatarBorderThickness * 2f;
|
|
borderRect.sizeDelta = avatarRect.sizeDelta + new Vector2(padding, padding);
|
|
}
|
|
|
|
private Image EnsureAvatarBorderImage(Image avatarImage)
|
|
{
|
|
if (avatarImage == null) return null;
|
|
var parent = avatarImage.transform.parent;
|
|
if (parent == null) return null;
|
|
|
|
const string borderName = "Image_AvatarBorder";
|
|
Transform borderTransform = null;
|
|
for (int i = 0; i < parent.childCount; i++)
|
|
{
|
|
var child = parent.GetChild(i);
|
|
if (child != null && child.name == borderName)
|
|
{
|
|
borderTransform = child;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (borderTransform == null)
|
|
{
|
|
var borderGO = new GameObject(borderName, typeof(RectTransform), typeof(Image));
|
|
borderTransform = borderGO.transform;
|
|
borderTransform.SetParent(parent, false);
|
|
}
|
|
|
|
var borderImage = borderTransform.GetComponent<Image>();
|
|
if (borderImage == null)
|
|
{
|
|
borderImage = borderTransform.gameObject.AddComponent<Image>();
|
|
}
|
|
|
|
borderImage.raycastTarget = false;
|
|
borderImage.preserveAspect = false;
|
|
borderImage.type = Image.Type.Simple;
|
|
|
|
var layoutElement = borderTransform.GetComponent<LayoutElement>();
|
|
if (layoutElement == null)
|
|
{
|
|
layoutElement = borderTransform.gameObject.AddComponent<LayoutElement>();
|
|
}
|
|
layoutElement.ignoreLayout = true;
|
|
|
|
var avatarIndex = avatarImage.transform.GetSiblingIndex();
|
|
borderTransform.SetSiblingIndex(avatarIndex);
|
|
avatarImage.transform.SetSiblingIndex(borderTransform.GetSiblingIndex() + 1);
|
|
|
|
return borderImage;
|
|
}
|
|
|
|
private void RebuildPlayers()
|
|
{
|
|
if (contentPlayers == null) return;
|
|
// Clear current (keep template)
|
|
var toDestroy = new List<GameObject>();
|
|
foreach (Transform child in contentPlayers)
|
|
{
|
|
if (playerItemTemplate != null && child.gameObject == playerItemTemplate) continue;
|
|
toDestroy.Add(child.gameObject);
|
|
}
|
|
foreach (var go in toDestroy) DestroyImmediate(go);
|
|
|
|
var members = GetSteamMembers();
|
|
CacheMemberReadyStates(members);
|
|
if (textPlayerCount) textPlayerCount.text = $"{members.Count}/{(ddMaxPlayers ? int.Parse(ddMaxPlayers.options[ddMaxPlayers.value].text) : 4)}";
|
|
|
|
if (members.Count == 0)
|
|
{
|
|
if (emptyPlayers) emptyPlayers.SetActive(true);
|
|
return;
|
|
}
|
|
if (emptyPlayers) emptyPlayers.SetActive(false);
|
|
|
|
foreach (var m in members)
|
|
{
|
|
var item = CreatePlayerItem(m.steamId, m.name, m.isReady, m.isHost);
|
|
item.transform.SetParent(contentPlayers, false);
|
|
item.SetActive(true);
|
|
}
|
|
}
|
|
|
|
private GameObject CreatePlayerItem(string steamId, string name, bool ready, bool host)
|
|
{
|
|
GameObject go;
|
|
if (playerItemTemplate)
|
|
{
|
|
go = Instantiate(playerItemTemplate);
|
|
go.name = $"PlayerItem_{steamId}";
|
|
}
|
|
else
|
|
{
|
|
go = new GameObject("PlayerItem", typeof(RectTransform), typeof(HorizontalLayoutGroup), typeof(Image));
|
|
var img = go.GetComponent<Image>(); img.color = new Color(0.2f,0.2f,0.2f,0.9f);
|
|
}
|
|
|
|
// Ensure image background for highlight
|
|
var bg = go.GetComponent<Image>(); if (!bg) bg = go.AddComponent<Image>();
|
|
|
|
// Ensure button for selection
|
|
var btn = go.GetComponent<Button>(); if (!btn) btn = go.AddComponent<Button>();
|
|
|
|
// Ensure layout element to reserve height
|
|
var goLE = go.GetComponent<LayoutElement>(); if (!goLE) goLE = go.AddComponent<LayoutElement>();
|
|
goLE.minHeight = 100; goLE.flexibleWidth = 1f;
|
|
|
|
var horizontalLayout = go.GetComponent<HorizontalLayoutGroup>();
|
|
if (horizontalLayout)
|
|
{
|
|
var paddingValue = Mathf.CeilToInt(avatarBorderThickness);
|
|
if (horizontalLayout.padding.left < paddingValue) horizontalLayout.padding.left = paddingValue;
|
|
if (horizontalLayout.padding.right < paddingValue) horizontalLayout.padding.right = paddingValue;
|
|
}
|
|
|
|
// Children
|
|
// Find avatar inside this item (not globally)
|
|
Image avatarGO = null;
|
|
var avatarTr = FindChildRecursive(go.transform, "Image_Avatar");
|
|
if (avatarTr)
|
|
{
|
|
avatarGO = avatarTr.GetComponent<Image>();
|
|
}
|
|
else
|
|
{
|
|
// Create avatar placeholder if missing
|
|
var av = new GameObject("Image_Avatar", typeof(RectTransform), typeof(Image));
|
|
av.transform.SetParent(go.transform, false);
|
|
avatarGO = av.GetComponent<Image>();
|
|
var avRT = (RectTransform)av.transform; avRT.sizeDelta = new Vector2(40,40);
|
|
}
|
|
// Remove any name/status texts if present (UX: avatar only)
|
|
var texts = go.GetComponentsInChildren<TMP_Text>(true);
|
|
foreach (var t in texts)
|
|
{
|
|
if (t.name.Contains("Text_PlayerName") || t.name.Contains("Text_PlayerStatus"))
|
|
{
|
|
DestroyImmediate(t.gameObject);
|
|
}
|
|
}
|
|
|
|
// Load Steam avatar sprite if available
|
|
if (avatarGO)
|
|
{
|
|
if (steam != null && steam.TryGetAvatarSprite(steamId, out var spr, large: true))
|
|
{
|
|
avatarGO.sprite = spr;
|
|
avatarGO.color = Color.white;
|
|
avatarGO.type = Image.Type.Simple;
|
|
avatarGO.preserveAspect = true;
|
|
}
|
|
else
|
|
{
|
|
// Fallback color avatar
|
|
avatarGO.sprite = null;
|
|
avatarGO.color = new Color(0.3f,0.6f,0.3f,1f);
|
|
}
|
|
|
|
// Make avatar centered with compact size for PC layout
|
|
var avRT = (RectTransform)avatarGO.transform;
|
|
avRT.anchorMin = new Vector2(0.5f, 0.5f);
|
|
avRT.anchorMax = new Vector2(0.5f, 0.5f);
|
|
avRT.pivot = new Vector2(0.5f, 0.5f);
|
|
avRT.anchoredPosition = Vector2.zero;
|
|
avRT.sizeDelta = new Vector2(96, 96);
|
|
var avLE = avatarGO.GetComponent<LayoutElement>() ?? avatarGO.gameObject.AddComponent<LayoutElement>();
|
|
var avatarSize = Mathf.Max(avRT.sizeDelta.x, avRT.sizeDelta.y);
|
|
var paddedSize = avatarSize + avatarBorderThickness * 2f;
|
|
avLE.minWidth = paddedSize;
|
|
avLE.minHeight = paddedSize;
|
|
avLE.preferredWidth = paddedSize;
|
|
avLE.preferredHeight = paddedSize;
|
|
|
|
if (goLE != null)
|
|
{
|
|
goLE.minWidth = Mathf.Max(goLE.minWidth, paddedSize);
|
|
goLE.preferredWidth = Mathf.Max(goLE.preferredWidth, paddedSize);
|
|
}
|
|
|
|
ApplyReadyBorder(avatarGO, ready);
|
|
}
|
|
|
|
// Selection handler
|
|
btn.onClick.RemoveAllListeners();
|
|
btn.onClick.AddListener(() => { selectedPlayerSteamId = steamId; HighlightSelection(); });
|
|
|
|
// Highlight if selected
|
|
if (!string.IsNullOrEmpty(selectedPlayerSteamId) && selectedPlayerSteamId == steamId)
|
|
{
|
|
bg.color = new Color(0.3f,0.5f,0.3f,0.9f);
|
|
}
|
|
else
|
|
{
|
|
bg.color = new Color(0.2f,0.2f,0.2f,0.9f);
|
|
}
|
|
|
|
return go;
|
|
}
|
|
|
|
private TMP_Text CreateChildText(Transform parent, string name, int fontSize)
|
|
{
|
|
var tgo = new GameObject(name, typeof(RectTransform), typeof(TextMeshProUGUI));
|
|
tgo.transform.SetParent(parent, false);
|
|
var t = tgo.GetComponent<TextMeshProUGUI>();
|
|
// Use LegacyRuntime.ttf on newer Unity; fallback to Arial.ttf for older versions
|
|
Font builtinFont = null;
|
|
try { builtinFont = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf"); } catch {}
|
|
if (builtinFont == null) { try { builtinFont = Resources.GetBuiltinResource<Font>("Arial.ttf"); } catch {} }
|
|
if (TMP_Settings.defaultFontAsset != null) t.font = TMP_Settings.defaultFontAsset;
|
|
t.fontSize = fontSize; t.color = Color.white; t.alignment = TextAlignmentOptions.MidlineLeft;
|
|
var rt = (RectTransform)tgo.transform; rt.sizeDelta = new Vector2(0, fontSize + 10);
|
|
return t;
|
|
}
|
|
|
|
private void HighlightSelection()
|
|
{
|
|
if (contentPlayers == null) return;
|
|
foreach (Transform child in contentPlayers)
|
|
{
|
|
var img = child.GetComponent<Image>();
|
|
if (!img) continue;
|
|
if (child.name.EndsWith(selectedPlayerSteamId)) img.color = new Color(0.3f,0.5f,0.3f,0.9f);
|
|
else img.color = new Color(0.2f,0.2f,0.2f,0.9f);
|
|
}
|
|
}
|
|
}
|
|
}
|