Часто в играх необходимо получать обновления игрового баланса, обновлять профиль игрока, сохранять достижения и выдавать награды. Если хранить данные прямо в клиенте, то придется ждать публикации нового патча командой. Как более гибкое решение — получать конфигурацию и ресурсы для игры с внешнего сервера. В посте рассмотрим как можно из клиента Unity подключиться к простейшему сервису для получения от него сообщения. Для реализации сервиса возьмем библиотеку SignalR.
Что стоит ждать от статьи
-
Пример создания простейшего сервиса SiglanR и разворачивание его на Ubuntu.
-
Демонстрация как подключить библиотеку из NuGet к Unity.
-
Несколько советов, которые могут ускорить работу с ассетами под редактором Unity.
-
Разбор нескольких частых ошибок при сборке на Android сторонних dll.
Чего не будет в этой статье
По мере написания статьи количество деталей, которые хотелось бы показать, очень быстро росло, поэтому их отложим на следующие статьи и в этой мы не будем касаться:
-
Для доступа к нашему сервису не будет использоваться HTTPS/SSL. Для релизных приложений использовать HTTPS/SSL необходимо.
-
Не будем придираться к SOLID, пример небольшой и добавлять паттерны только чтобы они были не хочу.
-
Не будет разворачивания ASP.NET Core из Docker.
-
Пропустим Авторизацию в сервисе и управление правами доступа. Тема хорошая, затронем в следующий раз.
-
Маппинг моделей с игрового сервера в клиент и шаринг кода между клиентом и сервером вынесем в следующую статью.
-
Оптимизация трафика запросов. Использование инструментов подобных MessagePack. Обязательно вернемся к этому позже.
Пишем Demo SignalR Service
Создаем решение из шаблона Web API Asp.NET Core. У Microsoft уже есть отличный пошаговый тутор на русском языке SignalR для ASP.NET Core с Blazor, возьмем его за основу. Запускать на сервис мы будем на Ubuntu.
Для начала поставим .NET SDK на машину:
sudo apt-get update sudo apt-get install -y apt-transport-https sudo apt-get update sudo apt-get install -y dotnet-sdk-7.0
Проверяем установленную версию:
dotnet --list-sdks
Дальше мы соберем наш проект на машине с Ubuntu и оставим его висеть демоном. Сначала перейдем в каталог с нашим сервисом SignalR и выполним команду сборки:
dotnet build -c Release
Опубликуем в нужной нам директории из которой планируем запускать:
dotnet publish -c Release -o %OUTPUT_DIRECTORY%
Нам достаточно, чтобы SIgnalR Hub висел в фоновом процессе, поэтому не будем придумывать лишнего. Нам хватит простой команды:
(cd %OUTPUT_DIRECTORY%;dotnet %PROJECT_NAME%.dll > /dev/null 2>&1 &)
Если теперь попробуем открыть с http://localhost:500 то увидим наше demo web приложение. Если наша машина имеет публичный ip, пусть это будет 53.53.53.53. Попытка открыть в браузере с другой машины http://53.53.53.53:5000 скажет нам лишь, что: “Не удается получить доступ к сайту”.
Проблема в редиректе. Решать ее будем через nginx. Сначала поставим его.
sudo apt-get install nginx
Теперь добавим конфиг в /etc/nginx/sites-available для редиректа в наше приложение:
map $http_connection $connection_upgrade { "~*Upgrade" $http_connection; default keep-alive; } server { listen 80 default_server; listen [::]:80 default_server; server_name _; location / { proxy_pass http://127.0.0.1:5000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; proxy_read_timeout 100s; proxy_cache off; } }
Перезапускаем сервис nginx:
sudo service nginx restart
В нашем демо сервисе SignalR мы зарегистрировали Hub под именем demohub:
app.MapHub<DemoHub>("/demohub");
Теперь Hub будет доступен публично: http://53.53.53.53:5000/demohub
Устанавливаем SignalR в Unity
Начнем с начала, получил саму библиотеку SignalR. Если вы попробуете просто взять пакет Microsoft.AspNetCore.SignalR.Client в NuGet, то наткнетесь на проблему, пакет не содержит зависимости.
Решим эту ошибку напряму скачав SignalR через NuGet CLI:
-
Качаем NuGet CLI и сохраняем где вам удобно.
-
Просим NuGet скачать нам SignalR.
nuget.exe install Microsoft.AspNetCore.SignalR.Client -Version 7.0.2 -OutputDirectory %OUTPUT_PATH%
Теперь у нас все необходимые зависимости и сам клиент SignalR
Если открыть один из скачанных пакетов, увидим следующее:
На момент написания статьи LTS версия Unity — 2021.3.16f1. Эта версия поддерживает:
-
.NET Standard 2.1 / 2.0
-
.NET Framework
Это значит, что перед тем как добавлять в проект необходимо удалить версию для .NET Core. Если в игре вы используете только .NET Standard или .NET Framework, то оставьте только нужную версию.
Теперь можно добавить библиотеки в клиент игры. В своем demo я расположил их адресу:
Assets/Plugins/Packages/SignalR
Если вам необходимо использовать .NET Standard и .NET Framework версии библиотеки, то после добавления придется помочь Unity понять какая dll для какой версии. В документации по компиляции проекта под разные платформы можно найти нужные defines
NET_STANDARD — Defined when building scripts against .NET Standard 2.1 API compatibility level on Mono and IL2CPP
Дальше необходимо указать это ограничение компиляции для наших dll. Для .NET Standard должно так:
А для .NET Framework необходимо поставить обратное ограничение:
Dll в проект мы добавили много. Настройки для каждой можно выставить руками, но это утомительно. Поэтому напишем небольшой и глупый сприпт, который сделает работу сам. Одноразовые скрипты должны быть написаны максимально прямолинейно и понятно.
using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; public static class ApplyPlatformDefines { public static string[] Paths = {"Assets/Plugins/Packages/SignalR"}; public const string Extension = "t:Object"; public const string NetStandard = "NET_STANDARD"; public const string NotNetStandard = "!NET_STANDARD"; [MenuItem(itemName: "Tools/Apply SignalR Constraints")] public static void ApplyDefines() { //Говорим Unity, что мы начинаем редактирование ассетов и пока мы не закончим, //не нужно импортировать их и тратить очень много времени на это AssetDatabase.StartAssetEditing(); //При любом исходе выполнения нашего скрипта мы должны вызвать //AssetDatabase.StopAssetEditing(); после окончания изменения ассетов try { Execute(); } catch (Exception e) { Debug.LogException(e); } AssetDatabase.StopAssetEditing(); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } private static void Execute() { var assets = AssetDatabase.FindAssets(Extension,Paths); var constraints = new List<string>(); foreach (var guid in assets) { var path = AssetDatabase.GUIDToAssetPath(guid); //Получаем по пути к ассету его импортер для управления его настройками var importer = AssetImporter.GetAtPath(path); //Нас интересуют именно импортеры dll, они в unity идут как PluginImporter //поэтому берем только их if(importer is not PluginImporter pluginImporter) continue; var restriction = path.Contains("netstandard") ? NetStandard : NotNetStandard; //формируем новый список ограничения для dll constraints.Clear(); constraints.AddRange(pluginImporter.DefineConstraints); constraints.RemoveAll( x => x.Equals(NetStandard, StringComparison.OrdinalIgnoreCase) || x.Equals(NotNetStandard, StringComparison.OrdinalIgnoreCase)); constraints.Add(restriction); //Задаем новые ограничения по тому, в какой папке находится dll pluginImporter.DefineConstraints = constraints.ToArray(); Debug.Log($"ADD constraints {restriction} to {path}"); //Сохраняем настройки pluginImporter.SaveAndReimport(); } } }
Кратко отмечу полезные моменты:
-
Используйте AssetDatabase.StartAssetEditing/AssetDatabase.StopAssetEditing, для модификации группы ассетов, чтобы не ждать реимпорт каждого отдельного ассета.
-
PluginImporter позволяет вам менять настройки dll и всего, что Unity относит к категории плагинов
Теперь мы вызываем выполнение нашего скрипта через меню — Tools/Apply SignalR Constraints. После того как он закончит выполнение мы получим библиотеки разделенные по версии рантайма, который будем использовать. Теперь переключения между .NET Standard и .NET Framework не будут вызывать ошибок.
Подключаемся к SignalR Hub
Проверяем под редактором
Дальше мы напишем небольшой скрипт подключения к SignalR из под редактора. После чего приступим к сборке под Android и проверим наше приложение на устройстве.
public async UniTask<HubConnection> ConnectToHubAsync() { Debug.Log("ConnectToHubAsync start"); //Создаем соединение с нашим написанным тестовым хабом var connection = new HubConnectionBuilder() .WithUrl(“http://53.53.53.53:5000/demohub”) .WithAutomaticReconnect() .Build(); Debug.Log("connection handle created"); //подписываемся на сообщение от хаба, чтобы проверить подключение connection.On<string, string>("ReceiveMessage", (user, message) => LogAsync($"{user}: {message}").Forget()); while (connection.State != HubConnectionState.Connected) { try { if (connection.State == HubConnectionState.Connecting) { await UniTask.Delay(TimeSpan.FromSeconds(connectionDelay)); continue; } Debug.Log("start connection"); await connection.StartAsync(); Debug.Log("connection finished"); } catch (Exception e) { Debug.LogException(e); } } return connection; }
В Unity connection.StartAsync() может вызвать ошибку подключения. На устройстве она будет выглядеть следующим образом:
Error Unity SocketException: mono-io-layer-error (111)
Она может произойти, если в момент подключения у пользователя не было сети или сервер с сервисом хаба SignalR был не доступен.
Собираем на устройство
Мы дошли до этапа тестирования на устройстве. Тестировать будем на Android. Еще одно важное ограничение наш билд сразу будет под IL2CPP. И при первом запуске после сборки нас ждет ошибка. В сообщении будет сказано, что нужные для работы SignalR зависимости не найдены и создать инстанс интересующего нас типа невозможно. Причина ошибки в том, что Unity старается вырезать (https://docs.unity3d.com/Manual/ManagedCodeStripping.html) неиспользуемый код и библиотеки и порой под нож попадают и нужные нам зависимости. Чтобы починить эту проблему достаточно добавить файл link.xml. Документация по Unity linker.
Для нас решением проблемы будет вот такой файл link.xml:
<linker> <assembly fullname="Microsoft.AspNetCore.Connections.Abstractions" preserve="all"/> <assembly fullname="Microsoft.AspNetCore.Http.Connections.Client" preserve="all"/> <assembly fullname="Microsoft.AspNetCore.Http.Connections.Common" preserve="all"/> <assembly fullname="Microsoft.AspNetCore.Http.Features" preserve="all"/> <assembly fullname="Microsoft.AspNetCore.SignalR.Client.Core" preserve="all"/> <assembly fullname="Microsoft.AspNetCore.SignalR.Client" preserve="all"/> <assembly fullname="Microsoft.AspNetCore.SignalR.Common" preserve="all"/> <assembly fullname="Microsoft.AspNetCore.SignalR.Protocols.Json" preserve="all"/> <assembly fullname="Microsoft.Extensions.Configuration.Abstractions" preserve="all"/> <assembly fullname="Microsoft.Extensions.Configuration.Binder" preserve="all"/> <assembly fullname="Microsoft.Extensions.Configuration" preserve="all"/> <assembly fullname="Microsoft.Extensions.DependencyInjection.Abstractions" preserve="all"/> <assembly fullname="Microsoft.Extensions.DependencyInjection" preserve="all"/> <assembly fullname="Microsoft.Extensions.Logging.Abstractions" preserve="all"/> <assembly fullname="Microsoft.Extensions.Logging" preserve="all"/> <assembly fullname="Microsoft.Extensions.Options" preserve="all"/> <assembly fullname="Microsoft.Extensions.Primitives" preserve="all"/> <assembly fullname="System.Buffers" preserve="all"/> <assembly fullname="System.ComponentModel.Annotations" preserve="all"/> <assembly fullname="System.IO.Pipelines" preserve="all"/> <assembly fullname="System.Memory" preserve="all"/> <assembly fullname="System.Numerics.Vectors" preserve="all"/> <assembly fullname="System.Runtime.CompilerServices.Unsafe" preserve="all"/> <assembly fullname="System.Text.Json" preserve="all"/> <assembly fullname="System.Threading.Channels" preserve="all"/> <assembly fullname="System.Process" preserve="all"/> <assembly fullname="System.Threading.Tasks.Extensions" preserve="all"/> <assembly fullname="System.Threading" preserve="all"/> <assembly fullname="System.Net.Http" preserve="all"/> <assembly fullname="System.Core" preserve="all"> <type fullname="System.Linq.Expressions.Interpreter.LightLambda" preserve="all" /> </assembly> </linker>
После добавления указания линкеру и сборки, на старте ошибок быть уже не должно.
Мы перешагнули главный страх многих новичков при работе с сервером в GameDev — получили первое сообщение от сервиса, который реализовали сами.
Что дальше?
В планах осветить дальше то, что не попало сюда из-за объема. А поскольку в игре, которую создаем сейчас для мета гейма выбрали именно SignalR, получится добавить реальных кейсов применения и проблем, с которыми столкнулись:
-
Работа с дополнительными параметрами запросов под Unity, авторизация, HTTPS/SSL
-
Сколько стоит сил и денег подключить облако для игры, какие плюсы и минусы это несет
-
Оптимизация трафика и маппинг моделей между клиентом и сервером
-
Как организовать обработку данных на клиенте и связать с ECS геймплеем
На этом всё. Спасибо за внимание.
ссылка на оригинал статьи https://habr.com/ru/post/712918/
Добавить комментарий