Запуск игрового сервера/комнаты

Как и во многих многопользовательских играх в вашей игре тоже может понадобиться так называемый "матчмейкинг". Что это такое? Это возможность запускать необходимое количество игровых серверов/комнат, к которым могут подключаться игроки. Существуют разные методы подключения к игровым серверам/комнатам. Например в играх жанра "Королевская битва" игроки, прежде чем попасть в игровую комнату собираются в так называемом "лобби". И пока не будет достигнуто определенное условие запуска игры они не смогут войти в игровую комнату. Существует способ, который позволяет входить в игровую комнату без каких либо условий, например, как в игре DayZ. В ней игроки подключаются к игровой комнате просто выбрав ее из списка. В данном руководстве мы рассмотрим второй способ.

Настройка игровых сцен

Прежде чем мы пойдем дальше, вам необходимо выполнить все, что рассказано в данном руководстве.

Настройка системы запуска игрового сервера/комнаты

Далее нам необходимо подключить так называемый "spawner". Он позволяет мастер серверу выполнять запуск любого процесса в операционной системе. В данном случае мы будем запускать игровой сервер/комнату. Для настройки "spawner" выполните следующие действия:

  • Откройте сцену Мастер сервера, которую мы создали ранее.
  • Создайте пустой объект, как дочерний для объекта --MASTER_SERVER. Это будет модуль запуска. Внутри --MASTER_SERVER уже должны находиться два наших модуля - MatchmakerModule и RoomsModule.
  • Привяжите к вновь созданному объекту компонент SpawnersModule.
  • Переименуйте этот объект в SpawnersModule, чтобы нам было понятно, что делает этот модуль.
  • В инспекторе свойств установите его LogLevel как All.

  • Проверьте сцену в редакторе, запустив игровой режим или выполните сборку мастер сервера, как рассказано здесь и запустите его.

В консоли запущеного мастер сервера вы должны видеть, что модуль SpawnersModule успешно запущен наряду с другими модулями.

Есть два варианта использования системы запуска игровых серверов/комнат.

  1. Создать компонент SpawnerBehaviour непосредственно в сцене с мастер сервером. Получится двойной сервис в одной сборке.
  2. Создать отдельную сцену для SpawnerBehaviour и выполнить сборку этой сцены как отдельного сервиса, который тоже необходимо будет запускать вместе с мастер сервером.

Мы воспользуемся первым способом.

Настройка SpawnerBehaviour

  • Откройте сцену мастер сервера и создайте в сцене пустой объект и назовите его --SPAWNER.
  • Подключите к этому объекту компонент SpawnerBehaviour. Подробно данный компонент описан здесь.
  • Откройте инспектор свойств данного компонента.

Нам необходимо указать путь к исполняемому файлу игрового сервера/комнаты и максимальное количество процессов MaxProcesses, которые данный компонент имеет права запускать. Оставим MaxProcesses как есть, по молчанию. А путь к исполняемому мы укажем через аргументы командной строки.

Для того чтобы наш SpawnerBehaviour смог взаимодействовать с мастер сервером он должен быть к нему подключен как клиент. Для этого нам необходимо добавить в сцену помощник подключения к серверу.

  • Создайте пустой объект в сцене и назовите его, например, --MASTER_CONNECTION_HELPER.
  • Прицепите к данному объекту компонент ClientToMasterConnector.
  • В его настройках укажите IP адрес и порт подключения к мастер серверу.
  • В нашем случае мы тестируем нашу логику локально, но если вы уже планируете запускать ваш мастер сервер на виртуальном выделенном сервере(VDS), то укажите в поле serverIp и в поле serverPort соответствующие параметры подключения. Также установите флажок connectOnStart, чтобы наше подключение выполнялось сразу после запуска. Эти данные вы можете также указать через аргументы командной строки.

Проверьте сцену, запустив ее в редакторе. В консоли должно быть отображено следующее:

 

Видно как к мастер серверу подключился клиент, и что это клиент является "spawner". Видно как он зарегистрировался у мастер сервера. Это говорит о том, что мастер сервер может принимать команды запуска игровых серверов/комнат и отправлять их системе SpawnerBehaviour, которая в свою очередь, используя указанный путь, будет запускать исполняемый файл.

Выполните сборку нашей сцены мастер сервера с системой запуска игровых серверов/комнат. Откройте свойства ярлыка и укажите в поле Объект, сразу после -mstStartMaster, следующие аргументы командной строки:

  • -mstStartSpawner - запускает SpawnerBehaviour при старте.
  • -mstStartClientConnection - запускает автоматическое подключение к серверу при старте.
  • -mstRoomExe - в кавычках указываем путь к исполняемому файлу нашего игрового сервера/комнаты. В моем случае я указал путь "C:\UnityProjects\MSF\Builds\BasicSpawnerMirror\Room\Room.exe"

Если вы все сделали правильно, то консоль запущеного мастер сервера будет выглядеть примерно так:

Вот и все! Ваш мастер сервер с системой запуска игровых серверов/комнат готовы. Теперь необходимо начать писать программную часть.

Пишем программную часть. Клиентский код.

Итак, все системы у нас подключены! Теперь нам необходимо начать взаимодействие с ними. Ниже приведен простой класс, в котором выполняются все основные процедуры запуска игрового сервера/комнаты.

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
        }
    }
}

Давайте рассмотрим этот код подробнее.

В методе Start() мы видим два вида опций, которые нам нам понадобятся для запуска игрового сервера/комнаты.

  • spawnOptions - обязательный параметр. В нем мы передаем все необходимые опции. Эти опции будут переданы игровому серверу/комнате через мастер сервер в "spawner" и затем в сам игровой сервер/комнату, после регистрации процесса, в классе SpanTaskController. В этих опциях можно передавать любые строковые значения.
  • customSpawnOptions - необязательный параметр. В нем можно передавать любые строковые значения, которые будут передаваться при запуске процесса в виде аргументов командной строки. О том как можно использовать аргументы командной строки описано на этой страницу.

Далее мы передаем все эти данные в метод Mst.Client.Spawners.RequestSpawn(). Давайте рассмотрим его поближе.

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);
});

После запроса данный метод возвращает нам SpawnRequestController и ошибку. Если контроллер пуст, то, скорее всего, произошла ошибка. Это показано в блоке ниже. Вы также можете обработать эту ошибку, например, каким-нибудь всплывающим окном.

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

    // Invoke your error event here

    return;
}

Если ошибок нет, то мы получаем контроллер с данными. Далее мы подписываемся на изменение статуса задачи запуска игрового сервера/комнаты. В обработчике события можно выводить какие-либо подсказки в всплывающем окне о статусе запуска.

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

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

Далее идет блок ожидания. В данном блоке мы постоянно проверяем какой текущий статус запуска игрового сервера/комнаты. Также мы устанавливаем таймер, чтобы завершить выполнении задачи если время ее выполнения выше 30 секунд. Если время на запуск завершилось, то отправляем запрос на прекращение задачи и выводим ошибку на экран, а также отписываемся от прослушивания статуса.

// 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);

Если задача выполнена успешно, то мы получаем статус как "Finalized". Это означает, что все процедуры запуска игрового сервера/комнаты завершены успешно. Но это еще не все! Мы лишь рассмотрели клиентскую сторону процесса запуска. Теперь давайте посмотрим, что должно происходить на стороне запускаемого игрового сервера/комнаты.

Пишем программную часть. Серверный код.

Дело в том, что после запуска игрового сервера/комнаты мы должны его зарегистрировать как процесс, чтобы контролировать его работу. Для этого, после того как ваш игровой сервер/комната будет запущен вы должны подключиться к мастер серверу. Ранее, из этого руководства, вы создали на стороне вашего игрового сервера/комнаты помощник подключения к мастер серверу. Мы внесем небольшие изменения в код.

 

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...
            });
        }
    }
}

Как вы видите класс ServersListTutorial изменен под наши нужды. Появились два метода. Первый регистрирует процесс, а второй регистрирует игровой сервер/комнату в списке. Давайте рассмотрим метод OnConnectedToMasterServerHandler(). В нем мы проверяем является ли комната запущенным процессом или нет. Если процесс запущен при помощи системы запуска игрового сервера/комнаты, то регистрируем этот процесс в мастер сервере. В противном случае просто регистрируем игровой сервер/комнату в списке.

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

В методе RegisterSpawnedProcess() мы запускаем регистрацию процесса в списке. При помощи метода Mst.Server.Spawners.RegisterSpawnedProcess() отправляем запрос в мастер сервер для регистрации процесса и получаем ответ с двумя параметрами. Первый параметр - это класс SpawnTaskController и второй является информацией об ошибке.

SpawnTaskController - содержит данные о запущенной задаче и опции. Это те самые опции, которые мы передавали при запуске нашего игрового сервера/комнаты на клиенте. Перед тем как запустить регистрацию в комнате мы устанавливаем все опции игрового сервера/комнаты полученные из SpawnTaskController , а затем отправляем запрос на завершение запуска процесса.

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();
        });
    });
}

Далее мы регистрируем игровой сервер/комнату в списке. Метод 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");
    });
}

Далее мы рассмотрим способ безопасного подключения к игровому серверу/комнате.