Multiplayer 4. Knowing who's who

You can try just guessing which players are which, but although it's better than Unity's default options, it's still not the best idea in the world.

Like all good government agencies know, being able to track down people is really, really handy. Unity provides us with a way of getting all of syncing all of our players and managing their game objects, but it's not great at letting you get access to a list of those players, especially with knowing both with are local players and which is the server.

This information would be really helpful for us as we're going to be building a lobby and a leaderboard, both of which do normally require a list of all players in the game. To do this, then, we're going to add a tracking script to every player, and then create a nice singleton to allow access to this data, together with events when it changes.

The watch list

The following is the code required for our singleton that'll track all players in the game. It handles tracking when players are added and removed from it's list, and when the local player and server player objects are updated.

Player objects in Unity are created fresh on every scene change, and destroyed when that scene is done with. What this means for our list is that when a scene change occurs, all active players will be removed, the scene change will occur, and then the players will be re-added. This does mean that the local and server player will have to be re-registered every scene change.

// Player/Tracking/PlayerTracker.cs

using System.Collections.Generic;
using UnityEngine;

namespace Player.Tracking
{
    public class PlayerTracker
    {
        private static PlayerTracker instance;

        public delegate void LocalPlayerUpdated(GameObject localPlayer);
        public event LocalPlayerUpdated OnLocalPlayerUpdated;
        public delegate void ServerPlayerUpdated(GameObject serverPlayer);
        public event ServerPlayerUpdated OnServerPlayerUpdated;
        public delegate void PlayerAdded(GameObject newPlayer);
        public event PlayerAdded OnPlayerAdded;
        public delegate void PlayerRemoved(GameObject oldPlayer);
        public event PlayerRemoved OnPlayerRemoved;

        private List<GameObject> players = new List<GameObject>();
        private GameObject localPlayer;
        private GameObject serverPlayer;

        private PlayerTracker() { }
        
        public static PlayerTracker GetInstance()
        {
            if (instance == null) {
                instance = new PlayerTracker();
            }
            return instance;
        }

        public void AddPlayer(GameObject obj)
        {
            players.Add(obj);
            if (this.OnPlayerAdded != null) {
                this.OnPlayerAdded(obj);
            }
        }

        public void RemovePlayer(GameObject obj)
        {
            players.Remove(obj);
            if (this.OnPlayerRemoved != null) {
                this.OnPlayerRemoved(obj);
            }

            if (localPlayer == obj) {
                SetLocalPlayer(obj);
            }
            if (serverPlayer == obj) {
                SetServerPlayer(obj);
            }
        }

        public List<GameObject> GetPlayers()
        {
            return players;
        }

        public void SetLocalPlayer (GameObject obj)
        {
            localPlayer = obj;
            if (this.OnLocalPlayerUpdated != null) {
                this.OnLocalPlayerUpdated(obj);
            }
        }

        public GameObject GetLocalPlayer()
        {
            return localPlayer;
        }

        public void SetServerPlayer (GameObject obj)
        {
            serverPlayer = obj;
            if (this.OnServerPlayerUpdated != null) {
                this.OnServerPlayerUpdated(obj);
            }
        }

        public GameObject GetServerPlayer()
        {
            return serverPlayer;
        }
    }
}

We've got events on all of these updates to the tracked players list so we can easily alter our UI when the game's players change. To use this correctly for a simple player list, for instance, we'll first have to read GetPlayers() when the UI first loads. Next, we'll need to subscribe to add and remove player event hooks.

This ordering is required so if the UI loads after the players have been added to the list then we won't forget anyone, and any player that loads after the UI will trigger an event.

Hmm, upgrades

One thing that is particularly hard in Unity is keeping track of which player is also the server. This isn't necessary, but it's quite helpful for the user to know when they're waiting in the lobby, and also if choose to display pings of connected players on an in-game leaderboard.

To get the server player, we're going to leverage the code we wrote in Multiplayer 2. We, over time, will keep coming back to that code as it controls which data we want to synchronise between scenes for a given player. In this case, we want to track which player is actually the server.

We'll need to update all 3 files in the Player/SyncedData folder, but to prevent this just becoming a repeat of that blog, I'm just going to cover the areas we've updated.

First up, we'll want to update the store, so we've somewhere to secret the data away when a scene change tries to purge everything. Add the isServer variable to this store:

// Player/SyncedData/LocalPlayerDataStore.cs

using UnityEngine;

namespace Player.SyncedData {
    public class LocalPlayerDataStore {
        private static LocalPlayerDataStore instance;

        public Color playerColour;
        public bool isServer = false;

        ...
    }
}

Next up we'll update the code that makes sure all the clients stay up-to-date with the new setting. Note with this that we're revisited UpdateColour() and removed the forced colour change from the second blog post. We'll do this in another way at the bottom of this post.

// Player/SyncedData/PlayerDataForClients.cs

using UnityEngine;
using UnityEngine.Networking;

namespace Player.SyncedData {
    public class PlayerDataForClients : NetworkBehaviour {

        ...

        public delegate void IsServerFlagUpdated (GameObject player, bool isServer);
        public event IsServerFlagUpdated OnIsServerFlagUpdated;

        ...

        [SyncVar(hook = "UpdateIsServerFlag")]
        private bool isServerFlag;

        public override void OnStartClient()
        {
            if (!isLocalPlayer && !isServer) {
                UpdateColour(colour);
                UpdateIsServerFlag(isServerFlag);
            }
        }

        ...

        [Client]
        public void UpdateColour (Color newColour)
        {
            colour = newColour;
            if (this.OnColourUpdated != null) {
                this.OnColourUpdated(gameObject, newColour);
            }
        }

        public bool GetIsServerFlag ()
        {
            return isServerFlag;
        }

        [Client]
        public void SetIsServerFlag (bool newIsServer)
        {
            CmdSetIsServerFlag(newIsServer);
        }

        [Command]
        public void CmdSetIsServerFlag (bool newIsServer)
        {
            isServerFlag = newIsServer;
        }

        [Client]
        public void UpdateIsServerFlag (bool newIsServer)
        {
            isServerFlag = newIsServer;

            if (this.OnIsServerFlagUpdated != null) {
                this.OnIsServerFlagUpdated(gameObject, newIsServer);
            }
        }
    }
}

And last for this trip down memory lane is a change to the manager that actually sets the value for the local player. You'll notice that with this variable we rely on the manager code extending the NetworkBehaviour class, as that gives us direct access to the isServer flag. This flag is for a given connection, not an individual player object, so only a local client can know if it's the server or not.

// Player/SyncedData/LocalPlayerDataManager.cs

using UnityEngine;
using UnityEngine.Networking;

namespace Player.SyncedData {
    public class LocalPlayerDataManager : NetworkBehaviour {

        public PlayerDataForClients clientData;

        public override void OnStartLocalPlayer()
        {
            LocalPlayerDataStore store = LocalPlayerDataStore.GetInstance();

            ...

            if (isServer) {
                store.isServer = isServer;
            }

            ...

            clientData.SetIsServerFlag(store.isServer);
            clientData.OnIsServerFlagUpdated += OnIsServerFlagUpdated;
        }

        ...

        public void OnIsServerFlagUpdated (GameObject player, bool isServer)
        {
            LocalPlayerDataStore.GetInstance().isServer = isServer;
        }
    }
}

Track the planet

Now we've got a list for tracking current players and we've got access to which player is secretly the server, we need the code that's going to add and remove each individual player on and off the player list.

Note that all of this tracking occurs on a per-client basis, so each client keeps their own records of which players are connected, who is the local player, and who is the server player. The only information that we send via the server is simply that isServer check.

// Player/Tracking/TrackedPlayer.cs

using Player.SyncedData;
using UnityEngine.Networking;

namespace Player.Tracking
{
    class TrackedPlayer : NetworkBehaviour
    {
        public override void OnStartClient ()
        {
            PlayerTracker.GetInstance().AddPlayer(gameObject);
            
            gameObject.GetComponent<PlayerDataForClients>().OnIsServerFlagUpdated += UpdatePlayerIsServer;
        }
        
        private void UpdatePlayerIsServer (GameObject player, bool isServer)
        {
            PlayerTracker.GetInstance().SetServerPlayer(gameObject);
        }

        public override void OnStartLocalPlayer ()
        {
            PlayerTracker.GetInstance().SetLocalPlayer(gaObject);
        }

        public void OnDestroy ()
        {
            PlayerTracker.GetInstance().RemovePlayer(gameObject);
        }
    }
}

This code is relatively straight forward. Every new player object on a client triggers the OnStartClient() method, so we use this to register our player, and also for flagging up which player is also the server object. The OnStartLocalPlayer() method is only called by the local client, so is perfect for tracking which player is the local one. Lastly, the OnDestroy() method will make sure and and all players are removed from list if they choose to leave, or if the scene changes.

Add your tracker to the PlayerPrefab and you can now track any current player you wish.

Now we have the ability to track our players with ease. It's time to revisit the hack we put in to change the colour of a player.

One way we could have solved it would have been to create a new class that was attached to the player prefab and added itself to the events automatically. This would have been clean and simple, but we can figure that out easily enough.

What we're going to do below is create a class that will be attached to a simple game object in every scene. It'll add an event so it can read every player and then add another so it can track every time the colour changes for a player object.

// ChangePlayerColours.cs

using Player.SyncedData;
using Player.Tracking;
using UnityEngine;

class ChangePlayerColours:MonoBehaviour {

    public void Start ()
    {
        foreach (GameObject player in PlayerTracker.GetInstance().GetPlayers()) {
            AddColourChangeEvent(player);
        }
        PlayerTracker.GetInstance().OnPlayerAdded += AddColourChangeEvent;
    }

    public void AddColourChangeEvent (GameObject player)
    {
        PlayerDataForClients playerData = player.GetComponent<PlayerDataForClients>();
        HandlePlayerColourChange(player, playerData.GetColour());
        playerData.OnColourUpdated += HandlePlayerColourChange;
    }

    public void HandlePlayerColourChange (GameObject player, Color newColour)
    {
        player.GetComponentInChildren<MeshRenderer>().material.color = newColour;
    }
}

As you can see from above, as soon as the class is active it picks up any pre-registered player using GetPlayers()and then adds an event to pickup any new players. The method called by both of these registrations follows the same pattern: if there's a pre-chosen colour then a method is called, and otherwise an event is registered to wait for the colour change.

The ChangePlayerColours class being attached to a simple game object in a scene. This is replicated in both "Level A" and "Level B".

The code for this blog and all of the posts so far can be found at github.com/dittto/unity-multiplayer-4.