Skip to content

Spawning Game Servers

Alvydas Poškevičius edited this page Mar 24, 2017 · 7 revisions

This page demonstrates the whole process of spawning game servers. It doesn't matter which technology you're using for your game servers, the process will be pretty much identical (except for starting the actual server)

ℹ️ I've added examples with comments and without them - it might be easier to see what's happening when there are no comments :)

Overview

  1. Client sends request to master server to spawn a game server
  2. Master server finds an appropriate spawner and passes it the request
  3. Spawner starts a new unity instance (process)
    1. Process switches to necessary scene (optional)
    2. In that scene, there should be scripts that:
      1. Connect to master server
      2. Notifies master server that a new process was started
      3. Starts a game server
      4. Registers the game server (room)
      5. Finalizes spawn request
  4. Master server notifies client that a server that he requested was created (finalized)

Registering a Spawner

When client sends a request to spawn a server, master server picks one of the spawners from the list of registered spawners.

Spawner is essentially an instance of unity, which starts other unity instances. It can be an almost empty unity build, with a script which connects to master server, and registers itself as a spawner.

To register a spawner:

var options = new SpawnerOptions()
{
    MaxProcesses = 0, // Unlimited,
    MachineIp = "127.0.0.1", // Public IP address of this machine
    Region = "US", // Region identifier, can be anything
    Properties = new Dictionary<string, string>()
    {
        // If you need spawner to have some extra properties
        {"ExtraProperty", "Whatever" }
    }
};

// Registers a spawner to master server, so that master server knows about it's existance.
// Your spawner will receive requests to spawn unity instances
Msf.Server.Spawners.RegisterSpawner(options, (spawner, error) =>
{
    if (spawner == null)
    {
        Logs.Error(error);
        return;
    }

    // Set the build path (default ""(empty string))
    // When spawner receives a request, it will use this build to start unity instances
    // If you set it to an empty string (""), spawned instance will use same binaries
    // as spawner does
    spawner.DefaultSpawnerSettings.ExecutablePath = "C:/Win/Build.exe";
});

Sending a request to spawn server

This should be done on Client

⚠️ Before executing this script, make sure you're connected to master server and logged in. If you don't know how to do it - check out other guides - you're probably getting ahead of yourself 😄

Below is an example of a script, which sends a request to spawn a game server. Read the commends to understand how it works.

This script can be attached to a button or any other game object.

ℹ️ This is a not-commented and unsafe version (without checking for errors). I think this way it's easier to see what's actually happening. Below it, you'll find an example of a safer usage with comments

using System.Collections;
using System.Collections.Generic;
using Barebones.MasterServer;
using UnityEngine;
using UnityEngine.UI;

public class SpawnRequestExampleUnsafe : MonoBehaviour
{
    public Button SendRequestButton;

    void Awake()
    {
        SendRequestButton = SendRequestButton ?? GetComponent<Button>();

        // When button is clicked, invoke SendRequest method
        SendRequestButton.onClick.AddListener(SendRequest);
    }

    public void SendRequest()
    {
        var spawnOptions = new Dictionary<string, string>
        {
            {MsfDictKeys.MaxPlayers, "5"},
            {MsfDictKeys.RoomName, "Name of your Room"},
            {MsfDictKeys.MapName, "Map Name"},
            {MsfDictKeys.SceneName, "GameScene"}
        };

        Msf.Client.Spawners.RequestSpawn(spawnOptions, "", (controller, error) =>
        {
            StartCoroutine(WaitForServerToBeFinalized(controller));
        });
    }

    private IEnumerator WaitForServerToBeFinalized(SpawnRequestController request)
    {
        while (request.Status != SpawnStatus.Finalized)
            yield return null;

        request.GetCompletionData((data, error) =>
        {
            var roomId = int.Parse(data[MsfDictKeys.RoomId]);

            GetRoomAccess(roomId);
        });
    }

    public void GetRoomAccess(int roomId)
    {
        Msf.Client.Rooms.GetAccess(roomId, (access, error) =>
        {
            // Use the access
        });
    }
}

Safer version (with checks for errors and comments)

using System.Collections;
using System.Collections.Generic;
using Barebones.MasterServer;
using UnityEngine;
using UnityEngine.UI;

public class SpawnRequestExample : MonoBehaviour
{
    /// <summary>
    /// Set this button through inspector,
    /// or add this script on the button itself
    /// </summary>
    public Button SendRequestButton;

    void Awake()
    {
        SendRequestButton = SendRequestButton ?? GetComponent<Button>();

        // When button is clicked, invoke SendRequest method
        SendRequestButton.onClick.AddListener(SendRequest);
    }

    /// <summary>
    /// This method will be called when you click on a button
    /// </summary>
    public void SendRequest()
    {
        if (!Msf.Connection.IsConnected)
        {
            Debug.LogError("Not connected to master server");
            return;
        }

        if (!Msf.Client.Auth.IsLoggedIn)
        {
            Debug.LogError("You're not logged in");
            return;
        }

        // These options will be send to spawner, and then passed
        // to spawned process, so that it knows what kind of game server to start.
        // You can add anything to this dictionary
        var spawnOptions = new Dictionary<string, string>
        {
            {MsfDictKeys.MaxPlayers, "5"},
            {MsfDictKeys.RoomName, "Name of your Room"},
            {MsfDictKeys.MapName, "Map Name"},

            // Make sure you set this right, and that this scene
            // is added to the build of your game server
            {MsfDictKeys.SceneName, "GameScene"}
        };

        // We don't care about the region
        var region = "";

        // Send the request to spawn a game server
        Msf.Client.Spawners.RequestSpawn(spawnOptions, region, (controller, error) =>
        {
            if (controller == null)
            {
                Debug.LogError("Failed: " + error);
                return;
            }

            // If we got here, the request is being handled, but we need
            // to wait until it's done

            // We'll start a coroutine for that (they are perfect for waiting ^_^)
            StartCoroutine(WaitForServerToBeFinalized(controller));
        });
    }

    /// <summary>
    /// I hope you know how coroutines work. If you don't - it's worth checking it out
    /// </summary>
    private IEnumerator WaitForServerToBeFinalized(SpawnRequestController request)
    {
        var currentStatus = request.Status;

        // Keep looping until spawn request is finalized
        // (if spawn request is aborted, this will loop infinitely, 
        // because request will never be finalized, but I think you'll know how to
        // handle it)
        while (request.Status != SpawnStatus.Finalized)
        {
            // Skip a frame, if it's still not finalized
            yield return null;

            // If status has changed
            if (currentStatus != request.Status)
            {
                Debug.Log("Status changed to: " + request.Status);
                currentStatus = request.Status;
            }
        }

        // If we got here, the spawn request has been finalized

        // When spawned process finalizes, it gives master server some,
        // information about itself, which includes room id

        // We can retrieve this data from master server:
        // This method will be renamed to `GetFinalizationData`
        request.GetCompletionData((data, error) =>
        {
            if (data == null)
            {
                Debug.LogError("Failed to get finalization data: " + error);
                return;
            }

            if (!data.ContainsKey(MsfDictKeys.RoomId))
            {
                Debug.LogError("Spawned server didn't add a room ID to finalization data");
                return;
            }

            // So we've received the roomId of the game server that
            // we've requested to spawn
            var roomId = int.Parse(data[MsfDictKeys.RoomId]);

            GetRoomAccess(roomId);
        }); 
    }

    /// <summary>
    /// Retrieves an access to a specified room
    /// </summary>
    public void GetRoomAccess(int roomId)
    {
        Msf.Client.Rooms.GetAccess(roomId, (access, error) =>
        {
            if (access == null)
            {
                Debug.LogError("Failed to get room access: " + error);
                return;
            }

            // We have the access, and we can use it to access the game server
            var sceneName = access.SceneName;
            var ipAddress = access.RoomIp;
            var port = access.RoomPort;
            var token = access.Token;
            // (There's more data available in the access object)

            // It's now up to you to switch to the specified scene,
            // connect to the game server and send it the access
        });
    }
}

Starting game server on the spawned instance

When unity instance is spawned, it needs to do three things:

  1. Connect to Master Server
  2. Register spawned process (tell master server that current instance is a spawned process). In response, master server sends back all the necessary data (which client provided when he requested a spawn)
  3. Finalize the spawn request - this way, master server can know when a spawn request was fully finished.

When you finalize a spawn request, you can add finalization data, which can be access by player who requested the spawn. When game server registers a room, it can add the room id to finalization data, so that player can know to which game room he needs to connect.

Here's a script that waits for connection to be established, registers spawned process, and finalizes the spawn request:

using System;
using System.Collections;
using System.Collections.Generic;
using Barebones.MasterServer;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;

/// <summary>
/// Add it to the starting scene, and to game server scenes
/// </summary>
public class StartSpawnedServerExample : MonoBehaviour
{
    void Awake()
    {
        if (Msf.Args.IsProvided(Msf.Args.Names.LoadScene))
        {
            // If we are supposed to go to another scene first
            // (used to start servers on different scenes)
            SceneManager.LoadScene(Msf.Args.LoadScene);
            return;
        }

        // We need to make sure that we are connected to master server first

        // If second parameter is true and we're already connected,
        // the event will be invoked instantly
        Msf.Connection.AddConnectionListener(OnConnectionEstablished, true);
    }

    /// <summary>
    /// This will be called only if we're connected to master server
    /// </summary>
    public void OnConnectionEstablished()
    {
        if (!Msf.Server.Spawners.IsSpawnedProccess)
        {
            Logs.Warn("This is not a spawned instance");
            return;
        }

        // We need to register to master server first
        // to notify it that the process was started
        Msf.Server.Spawners.RegisterSpawnedProcess(Msf.Args.SpawnId, Msf.Args.SpawnCode, (controller, error) =>
        {
            if (controller == null)
            {
                Debug.LogError("Failed to register a spawned process: " + error);
                return;
            }

            // We've successfully registered, and we can use the data, which 
            // was provided by the client to start our game server
            StartGameServer(controller);

        });
    }

    public void StartGameServer(SpawnTaskController controller)
    {
        // We can access data, which Client sent to spawner
        var data = controller.Properties;

        // You can start any kind of server here.
        // For example, we'll start Unet HLAPI server

        var networkManager = FindObjectOfType<NetworkManager>();

        // If an arg was provided to use websockets
        if (Msf.Args.IsProvided(Msf.Args.Names.WebGl))
            networkManager.useWebSockets = true;

        // Spawner added an argument with assigned port number that we can use
        networkManager.networkPort = Msf.Args.AssignedPort;

        // Start the server
        networkManager.StartServer();

        // We could finalize it now, but we haven't registered the room yet
        RegisterRoomAndFinalize(networkManager, controller);
    }

    public void RegisterRoomAndFinalize(NetworkManager networkManager, SpawnTaskController controller)
    {
        // Create room options
        var options = new RoomOptions()
        {
            IsPublic = true,
            Properties = new Dictionary<string, string>(),
            RoomPort = networkManager.networkPort, // or Msf.Args.AssignedPort
            RoomIp = Msf.Args.MachineIp // Spawner should have passed us his own IP

        };

        // We can read some of the options from what player provided
        // when he sent a request
        var prop = controller.Properties;

        if (prop.ContainsKey(MsfDictKeys.RoomName))
            options.Name = prop[MsfDictKeys.RoomName];

        if (prop.ContainsKey(MsfDictKeys.MaxPlayers))
            options.MaxPlayers = int.Parse(prop[MsfDictKeys.MaxPlayers]);

        if (prop.ContainsKey(MsfDictKeys.RoomPassword))
            options.Password = prop[MsfDictKeys.RoomPassword];

        if (prop.ContainsKey(MsfDictKeys.MapName))
            options.Properties[MsfDictKeys.MapName] = prop[MsfDictKeys.MapName];

        // Also, add the scene name
        options.Properties[MsfDictKeys.SceneName] = SceneManager.GetActiveScene().name;

        // Register the room to master server
        Msf.Server.Rooms.RegisterRoom(options, (roomController, error) =>
        {
            if (roomController == null)
            {
                Debug.LogError(error);
                return;
            }

            // So the room was registered successfully, we can now finalize the 
            // spawn request
            controller.FinalizeTask(new Dictionary<string, string>()
            {
                // Add our room id to finalization data
                {MsfDictKeys.RoomId, roomController.RoomId.ToString()} 
            });
        });
    }
}

Here's the same example without comments (might be easier to see what's happening)

using System;
using System.Collections;
using System.Collections.Generic;
using Barebones.MasterServer;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;

public class StartSpawnedServerUnsafe : MonoBehaviour
{
    void Awake()
    {
        if (Msf.Args.IsProvided(Msf.Args.Names.LoadScene))
        {
            SceneManager.LoadScene(Msf.Args.LoadScene);
            return;
        }

        Msf.Connection.AddConnectionListener(OnConnectionEstablished, true);
    }

    public void OnConnectionEstablished()
    {
        Msf.Server.Spawners.RegisterSpawnedProcess(Msf.Args.SpawnId, Msf.Args.SpawnCode, (controller, error) =>
        {

            StartGameServer(controller);
        });
    }

    public void StartGameServer(SpawnTaskController controller)
    {
        var networkManager = FindObjectOfType<NetworkManager>();

        if (Msf.Args.IsProvided(Msf.Args.Names.WebGl))
            networkManager.useWebSockets = true;

        networkManager.networkPort = Msf.Args.AssignedPort;
        networkManager.StartServer();

        RegisterRoomAndFinalize(networkManager, controller);
    }

    public void RegisterRoomAndFinalize(NetworkManager networkManager, SpawnTaskController controller)
    {
        // Create room options
        var options = new RoomOptions()
        {
            IsPublic = true,
            Properties = new Dictionary<string, string>(),
            RoomPort = networkManager.networkPort, // or Msf.Args.AssignedPort
            RoomIp = Msf.Args.MachineIp // Spawner should have passed us his own IP
        };

        var prop = controller.Properties;

        if (prop.ContainsKey(MsfDictKeys.RoomName))
            options.Name = prop[MsfDictKeys.RoomName];

        if (prop.ContainsKey(MsfDictKeys.MaxPlayers))
            options.MaxPlayers = int.Parse(prop[MsfDictKeys.MaxPlayers]);

        options.Properties[MsfDictKeys.SceneName] = SceneManager.GetActiveScene().name;

        Msf.Server.Rooms.RegisterRoom(options, (roomController, error) =>
        {
            controller.FinalizeTask(new Dictionary<string, string>()
            {
                {MsfDictKeys.RoomId, roomController.RoomId.ToString()}
            });
        });
    }
}