Подключаем 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/


Интересные статьи

Интересные статьи

На Хабре ни для кого не секрет, что в текущей повестке практически все сферы частного бизнеса вынуждены реагировать на происходящие изменения. Большое количество привычных всем нам зарубежных сервисов...
Я делаю ремейк своей старой игры Loca Deserta: Sloboda Первая версия была написана на Flutter, но сейчас в качестве движка для игры я выбрал Unity.Я начал работу с нуля и реализовал множество новых фу...
На просторах интернета, и в частности хабра, очень трудно встретить статьи с критикой игрового движка Unity. Я решил это исправить, и приготовил вам текст о переходе на DOTS, насилию над C#, знамениты...
Подключаем нагрудный пульсометр к IOS устройству на Swift по протоколу Bluetooth без смс и регистраций.У меня был китайский пульсометр CooSpo, MacBook, iPhone и немного с...
Получить трафик для интернет-магазина сегодня не проблема. Есть много каналов его привлечения: органическая выдача, контекстная реклама, контент-маркетинг, RTB-сети и т. д. Вопрос в том, как вы распор...