かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

SignalR を Windows Mixed Reality で使いたい

追記

Unity Editor 上でも動くみたいです。やったね!

tarukosu.hatenablog.com

本文

SignalR は簡単に言うと WebSocket とかをいい感じに隠してくれてサーバーからクライアント(Webやネイティブアプリなど)に対して処理を実行することが出来るライブラリです。

最近は ASP.NET Core でも使えるようになってるんですね。私が前にやってたときは ASP.NET でやってました。

こっちが ASP.NET Core で

docs.microsoft.com

こっちがクラシカルな ASP.NET のほうですね

docs.microsoft.com

便利なんです

Web 系の技術をベースに実装されたリモートプロシージャーコールのライブラリなので非常に便利なんですよね。 サーバーからクライアントに一斉に指示をばらまいたりとかなんとか。ということで Windows Mixed Reality でも使いたい!!ってことで試してみました。

よく見てないんですが ASP.NET Core SignalR って ASP.NET Core 2.1 Preview に依存してるんですかね?

docs.microsoft.com

.NET core 2.1.0 Preview 1 SDK以降
Visual Studio 2017 15.6 またはそれ以降のバージョン、 ASP.NET および web 開発ワークロード
npm

まだプレビューならとりあえず今回は古き良きほうで試してみたいと思います。

サーバーサイドの準備

ではサーバーを用意しましょう。 ASP.NET MVC の空のプロジェクトをさくっと作ります。

そして NuGet から SignalR のライブラリを追加しましょう。Microsoft.AspNet.SingalR ですね。

SingalR 2.x 系は Owin 使ってるんですかね?ということで NuGet でインストールしたら表示される readme に従って Startup クラスを作ります。

OWIN Startup クラスというテンプレートが Visual Studio にあるので、それを使ってさくっと作りましょう。 MapSignalR を呼びます(自分がしてたころは MapHub とかいうメソッドだった気がする)

using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(SignalRServer.Startup))]

namespace SignalRServer
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapSignalR();
        }
    }
}

確か、このスタートアップクラスは Microsoft.Owin.Host.SystemWeb とかが入ってないと動かなかった気がするので何か動かないなぁって人はチェックしてみましょう。

では、Hubを作りましょう。今回はクライアントに対して Cube を出せ!ってサーバーから命令するようなのを作りたいなぁと思ったのでそういうハブを1つ作りました。

using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;

namespace SignalRServer.Hubs
{
    [HubName("createCubeHub")]
    public class CreateCubeHub : Hub
    {
        public void CreateCube()
        {
            Clients.All.create("Cube");
        }
    }
}

今回は Clients.All を使ってつないでる人全員に命令を投げてます。特定のグループに対して送るとか制御もできるので実際には、グループを作って特定の人達に指示を投げるみたいなのになるんでしょうね。

あとは、この Hub をたたく Web 画面を作ります。 HomeController とかを作って View を適用に用意しましょう。 SignalR 関連の JavaScript を jquery のあとに読み込む必要があるので追加します。 とりあえず、普通に Visual Studio にお願いして各種 View を作っていたら _Layout.cshtml というのが出来てるはずなので、そこの jquery を読み込んでる src タグの下らへんに2つの script タグを追加します。

あと、スクリプトが全部読み込まれた後に自前のスクリプトを読み込むためのセクションも追加するために @RenderSection も追加しておきます。

    <script src="~/Scripts/jquery-1.10.2.min.js"></script>
    <script src="~/Scripts/bootstrap.min.js"></script>
    <!-- ここから追加するやつ -->
    <script type="text/javascript" src="~/Scripts/jquery.signalR-2.2.3.min.js"></script>
    <script src="~/signalr/hubs"></script>
    @RenderSection("script")
    <!-- ここまで追加するやつ -->

重要なのは jquery の後に signalr 系のものを読み込むことなので、_Layout.cshtml を使ってない場合には Index.cshtml とかに全部まとめて書いても OK です。

View の中で以下のような感じで SignalR の Hub の CreateCube メソッドを叩く処理を書きます。

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

<button id="createCubeButton">Create cube</button>

@section script {
    <script type="text/javascript">
        $(function () {
            $.connection.hub.start().done(function () {
                $('#createCubeButton').click(invokeCreateCube);
            });
        });

        function invokeCreateCube() {
            $.connection.createCubeHub.server.createCube();
        }
    </script>
}

これだけでサーバーの CreateCube メソッドを呼べるのはお手軽ですね。サーバーの CreateCube ではクライアントの create メソッドを叩いてるって感じのコード書いてるので、これで Web 画面からクライアントの create メソッドを叩く下準備が出来ました。

一応ここらへんで、CreateCubeHub クラスの CreateCube メソッドにブレークポイントをはって実行して画面のボタンを押すとブレークポイントで止まるかというのを確認しておくとあとがスムーズだと思います。

デプロイ!

では Windows MR デバイスからアクセスできるところ(ローカルのネットワーク内でもお手持ちのクラウドサーバー上でもなんでも)に、上記のアプリをデプロイしてきましょう。次はいよいよ Windows MR 側の話になります。

Windows MR 側

ついに Windows MR 側の世界に来ました。長かった。 Unity で適当にプロジェクトを作ります。

各種設定がめんどくさいので MRTK を入れて MRTK のメニューからプロジェクトとシーンの設定をしてしまいましょう。 注意点はインターネットにつなぐので Capability で Internet Client にチェックを入れておきます。

そして、シーンに空のゲームオブジェクトを1つ作って SignalRManager とかいう名前で作りましょう。 そして、SignalRManager とかいう名前で C# のスクリプトを作成して先ほど作成した SignalRManager ゲームオブジェクトにアタッチしておきます。

準備ができたので、UWPのプロジェクトを Unity からビルドして出力しましょう。 出力先でゴリゴリコードを書くので Unity C# Project にはチェックを入れておくのを忘れずに。

f:id:okazuki:20180426115524p:plain

出力したプロジェクトを Visual Studio で開いたら Microsoft.AspNet.SignalR.Client を NuGet から出力されたプロジェクトに対して追加します。

ここらへんは新規に Visual Studio のプロジェクト吐き出したらまたやらないといけないので、何らかの簡略化の仕組みは本番では取り入れておくといいと思います。以下のような感じで。

以下のは、私のやったゴリゴリ project.json をビルドプロセスのあとに書き換えるアプローチ。

blog.okazuki.jp

以下のは、インストールする NuGet ライブラリをインストールする PowerShell のスクリプトを用意しておいて、再作成後は一発それを叩けば OK な状態にしておくアプローチ

http://yotiky.hatenablog.com/entry/2018/04/26/HoloLensNuGetUWP向けのLibrary_を利用したい

とりあえず今回は手動で追加します。

追加したら SignalRManager.cs を以下のようにしましょう。単純につないで SignalR から何か呼ばれたらものを作るといった感じにしています。

using UnityEngine;
using System;
using System.Threading;

#if UNITY_UWP
using Microsoft.AspNet.SignalR.Client;
#endif

public class SignalRManager : MonoBehaviour
{
#if UNITY_UWP
    private HubConnection _connection;
    private SynchronizationContext _unityThreadContext;


    private void Start()
    {
        // Unity のメインスレッドの SynchronizationContext をとっておく
        _unityThreadContext = SynchronizationContext.Current;
        InitializeConnection();
    }


    private void InitializeConnection()
    {
        // SignalR を配備した先の URL (例は Azure 上の Web app に配備した場合)
        _connection = new HubConnection("http://<your site name>.azurewebsites.net/");
        var createCubeProxy = _connection.CreateHubProxy("createCubeHub");
        createCubeProxy.On<string>("create", Create);
        _connection.Start().ContinueWith(x =>
        {
            UnityEngine.Debug.Log(x.Exception?.Message ?? "Connected");
        });
    }

    private void Create(string name)
    {
        // Unity のメインスレッド上で処理しないとまずいと思うので
        _unityThreadContext.Post(_ =>
        {
            PrimitiveType type;
            if (!Enum.TryParse<PrimitiveType>(name, out type))
            {
                UnityEngine.Debug.LogWarning($"{name} is not defined.");
                return;
            }
            var obj = GameObject.CreatePrimitive(type);
            obj.transform.position = Camera.main.transform.position + Camera.main.transform.forward * 3;
            UnityEngine.Debug.Log($"{name} created at {obj.transform.position}");
        }, null);
    }
#endif
}

これで完成です。実行すると以下のようになります。

https://github.com/runceel/WinMRSignalR/blob/master/movie.gif?raw=true

まとめ

すなおに NuGet 使いたい欲が高まりますね。

ソースコードは以下に上げています。

github.com