MagicOnionで機能を追加する時の流れ

はじめに

送受信に使うカスタムオブジェクトを定義

自分で定義したクラスを送受信で使いたい場合は次のようにし、共有用のディレクトリに入れる。

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

API通信の場合

全体の流れは以下の通り

  1. インターフェースを定義(共有ディレクトリ)
  2. サーバー側でインターフェースを実装する
  3. クライアント側から呼び出す

インターフェースを定義(共有ディレクトリ)

実装したいメソッドを持つインターフェースを定義する。 その定義が書かれたスクリプトは、サーバーと共有するディレクトリ内に入れる

using Grpc.Core;
using MagicOnion;
using MagicOnion.Server;
using System;

public interface IMyFirstService : IService<IMyFirstService>
{
    // Return type must be `UnaryResult<T>` or `Task<UnaryResult<T>>`.
    // If you can use C# 7.0 or newer, recommend to use `UnaryResult<T>`.
    UnaryResult<int> SumAsync(int x, int y);
}

インターフェースを実装する(サーバー)

定義したインターフェースを実装し、そのスクリプトをサーバー側に置く。

using Grpc.Core;
using MagicOnion;
using MagicOnion.Server;
using System;

// implement RPC service to Server Project.
// inehrit ServiceBase<interface>, interface
public class MyFirstService : ServiceBase<IMyFirstService>, IMyFirstService
{
    // You can use async syntax directly.
    public async UnaryResult<int> SumAsync(int x, int y)
    {
        Logger.Debug($"Received:{x}, {y}");

        return x + y;
    }
}

Unity側で呼び出す(クライアント)

// standard gRPC channel
var channel = new Channel("localhost", 12345, ChannelCredentials.Insecure);

// get MagicOnion dynamic client proxy
var client = MagicOnionClient.Create<IMyFirstService>(channel);

// call method.
var result = await client.SumAsync(100, 200);
Console.WriteLine("Client Received:" + result);

(注:メソッドを実行する時はawaitを必ずつける)

リアルタイム通信の場合

全体の流れは以下の通り

  1. クライアント側のインターフェース(Receiver)を作成
  2. サーバー側のインターフェース(Hub)を作成
  3. サーバー側(Hub)を実装
  4. クライアント側(Receiver)を実装

インターフェースを定義

インターフェースを定義し、共有ディレクトリに入れる。

サーバーが呼び出す、クライアント(Receiver)の定義

public interface ISampleHubReceiver
{
    /// <summary>
    /// 誰かがゲームに接続したことをクライアントに伝える
    /// </summary>
    void OnJoin(string name);
    /// <summary>
    /// 誰かがゲームから切断したことをクライアントに伝える
    /// </summary>
    void OnLeave(string name);
    /// <summary>
    /// 誰かが発言した事をクライアントに伝える
    /// </summary>
    void OnSendMessage(string name, string message);
    /// <summary>
    /// 誰かが移動した事をクライアントに伝える
    /// </summary>
    void OnMovePosition(Player player);
}

クライアントが呼び出す、サーバー(Hub)の定義

using MagicOnion;
using System.Threading.Tasks;
using UnityEngine;

public interface ISampleHub : IStreamingHub<ISampleHub, ISampleHubReceiver>
{
    /// <summary>
    /// ゲームに接続することをサーバに伝える
    /// </summary>
    Task JoinAsync(Player player);
    /// <summary>
    /// ゲームから切断することをサーバに伝える
    /// </summary>
    Task LeaveAsync();
    /// <summary>
    /// メッセージをサーバに伝える
    /// </summary>
    Task SendMessageAsync(string message);
    /// <summary>
    /// 移動したことをサーバに伝える
    /// </summary>
    Task MovePositionAsync(Vector3 position);
}

サーバー側(Hub)の実装

using MagicOnion.Server.Hubs;
using System.Threading.Tasks;
using UnityEngine;

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

    public async Task JoinAsync(Player player)
    {
        //ルームは全員固定
        const string roomName = "SampleRoom";
        //ルームに参加&ルームを保持
        this.room = await this.Group.AddAsync(roomName);
        //自分の情報も保持
        me = player;
        //参加したことをルームに参加している全メンバーに通知
        this.Broadcast(room).OnJoin(me.Name);
    }

    public async Task LeaveAsync()
    {
        //ルーム内のメンバーから自分を削除
        await room.RemoveAsync(this.Context);
        //退室したことを全メンバーに通知
        this.Broadcast(room).OnLeave(me.Name);
    }

    public async Task SendMessageAsync(string message)
    {
        //発言した内容を全メンバーに通知
        this.Broadcast(room).OnSendMessage(me.Name, message);
    }

    public async Task MovePositionAsync(Vector3 position)
    {
        // サーバー上の情報を更新
        me.Position = position;

        //更新したプレイヤーの情報を全メンバーに通知
        this.Broadcast(room).OnMovePosition(me);
    }

    protected override ValueTask OnDisconnected()
    {
        //nop
        return CompletedTask;
    }
}

クライアント側(Receiver)の実装

using Grpc.Core;
using MagicOnion.Client;
using UnityEngine;

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

    private async void Start()
    {
        // gRPCのchannelを作成する
        // Insecure の場合はポート番号80がデフォルト
        // Secure の場合はポート番号443がデフォルト
        this.channel = new Channel("localhost:12345", ChannelCredentials.Insecure);

        // 使用するgRPC channelと、サーバーからの一斉呼び出し(Broadcast)を受けるReceiverを指定し、
        // サーバー側で定義したメソッドを呼び出すための Hub を生成する。
        this.sampleHub = StreamingHubClient.Connect<ISampleHub, ISampleHubReceiver>(this.channel, this);

        // 自分のプレイヤー情報を作ってみる
        var player = new Player
        {
            Name = "Sakai",
            Position = new Vector3(0, 0, 0),
            Rotation = new Quaternion(0, 0, 0, 0)
        };

        // ゲームに接続する
        await this.sampleHub.JoinAsync(player);

        // チャットで発言してみる
        await this.sampleHub.SendMessageAsync("こんにちは!");

        // 位置情報を更新してみる
        player.Position = new Vector3(1, 0, 0);
        await this.sampleHub.MovePositionAsync(player.Position);

        // ゲームから切断してみる
        await this.sampleHub.LeaveAsync();
    }

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

    public void OnJoin(string name)
    {
        Debug.Log($"{name}さんが入室しました");
    }

    public void OnLeave(string name)
    {
        Debug.Log($"{name}さんが退室しました");
    }

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

    public void OnMovePosition(Player player)
    {
        Debug.Log($"{player.Name}さんが移動しました: {{ x: {player.Position.x}, y: {player.Position.y}, z: {player.Position.z} }}");
    }
}

参考