Трюки автоматизации, которые помогут быстро протестировать сборку многопользовательского сервера

Гитхаб

Борьба за развитие

При работе над многопользовательской игрой уже достаточно сложно отлаживать сетевые проблемы и другие особенности игрового процесса. Упаковка и загрузка серверных файлов на сервер — это совсем другая проблема, и я потратил часы на поиск способов сделать это эффективно. Хотя это не идеально, я думаю, что у меня есть довольно хороший рабочий процесс, чтобы упростить его, чтобы вы могли уделять больше времени разработке своей игры.

ПРИМЕЧАНИЕ. Этот процесс не является каким-либо конвейером, поэтому я бы рекомендовал использовать этот метод только в том случае, если вы хотите быстро протестировать новые функции перед надлежащим развертыванием.

Требования

  • Windows 10 (мы будем использовать пакетный сценарий. Если вы можете написать сценарий bash или какой-либо другой сценарий, который может архивировать и перемещать файлы, возможно, вы сможете заставить его работать на другой ОС. )
  • "Единство"
  • "Зеркало"
  • ВинРАР
  • Renci.SshNet C# DLL (Используйте версию net35. Импорт dll может быть сложным, поэтому вот инструкция, как это сделать)
  • Экземпляр AWS Lightsail (или другой службы хостинга Linux)

Обзор

Будем в таком порядке:

  • Построить сервер,
  • Заархивируйте собранный сервер с помощью WinRAR и
  • Загрузить заархивированный сервер на удаленный хостинг с помощью SFTP.

все с помощью одной кнопки в редакторе Unity.

Начиная с

В качестве отправной точки мы будем использовать репозиторий учебных пособий ShrineGames для BuildScript (Посмотрите на Shrine, у них есть отличные видео с зеркалами!). Мы будем опираться на это, особенно на сценарии Linux Server.

Обязательно просмотрите код, который уже присутствует в этом скрипте, и посмотрите, нужно ли вам внести какие-либо изменения в значения, чтобы он применялся к вашей текущей игре. Например, buildPlayerOptions.scenes просто имеет одну сцену, и она может не соответствовать ни одной из ваших сцен. После этого мы можем запустить процесс архивации.

Архивирование

Чтобы загружать файлы сервера, он должен быть заархивирован. Мы будем использовать WinRAR, так как вы можете запустить его через командную строку, что удобно для запуска через пакетный сценарий.
Вам нужно будет добавить путь к файлу winrar.exe в переменную пути. Это будет использоваться позже, когда мы запустим скрипт архиватора.

Я бы рекомендовал обернуть этот код в try/catch, чтобы вы могли регистрировать любую ошибку в Unity.

Далее, это будет метод, который архивирует файл. Мы назовем его ArchiveFile(). Мы будем использовать новый Process, который будет нашим пакетным скриптом, представляющим собой простую однострочную команду, запускающую WinRAR.

Process archiveScript = new Process();

Далее нам нужно найти абсолютный путь к winrar.exe. Это связано с тем, что при создании нового процесса этот процесс не знает никаких значений пути. Поэтому нам нужно сообщить сценарию, где находится winrar.exe перед его запуском, так как мы не можем просто использовать значение пути в сценарии. Это достигается с помощью всего нескольких строк кода, где мы разбиваем строку пути на массив строк, разделенных точкой с запятой. Затем для каждой строки проверьте, представляет ли каталог в сочетании с именем исполняемого файла (winrar.exe) допустимый путь к файлу.

string exe = "winrar.exe";
string result = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine)
    .Split(';')
    .Where(s => File.Exists(Path.Combine(s, exe)))
    .FirstOrDefault();
result = "\"" + result + "\"\\";

Затем мы хотим установить Имя файла и Аргументы. После того, как эти значения Process установлены, можно запускать скрипт.

string path = Directory.GetCurrentDirectory() + "/archiver.bat";
archiveScript.StartInfo.FileName = path;
archiveScript.StartInfo.Arguments = result;
archiveScript.Start();
archiveScript.WaitForExit();

Из опыта я узнал, что попытка архивировать напрямую в C# — ужасно сложная задача, поэтому вместо этого я позволил пакетному сценарию сделать это. Прежде чем двигаться дальше, ваш метод ArchiveFile() должен выглядеть примерно так:

В корневом каталоге проекта создайте пакетныйскрипт с именем archiver.bat и всего одной строкой:

start cmd /k "cd Builds/Linux && %1winrar a -r Server.zip && move Server.zip ../../BuildsForRemote && exit"

Этот пакетный скрипт делает несколько вещей, каждая из которых разделена && :

  1. Установка каталога на Builds/Linux
  2. Запустите winrar.exe, заархивируйте папку сервера и назовите ее Server.zip . %1 относится к первому аргументу, который мы установили как каталог winrar.exe.
  3. Перемещение Server.zip в папку BuildsForRemote . Это делается для того, чтобы не загромождать корневой каталог.
  4. Выход из скрипта

Это все архивирование. Следующая часть — это загрузка файла с помощью SFTP.

Загрузка

В этом объяснении предполагается, что вы используете закрытый ключ для подключения к SFTP. Я тестировал это с помощью Debian и Ubuntu на AWS Lightsail.

Во-первых, мы собираемся убедиться, что файл завершен архивированием и перемещен в папку BuildsForRemote. Параметр fileChecks записывает, сколько раз скрипт проверяет, заархивирован ли файл присутствует. Это необходимо, потому что UploadFile() вызывается, когда архивирование все еще продолжается, а это означает, что файл не будет существовать, пока архивирование не будет завершено.

Прежде чем мы попытаемся загрузить файл, мы проверим, существуют ли файлы сервера в указанном каталоге. Чтобы убедиться, что Unity никогда не застрянет в цикле при попытке загрузить файл, вам нужно установить ограничение на количество проверок файла. Это будет сделано с помощью глобальной переменной с именем maxFileChecks.

private short maxFileChecks = 50;

В методе добавьте параметр int fileChecks, чтобы он выглядел так:

private static void UploadFile(int fileChecks = 0)

Если файл не существует, мы увеличим fileChecks и повторим попытку через одну секунду (Thread.Sleep(1000)). Если значение fileChecks достигнет максимального количества проверок файла, а файл еще не существует, загрузка будет отменена. Вы можете изменить значение maxFileChecks в зависимости от того, сколько времени потребуется вашему компьютеру для архивирования. Для каждой проверки файла скрипту будет отведена одна секунда для завершения архивирования.

if (!File.Exists(Directory.GetCurrentDirectory() + "/BuildsForRemote/Server.zip")) {
    if (fileChecks >= maxFileChecks) {
        UnityEngine.Debug.Log(string.Format("Could not upload file after {0} attempts. Please try again by clicking \"Retry Upload\"", maxUploadAttempts));
        return;
    }
    Thread.Sleep(1000);
    UploadFile(uploadAttempts, fileChecks + 1);
    return;
}

Как только будет обнаружено, что файл существует в данном каталоге, мы можем продолжить настройку загрузки файлов сервера, начиная с учетных данных сервера.

Есть два способа получить IP, имя пользователя, порт и закрытый ключ:

  • Переменные среды (рекомендуется, безопаснее)
  • Жестко запрограммировано

Если вы идете по маршруту переменной среды, хорошим соглашением об именах для них будет GAME-ABBREVIATION_SERVER_DEV_VALUE
т. е.: PONG_SERVER_DEV_IP , PONG_SERVER_DEV_PRIVATE_KEY

  • IP: IP-адрес сервера
  • PORT: порт для подключения к SFTP, почти всегда 22.
  • USER: Имя пользователя, которое сервер использует для подключения
  • PRIVATE_KEY: Путь к закрытому ключу, который используется для удаленного подключения к серверу.

Вся наша загрузка будет выполняться в методе UploadFile() .

Первым шагом является динамическое извлечение этих переменных среды (если вы идете по жестко закодированному маршруту, просто замените Environment.GetEnvironmentVariable(...) значением)

String ip = Environment.GetEnvironmentVariable("FB_SERVER_DEV_IP", EnvironmentVariableTarget.User);
int port = Int32.Parse(Environment.GetEnvironmentVariable("FB_SERVER_DEV_PORT", EnvironmentVariableTarget.User));
String user = Environment.GetEnvironmentVariable("FB_SERVER_DEV_USER", EnvironmentVariableTarget.User);
String privateKeyStr = Environment.GetEnvironmentVariable("FB_SERVER_DEV_PRIVATE_KEY", EnvironmentVariableTarget.User);

Затем получите закрытый ключ, используя полученный путь.

PrivateKeyFile privateKey = new PrivateKeyFile(privateKeyStr);

Затем мы хотим получить абсолютный путь к файлу Server.zip, который мы создали в ArchiveFile(), чтобы указать правильный файл для загрузки.

string path = Directory.GetCurrentDirectory() + @"\BuildsForRemote\Server.zip";

Затем мы используем библиотеку SFTP Renci.SshNet для загрузки Server.zip.

using (var client = new SftpClient(ip, 22, user, privateKey)) {
    client.Connect();

    if (client.IsConnected) {
        byte[] byteData = File.ReadAllBytes(path);
        using (var ms = new MemoryStream(byteData)) {
            ms.Write(byteData, 0, byteData.Length);
            ms.Position = 0;
            client.UploadFile(ms, "/home/ubuntu/fbrev/Server.zip", true);
        }
    } else {    
        UnityEngine.Debug.LogError("Could not connect to server with the given environment credentials.");
    }
}

Затем, как обычное дело, когда загрузка файла завершена, вы можете просто удалить файл Server.zip, если он вам больше не нужен.

File.Delete(path);

Обработка исключений/оптимизация

Что касается функциональности, этого достаточно, чтобы заставить его работать. Однако программное обеспечение никогдане бывает идеальным, поэтому вам понадобится система безопасности в виде попробовать/отловить, чтобы убедиться в любых ошибках рассматриваются соответственно. Во-первых, вам нужно правильно зарегистрировать проблему. Кроме того, если вы хотите повторить загрузку, которая не удалась, вам нужно записать, сколько попыток было сделано, чтобы вы могли полностью остановить попытку загрузки и указать причины каждой неудачной попытки.

Во-первых, вам нужно добавить еще одну глобальную переменную:

private static short maxUploadAttempts = 5;

В параметрах метода вы захотите добавить еще одну переменную int, установленную на 0, с именем uploadAttempts.

private static void UploadFile(int uploadAttempts = 0, int fileChecks = 0)

Параметр uploadAttempts регистрирует количество попыток загрузки по SFTP. После возникновения ошибки вы хотите увеличить это значение и перезапустить метод.

Существует несколько причин, по которым загрузка не удалась. Если это ArgumentException, это проблема с именем файла или каким-то другим аргументом. В этом случае нет смысла повторять загрузку, так как это фундаментальная проблема с параметрами, которые мы передаем, поэтому мы можем просто не выполнить загрузку и зарегистрировать ее в Unity. . В противном случае мы повторим загрузку, увеличив uploadAttempts . В конце концов, если будет достигнуто максимальное количество загрузок, загрузка будет отменена, а ошибка зарегистрирована.

В try/catch сначала будет идти конкретное исключение ArgumentException, затем следующее общее Exception , где будет увеличиваться uploadAttempts и повторяться попытка, если только не будет достигнуто максимальное количество попыток.

try {
    // Previous Upload Logic...
} catch (ArgumentException argEx) {
    UnityEngine.Debug.LogError(argEx.StackTrace);
    return;
} catch (Exception ex) {
    if (uploadAttempts >= maxUploadAttempts) {
        UnityEngine.Debug.LogError(ex.ToString());
        return;
    }
    UnityEngine.Debug.LogWarning(string.Format("Upload attempt failed on attempt #{0}. Attempting to upload again. ({0} of {1} max attempts)", uploadAttempts, maxUploadAttempts));
    UploadFile(uploadAttempts + 1, fileChecks);
}

После этого UploadFile() должен выглядеть примерно так:

Как только эти два метода будут завершены, останется только создать метод, который будет выполнять обычное создание, а затем вызвать ArchiveFile() , а затем UploadFile() .

Затем, после повторной компиляции скриптов, вы должны увидеть вкладку Build в верхней части редактора Unity. Разверните его, после чего вы должны увидеть «Build/Build Server + Upload (Linux)».

И это все! С помощью нескольких скриптов будет намного проще загружать и тестировать новые сборки вашей игры. Вам все равно придется разархивировать и правильно запустить серверное приложение в Linux, но это уже другая тема, о которой я могу написать в другой раз. Удачи в создании вашей игры!