Connecting SignalR to Unity

Often, games need to receive game balance updates, update the player profile, save achievements and give out rewards. If you store data directly in the client, you will have to wait for the team to publish a new patch. As a more flexible solution – get the configuration and resources for the game from an external server. In the post, we will consider how you can connect to the simplest service from the Unity client to receive a message from it. To implement the service, let’s take the SignalR library.

What to expect from the article

  • An example of creating a simple SiglanR service and deploying it on Ubuntu

  • Demonstration how to connect a library from NuGet to Unity

  • A few tips that can speed up working with assets under the Unity editor

  • Analysis of several common errors when building third-party dlls on Android

What will not be in this article

As the article was being written, the number of details that I would like to show grew very quickly, so we will put them aside for the following articles and we will not touch on this one:

  • HTTPS/SSL will not be used to access our service. It is necessary to use HTTPS/SSL for release applications.

  • Let’s not find fault with SOLID, the example is small and add patterns just so that they don’t want to.

  • There will be no deployment of ASP.NET Core from Docker.

  • Let’s skip Authorization in the service and access rights management. The topic is good, we will touch on it next time.

  • Mapping of models from the game server to the client and code sharing between the client and the server will be covered in the next article.

  • Request traffic optimization. Using tools like message pack. We’ll definitely come back to this later.

Writing Demo SignalR Service

Create a solution from an Asp.NET Core Web API template. Microsoft already has a great step-by-step tutorial in Russian SignalR for ASP.NET Core with Blazor, let’s take it as a basis. We will run the service on Ubuntu.

First, let’s install the .NET SDK on the machine

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

Checking the installed version

dotnet --list-sdks

Next, we will build our project on the Ubuntu machine and leave it hanging as a daemon. First, let’s go to the directory with our SignalR service and run the build command:

dotnet build -c Release

We will publish in the directory we need from which we plan to launch

dotnet publish -c Release -o %OUTPUT_DIRECTORY%

It is enough for us that SIgnalR Hub hangs in the background process, so we will not invent anything extra. A simple command will suffice for us:

(cd %OUTPUT_DIRECTORY%;dotnet %PROJECT_NAME%.dll > /dev/null 2>&1 &)

Now if we try to open with http://localhost:500 then we will see our demo web application. If our machine has a public ip, let it be 53.53.53.53. Trying to open in browser from another machine http://53.53.53.53:5000 will tell us only that: “Unable to access the site.”

The problem is in the redirect. We will solve it through nginx. Let’s put it in first.

sudo apt-get install nginx

Now let’s add the config to /etc/nginx/sites-available to redirect to our application

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;
	}
}

Restarting the nginx service

sudo service nginx restart

In our SignalR demo service, we registered the Hub under the name demohub

app.MapHub<DemoHub>("/demohub"); 

The Hub will now be publicly available: http://53.53.53.53:5000/demohub

Installing SignalR in Unity

Let’s start from the beginning, got the SignalR library itself. If you try just take the pack Microsoft.AspNetCore.SignalR.Client in NuGet, you will run into the problem that the package does not contain a dependency.

Let’s solve this error by directly downloading SignalR via NuGet CLI:

  • Downloading NuGet CLI and save where you want

  • We ask NuGet to download SignalR for us

nuget.exe install Microsoft.AspNetCore.SignalR.Client  -Version 7.0.2 -OutputDirectory %OUTPUT_PATH%

Now we have all the necessary dependencies and the SignalR client itself

If you open one of the downloaded packages, we will see the following:

At the time of writing, the LTS version of Unity is 2021.3.16f1. This version supports:

  • .NET Standard 2.1/2.0

  • .NET Framework

This means that before adding to the project, you must remove the version for .NET Core. If you only use .NET Standard or .NET Framework in your game, then leave only the required version.

Now you can add libraries to the game client. In my demo, I placed them at:

Assets/Plugins/Packages/SignalR

If you need to use the .NET Standard and .NET Framework versions of the library, then after adding it, you will have to help Unity understand which dll is for which version. In the documentation on compiling a project for different platforms, you can find the right defines

NET_STANDARD – Defined when building scripts against .NET Standard 2.1 API compatibility level on Mono and IL2CPP

Next, you need to specify this compilation restriction for our dlls. For .NET Standard it should be like this:

And for the .NET Framework, you need to set the reverse constraint:

We added a lot of Dll to the project. Settings for each can be set by hand, but it’s tedious. Therefore, we will write a small and stupid sprit that will do the job itself. One-time scripts should be written as straightforward and understandable as possible.

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();
        }
    }
}

I will briefly highlight the useful points:

  • Use AssetDatabase.StartAssetEditing/AssetDatabase.StopAssetEditing to modify a group of assets so you don’t have to wait for each individual asset to be reimported.

  • PluginImporter allows you to change settings for dlls and everything that Unity categorizes as plugins

Now we call the execution of our script through the menu – Tools/Apply SignalR Constraints. After it finishes executing, we will get libraries separated by the version of the runtime that we will use. Now switching between .NET Standard and .NET Framework will not cause errors.

Connecting to SignalR Hub

Checking under the editor

Next, we will write a small script for connecting to SignalR from under the editor. After that, let’s start building for Android and check our application on the device.

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;
}

In Unity, connection.StartAsync() may throw a connection error. On the device, it will look like this:

Error Unity SocketException: mono-io-layer-error (111)

It can occur if the user did not have a network at the time of connection or the server with the SignalR hub service was not available.

Received message from hub to Unity editor
Received message from hub to Unity editor

We collect on the device

We have reached the stage of testing on the device. We will test on Android. Another important limitation, our build will immediately be under IL2CPP. And at the first start after assembly, an error awaits us. The message will say that the dependencies necessary for SignalR to work were not found and it is impossible to create an instance of the type of interest to us. The reason for the error is that Unity tries to cut (https://docs.unity3d.com/Manual/ManagedCodeStripping.html) unused code and libraries, and sometimes the dependencies we need fall under the knife. To fix this problem, just add the link.xml file. Unity linker documentation.

For us, the solution to the problem will be this link.xml file:

<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>

After adding instructions to the linker and assembly, there should be no more errors at the start.

We got over the main fear of many newbies when working with a server in GameDev – we received the first message from a service that we implemented ourselves.

What’s next?

The plans are to cover further what did not get here because of the volume. And since SignalR was chosen for the meta game in the game that we are currently creating, it will be possible to add real use cases and problems that we encountered:

  • Working with advanced request parameters under Unity, authorization, HTTPS/SSL

  • How much effort and money does it cost to connect a cloud for a game, what are the pros and cons of this

  • Traffic optimization and model mapping between client and server

  • How to organize data processing on the client and connect it with ECS gameplay

That’s all. Thank you for your attention.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *