Подключаем SignalR к Unity

от автора

Часто в играх необходимо получать обновления игрового баланса, обновлять профиль игрока, сохранять достижения и выдавать награды. Если хранить данные прямо в клиенте, то придется ждать публикации нового патча командой. Как более гибкое решение — получать конфигурацию и ресурсы для игры с внешнего сервера. В посте рассмотрим как можно из клиента 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 был не доступен.

Полученное сообщение из хаба в редактор Unity
Полученное сообщение из хаба в редактор Unity

Собираем на устройство

Мы дошли до этапа тестирования на устройстве. Тестировать будем на 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 геймплеем

На этом всё. Спасибо за внимание.

P.S ссылка на мой telegram канал о разработке игр


ссылка на оригинал статьи https://habr.com/ru/post/712918/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *