Запуск игрового сервера/комнаты
Как и во многих многопользовательских играх в вашей игре тоже может понадобиться так называемый "матчмейкинг". Что это такое? Это возможность запускать необходимое количество игровых серверов/комнат, к которым могут подключаться игроки. Существуют разные методы подключения к игровым серверам/комнатам. Например в играх жанра "Королевская битва" игроки, прежде чем попасть в игровую комнату собираются в так называемом "лобби". И пока не будет достигнуто определенное условие запуска игры они не смогут войти в игровую комнату. Существует способ, который позволяет входить в игровую комнату без каких либо условий, например, как в игре DayZ. В ней игроки подключаются к игровой комнате просто выбрав ее из списка. В данном руководстве мы рассмотрим второй способ.
Настройка игровых сцен
Прежде чем мы пойдем дальше, вам необходимо выполнить все, что рассказано в данном руководстве.
Настройка системы запуска игрового сервера/комнаты
Далее нам необходимо подключить так называемый "spawner". Он позволяет мастер серверу выполнять запуск любого процесса в операционной системе. В данном случае мы будем запускать игровой сервер/комнату. Для настройки "spawner" выполните следующие действия:
- Откройте сцену Мастер сервера, которую мы создали ранее.
- Создайте пустой объект, как дочерний для объекта --MASTER_SERVER. Это будет модуль запуска. Внутри --MASTER_SERVER уже должны находиться два наших модуля - MatchmakerModule и RoomsModule.
- Привяжите к вновь созданному объекту компонент SpawnersModule.
- Переименуйте этот объект в SpawnersModule, чтобы нам было понятно, что делает этот модуль.
- В инспекторе свойств установите его LogLevel как All.
- Проверьте сцену в редакторе, запустив игровой режим или выполните сборку мастер сервера, как рассказано здесь и запустите его.
В консоли запущеного мастер сервера вы должны видеть, что модуль SpawnersModule успешно запущен наряду с другими модулями.
Есть два варианта использования системы запуска игровых серверов/комнат.
- Создать компонент SpawnerBehaviour непосредственно в сцене с мастер сервером. Получится двойной сервис в одной сборке.
- Создать отдельную сцену для 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");
});
}
Далее мы рассмотрим способ безопасного подключения к игровому серверу/комнате.