From e1abeeb54793d3edf2836dfbf6e2a064ce3b38d7 Mon Sep 17 00:00:00 2001 From: Marek Sorokin Date: Sun, 12 Oct 2025 14:36:59 +0200 Subject: [PATCH] Join and leave improvements --- .../Networking/LobbyGameSceneCoordinator.cs | 128 ++++++++++ .../Networking/SteamCharacterNetworkBridge.cs | 1 + .../Networking/SteamCoopNetworkManager.cs | 220 +++++++++++++++++- 3 files changed, 347 insertions(+), 2 deletions(-) diff --git a/Game/Scripts/Networking/LobbyGameSceneCoordinator.cs b/Game/Scripts/Networking/LobbyGameSceneCoordinator.cs index d870010..42a5c93 100644 --- a/Game/Scripts/Networking/LobbyGameSceneCoordinator.cs +++ b/Game/Scripts/Networking/LobbyGameSceneCoordinator.cs @@ -18,15 +18,18 @@ namespace MegaKoop.Game.Networking public class LobbyGameSceneCoordinator : MonoBehaviour { private static LobbyGameSceneCoordinator Instance; + public static LobbyGameSceneCoordinator Current => Instance; [SerializeField] private string characterSceneName = "CharacterScene"; [SerializeField] private float spawnRadius = 3f; [SerializeField] private float minimumSpawnSpacing = 2.5f; private readonly List pendingPlayers = new(); + private readonly Dictionary spawnedPlayerObjects = new(); private string localSteamId = string.Empty; private bool loadPending; private bool hasSpawned; + private SteamCoopNetworkManager coopNetworkManager; private void Awake() { @@ -40,11 +43,13 @@ namespace MegaKoop.Game.Networking Instance = this; DontDestroyOnLoad(gameObject); SceneManager.sceneLoaded += HandleSceneLoaded; + EnsureNetworkSubscription(); } private void OnDestroy() { SceneManager.sceneLoaded -= HandleSceneLoaded; + UnsubscribeFromNetwork(); } /// @@ -113,6 +118,14 @@ namespace MegaKoop.Game.Networking pendingPlayers.Sort((a, b) => string.CompareOrdinal(a.SteamId ?? string.Empty, b.SteamId ?? string.Empty)); } + private void LateUpdate() + { + if (coopNetworkManager == null) + { + EnsureNetworkSubscription(); + } + } + private void HandleSceneLoaded(Scene scene, LoadSceneMode mode) { if (!string.Equals(scene.name, characterSceneName, StringComparison.OrdinalIgnoreCase)) @@ -125,6 +138,7 @@ namespace MegaKoop.Game.Networking return; } + EnsureNetworkSubscription(); loadPending = false; hasSpawned = false; SpawnPlayersInScene(scene); @@ -137,6 +151,7 @@ namespace MegaKoop.Game.Networking return; } + EnsureNetworkSubscription(); var template = FindWizardTemplate(scene); if (template == null) { @@ -148,6 +163,7 @@ namespace MegaKoop.Game.Networking // Proactively remove any previously spawned character clones (e.g., from a duplicate coordinator) DespawnExistingClones(scene, template); + spawnedPlayerObjects.Clear(); Vector3 basePosition = template.transform.position; Quaternion baseRotation = template.transform.rotation; @@ -168,6 +184,17 @@ namespace MegaKoop.Game.Networking ConfigureCloneForPlayer(clone, info, i); + ulong ownerSteamId = ParseSteamId(info.SteamId); + if (ownerSteamId == 0UL && info.IsLocal) + { + ownerSteamId = ParseSteamId(localSteamId); + } + + if (ownerSteamId != 0UL) + { + spawnedPlayerObjects[ownerSteamId] = clone; + } + clone.SetActive(true); } } @@ -359,5 +386,106 @@ namespace MegaKoop.Game.Networking public string DisplayName; public bool IsLocal; } + + private void EnsureNetworkSubscription() + { + var manager = SteamCoopNetworkManager.Instance; + if (manager == null || manager == coopNetworkManager) + { + return; + } + + UnsubscribeFromNetwork(); + + coopNetworkManager = manager; + coopNetworkManager.HostClosed += HandleHostClosed; + coopNetworkManager.PlayerDisconnected += HandlePlayerDisconnected; + } + + private void UnsubscribeFromNetwork() + { + if (coopNetworkManager == null) + { + return; + } + + coopNetworkManager.HostClosed -= HandleHostClosed; + coopNetworkManager.PlayerDisconnected -= HandlePlayerDisconnected; + coopNetworkManager = null; + } + + private void HandleHostClosed() + { + Debug.Log("[LobbyGameSceneCoordinator] Host closed session; despawning all players."); + RemoveAllSpawnedPlayers(); + } + + private void HandlePlayerDisconnected(ulong steamId) + { + RemoveSpawnedPlayer(steamId); + } + + private void RemoveAllSpawnedPlayers() + { + if (spawnedPlayerObjects.Count == 0) + { + return; + } + + foreach (var avatar in spawnedPlayerObjects.Values) + { + if (avatar != null) + { + Destroy(avatar); + } + } + + spawnedPlayerObjects.Clear(); + hasSpawned = false; + } + + private void RemoveSpawnedPlayer(ulong steamId) + { + if (steamId == 0) + { + return; + } + + if (spawnedPlayerObjects.TryGetValue(steamId, out GameObject avatar)) + { + if (avatar != null) + { + Destroy(avatar); + } + spawnedPlayerObjects.Remove(steamId); + Debug.Log($"[LobbyGameSceneCoordinator] Removed avatar for {steamId}."); + return; + } + + ulong? keyToRemove = null; + foreach (var kvp in spawnedPlayerObjects) + { + var target = kvp.Value; + if (target == null) + { + keyToRemove = kvp.Key; + continue; + } + + var bridge = target.GetComponent(); + if (bridge != null && bridge.OwnerSteamId == steamId) + { + Destroy(target); + keyToRemove = kvp.Key; + break; + } + } + + if (keyToRemove.HasValue) + { + spawnedPlayerObjects.Remove(keyToRemove.Value); + Debug.Log($"[LobbyGameSceneCoordinator] Removed avatar for {steamId} (fallback search)."); + } + } } } diff --git a/Game/Scripts/Networking/SteamCharacterNetworkBridge.cs b/Game/Scripts/Networking/SteamCharacterNetworkBridge.cs index 6c5322d..9f49570 100644 --- a/Game/Scripts/Networking/SteamCharacterNetworkBridge.cs +++ b/Game/Scripts/Networking/SteamCharacterNetworkBridge.cs @@ -41,6 +41,7 @@ namespace MegaKoop.Game.Networking public bool IsLocalPlayer => isLocalPlayer; public bool IsAuthority => isAuthority; + public ulong OwnerSteamId => ownerSteamId; private void Awake() { diff --git a/Game/Scripts/Networking/SteamCoopNetworkManager.cs b/Game/Scripts/Networking/SteamCoopNetworkManager.cs index 896a31f..24d5aec 100644 --- a/Game/Scripts/Networking/SteamCoopNetworkManager.cs +++ b/Game/Scripts/Networking/SteamCoopNetworkManager.cs @@ -17,13 +17,20 @@ namespace MegaKoop.Game.Networking [SerializeField] private SteamP2PTransport p2pTransport; private readonly Dictionary> handlers = new(); + private readonly HashSet lobbyMembers = new(); + private readonly Dictionary memberNames = new(); + private readonly List onScreenMessages = new(); private bool isHost; private bool isConnected; + private ulong lobbyOwnerSteamId; public bool IsHost => isHost; public bool IsConnected => isConnected; public CSteamID ActiveLobby => lobbyManager != null ? lobbyManager.GetActiveLobby() : CSteamID.Nil; + public event Action HostClosed; + public event Action PlayerDisconnected; + private void Awake() { if (Instance != null) @@ -131,6 +138,44 @@ namespace MegaKoop.Game.Networking isConnected = steamService.IsInLobby; isHost = steamService.IsHost; + if (steamService.IsInLobby) + { + lobbyMembers.Clear(); + memberNames.Clear(); + lobbyOwnerSteamId = 0; + + var members = steamService.GetMembers(); + if (members != null) + { + foreach (var member in members) + { + if (!ulong.TryParse(member.steamId, out ulong steamId)) + { + continue; + } + + lobbyMembers.Add(steamId); + memberNames[steamId] = string.IsNullOrWhiteSpace(member.name) ? steamId.ToString() : member.name; + + if (member.isHost) + { + lobbyOwnerSteamId = steamId; + } + } + } + + if (lobbyOwnerSteamId == 0 && steamService.IsHost && SteamBootstrap.IsInitialized) + { + lobbyOwnerSteamId = SteamUser.GetSteamID().m_SteamID; + } + } + else + { + lobbyMembers.Clear(); + memberNames.Clear(); + lobbyOwnerSteamId = 0; + } + #if STEAMWORKSNET if (p2pTransport != null) { @@ -167,6 +212,10 @@ namespace MegaKoop.Game.Networking isHost = true; isConnected = true; p2pTransport?.SetActiveLobby(lobbyId); + lobbyOwnerSteamId = SteamUser.GetSteamID().m_SteamID; + lobbyMembers.Clear(); + memberNames.Clear(); + RegisterMember(lobbyOwnerSteamId); } private void HandleLobbyJoined(CSteamID lobbyId) @@ -177,22 +226,189 @@ namespace MegaKoop.Game.Networking string ownerId = SteamMatchmaking.GetLobbyData(lobbyId, "owner"); if (!string.IsNullOrEmpty(ownerId) && ulong.TryParse(ownerId, out ulong ownerSteamId)) { + lobbyOwnerSteamId = ownerSteamId; isHost = ownerSteamId == SteamUser.GetSteamID().m_SteamID; } else { - isHost = SteamMatchmaking.GetLobbyOwner(lobbyId) == SteamUser.GetSteamID(); + CSteamID owner = SteamMatchmaking.GetLobbyOwner(lobbyId); + lobbyOwnerSteamId = owner.m_SteamID; + isHost = owner == SteamUser.GetSteamID(); } + + RegisterExistingMembers(lobbyId); } private void HandleLobbyMemberJoined(CSteamID member) { + RegisterMember(member.m_SteamID); + + if (ActiveLobby != CSteamID.Nil) + { + CSteamID owner = SteamMatchmaking.GetLobbyOwner(ActiveLobby); + if (owner != CSteamID.Nil) + { + lobbyOwnerSteamId = owner.m_SteamID; + } + } + Debug.Log("[SteamCoopNetworkManager] Member joined: " + member); } private void HandleLobbyMemberLeft(CSteamID member) { - Debug.Log("[SteamCoopNetworkManager] Member left: " + member); + ulong steamId = member.m_SteamID; + string displayName = ResolveMemberName(steamId); + + bool hostLeft = lobbyOwnerSteamId != 0 && steamId == lobbyOwnerSteamId; + lobbyMembers.Remove(steamId); + memberNames.Remove(steamId); + + if (hostLeft) + { + Debug.Log("[SteamCoopNetworkManager] Host left lobby: " + member); + ShowOnScreenMessage("Host closed the game.", 6f); + HostClosed?.Invoke(); + + if (!isHost) + { + ForceDisconnectFromLobby(); + } + } + else + { + Debug.Log("[SteamCoopNetworkManager] Member left: " + member); + string message = string.IsNullOrEmpty(displayName) ? "A player disconnected." : $"{displayName} disconnected."; + ShowOnScreenMessage(message, 4f); + PlayerDisconnected?.Invoke(steamId); + } + } + + private void Update() + { + if (onScreenMessages.Count == 0) + { + return; + } + + float now = Time.unscaledTime; + for (int i = onScreenMessages.Count - 1; i >= 0; i--) + { + if (onScreenMessages[i].ExpiresAt <= now) + { + onScreenMessages.RemoveAt(i); + } + } + } + + private void OnGUI() + { + if (onScreenMessages.Count == 0) + { + return; + } + + float y = 20f; + foreach (var entry in onScreenMessages) + { + if (entry.ExpiresAt <= Time.unscaledTime) + { + continue; + } + + GUIContent content = new(entry.Text); + Vector2 size = GUI.skin.box.CalcSize(content); + Rect rect = new(20f, y, Mathf.Max(220f, size.x + 20f), size.y + 10f); + GUI.Box(rect, content); + y += rect.height + 6f; + } + } + + private void RegisterExistingMembers(CSteamID lobbyId) + { + lobbyMembers.Clear(); + memberNames.Clear(); + + int count = SteamMatchmaking.GetNumLobbyMembers(lobbyId); + for (int i = 0; i < count; i++) + { + CSteamID member = SteamMatchmaking.GetLobbyMemberByIndex(lobbyId, i); + RegisterMember(member.m_SteamID); + } + } + + private void RegisterMember(ulong steamId) + { + if (steamId == 0) + { + return; + } + + lobbyMembers.Add(steamId); + memberNames[steamId] = ResolveMemberNameInternal(steamId); + } + + private string ResolveMemberName(ulong steamId) + { + if (memberNames.TryGetValue(steamId, out string cached) && !string.IsNullOrEmpty(cached)) + { + return cached; + } + + return ResolveMemberNameInternal(steamId); + } + + private string ResolveMemberNameInternal(ulong steamId) + { +#if STEAMWORKSNET + if (steamId != 0 && SteamBootstrap.IsInitialized) + { + try + { + return SteamFriends.GetFriendPersonaName(new CSteamID(steamId)); + } + catch + { + // Ignore Steam API hiccups and fall back to id string. + } + } +#endif + return steamId != 0 ? steamId.ToString() : string.Empty; + } + + private void ShowOnScreenMessage(string message, float lifetimeSeconds) + { + if (string.IsNullOrWhiteSpace(message)) + { + return; + } + + float expiry = Time.unscaledTime + Mathf.Max(0.5f, lifetimeSeconds); + onScreenMessages.Add(new OnScreenMessage + { + Text = message, + ExpiresAt = expiry + }); + } + + private void ForceDisconnectFromLobby() + { + if (ActiveLobby != CSteamID.Nil && SteamBootstrap.IsInitialized) + { + SteamMatchmaking.LeaveLobby(ActiveLobby); + } + + lobbyMembers.Clear(); + memberNames.Clear(); + lobbyOwnerSteamId = 0; + isConnected = false; + p2pTransport?.SetActiveLobby(CSteamID.Nil); + } + + private struct OnScreenMessage + { + public string Text; + public float ExpiresAt; } } }