Join and leave improvements

This commit is contained in:
2025-10-12 14:36:59 +02:00
parent ba150ac5d3
commit e1abeeb547
3 changed files with 347 additions and 2 deletions

View File

@@ -18,15 +18,18 @@ namespace MegaKoop.Game.Networking
public class LobbyGameSceneCoordinator : MonoBehaviour public class LobbyGameSceneCoordinator : MonoBehaviour
{ {
private static LobbyGameSceneCoordinator Instance; private static LobbyGameSceneCoordinator Instance;
public static LobbyGameSceneCoordinator Current => Instance;
[SerializeField] private string characterSceneName = "CharacterScene"; [SerializeField] private string characterSceneName = "CharacterScene";
[SerializeField] private float spawnRadius = 3f; [SerializeField] private float spawnRadius = 3f;
[SerializeField] private float minimumSpawnSpacing = 2.5f; [SerializeField] private float minimumSpawnSpacing = 2.5f;
private readonly List<LobbyPlayerInfo> pendingPlayers = new(); private readonly List<LobbyPlayerInfo> pendingPlayers = new();
private readonly Dictionary<ulong, GameObject> spawnedPlayerObjects = new();
private string localSteamId = string.Empty; private string localSteamId = string.Empty;
private bool loadPending; private bool loadPending;
private bool hasSpawned; private bool hasSpawned;
private SteamCoopNetworkManager coopNetworkManager;
private void Awake() private void Awake()
{ {
@@ -40,11 +43,13 @@ namespace MegaKoop.Game.Networking
Instance = this; Instance = this;
DontDestroyOnLoad(gameObject); DontDestroyOnLoad(gameObject);
SceneManager.sceneLoaded += HandleSceneLoaded; SceneManager.sceneLoaded += HandleSceneLoaded;
EnsureNetworkSubscription();
} }
private void OnDestroy() private void OnDestroy()
{ {
SceneManager.sceneLoaded -= HandleSceneLoaded; SceneManager.sceneLoaded -= HandleSceneLoaded;
UnsubscribeFromNetwork();
} }
/// <summary> /// <summary>
@@ -113,6 +118,14 @@ namespace MegaKoop.Game.Networking
pendingPlayers.Sort((a, b) => string.CompareOrdinal(a.SteamId ?? string.Empty, b.SteamId ?? string.Empty)); 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) private void HandleSceneLoaded(Scene scene, LoadSceneMode mode)
{ {
if (!string.Equals(scene.name, characterSceneName, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(scene.name, characterSceneName, StringComparison.OrdinalIgnoreCase))
@@ -125,6 +138,7 @@ namespace MegaKoop.Game.Networking
return; return;
} }
EnsureNetworkSubscription();
loadPending = false; loadPending = false;
hasSpawned = false; hasSpawned = false;
SpawnPlayersInScene(scene); SpawnPlayersInScene(scene);
@@ -137,6 +151,7 @@ namespace MegaKoop.Game.Networking
return; return;
} }
EnsureNetworkSubscription();
var template = FindWizardTemplate(scene); var template = FindWizardTemplate(scene);
if (template == null) if (template == null)
{ {
@@ -148,6 +163,7 @@ namespace MegaKoop.Game.Networking
// Proactively remove any previously spawned character clones (e.g., from a duplicate coordinator) // Proactively remove any previously spawned character clones (e.g., from a duplicate coordinator)
DespawnExistingClones(scene, template); DespawnExistingClones(scene, template);
spawnedPlayerObjects.Clear();
Vector3 basePosition = template.transform.position; Vector3 basePosition = template.transform.position;
Quaternion baseRotation = template.transform.rotation; Quaternion baseRotation = template.transform.rotation;
@@ -168,6 +184,17 @@ namespace MegaKoop.Game.Networking
ConfigureCloneForPlayer(clone, info, i); 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); clone.SetActive(true);
} }
} }
@@ -359,5 +386,106 @@ namespace MegaKoop.Game.Networking
public string DisplayName; public string DisplayName;
public bool IsLocal; 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<SteamCharacterNetworkBridge>();
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).");
}
}
} }
} }

View File

@@ -41,6 +41,7 @@ namespace MegaKoop.Game.Networking
public bool IsLocalPlayer => isLocalPlayer; public bool IsLocalPlayer => isLocalPlayer;
public bool IsAuthority => isAuthority; public bool IsAuthority => isAuthority;
public ulong OwnerSteamId => ownerSteamId;
private void Awake() private void Awake()
{ {

View File

@@ -17,13 +17,20 @@ namespace MegaKoop.Game.Networking
[SerializeField] private SteamP2PTransport p2pTransport; [SerializeField] private SteamP2PTransport p2pTransport;
private readonly Dictionary<NetworkMessageType, Action<NetworkMessage>> handlers = new(); private readonly Dictionary<NetworkMessageType, Action<NetworkMessage>> handlers = new();
private readonly HashSet<ulong> lobbyMembers = new();
private readonly Dictionary<ulong, string> memberNames = new();
private readonly List<OnScreenMessage> onScreenMessages = new();
private bool isHost; private bool isHost;
private bool isConnected; private bool isConnected;
private ulong lobbyOwnerSteamId;
public bool IsHost => isHost; public bool IsHost => isHost;
public bool IsConnected => isConnected; public bool IsConnected => isConnected;
public CSteamID ActiveLobby => lobbyManager != null ? lobbyManager.GetActiveLobby() : CSteamID.Nil; public CSteamID ActiveLobby => lobbyManager != null ? lobbyManager.GetActiveLobby() : CSteamID.Nil;
public event Action HostClosed;
public event Action<ulong> PlayerDisconnected;
private void Awake() private void Awake()
{ {
if (Instance != null) if (Instance != null)
@@ -131,6 +138,44 @@ namespace MegaKoop.Game.Networking
isConnected = steamService.IsInLobby; isConnected = steamService.IsInLobby;
isHost = steamService.IsHost; 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 STEAMWORKSNET
if (p2pTransport != null) if (p2pTransport != null)
{ {
@@ -167,6 +212,10 @@ namespace MegaKoop.Game.Networking
isHost = true; isHost = true;
isConnected = true; isConnected = true;
p2pTransport?.SetActiveLobby(lobbyId); p2pTransport?.SetActiveLobby(lobbyId);
lobbyOwnerSteamId = SteamUser.GetSteamID().m_SteamID;
lobbyMembers.Clear();
memberNames.Clear();
RegisterMember(lobbyOwnerSteamId);
} }
private void HandleLobbyJoined(CSteamID lobbyId) private void HandleLobbyJoined(CSteamID lobbyId)
@@ -177,22 +226,189 @@ namespace MegaKoop.Game.Networking
string ownerId = SteamMatchmaking.GetLobbyData(lobbyId, "owner"); string ownerId = SteamMatchmaking.GetLobbyData(lobbyId, "owner");
if (!string.IsNullOrEmpty(ownerId) && ulong.TryParse(ownerId, out ulong ownerSteamId)) if (!string.IsNullOrEmpty(ownerId) && ulong.TryParse(ownerId, out ulong ownerSteamId))
{ {
lobbyOwnerSteamId = ownerSteamId;
isHost = ownerSteamId == SteamUser.GetSteamID().m_SteamID; isHost = ownerSteamId == SteamUser.GetSteamID().m_SteamID;
} }
else 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) 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); Debug.Log("[SteamCoopNetworkManager] Member joined: " + member);
} }
private void HandleLobbyMemberLeft(CSteamID member) private void HandleLobbyMemberLeft(CSteamID 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); 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;
} }
} }
} }