Notifications
Article
Game Setup - Pt. 1: Lobby
Published 2 years ago
1.2 K
0
How to setup a multiplayer game with player customisation
Cone Wars is the latest self-initiated project at GLITCHERS, a team of wild cat game designers operating out of Dalston, London.
Cone Wars has been one of our most recent challenges. Having released KANO for mobile and quickly followed up by Sea Hero Quest, we now turn to the next hurdle, desktop online 3d multiplayer with custom vehicle physics. It's a big one — but one we're tackling the kind of refined detail that GLITCHERS is becoming known for and we think Cone Wars is the best thing we've made so far. In our careers. Ever.

OVERVIEW

Cone Wars has a lot of player customisation ranging between your weapon and truck to the purely cosmetic. Customisation is an important part of any online multiplayer game and getting all players setup correctly is fundamental. When you join a game lobby you need to see who you're up against so you can plan for the game ahead and see how your truck stacks up against theirs.
In this post I will cover how to setup an online multiplayer game with a lobby based on Unity’s Networking (UNET) and the high level networking API (HLAPI). The full source code for the HLAPI is available on bitbucket and I highly encourage anyone using UNET to download it and poke around because the code that comes by default is compiled inside a DLL and inaccessible.
Before we jump in I just want to clarify some commonplace terms so we are all on the same page.
  • Player - This means the local client. When you’re playing online this is you.
  • Remote Player - This is all other clients. Online this would be anyone you’re playing with - allies, enemies and spectators.
  • Players - All clients - you and everyone else.
  • Server - This handles the management of the data and the game. You send your data to the server and the server sends you data for all other players.
  • Host - A host is both a server and a player. Someone who has set up a game and is also participating.

REQUIREMENTS

These are our requirements for setting up a lobby and ensuring that each player’s customisation is correct for everyone playing in that game.
  • Players send their data to the server for validation and storage
  • Players have the data for each player in the Lobby.
  • Players can request & receive data for any player from server.
  • Each player only sends their data when they join.
  • Players are setup in game correctly with their data.
We call the data describing each player a PlayerDef. Here is a very basic version of what we want to send over the network for each player know how to display. In the full game this is much larger but the principle is identical.
struct PlayerDef { string m_Name; Color m_Color; public PlayerDef( string name, Color color ) { m_Name = name; m_Color = color; } }

LOBBY

Unity has an out of the box implementation of a lobby called NetworkLobbyManager. This extends their base NetworkManager and adds all the functionality needed for managing connections / disconnections and change in ready state. For the purposes of getting started with a lobby in UNET, this is perfect. When using NetworkLobbyManager each player has two forms they take: either be a lobby player or a game player. Unity gives a base script for the lobby player in the form of NetworkLobbyPlayer and we will use this as the base for our own.

SYNCING THE DATA

UNET has an attribute called a SyncVar you can give your variables to make them synced over the network. Whatever values are set by the server are sent to each client when they first create that object in their game and then periodically after that. For us this would be ideal, we should send the PlayerDef to the server and it would distribute to any already connected players as well as players that are yet to join.
However they only work on certain types of variables and despite what is listed in the manualI could not get them to work on user-defined structs like PlayerDef so I opted for a slightly different solution.
I use a SyncVar on each player that keeps track of whether or not the server has the fullPlayerDef for that player. When you join the lobby you send your PlayerDef to the server and it sets a value for all other players that it has a copy of your data. The other players can then request the data from the server.
Newly joining players get an instance of your local player with a little flag indicating that the server has the data for this player, whereas existing players get an update to their instance of your local player saying the server now has your data. This update can be listened for easily by using an attribute on SyncVars called a hook. This hook is a function that will run on a client anytime the value is changed on the server.
[SyncVar(hook="OnHasPlayerDefUpdated")] public bool m_HasPlayerDef; void OnHasPlayerDefUpdated( bool hasPlayerDef ) { if( m_HasPlayerDef == false && hasPlayerDef ) { // The server now has the data we need! } m_HasPlayerDef = hasPlayerDef; }

THE HANDSHAKE

Now players know if the server has the data they still need to ask the server for it. Additionally the server needs to be able to respond to this request and send the data. I handle this by using two NetworkMessage. There is one message which handles the request and one for sending the data for a specific player.
public class PlayerRequestPlayerDataMessage : MessageBase { public NetworkInstanceId m_SenderNetId; public NetworkInstanceId m_SubjectNetId; public PlayerRequestPlayerDataMessage(){} public PlayerRequestPlayerDataMessage( NetworkInstanceId senderNetId, NetworkInstanceId subjectNetId ) { m_SenderNetId = senderNetId; m_SubjectNetId = subjectNetId; } } public class PlayerDefMessage : MessageBase { public NetworkInstanceId m_PlayerNetId; public string m_PlayerName; public Color m_PlayerColor; public PlayerDefMessage(){} public PlayerDefMessage( NetworkInstanceId playerNetId, PlayerDef playerDef ) { m_PlayerNetId = playerNetId; m_PlayerName = playerDef.m_Name; m_PlayerColor = playerDef.m_Color; } // this is a small helper function to create PlayerDef directly from a message public PlayerDef CreatePlayerDef() { return new PlayerDef( m_PlayerName, m_PlayerColor ); } }
You don’t need to specify what the message is used for, this is handled when you register your message handler on either the client or the server. In this instance I actually use thePlayerDefMessage to send the local player’s data to the server as well as sending a player’s data from the server. The PlayerRequestPlayerDataMessage takes who is asking for the data as well as whom the request is about to make ensure the data is sent to the correct player.
This diagram shows the order of events that happen in the lobby.

OUTCOME

Here is a copy of the key parts of our final lobby script. You will notice an addition of a request cache. This is added because you don’t know your own netId until your local player is created in your game. It is not guaranteed that your game will create a remote player before it creates your player so you have to keep track of what players you need data for.
public class LobbyPlayer : NetworkLobbyPlayer { [SyncVar(hook="OnHasPlayerDefUpdated")] public bool m_HasPlayerDef = false; #endregion private PlayerDef m_PlayerDef; public static LobbyPlayer LocalPlayer { get; private set; } #region Lifecycle public override void OnStartServer() { base.OnStartServer(); NetworkServer.RegisterHandler( MessageType.ServerReceivePlayerDef, OnServerReceivePlayerDef ); NetworkServer.RegisterHandler( MessageType.ClientRequestPlayerDef, OnClientRequestPlayerDef ); } public override void OnStartClient () { base.OnStartClient (); NetworkClient.allClients[0].RegisterHandler( MessageType.ClientReceivePlayerDef, OnClientReceivePlayerDef ); // if this client exists on the server already then hasPlayerDef could be true, so request their data if( m_HasPlayerDef ) { RequestPlayerDef(); } } public override void OnStartLocalPlayer () { base.OnStartLocalPlayer (); LocalPlayer = this; SendCachedPlayerDefRequests(); // send any requests before we had a local player SendPlayerDef( localPlayerDef ); } #endregion #region Request // this must be static so we can access it from our local lobby player instance. private static List<NetworkInstanceId> s_CachedRequestNetIds; [Client] private void RequestPlayerDef() { // host doesn't need to request. if the host doesn't have it, it'll come from the player once they have setup. if( isServer == false ) { if( LobbyPlayer.LocalPlayer == null ) { // if we don't know our localNetId, keep hold of this request and send when we do know it. if( s_CachedRequestNetIds == null ) { s_CachedRequestNetIds = new List<NetworkInstanceId>(); } s_CachedRequestNetIds.Add( netId ); } else { SendPlayerDefRequestForNetId( netId ); } } } private void SendCachedPlayerDefRequests() { if( s_CachedRequestNetIds != null ) { foreach( NetworkInstanceId reqNetId in s_CachedRequestNetIds ) { SendPlayerDefRequestForNetId( reqNetId ); } s_CachedRequestNetIds.Clear(); } } private void SendPlayerDefRequestForNetId( NetworkInstanceId reqNetId ) { NetworkClient.allClients[0].Send( MessageType.ClientRequestPlayerDef, new PlayerRequestPlayerDataMessage( LobbyPlayer.LocalPlayer.netId, reqNetId ) ); } #endregion #region Send private void SendPlayerDef( PlayerDef playerDef ) { NetworkClient.allClients[0].Send( MessageType.ServerReceivePlayerDef, new PlayerDefMessage( netId, playerDef ) ); } private void OnServerReceivePlayerDef( NetworkMessage netMsg ) { PlayerDefMessage playerDefMsg = netMsg.ReadMessage<PlayerDefMessage>(); GameObject sendingPlayerObject = NetworkServer.FindLocalObject( playerDefMsg.m_PlayerNetId ); LobbyPlayer sendingPlayer = sendingPlayerObject.GetComponent<LobbyPlayer>(); sendingPlayer.m_HasPlayerDef = true; sendingPlayer.m_PlayerDef = playerDefMsg.CreatePlayerDef();; } private void OnClientRequestPlayerDef( NetworkMessage netMsg ) { PlayerRequestPlayerDataMessage reqMsg = netMsg.ReadMessage<PlayerRequestPlayerDataMessage>(); NetworkInstanceId senderNetId = reqMsg.m_SenderNetId; NetworkInstanceId subjectNetId = reqMsg.m_SubjectNetId; GameObject subjectPlayerObject = NetworkServer.FindLocalObject( subjectNetId ); LobbyPlayer subjectPlayer = subjectPlayerObject.GetComponent<LobbyPlayer>(); NetworkUtil.SendToClientWithNetId( senderNetId, MessageType.ClientReceivePlayerDef, new PlayerDefMessage( subjectNetId, subjectPlayer.m_PlayerDef ) ); } private void OnClientReceivePlayerDef( NetworkMessage netMsg ) { PlayerDefMessage playerDefMsg = netMsg.ReadMessage<PlayerDefMessage>(); GameObject targetPlayerObject = ClientScene.FindLocalObject( playerDefMsg.m_PlayerNetId ); LobbyPlayer targetPlayer = targetPlayerObject.GetComponent<LobbyPlayer>(); targetPlayer.m_PlayerDef = playerDefMsg.CreatePlayerDef(); } private void OnHasPlayerDefUpdated( bool hasDef ) { m_HasPlayerDef = hasDef; if( hasDef && isLocalPlayer == false ) { RequestPlayerDef(); } } #endregion }

WHAT'S NEXT?

This post covers a basic setup for handling the transfer of data for all players and is the first step towards getting your game working. In the next post I will cover getting the data from the lobby player to the game player, correcting Unity’s OnLevelWasLoaded misfiring between levels as well ensuring the game and players are fully setup for each player before starting the game.
 
 
GlitchersDeveloper
Game Design Director - Executive
11
Comments