Files
megakoop/UI/Scripts/UGUIMultiplayerLobbyController.cs
2025-10-05 19:15:37 +02:00

1057 lines
40 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 & Code
private TMP_Text textStatus;
private TMP_Text textLobbyCodeValue;
private Button btnCopyCode;
// Tabs
private Button btnHostTab;
private Button btnJoinTab;
// Join
private TMP_InputField inputLobbyCode;
private Button btnConnect;
// Host
private TMP_Dropdown ddMaxPlayers;
private Toggle tgPublicLobby;
private Button btnCreateLobby;
// Players
private TMP_Text textPlayerCount;
private ScrollRect scrollPlayers;
private Transform contentPlayers;
private GameObject playerItemTemplate;
private GameObject emptyPlayers;
// Friends picker (fallback when overlay unavailable)
private GameObject panelFriends;
private Transform contentFriends;
private GameObject emptyFriends;
private Button btnBackFromFriends;
private Button btnCloseFriendsOverlay;
// Host controls
private Button btnInviteFriends;
private Button btnKickSelected;
// Ready & footer
private Button btnToggleReady;
private Button btnStartGame;
private Button btnLeaveLobby;
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;
// 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 void Awake()
{
// Find UI (including inactive)
panelLobby = FindAnyGO("Panel_Lobby");
groupJoin = FindAnyGO("Group_Join");
groupHost = FindAnyGO("Group_Host");
textStatus = FindText("Text_Status");
textLobbyCodeValue = FindText("Text_LobbyCodeValue");
btnCopyCode = FindButton("Button_CopyCode");
btnHostTab = FindButton("Button_HostTab");
btnJoinTab = FindButton("Button_JoinTab");
inputLobbyCode = FindInput("Input_LobbyCode");
btnConnect = FindButton("Button_Connect");
ddMaxPlayers = FindDropdown("Dropdown_MaxPlayers");
tgPublicLobby = FindToggle("Toggle_PublicLobby");
btnCreateLobby = FindButton("Button_CreateLobby");
textPlayerCount = FindText("Text_PlayerCount");
scrollPlayers = FindScroll("Scroll_Players");
contentPlayers = FindAnyGO("Content_PlayersList")?.transform;
emptyPlayers = FindAnyGO("Empty_Players");
playerItemTemplate = FindAnyGO("PlayerItemTemplate");
btnInviteFriends = FindButton("Button_InviteFriends");
btnKickSelected = FindButton("Button_KickSelected");
btnToggleReady = FindButton("Button_ToggleReady");
btnStartGame = FindButton("Button_StartGame");
btnLeaveLobby = FindButton("Button_LeaveLobby");
btnBackFromLobby = FindButton("Button_BackFromLobby");
// Friends picker
panelFriends = FindAnyGO("Panel_Friends");
contentFriends = FindAnyGO("Content_FriendsList")?.transform;
emptyFriends = FindAnyGO("Empty_Friends");
btnBackFromFriends = FindButton("Button_BackFromFriends");
btnCloseFriendsOverlay = FindButton("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;
}
private TMP_Text FindText(string name) => FindAnyComponent<TMP_Text>(name);
private Button FindButton(string name) => FindAnyComponent<Button>(name);
private Toggle FindToggle(string name) => FindAnyComponent<Toggle>(name);
private TMP_Dropdown FindDropdown(string name) => FindAnyComponent<TMP_Dropdown>(name);
private TMP_InputField FindInput(string name) => FindAnyComponent<TMP_InputField>(name);
private ScrollRect FindScroll(string name) => FindAnyComponent<ScrollRect>(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>();
}
lobbyGameCoordinator = servicesRoot.GetComponent<LobbyGameSceneCoordinator>() ?? servicesRoot.AddComponent<LobbyGameSceneCoordinator>();
}
}
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;
}
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;
}
private void OnLobbyCreated()
{
selectedPlayerSteamId = string.Empty;
memberReadyCache.Clear();
UpdateUIFromSteam();
// Auto-open invite overlay for the host
if (steam != null && steam.IsInLobby && steam.IsHost)
{
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 (steam != null && steam.IsInLobby && steam.IsHost)
{
steam.InviteFriends();
if (!steam.IsOverlayEnabled()) ShowFriendsPanel();
}
}
private void OnLobbyLeft()
{
selectedPlayerSteamId = string.Empty;
memberReadyCache.Clear();
UpdateUIFromSteam();
}
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
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);
}
}
}
}