Multiplayer 3. The state manager

Turns out state is a hard word to search for with stock photography, so here's a state capitol building instead.

It's always useful to know how things stand. Whether that's how many ferrets you've kidnapped in the past 6 months, which players have currently lost in your game of Russian Roulette, or simply keeping track of whether or not your connected to a server in your game.

Unity has some in-built methods that would, in theory, track the state of the game, but in reality either don't do what you'd expect or don't actually work. We need to manage the state of the game so we can automatically create modal windows and update which UI items should be displayed.

For this task we're going to create something like a finite state machine together with a pub / sub setup. The game will only be allowed to be in a single state for both network types and the game state itself. The game state will consist of being offline, connecting, lobby (eventually), online, and disconnecting.

The publisher / subscriber model will be used to trigger events when the state changes. We're going to need to know what the previous state was so we can track when specific changes occur, and also allow subscriptions on fairly generic state changes such as whenever the game state becomes offline.

Options, options, options

First up we're going to create a struct that we'll use for storing the previous and current state options for a subscription.

// GameState/StateOption.cs

namespace GameState
{
    public struct StateOption
    {
        public string oldNetwork;
        public string newNetwork;
        public string oldGame;
        public string newGame;

        public StateOption (string oldNetworkState = "", string oldGameState = "", string newNetworkState = "", string newGameState = "")
        {
            oldNetwork = oldNetworkState;
            oldGame = oldGameState;
            newNetwork = newNetworkState;
            newGame = newGameState;
        }

        public StateOption PreviousNetworkState (string oldNetworkState)
        {
            oldNetwork = oldNetworkState;

            return this;
        }

        public StateOption PreviousGameState (string oldGameState)
        {
            oldGame = oldGameState;

            return this;
        }

        public StateOption NetworkState (string newNetworkState)
        {
            newNetwork = newNetworkState;

            return this;
        }

        public StateOption GameState (string newGameState)
        {
            newGame = newGameState;

            return this;
        }

        public bool Matches (
            string oldNetworkState, 
            string oldGameState, 
            string newNetworkState, 
            string newGameState,
            bool isNetworkDirty,
            bool isGameDirty
        ) {
            if (oldNetwork != null && oldNetwork != "" && oldNetworkState != "" && oldNetwork != oldNetworkState) {
                return false;
            }

            if (oldGame != null && oldGame != "" && oldGameState != "" && oldGame != oldGameState) {
                return false;
            }

            if (newNetwork != null && newNetwork != "" && newNetworkState != "" && newNetwork != newNetworkState) {
                return false;
            }

            if (newGame != null && newGame != "" && newGameState != "" && newGame != newGameState) {
                return false;
            }

            bool anyDirty = false;
            if ((oldNetwork != null && oldNetwork != "") || (newNetwork != null && newNetwork != "") && isNetworkDirty) {
                anyDirty = true;
            }
            if ((oldGame != null && oldGame != "") || (newGame != null && newGame != "") && isGameDirty) {
                anyDirty = true;
            }
            if (!anyDirty) {
                return false;
            }

            return true;
        }
    }
}

This is to be used by creating a new struct with all of the required values, or creating an empty one and using method chaining for setting it's options. The Matches() method is how we're going to check if this subscriber can be triggered. This method first checks if we have no matching data. If we don't, it drops out. If we do, then we check using the "dirty bits" to see if the information's actually changed and has been flagged as "dirty" before acknowledging a change.

The finite(ish) state machine

The State object covers a number of different patterns, mainly because it's a handy way to write code for a tutorial, but also because it keeps the code relatively simple.

To start with, the State object's another singleton. This is because we're going to want to call it from a lot of different places and while I could use a service locator, the singleton's just a bit easier to work with in Unity in these instances.

// GameState/State.cs

using System.Collections.Generic;
using UnityEngine;

namespace GameState
{
    public struct SubscriberOptions
    {
        public StateOption option;
        public State.Subscriber subscriber;
    }

    public class State
    {
        public const string NETWORK_CLIENT = "network_client";
        public const string NETWORK_SERVER = "network_server";

        public const string GAME_OFFLINE = "game_offline";
        public const string GAME_CONNECTING = "game_connecting";
        public const string GAME_ONLINE = "game_online";
        public const string GAME_DISCONNECTING = "game_disconnecting";

        public delegate void Subscriber ();
        private List<SubscriberOptions> subscribers = new List<SubscriberOptions>();

        private static State instance;

        private string previousNetworkState;
        private string networkState;
        private string previousGameState;
        private string gameState;

        private bool isNetworkDirty = false;
        private bool isGameDirty = false;

        private State () { }

        public static State GetInstance ()
        {
            if (instance == null) {
                instance = new State();
            }

            return instance;
        }

        public State Network (string newNetworkState)
        {
            if (newNetworkState != networkState) {
                previousNetworkState = networkState;
                networkState = newNetworkState;
                isNetworkDirty = true;
            }

            return this;
        }

        public string Network ()
        {
            return networkState;
        }

        public State Game (string newGameState)
        {
            if (newGameState != gameState) {
                previousGameState = gameState;
                gameState = newGameState;
                isGameDirty = true;
            }

            return this;
        }

        public string Game ()
        {
            return gameState;
        }

        public void Subscribe (StateOption options, Subscriber callback)
        {
            SubscriberOptions subscriberOption = new SubscriberOptions();
            subscriberOption.option = options;
            subscriberOption.subscriber = callback;

            if (!subscribers.Contains(subscriberOption)) {
                subscribers.Add(subscriberOption);
            }
            PublishIfMatches(subscriberOption, true);
        }

        public void Publish ()
        {
            // Debug.Log("State: " + networkState + " | " + previousGameState + " > " + gameState);

            foreach (SubscriberOptions subscriberOption in subscribers) {
                PublishIfMatches(subscriberOption);
            }

            isNetworkDirty = isGameDirty = false;
        }

        private void PublishIfMatches (SubscriberOptions subscriberOption, bool forceDirtyBit = false)
        {
            if (
                subscriberOption.option.Matches(
                    previousNetworkState, 
                    previousGameState, 
                    networkState, 
                    gameState,
                    isNetworkDirty,
                    isGameDirty
                )
            ) {
                subscriberOption.subscriber();
            }
        }
    }
}

The Network() and Game() methods use method chaining again for setting the current state, and overrides for getting the current state. That getting of state's going to be useful when we're in game, as it'll allow our UI to know which options it should show (assuming our server has access to different options than the client).

The Subscribe() method allows us to register new method callbacks for state changes, using the StateOption struct covered above. It also uses the SubscriberOptions struct defined above the State object.

Lastly, and perhaps most importantly, is the Publish()method. This checks what the previous and current state are, and checks them against all subscribers. Any matching subscribers have their callbacks run. It's worth noting that the Publish()method needs to be manually called every time the Network() and Game() methods are set. This is so the state can be manipulated as many times as you like before publishing. If you publish too early then you may have erroneous callbacks being fired in the wrong order. It also clears out the "dirty bits", ready for the next update.

You can't tell me what to do

The above code can now be used to manage state and fire events based on changes, but we can go one step further and have some automatically change for us.

Unity has most (all?) of their source code publicly available in the second-rate repository store, BitBucket. I may hate their NetworkManagerHUD UI with fire of a thousand suns, but it does function well. Therefore I can look at their source code behind it and replicate how it changes, and then just replace the UI component.

// GameState/StateManager.cs

using UnityEngine;
using UnityEngine.Networking;

namespace GameState
{
    public class StateManager:MonoBehaviour
    {
        private NetworkManager network;

        private State state;

        public void Start()
        {
            state = State.GetInstance();
            state.Game(State.GAME_OFFLINE).Publish();
        }

        public void Update()
        {
            if (network == null) {
                network = NetworkManager.singleton;
            }

            SetNetworkState();
            if (SetConnectionState()) {
                state.Publish();
            }
        }

        private void SetNetworkState()
        {
            bool isServer = NetworkServer.active;
            bool isClient = NetworkClient.active && !isServer;

            if (isServer && state.Network() != State.NETWORK_SERVER) {
                state.Network(State.NETWORK_SERVER);
            }

            if (isClient && state.Network() != State.NETWORK_CLIENT) {
                state.Network(State.NETWORK_CLIENT);
            }
        }
        
        private bool SetConnectionState()
        {
            bool isOffline = !network.IsClientConnected() &&
                !NetworkServer.active &&
                network.matchMaker == null;

            bool hasConnection = network.client != null &&
                network.client.connection != null &&
                network.client.connection.connectionId != -1;

            if (isOffline && !hasConnection && state.Game() != State.GAME_OFFLINE) {
                state.Game(State.GAME_OFFLINE)).Level(State.LEVEL_NOT_READY);
                return true;
            }
            
            if (isOffline && hasConnection && state.Game() != State.GAME_CONNECTING) {
                state.Game(State.GAME_CONNECTING);
                return true;
            }

            if (!isOffline && state.Game() != State.GAME_ONLINE) {
                state.Game(State.GAME_ONLINE);
                return true;
            }

            return false;
        }
    }
}

We're going to attach this StateManager object to the NetworkManager again, so it'll persist throughout our game. The first state we set then is offline, as it'll always be that when the game first starts up.

Nothing else needed for the blog post, just simply add it to the Network Manager and everything else will work.

In the above code, we don'tPublish()when the network changes state because we know that whenever that occurs, the game will also change state.

The SetConnectionState() method is taken from NetworkManagerHUD, reverse-engineering the code switches in their OnGUI() method. This automatically switches the game between offline, connecting, and online. Other, future code will handle changing the state to disconnecting, and also when the lobby is active.

And there we have it, the state manager should now be active. If you've got some debugging in the State.Publish() method and run with the NetworkManagerHUD, you'll see the state's still being automatically updated and triggered, although we have no current callbacks on it.

The code can be found at github.com/dittto/unity-multiplayer-3.