Start game server/room

As with many multiplayer games, your game may also need so-called "matchmaking". What is it? Matchmaking allows you to run the required number of game servers/rooms that players can connect to. There are different methods for connecting to game servers/rooms. For example, in games of the "Battle Royale" genre, players gather in the so-called "lobby" before entering the game room. And until a certain condition is reached for starting the game, they will not be able to enter the game room. There is a method that allows you to enter the game room without any conditions, for example, as in the game DayZ. In it, players connect to the game room by simply selecting it from the list. In this guide, we will look at the second method.

Setting up game scenes

Before we go any further, you need to follow all the instructions in this guide.

Setting up the game server/room launch system

Next, we need to connect the so-called "spawner". It allows the master server to run any process in the operating system. In this case, we will run the game server/room. To configure "spawner", follow these steps:

  • Open the Master server scene that we created earlier.
  • Create an empty object as a child of the --MASTERSERVER object. This will be the launch module. There should already be two of our modules inside --MASTERSERVER - MatchmakerModule and RoomsModule.
  • Add the SpawnersModule component to the newly created object.
  • Rename this object to SpawnersModule so that we can understand what This module does.
  • In the property inspector, set its LogLevel to All.

  • Check the scene in the editor by running game mode, or build the master server as described here and run it.

In the console of the running master server, you should see that the SpawnersModule module was successfully launched along with other modules.

There are two ways to use the game server/room startup system.

  1. Creating a SpawnerBehavior component directly in the scene with the master server. This will result in a double service in a single build.
  2. Creating a separate scene for SpawnerBehaviour and build this scene as a separate service, which will also need to be run together with the master server.

We will use the first one.

Setting Up SpawnerBehaviour

  • Open the master server scene and create an empty object in the scene and name it --SPAWNER.
  • Add the SpawnerBehavior component to this object. This component is described in detail here.
  • Open the property inspector for this component.

We need to specify the path to the game server/room executable file and the maximum number of MaxProcesses that this component can run. Leave MaxProcesses as is, by default. But the path to the executable file will be specified with using command-line arguments.

In order for our SpawnerBehaviour to interact with the master server, it must be connected to it as a client. To do this, we need to add the server connection helper to the scene.

  • Create an empty object in the scene and name it, for example, --MASTER_CONNECTION_HELPER.
  • Add the ClientToMasterConnector component to this object. In its settings, specify the IP address and port for connecting to the master server.
  • In our case, we test our logic locally, but if you are already planning to run your master server on a virtual dedicated server (VDS), specify the appropriate connection parameters in the serverIp field and in the serverPort field. Also check the connectOnStart checkbox so that our connection is made immediately after starting. You can also specify this data using command-line arguments.

Check the scene by running it in the editor. The console should display the following:

You can see how the client connected to the master server, and that this client is a "spawner". You can also see how it registered with the master server. This means that the master server can accept commands to start game servers/rooms and send them to the SpawnerBehaviour system, which in turn will run the executable file using the specified path.

Build our master server scene that also has the game server/room launch system. Open the shortcut properties and specify the following command-line arguments in the Target field right after -mstStartMaster:

  • -mstStartSpawner - launches Spawner Behavior at startup.
  • -mstStartClientConnection - starts automatic connection to the server at startup.
  • -mstRoomExe - specify the path to the executable file of our game server/room in quotation marks. In my case I specified this path  "C:\UnityProjects\MSF\Builds\BasicSpawnerMirror\Room\Room.exe".

If you did everything correctly, the console of the running master server will look something like this:

That's it! Your master server with the game server/room startup system is ready. Let's start writing the program part.

Writing Code. Client.

So, all our systems are ready! Now we need to start interacting with them. Below is a simple class that performs all the basic procedures for starting a game server / room.

using MasterServerToolkit.MasterServer;
using MasterServerToolkit.Networking;
using UnityEngine;

namespace Aevien.Tutorials
{
    public class SpawnServerTutorial : MonoBehaviour
    {
        private void Start()
        {
            // Spawn options for spawner controller and spawn task controller
            var spawnOptions = new MstProperties();
            spawnOptions.Add(MstDictKeys.roomMaxPlayers, 10);
            spawnOptions.Add(MstDictKeys.roomName, "Deathmatch [West 7]");
            spawnOptions.Add(MstDictKeys.roomPassword, "my-password-here-123");
            spawnOptions.Add(MstDictKeys.roomIsPublic, true);

            // Custom options that will be given to room as command-line arguments
            var customSpawnOptions = new MstProperties();
            customSpawnOptions.Add(Mst.Args.Names.StartClientConnection);

            // Start new game server/room instance
            StartNewServerInstance(spawnOptions, customSpawnOptions);
        }

        /// <summary>
        /// Starts new game server/room instance with given options
        /// </summary>
        /// <param name="spawnOptions"></param>
        /// <param name="customSpawnOptions"></param>
        public void StartNewServerInstance(MstProperties spawnOptions, MstProperties customSpawnOptions)
        {
            Mst.Client.Spawners.RequestSpawn(spawnOptions, customSpawnOptions, "Region Name", (controller, error) =>
            {
                // If controller is null it means an error occurred
                if (controller == null)
                {
                    Debug.LogError(error);

                    // Invoke your error event here

                    return;
                }

                Mst.Events.Invoke(MstEventKeys.showLoadingInfo, "Room started. Finalizing... Please wait!");

                // Listen to spawn status
                controller.OnStatusChangedEvent += Controller_OnStatusChangedEvent;

                // Wait for spawning status until it is finished
                MstTimer.WaitWhile(() =>
                {
                    return controller.Status != SpawnStatus.Finalized;
                }, (isSuccess) =>
                {
                    // Unregister listener
                    controller.OnStatusChangedEvent -= Controller_OnStatusChangedEvent;

                    Mst.Events.Invoke(MstEventKeys.hideLoadingInfo);

                    if (!isSuccess)
                    {
                        Mst.Client.Spawners.AbortSpawn(controller.SpawnTaskId);

                        Debug.LogError("Failed spawn new room. Time is up!");

                        // Invoke your error event here

                        return;
                    }

                    Debug.Log("You have successfully spawned new room");

                    // Invoke your success event here

                }, 30f);
            });
        }

        private void Controller_OnStatusChangedEvent(SpawnStatus status)
        {
            // Invoke your status event here to show in status window
        }
    }
}

Let's look at this code in more detail.

In the Start() method, we see two kinds of options that we will need to start the game server/room.

  • spawnOptions - required parameter. In has all the necessary options to start game server/room. These options will be passed to the game server/room via the master server in "spawner" and then to the game server/room itself, after registering the process, in the SpanTaskController class. You can pass any string values in these options.
  • customSpawnOptions - optional parameter. It can pass any string values that will be passed as command-line arguments when the process starts. This page explains how to use command-line arguments.

Next we pass these options to Mst.Client.Spawners.RequestSpawn(). Let's look into this method more detail.

Mst.Client.Spawners.RequestSpawn(spawnOptions, customSpawnOptions, "Region Name", (controller, error) =>
{
    // If controller is null it means an error occurred
    if (controller == null)
    {
        Debug.LogError(error);

        // Invoke your error event here

        return;
    }

    Mst.Events.Invoke(MstEventKeys.showLoadingInfo, "Room started. Finalizing... Please wait!");

    // Listen to spawn status
    controller.OnStatusChangedEvent += Controller_OnStatusChangedEvent;

    // Wait for spawning status until it is finished
    MstTimer.WaitWhile(() =>
    {
        return controller.Status != SpawnStatus.Finalized;
    }, (isSuccess) =>
    {
        // Unregister listener
        controller.OnStatusChangedEvent -= Controller_OnStatusChangedEvent;

        Mst.Events.Invoke(MstEventKeys.hideLoadingInfo);

        if (!isSuccess)
        {
            Mst.Client.Spawners.AbortSpawn(controller.SpawnTaskId);

            Debug.LogError("Failed spawn new room. Time is up!");

            // Invoke your error event here

            return;
        }

        Debug.Log("You have successfully spawned new room");

        // Invoke your success event here

    }, 30f);
});

After the request, this method returns the SpawnRequestController and an error. If the controller is empty, an error most likely occurred. This is shown in the code block below. You can also handle this error with a pop-up window, for example.

// If controller is null it means an error occurred
if (controller == null)
{
    Debug.LogError(error);

    // Invoke your error event here

    return;
}

If there are no errors, we get the controller with the data. Next, we subscribe to change the status of the game server/room startup task. You can use this event handler to output any info in a popup window about the launch status.

// Listen to spawn status
controller.OnStatusChangedEvent += Controller_OnStatusChangedEvent;

// ...
private void Controller_OnStatusChangedEvent(SpawnStatus status)
{
    // Invoke your status event here to show in status window
}

Next comes the waiting block. In this section, we constantly check the current launch status of the game server / room. We also set a timer to abort a task if it takes more than 30 seconds to complete. If time is out, we send a request to abort the task and display the error on the screen, as well as unsubscribe from listening to the status.

// Wait for spawning status until it is finished
MstTimer.WaitWhile(() =>
{
    return controller.Status != SpawnStatus.Finalized;
}, (isSuccess) =>
{
    // Unregister listener
    controller.OnStatusChangedEvent -= Controller_OnStatusChangedEvent;

    Mst.Events.Invoke(MstEventKeys.hideLoadingInfo);

    if (!isSuccess)
    {
        Mst.Client.Spawners.AbortSpawn(controller.SpawnTaskId);

        Debug.LogError("Failed spawn new room. Time is up!");

        // Invoke your error event here

        return;
    }

    Debug.Log("You have successfully spawned new room");

    // Invoke your success event here

}, 30f);

If the task is completed successfully, we get the status as "Finalized". This means that all procedures for starting the game server / room have been completed successfully. But that's not all! We just looked at the client side of the startup process. Now let's see what should happen on the side of the game server/room being started.

Writing Code. Server/Room.

The fact is that after starting the game server / room, we need to register it as a process to control its operation. To do this, after your game server/room is started, you must connect to the master server. Previously, from this guide, you created an assistant for connecting to the master server on the side of your game server / room. We will make small changes to the code.

using MasterServerToolkit.MasterServer;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Aevien.Tutorials
{
    public class ServersListTutorial : MonoBehaviour
    {
        private RoomController roomController;
        private RoomOptions roomOptions;

        // Start is called before the first frame update
        void Start()
        {
            SetRoomOptions();

            ClientToMasterConnector.Instance.Connection.AddConnectionListener(OnConnectedToMasterServerHandler, true);
            ClientToMasterConnector.Instance.Connection.AddDisconnectionListener(OnDisconnectedFromMasterServerHandler, false);
        }

        private void SetRoomOptions()
        {
            roomOptions = new RoomOptions
            {
                IsPublic = true,
                // Your Game Server Name
                Name = "My Game With Friends",
                // If you want your server to be passworded
                Password = "",
                // Machine IP the server is running on
                RoomIp = "127.0.0.1",
                // Port of the game server
                RoomPort = 7777,
                // The max number of connections
                MaxConnections = 10
            };
        }

        private void OnConnectedToMasterServerHandler()
        {
            // If this room was spawned
            if (Mst.Server.Spawners.IsSpawnedProccess)
            {
                // Try to register spawned process first
                RegisterSpawnedProcess();
            }
            else
            {
                RegisterRoom();
            }
        }

        private void RegisterRoom()
        {
            Mst.Server.Rooms.RegisterRoom(roomOptions, (controller, error) =>
            {
                if (!string.IsNullOrEmpty(error))
                {
                    Debug.LogError(error);
                    return;
                }

                roomController = controller;

                Debug.Log("Our server was successfully registered");
            });
        }

        private void RegisterSpawnedProcess()
        {
            // Let's register this process
            Mst.Server.Spawners.RegisterSpawnedProcess(Mst.Args.SpawnTaskId, Mst.Args.SpawnTaskUniqueCode, (taskController, error) =>
            {
                if (taskController == null)
                {
                    Debug.LogError($"Room server process cannot be registered. The reason is: {error}");
                    return;
                }

                // If max players was given from spawner task
                if (taskController.Options.Has(MstDictKeys.roomName))
                {
                    roomOptions.Name = taskController.Options.AsString(MstDictKeys.roomName);
                }

                // If room is public or not
                if (taskController.Options.Has(MstDictKeys.roomIsPublic))
                {
                    roomOptions.IsPublic = taskController.Options.AsBool(MstDictKeys.roomIsPublic);
                }

                // If max players param was given from spawner task
                if (taskController.Options.Has(MstDictKeys.roomMaxPlayers))
                {
                    roomOptions.MaxConnections = taskController.Options.AsInt(MstDictKeys.roomMaxPlayers);
                }

                // If password was given from spawner task
                if (taskController.Options.Has(MstDictKeys.roomPassword))
                {
                    roomOptions.Password = taskController.Options.AsString(MstDictKeys.roomPassword);
                }

                // Finalize spawn task before we start server 
                taskController.FinalizeTask(new MstProperties(), () =>
                {
                    RegisterRoom();
                });
            });
        }

        private void OnDisconnectedFromMasterServerHandler()
        {
            Mst.Server.Rooms.DestroyRoom(roomController.RoomId, (isSuccess, error) =>
            {
                // Your code here...
            });
        }
    }
}

As you can see, the ServersListTutorial class has been changed to suit our needs. Two methods have appeared. The first registers the process, and the second registers the game server / room in the list. Let's look at the OnConnectedToMasterServerHandler() method. In it, we check whether the room is a running process or not. If the process was started using the game server/room startup system, we register this process in the master server. Otherwise, just register the game server/room in the list.

private void OnConnectedToMasterServerHandler()
{
    // If this room was spawned
    if (Mst.Server.Spawners.IsSpawnedProccess)
    {
        // Try to register spawned process first
        RegisterSpawnedProcess();
    }
    else
    {
        RegisterRoom();
    }
}

In the RegisterSpawnedProcess() method, we start registering the process in the list. Method Mst.Server.Spawners.RegisterSpawnedProcess() sends a request to the master server to register the process and get a response with two parameters. The first parameter is the SpawnTaskController class and the second is error information.

SpawnTaskController-contains data about the running task and options. These are the same options that we passed when launching our game server / room on the client. Before starting registration in a room, we set all the game server/room options received from the SpawnTaskController , and then send a request to complete the process.

private void RegisterSpawnedProcess()
{
    // Let's register this process
    Mst.Server.Spawners.RegisterSpawnedProcess(Mst.Args.SpawnTaskId, Mst.Args.SpawnTaskUniqueCode, (taskController, error) =>
    {
        if (taskController == null)
        {
            Debug.LogError($"Room server process cannot be registered. The reason is: {error}");
            return;
        }

        // If max players was given from spawner task
        if (taskController.Options.Has(MstDictKeys.roomName))
        {
            roomOptions.Name = taskController.Options.AsString(MstDictKeys.roomName);
        }

        // If room is public or not
        if (taskController.Options.Has(MstDictKeys.roomIsPublic))
        {
            roomOptions.IsPublic = taskController.Options.AsBool(MstDictKeys.roomIsPublic);
        }

        // If max players param was given from spawner task
        if (taskController.Options.Has(MstDictKeys.roomMaxPlayers))
        {
            roomOptions.MaxConnections = taskController.Options.AsInt(MstDictKeys.roomMaxPlayers);
        }

        // If password was given from spawner task
        if (taskController.Options.Has(MstDictKeys.roomPassword))
        {
            roomOptions.Password = taskController.Options.AsString(MstDictKeys.roomPassword);
        }

        // Finalize spawn task before we start server 
        taskController.FinalizeTask(new MstProperties(), () =>
        {
            RegisterRoom();
        });
    });
}

Next, we register the game server/room in the list. The RegisterRoom() method is launched after the game server/room process startup task is successfully completed.

private void RegisterRoom()
{
    Mst.Server.Rooms.RegisterRoom(roomOptions, (controller, error) =>
    {
        if (!string.IsNullOrEmpty(error))
        {
            Debug.LogError(error);
            return;
        }

        roomController = controller;

        Debug.Log("Our server was successfully registered");
    });
}

Next, we'll look at how to securely connect to the game server/room.