Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make it possible to leave lobbies ++ #665

Merged
merged 10 commits into from
May 31, 2024
337 changes: 321 additions & 16 deletions Assets/Scenes/Menu.unity

Large diffs are not rendered by default.

120 changes: 107 additions & 13 deletions Assets/Scripts/Control&Input/Peer2PeerTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using CollectionExtensions;
using Mirror;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.SceneManagement;

// TODO consider splitting this into match-specific state and general player metadata
public struct PlayerDetails
{
public uint id;
Expand Down Expand Up @@ -36,6 +38,16 @@ public PlayerConnectedMessage(int inputID)
public int inputID;
}

public struct PlayerLeftMessage : NetworkMessage
{
public PlayerLeftMessage(uint id)
{
this.id = id;
}

public uint id;
}

public struct InitialPlayerDetailsMessage : NetworkMessage
{
public InitialPlayerDetailsMessage(PlayerDetails details)
Expand Down Expand Up @@ -115,10 +127,16 @@ public class Peer2PeerTransport : NetworkManager

private PlayerFactory playerFactory;
private static Transform[] spawnPoints;
private static Stack<Transform> spawnPointStack;
private static int playerIndex;
private static Stack<Color> availableColors = new();

private static bool isInMatch;
public static bool IsInMatch => isInMatch;

private static Dictionary<uint, PlayerDetails> players = new();
public static int NumPlayers => players.Count;
public const int MaxPlayers = 4;
public static IEnumerable<PlayerDetails> PlayerDetails => players.Values;

private static List<uint> localPlayerIds = new();
Expand All @@ -136,17 +154,20 @@ public class Peer2PeerTransport : NetworkManager

public delegate void LobbyPlayerEvent(PlayerDetails details);
public LobbyPlayerEvent OnPlayerRecieved;
public LobbyPlayerEvent OnPlayerRemoved;

public delegate void ConnectionEvent(int connectionID);
public ConnectionEvent OnDisconnect;

private void ResetState()
{
isInMatch = false;
playerIndex = 0;
players = new();
playerInstances = new();
PlayerInstanceByID = new(playerInstances);
localPlayerIds = new();
availableColors = new(PlayerInputManagerController.Singleton.PlayerColors.Reverse());

connections = new();
connectedPlayers = new();
Expand All @@ -168,6 +189,7 @@ public override void OnClientConnect()
{
base.OnClientConnect();
NetworkClient.RegisterHandler<StartMatchMessage>(OnStartMatch);
NetworkClient.RegisterHandler<PlayerLeftMessage>(OnPlayerLeft);
NetworkClient.RegisterHandler<InitialPlayerDetailsMessage>(OnReceivePlayerDetails);
NetworkClient.RegisterHandler<InitializePlayerMessage>(InitializeFPSPlayer);
NetworkClient.RegisterHandler<UpdatedPlayerDetailsMessage>(OnReceiveUpdatedPlayerDetails);
Expand All @@ -185,21 +207,35 @@ public override void OnServerDisconnect(NetworkConnectionToClient connection)
return;
Debug.Log($"Removed connection {connections.IndexOf(connection)} which has players {playersForConnection[connection.connectionId].ToCommaSeparatedString()}");
connections.Remove(connection);
connectedPlayers.RemoveAll(id => playersForConnection[connection.connectionId].Contains(id));
var playerIDs = playersForConnection[connection.connectionId];
connectedPlayers.RemoveAll(id => playerIDs.Contains(id));
playersForConnection.Remove(connection.connectionId);
// TODO send msg to ItemSelectManager that this connection is invalid
if (!isInMatch)
{
foreach (var id in playerIDs)
{
if (!players.TryGetValue(id, out var details))
continue;
availableColors.Push(details.color);
NetworkServer.SendToAll(new PlayerLeftMessage(id));
}
}
OnDisconnect?.Invoke(connection.connectionId);
}

public override void OnStopServer()
{
Debug.Log("Stopped server");
if (SteamManager.IsSteamActive && SteamManager.Singleton.IsInLobby)
SteamManager.Singleton.LeaveLobby();
ResetState();
}

public override void OnStopClient()
{
Debug.Log("Stopped client");
if (SteamManager.IsSteamActive && SteamManager.Singleton.IsInLobby)
SteamManager.Singleton.LeaveLobby();
ResetState();
}

Expand All @@ -216,6 +252,23 @@ public override void OnClientDisconnect()
ResetState();
}

private void OnPlayerLeft(PlayerLeftMessage message)
{
if (!players.TryGetValue(message.id, out var playerDetails))
{
Debug.LogError($"Received leave message for invalid player {message.id}");
toberge marked this conversation as resolved.
Show resolved Hide resolved
return;
}

Debug.Log($"Player {message.id} {playerDetails.name} left");

players.Remove(message.id);
if (playerInstances.ContainsKey(message.id))
playerInstances.Remove(message.id);

OnPlayerRemoved?.Invoke(playerDetails);
}

public void JoinLobby(string address = "127.0.0.1")
{
if (NetworkServer.active)
Expand Down Expand Up @@ -254,10 +307,18 @@ public static void StartTrainingMode()
private static IEnumerator WaitAndSwitchToTrainingMode()
{
LoadingScreen.Singleton.Show();

// Wait for player details to be populated
while (!NetworkClient.isConnected && !singleton.isNetworkActive && players.Count < PlayerInputManagerController.Singleton.LocalPlayerInputs.Count)
yield return new WaitForEndOfFrame();
singleton.ServerChangeScene(Scenes.TrainingMode);
// TODO until after spawned

yield return new WaitForSeconds(LoadingScreen.Singleton.MandatoryDuration);

// Wait for player(s) to have spawned
// TODO add a timeout for these wait-for-spawn spins
while (FindObjectsByType<PlayerManager>(FindObjectsSortMode.None).Count() < players.Count)
yield return new WaitForEndOfFrame();
toberge marked this conversation as resolved.
Show resolved Hide resolved
LoadingScreen.Singleton.Hide();
}

Expand Down Expand Up @@ -288,8 +349,9 @@ private void OnSpawnPlayerInput(NetworkConnectionToClient connection, PlayerConn
{
// Avoid adding more than the four allowed players
// TODO prevent this in some other more sustainable way :)))))
if (NumPlayers >= 4)
if (NumPlayers >= MaxPlayers)
return;

// Register connection
PlayerInputManagerController.Singleton.NetworkClients.Add(connection);
if (!connections.Contains(connection))
Expand All @@ -301,6 +363,7 @@ private void OnSpawnPlayerInput(NetworkConnectionToClient connection, PlayerConn
playersForConnection[connection.connectionId] = new();
playersForConnection[connection.connectionId].Add((uint)playerIndex);

// Determine metadata
var playerType = PlayerType.Local;
var playerName = "Player";
ulong steamID = 0;
Expand All @@ -318,16 +381,24 @@ private void OnSpawnPlayerInput(NetworkConnectionToClient connection, PlayerConn
connection.Send(new InitialPlayerDetailsMessage(existingPlayer));
}

// TODO consider just putting this stuff into the InitialPlayerDetailsMessage
// Pick among the available colors
if (!availableColors.TryPop(out var color))
{
// Recycle colors if necessary
availableColors = new(PlayerInputManagerController.Singleton.PlayerColors.Reverse());
color = availableColors.Pop();
}

var details = new PlayerDetails
{
id = (uint)playerIndex,
localInputID = message.inputID,
steamID = steamID,
type = playerType,
name = playerName,
color = PlayerInputManagerController.Singleton.PlayerColors[playerIndex],
color = color,
};

// Send information about this player to all
NetworkServer.SendToAll(new InitialPlayerDetailsMessage(details));
playerIndex++;
Expand Down Expand Up @@ -380,7 +451,7 @@ private void OnReceiveUpdatedPlayerDetails(UpdatedPlayerDetailsMessage message)

private void AddAiPlayers()
{
for (var i = players.Count; i < 4; i++)
for (var i = players.Count; i < MaxPlayers; i++)
{
var details = new PlayerDetails
{
Expand Down Expand Up @@ -445,14 +516,16 @@ public override void OnServerChangeScene(string newSceneName)
switch (newSceneName)
{
case Scenes.Bidding:
isInMatch = true;
UpdatePlayerDetailsAfterShootingRound();
NetworkServer.ReplaceHandler<SpawnPlayerMessage>(OnSpawnBiddingPlayer);
break;
case Scenes.Menu:
isInMatch = false;
NetworkServer.RegisterHandler<PlayerConnectedMessage>(OnSpawnPlayerInput);
break;
default:
// From auction, so no need to update *here*
isInMatch = true;
NetworkServer.ReplaceHandler<SpawnPlayerMessage>(OnSpawnFPSPlayer);
break;
}
Expand Down Expand Up @@ -500,13 +573,16 @@ public override void OnClientChangeScene(string newSceneName, SceneOperation sce
{
// TODO consider just pushing music change into this switch block
case Scenes.Bidding:
isInMatch = true;
NetworkClient.ReplaceHandler<InitializePlayerMessage>(InitializeBiddingPlayer);
PlayerInputManagerController.Singleton.ChangeInputMaps("Bidding");
break;
case Scenes.Menu:
isInMatch = false;
PlayerInputManagerController.Singleton.ChangeInputMaps("Menu");
break;
default:
isInMatch = true;
NetworkClient.ReplaceHandler<InitializePlayerMessage>(InitializeFPSPlayer);
PlayerInputManagerController.Singleton.ChangeInputMaps("FPS");
break;
Expand Down Expand Up @@ -557,27 +633,39 @@ private void SpawnPlayer(NetworkConnectionToClient connection, SpawnPlayerMessag
Debug.LogError($"No such player: id={message.id}");
return;
}
Debug.Log($"Spawning player {message.id}");

if (!playerFactory)
{
playerFactory = FindAnyObjectByType<PlayerFactory>();
if (!playerFactory) // TODO shouldn't happen, seems to occur when you go back to menu after end of match
return;
spawnPoints = playerFactory.GetRandomSpawnpoints();
spawnPointStack = new(spawnPoints);
}
Debug.Log($"Spawning player {message.id}");

var spawnPoint = spawnPoints[message.id];
// Ensure we aren't providing invalid spawnpoints.
// We should never run out of spawnpoints in normal circumstances, but you never know 💀
if (!spawnPointStack.TryPop(out var spawnPoint))
spawnPoint = spawnPoints.RandomElement();

// Instantiate correct prefab for player and mode.
var prefabIndex = playerDetails.type is PlayerType.AI && NetworkServer.active ? AIFPSPlayerPrefabIndex : FPSPlayerPrefabIndex;
if (SceneManager.GetActiveScene().name == Scenes.TrainingMode)
prefabIndex = TrainingPlayerPrefabIndex;
var prefab = spawnPrefabs[prefabIndex + prefabIndexOffset];
var player = Instantiate(prefab, spawnPoint.position, spawnPoint.rotation);
player.GetComponent<PlayerManager>().id = message.id;
if (playerDetails.type is not PlayerType.AI && playerDetails.localInputID == 0)

// Spawn player, setting it as the player for a connection only for the first local player for each connection.
var isNotAI = playerDetails.type is not PlayerType.AI;
var isFirstLocalPlayer = playerDetails.localInputID == 0;
var isNotDisconnected = playerDetails.type is not PlayerType.Remote || connectedPlayers.Contains(playerDetails.id);
if (isNotAI && isFirstLocalPlayer && isNotDisconnected)
NetworkServer.AddPlayerForConnection(connection, player);
else
NetworkServer.Spawn(player, connection);

NetworkServer.SendToAll(new InitializePlayerMessage(message.id, spawnPoint.position, spawnPoint.rotation));
}

Expand All @@ -591,12 +679,18 @@ private void InitializeFPSPlayer(InitializePlayerMessage message)
StartCoroutine(WaitAndInitializeFPSPlayer(message));
}

private void UpdateIdentityFromDetails(PlayerIdentity identity, PlayerDetails playerDetails)
// TODO move this somewhere else?
public static string PlayerNameWithIndex(PlayerDetails playerDetails)
{
var playerName = playerDetails.name;
if (players.Values.Count(p => p.steamID == playerDetails.steamID) > 1)
playerName = $"{playerName} {playerDetails.localInputID + 1}";
identity.UpdateFromDetails(playerDetails, playerName);
return playerName;
}

private void UpdateIdentityFromDetails(PlayerIdentity identity, PlayerDetails playerDetails)
{
identity.UpdateFromDetails(playerDetails, PlayerNameWithIndex(playerDetails));
}

private IEnumerator WaitAndInitializeFPSPlayer(InitializePlayerMessage message)
Expand Down
6 changes: 2 additions & 4 deletions Assets/Scripts/Gamestate/MatchController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -317,9 +317,7 @@ public void ReturnToMainMenu()
PlayerInputManagerController.Singleton.ChangeInputMaps("Menu");
SceneManager.LoadSceneAsync(Scenes.Menu);

if (NetworkServer.active)
NetworkManager.singleton.StopHost();
else
NetworkManager.singleton.StopClient();
// TODO go back to lobby instead
NetworkManager.singleton.StopHost();
}
}
8 changes: 5 additions & 3 deletions Assets/Scripts/LoadingScreen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ public class LoadingScreen : MonoBehaviour
[SerializeField] private float normalRotationSpeed = 60;
[SerializeField] private float fastRotationSpeed = 120;

private float incrementTimer = 360f;
private float rotationSpeed = 60;

private static int loadingCounter = 0;
Expand Down Expand Up @@ -76,13 +75,16 @@ private void Start()
private IEnumerator UpdateTimer(float duration)
{
var secondsPerChamber = duration / 6f;
var chamberCoverageAngle = 360f;
rotationSpeed = normalRotationSpeed;

radialTimer.material.SetFloat("_Arc2", chamberCoverageAngle);

for (int i = 0; i < 6; i++)
{
yield return new WaitForSeconds(secondsPerChamber);
incrementTimer -= 60f;
radialTimer.material.SetFloat("_Arc2", incrementTimer);
chamberCoverageAngle -= 60f;
radialTimer.material.SetFloat("_Arc2", chamberCoverageAngle);

if (i == 4)
{
Expand Down
2 changes: 1 addition & 1 deletion Assets/Scripts/Platforms/PlatformMovement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ private void Start()

private void FixedUpdate()
{
if (isServer)
if (!isNetworked || isServer)
MovePlatform();
}

Expand Down
Loading