Unity + .NET Core + MagicOnion v3 environment construction hands-on

13 minute read

Introduction

I think MagicOnion is a framework that is comfortable to touch and very easy to use once the environment is built.

However, the initial construction is a little complicated, and I thought that if someone who had no experience with both Unity and the server (.NET Core) tried it, they would often stumble somewhere.

Therefore, this article was written with the aim of creating a material that allows people who have no experience with either of them to build an environment without having to stumble.

If you find yourself stumbling on the way, please let us know on Twitter.

Hands-on takes about 30 minutes to an hour, starting with Unity and Visual Studio installed.

table of contents

1. About the environment

This is the OS, tool, and software version used to write this article.

2. Building on the Unity side

2.1. Create a new project, change PlayerSettings, create various folders

This time we will create a 3D project.
The project name is arbitrary, but it should be Sample.Unity to distinguish it from the server-side project.

Enter any save destination as the save destination.

image.png

Open PlayerSettings when Unity starts.

image.png

Select Player and change the following two places.

–Change APICompatibilityLevel to .NET 4.x
–Check Allow unsafe Code

image.png

Next, create the following folder.

  • Replace Sample.Unity with your own project name.

  • Sample.Unity\Assets\Editor
  • Sample.Unity\Assets\Plugins
  • Sample.Unity\Assets\Scripts
  • Sample.Unity\Assets\Scripts\Generated
  • Sample.Unity\Assets\Scripts\ServerShared
  • Sample.Unity\Assets\Scripts\ServerShared\Hubs
  • Sample.Unity\Assets\Scripts\ServerShared\MessagePackObjects
  • Sample.Unity\Assets\Scripts\ServerShared\Services

Make sure that the folder structure is as shown below.

image.png

2.2. Installing MagicOnion

Download MagicOnion.Client.Unity.unitypackage from GitHub.

image.png

After downloading, double-click to import.

image.png

2.3. Installing MessagePack for C

Download MessagePack.Unity.2.1.152.unitypackage from GitHub.

image.png

After downloading, double-click to import.
A warning will be displayed because there is a file that overlaps with MagicOnion, but import it as it is.

image.png

2.4. Installing gRPC

From gRPC’s Daily Builds (2019/08/01) grpc_unity_package. Download 2.23.0-dev.zip.

image.png

After downloading, unzip it and copy the following folder to Sample.Unity \ Assets \ Plugins.

  • Google.Protobuf
  • Grpc.Core
  • Grpc.Core.Api

image.png

2.5. Preparing a script to connect to the server

Create a SampleController script in ʻAssets \ Scenes`.
image.png

Copy the code for SampleController below.
Start () connects to the server and OnDestroy () disconnects.

using Grpc.Core;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SampleController : MonoBehaviour
{
    private Channel channel;

    void Start()
    {
        this.channel = new Channel("localhost:12345", ChannelCredentials.Insecure);
    }

    async void OnDestroy()
    {
        await this.channel.ShutdownAsync();
    }
}

Add an empty GameObject to SampleScene.

image.png

Add SampleController to the added GameObject.

image.png

2.6. Preparation of class for checking the operation of code sharing between Unity ⇔ server

When using MagicOnion, it is common to share classes created on the Unity side with the server side.
This time, set to share all the scripts created under Assets \ Scripts \ ServerShared.
The code sharing settings will be set later on the server side, but first prepare a class for checking the operation.

Create a Player script in ʻAssets \ ServerShared \ MessagePackObjects`.

image.png

Copy and paste the Player code below.

using MessagePack;
using UnityEngine;

namespace Sample.Shared.MessagePackObjects
{
    [MessagePackObject]
    public class Player
    {
        [Key(0)]
        public string Name { get; set; }
        [Key(1)]
        public Vector3 Position { get; set; }
        [Key(2)]
        public Quaternion Rotation { get; set; }
    }
}

When using MagicOnion, the class used for communication between Client ⇔ server is defined as such MessagePackObject.

Keep in mind that you only need two things to define as a MessagePackObject:

–Give MessagePackObjectAttribute to class
–Give each property a KeyAttribute and number them in order

This is the end of the construction on the Unity side.

3. Server-side construction

Next, proceed with the construction work on the server side.

3.1. Add a server-side project to your solution

Right-click on the solution and add a new project.
image.png

Select the console app (.NET Core) and click Next.

image.png

Enter any project name. (Here, it is Sample.Server)
Please specify the root of the project as the location.
image.png

3.2. Installing MagicOnion

Install MagicOnion from NuGet.
Open Tools-> NuGet Package Manager-> Manage NuGet Packages for your solution.
image.png

Click Browse and search for MagicOnion.
Select MagicOnion.Hosting from the search results and check Sample.Server.
Select the version 3.0.12 and install it.
image.png

A preview of your changes will appear, press OK.
image.png

You will be asked to agree to the license, so I agree.
image.png

3.3. Editing Program.cs

Overwrite and save Program.cs with the following contents.
Now when you launch the server-side project, MagicOnion will launch.

using MagicOnion.Hosting;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading.Tasks;

namespace Sample.Server
{
    class Program
    {
        static async Task Main(string[] args)
        {
            await MagicOnionHost.CreateDefaultBuilder()
                .UseMagicOnion()
                .RunConsoleAsync();
        }
    }
}

3.4. Add class library project to solution

Use this class library to share code between Unity and server.

Right-click on the solution and add a new project.
image.png

Select the Class Library (C # .NET Standard).
image.png

Enter any project name. (Here, it is Sample.Shared)
Please specify the root of the project as the location.
image.png

The automatically created Class1.cs is unnecessary and should be deleted.
image.png

3.5. Install MagicOnion.Abstractions in class library

Search for and install MagicOnion.Abstractions from NuGet in the same way you installed MagicOnion earlier.
Select Sample.Shared for the target project and 3.0.12 for the version.
image.png

3.6. Install MessagePack.UnityShims in class library

Install MessagePack.UnityShims in Sample.Shared in the same way.
image.png

3.7. Refer to the code on the Unity side from the class library

Double-click Sample.Shared to open Sample.Shared.csproj and add the settings in the red frame.
image.png

Please copy here for the settings to be added.

  • Replace Sample.Unity with the project name on your Unity side.
<ItemGroup>
    <Compile Include="..\Sample.Unity\Assets\Scripts\ServerShared\**\*.cs" />
</ItemGroup>

If you look at Solution Explorer in this state, you can see that the files under the ServerShared folder prepared on the Unity side can be loaded into the class library.
image.png

3.8. Browse the class library from the server-side project

Right-click on Sample.Server and select Add, Project Reference.
image.png

Select Sample.Shared and press OK.
image.png

Now you have a server code-shared with Unity.

4. API implementation and operation check

From here, we will implement the API and check its operation.
MagicOnion can use two types of communication, normal API communication and real-time communication, so let’s test each one.

4.1. Ordinary API communication

Let’s start with normal API communication.

4.1.1. Create API definition on Unity side

Create a SampleService script under Assets \ Scripts \ ServerShared \ Services.
image.png

Please copy and save the contents of SampleService as follows.
This time, let’s define an API that adds and an API that multiplies.

using MagicOnion;

namespace Sample.Shared.Services
{
    public interface ISampleService : IService<ISampleService>
    {
        UnaryResult<int> SumAsync(int x, int y);
        UnaryResult<int> ProductAsync(int x, int y);
    }
}

4.1.2. Implement the API on the server side

Right-click on Sample.Server and add a new folder.
Name it Services.

image.png

image.png

Then add the class in the Services folder.

image.png

Name it SampleService.cs.
image.png

Please copy and save the contents of SampleService.cs as follows.

using MagicOnion;
using MagicOnion.Server;
using Sample.Shared.Services;

namespace Sample.Server.Services
{
    public class SampleService : ServiceBase<ISampleService>, ISampleService
    {
        public UnaryResult<int> SumAsync(int x, int y)
        {
            return UnaryResult(x + y);
        }

        public UnaryResult<int> ProductAsync(int x, int y)
        {
            return UnaryResult(x * y);
        }
    }
}

4.1.3. Implement the code that calls the API on the Unity side

Add code to SampleController that calls SampleService.
Please copy and paste the code below to overwrite it.

using Grpc.Core;
using MagicOnion.Client;
using Sample.Shared.Services;
using System.Threading.Tasks;
using UnityEngine;

public class SampleController : MonoBehaviour
{
    private Channel channel;
    private ISampleService sampleService;

    void Start()
    {
        this.channel = new Channel("localhost:12345", ChannelCredentials.Insecure);
        this.sampleService = MagicOnionClient.Create<ISampleService>(channel);

        this.SampleServiceTest(1, 2);
    }

    async void OnDestroy()
    {
        await this.channel.ShutdownAsync();
    }

    async void SampleServiceTest(int x, int y)
    {
        var sumReuslt = await this.sampleService.SumAsync(x, y);
        Debug.Log($"{nameof(sumReuslt)}: {sumReuslt}");

        var productResult = await this.sampleService.ProductAsync(2, 3);
        Debug.Log($"{nameof(productResult)}: {productResult}");
    }
}

4.1.4. Confirmation of API communication operation

First, start the server.

Right-click Sample.Server and click Set as Startup Project.
image.png

This will change the button that was normally labeled ʻUnity Attach to Sample.Server`.
image.png

You can start the server by pressing this button.

image.png

  • To restore the startup project, right-click ʻAssembly-CSharp` and specify it as the startup project.
  • If you want to start the server while attaching to Unity, use Multi-startup project. (See below)

Then play the Unity Scene with the server running.

The log is displayed in the Unity Console.
image.png

4.2. Real-time communication

Then try real-time communication.

4.2.1. Create API definition on Unity side

As with normal API communication, first create the API definition.
Create a SampleHub script under Assets \ Scripts \ ServerShared \ Hubs.

image.png

Please copy and save the contents of SampleHub as follows.
This time, we will create four APIs: log in to the game, speak in chat, update location information, and disconnect from the game.

using MagicOnion;
using Sample.Shared.MessagePackObjects;
using System.Threading.Tasks;
using UnityEngine;

namespace Sample.Shared.Hubs
{
    /// <summary>
    /// CLient ->Server API
    /// </summary>
    public interface ISampleHub : IStreamingHub<ISampleHub, ISampleHubReceiver>
    {
        /// <summary>
        ///Tell the server to connect to the game
        /// </summary>
        Task JoinAsync(Player player);
        /// <summary>
        ///Tell the server to disconnect from the game
        /// </summary>
        Task LeaveAsync();
        /// <summary>
        ///Send a message to the server
        /// </summary>
        Task SendMessageAsync(string message);
        /// <summary>
        ///Tell the server that you have moved
        /// </summary>
        Task MovePositionAsync(Vector3 position);
    }

    /// <summary>
    /// Server ->Client API
    /// </summary>
    public interface ISampleHubReceiver
    {
        /// <summary>
        ///Tell the client that someone has connected to the game
        /// </summary>
        void OnJoin(string name);
        /// <summary>
        ///Tell the client that someone has disconnected from the game
        /// </summary>
        void OnLeave(string name);
        /// <summary>
        ///Tell the client what someone said
        /// </summary>
        void OnSendMessage(string name, string message);
        /// <summary>
        ///Tell the client that someone has moved
        /// </summary>
        void OnMovePosition(Player player);
    }
}

4.2.2 Implement the API on the server side

Create a Hubs folder under Sample.Server and create SampleHub.cs in it in the same way as when implementing a normal API.
image.png

Please copy and save the contents of SampleHub.cs as follows.

using MagicOnion.Server.Hubs;
using Sample.Shared.Hubs;
using Sample.Shared.MessagePackObjects;
using System;
using System.Threading.Tasks;
using UnityEngine;

public class SampleHub : StreamingHubBase<ISampleHub, ISampleHubReceiver>, ISampleHub
{
    IGroup room;
    Player me;

    public async Task JoinAsync(Player player)
    {
        //All rooms are fixed
        const string roomName = "SampleRoom";
        //Join the room&Hold room
        this.room = await this.Group.AddAsync(roomName);
        //Keep your information
        me = player;
        //Notify all members participating in the room that they have participated
        this.Broadcast(room).OnJoin(me.Name);
    }

    public async Task LeaveAsync()
    {
        //Remove yourself from members in the room
        await room.RemoveAsync(this.Context);
        //Notify all members that they have left the room
        this.Broadcast(room).OnLeave(me.Name);
    }

    public async Task SendMessageAsync(string message)
    {
        //Notify all members of what they said
        this.Broadcast(room).OnSendMessage(me.Name, message);

        await Task.CompletedTask;
    }

    public async Task MovePositionAsync(Vector3 position)
    {
        //Update information on the server
        me.Position = position;

        //Notify all members of updated player information
        this.Broadcast(room).OnMovePosition(me);

        await Task.CompletedTask;
    }

    protected override ValueTask OnConnecting()
    {
        // handle connection if needed.
        Console.WriteLine($"client connected {this.Context.ContextId}");
        return CompletedTask;
    }

    protected override ValueTask OnDisconnected()
    {
        // handle disconnection if needed.
        // on disconnecting, if automatically removed this connection from group.
        return CompletedTask;
    }
}

4.2.3. Implement code that calls API on Unity side

Add code to the SampleController that calls each SampleHub API.
Please copy and paste the code below to overwrite it.

using Grpc.Core;
using MagicOnion.Client;
using Sample.Shared.Hubs;
using Sample.Shared.MessagePackObjects;
using Sample.Shared.Services;
using UnityEngine;

public class SampleController : MonoBehaviour, ISampleHubReceiver
{
    private Channel channel;
    private ISampleService sampleService;
    private ISampleHub sampleHub;

    void Start()
    {
        this.channel = new Channel("localhost:12345", ChannelCredentials.Insecure);
        this.sampleService = MagicOnionClient.Create<ISampleService>(channel);
        this.sampleHub = StreamingHubClient.Connect<ISampleHub, ISampleHubReceiver>(this.channel, this);

        //Comment out ordinary API calls
        //There is no problem if you leave it (both work with real-time communication)
        //this.SampleServiceTest(1, 2);

        this.SampleHubTest();
    }

    async void OnDestroy()
    {
        await this.sampleHub.DisposeAsync();
        await this.channel.ShutdownAsync();
    }

    /// <summary>
    ///A method for testing normal API communication
    /// </summary>
    async void SampleServiceTest(int x, int y)
    {
        var sumReuslt = await this.sampleService.SumAsync(x, y);
        Debug.Log($"{nameof(sumReuslt)}: {sumReuslt}");

        var productResult = await this.sampleService.ProductAsync(2, 3);
        Debug.Log($"{nameof(productResult)}: {productResult}");
    }

    /// <summary>
    ///Method for testing real-time communication
    /// </summary>
    async void SampleHubTest()
    {
        //Try to create your own player information
        var player = new Player
        {
            Name = "Minami",
            Position = new Vector3(0, 0, 0),
            Rotation = new Quaternion(0, 0, 0, 0)
        };

        //Connect to the game
        await this.sampleHub.JoinAsync(player);

        //Try to speak in chat
        await this.sampleHub.SendMessageAsync("Hello!");

        //Try updating location information
        player.Position = new Vector3(1, 0, 0);
        await this.sampleHub.MovePositionAsync(player.Position);

        //Try disconnecting from the game
        await this.sampleHub.LeaveAsync();
    }

    #region A group of methods called by the server in real-time communication

    public void OnJoin(string name)
    {
        Debug.Log($"{name}Entered the room");
    }

    public void OnLeave(string name)
    {
        Debug.Log($"{name}Has left the room");
    }

    public void OnSendMessage(string name, string message)
    {
        Debug.Log($"{name}: {message}");
    }

    public void OnMovePosition(Player player)
    {
        Debug.Log($"{player.Name}Has moved: {{ x: {player.Position.x}, y: {player.Position.y}, z: {player.Position.z} }}");
    }

    #endregion
}

4.2.4. Real-time communication operation check

Start the server in the same way as checking the operation of normal API communication, and then play the Scene in Unity.

The log is displayed in the Unity Console.
image.png

Now you can check the operation of both normal API communication and real-time communication.

5. IL2CPP compatible (code generation by code generator)

If you run it on UnityEditor, it’s okay as it is, but if you use IL2CPP (for example, when Platform is iOS), you will get an error like this.

image.png

IL2CPP does not support dynamic code generation, so you must use a code generator to generate the required code in advance.

5.1. Installing MagicOnion.MSBuild.Tasks

Install MagicOnion.MSBuild.Tasks with NuGet.
Select Sample.Shared for the project and 3.0.12 for the version.
image.png

5.2. Installing MessagePack.MSBuild.Tasks

Install MessagePack.MSBuild.Tasks in the same way.
image.png

5.3. Editing Sample.Shared.csproj

Double-click Sample.Shared to open Sample.Shared.csproj.
Add the code in the red frame and save it.
Please copy the code below.

image.png

<Target Name="GenerateMessagePack" AfterTargets="Compile">
  <MessagePackGenerator Input=".\Sample.Shared.csproj" Output="..\Sample.Unity\Assets\Scripts\Generated\MessagePack.Generated.cs" />
</Target>
<Target Name="GenerateMagicOnion" AfterTargets="Compile">
  <MagicOnionGenerator Input=".\Sample.Shared.csproj" Output="..\Sample.Unity\Assets\Scripts\Generated\MagicOnion.Generated.cs" />
</Target>

If you build Sample.Server in this state, the code generator will generate the code under Sample.Unity \ Assets \ Scripts \ Generated.

image.png

5.4. Use of generated code

Next, configure the settings to use this code.
Create a C # Script in the Scripts folder and name it InitialSettings.

image.png

Copy and save the code for InitialSettings below.

using MessagePack;
using MessagePack.Resolvers;
using UnityEngine;

namespace Assets.Scripts
{
    class InitialSettings
    {
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
        static void RegisterResolvers()
        {
            // NOTE: Currently, CompositeResolver doesn't work on Unity IL2CPP build. Use StaticCompositeResolver instead of it.
            StaticCompositeResolver.Instance.Register(
                MagicOnion.Resolvers.MagicOnionResolver.Instance,
                MessagePack.Resolvers.GeneratedResolver.Instance,
                BuiltinResolver.Instance,
                PrimitiveObjectResolver.Instance,
                MessagePack.Unity.UnityResolver.Instance
            );

            MessagePackSerializer.DefaultOptions = MessagePackSerializer.DefaultOptions
                .WithResolver(StaticCompositeResolver.Instance);
        }
    }
}

Now it works in the IL2CPP environment.

6. Supports iOS builds

The following additional work is required for building for iOS.

  • Disable Bitcode
  • Add libz.tbd

Add C # Script to Assets \ Editor and name it BuildIos.
image.png

Copy and paste the BuildIos code below.

#if UNITY_IPHONE
using System.IO;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;

public class BuildIos
{
    /// <summary>
    /// Handle libgrpc project settings.
    /// </summary>
    /// <param name="target"></param>
    /// <param name="path"></param>
    [PostProcessBuild(1)]
    public static void OnPostProcessBuild(BuildTarget target, string path)
    {
        var projectPath = PBXProject.GetPBXProjectPath(path);
        var project = new PBXProject();
        project.ReadFromString(File.ReadAllText(projectPath));
        var targetGuid = project.GetUnityFrameworkTargetGuid();

        // libz.tbd for grpc ios build
        project.AddFrameworkToProject(targetGuid, "libz.tbd", false);

        // libgrpc_csharp_ext missing bitcode. as BITCODE exand binary size to 250MB.
        project.SetBuildProperty(targetGuid, "ENABLE_BITCODE", "NO");

        File.WriteAllText(projectPath, project.WriteToString());
    }
}
#endif

This is the end of environment construction. Thank you for your hard work.

7. About multi-startup projects

This is how to use the multi-startup project, which I omitted from the explanation.
Right-click on the solution and click Startup Project Settings.

image.png

Check Multi-Startup Project, set the actions of ʻAssembly-CSharp and Sample.Server to Start` and press OK.

If you press Start in this state, you can start the server while attaching the debugger to Unity.
image.png

Thank you for reading this long article to the end.
I hope it helps even a little.

If you have succeeded in building an environment and need more technical content or practical code, the following articles are recommended.

-README of MagicOnion on GitHub
-Official MagicOnion Sample
-Cygames Engineers’ Blog MagicOnion-Real-time communication framework for .NET Core / Unity by C #
-I tried to implement real-time communication with Unity + MagicOnion
-GRPC communication with Unity IL2CPP using MagicOnion v2