かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

HoloLens で Managed Plugin を作ってみよう。そしてifディレクティブからの脱却。

要はUWP(HoloLens)で動くときはUWP用のDLLを使って、Editorで動くときはEditor用のDLL使おうぜってことみたいです。

作り方の説明は以下のサイトがとても参考になります。

satoshi-maemoto.hatenablog.com

プラグインを実機デバッグしたかったらこうするみたい

blog.d-yama7.com

やってみよう!

ということで VS2017 で .NET Framework 3.5 をターゲットにしたクラスライブラリプロジェクトと UWP 用のクラスライブラリプロジェクトを作ります。ここに同じインターフェースを持ったクラスを別々に作ればいいってことですね。

とりあえずプロジェクトのプロパティで生成されるアセンブリ名と規定の名前空間は同じに合わせておきました。

f:id:okazuki:20170817201626p:plain

ここに同じインターフェースを持つけど処理(というほどのことでもない)の違うクラスを作っていきます。

まずはUnityEditorで動くほう

namespace HelloWorldPlugin
{
    public class MessageProvider
    {
        public string CreateMessage(string name)
        {
            return string.Format("Hello {0}. UnityEditor version.", name);
        }
    }
}

次にHoloLensで動くほう

namespace HelloWorldPlugin
{
    public class MessageProvider
    {
        public string CreateMessage(string name) => $"Hello {name}. HoloLens version.";
    }
}

こんな簡単な処理でもレガシーなものとそうじゃないものの差が…。

Unity のプロジェクトを作って Plugins フォルダにコピーします(HoloLens用のはPlugins/WSAに)。私は手動コピーがだるかったのでReleaseビルドしたら、Unityのプロジェクトの該当フォルダに出力するように出力フォルダを変えました。

そして、Select platforms for pluginとPlatform settingsをちゃちゃっと設定します。

UnityEditor用のプラグイン(dll)に対する設定

f:id:okazuki:20170817204730p:plain

HoloLens用のプラグイン(dll)に対する設定

f:id:okazuki:20170817202820p:plain

プロジェクトの設定

HoloToolkit を入れてHoloToolkitのメニューからProject Settingします。

そして、Main Cameraを葬ってHoloToolkitからHoloLensCamera.prefabとInputManager.prefabとInteractiveMeshCursor.prefabをシーンに追加します。

動作確認コードの作成

3DTextPrefab.prefabを追加してX:0, Y:0, Z:2くらいに置きます。 C# スクリプトを PluginTestBehavior という名前で追加して 3DTextPrefab に張り付けましょう。

UWP のプロジェクトをビルド(C# Projectも出すように設定してね)で吐き出します。

プロジェクトを開いてターゲットプラットフォームをAny(なぜこれがデフォルトなのか)からx86にしましょう。 そして、PluginTestBehaviorを以下のように編集します。

using HelloWorldPlugin;
using UnityEngine;

public class PluginTestBehavior : MonoBehaviour
{
    private void Start()
    {
        var plugin = new MessageProvider();
        this.gameObject.GetComponent<TextMesh>().text = plugin.CreateMessage("okazuki");
    }
}

実行して動作確認

UnityEditor で実行してみましょう。こういう結果になるはずです。

f:id:okazuki:20170817205412p:plain

ちゃんとUnityEditorのが動いてますね。次はVisual StudioからHoloLens(私はエミュレータ)で実行してみましょう。

f:id:okazuki:20170817205826p:plain

ちゃんと HoloLens のものが動いてますね。

めんどい

同じようなコードを2つ書くのってだるいですよね。それにインターフェースに乖離が発生する可能性が高いです。 ということで少しでも軽減する方法を

共有プロジェクト

ソースコードを共有してしまおうという考えです。 ソリューションに共有プロジェクトを追加します。(HelloWorldPlugin.Sharedみたいな名前で)

そして、プロジェクトの参照設定から作成した共有プロジェクトをUnityEditor用プロジェクトとHoloLens用プロジェクトに追加します。

共有プロジェクトに、以下のようにコードを書きます。#if ディレクティブで UWP のとき(NETFX_CORE)とそうじゃないときでコードを分岐してます。

namespace HelloWorldPlugin
{
    public class MessageProvider
    {
        public string CreateMessage(string name)
        {
#if NETFX_CORE
            return $"Hello {name}. HoloLens version.";
#else
            return string.Format("Hello {0}. UnityEditor version.", name);
#endif
        }
    }
}

個別のプロジェクトにあったMessageProvider.csはさくっと消します。

これでビルドすればOKですね。ソースコードが1つになったよやったね!エディターの左上でプロジェクトを切り替えると、そのプロジェクトの条件でコンパイルされるときのコードだけが色付きで表示されて、そうじゃないやつはコメントみたいな色になるのでわかりやすいね!

この方法の嫌なところ

if 地獄はコードが見づらい。

ということで、ほかの方法も考えてみました。

partial class

C# には1つのクラスを複数ファイルで定義する方法が提供されています。 もともと自動生成コードと手で書くクラスを分離するためとかに用意されたやつですね。 これを使うとこうなります。

共有プロジェクト

namespace HelloWorldPlugin
{
    public partial class MessageProvider
    {
        public string CreateMessage(string name)
        {
            return this.CreateMessageCore(name);
        }
    }
}

UnityEditor用プロジェクト

namespace HelloWorldPlugin
{
    public partial class MessageProvider
    {
        private string CreateMessageCore(string name)
        {
            return string.Format("Hello {0}. UnityEditor version.", name);
        }
    }
}

HoloLens用プロジェクト

namespace HelloWorldPlugin
{
    public partial class MessageProvider
    {
        private string CreateMessageCore(string name) => $"Hello {name}. HoloLens version.";
    }
}

感想

一部の実装が違うときはこれでもいいかも。共有部分では、C#の最新機能使えないのがちょっと悲しいけど。

interface だけ共有

実装内容がそもそも UnityEditor と UWP でまるっきり違うときは、インターフェースの乖離がおきないように interface だけ共有プロジェクトで定義して、個別のプロジェクトでそれを実装するというアプローチもいいと思います。

共有プロジェクト

namespace HelloWorldPlugin
{
    public interface IMessageProvider
    {
        string CreateMessage(string name);
    }
}

UnityEditor用プロジェクト

namespace HelloWorldPlugin
{
    public class MessageProvider : IMessageProvider
    {
        public string CreateMessage(string name)
        {
            return string.Format("Hello {0}. UnityEditor version.", name);
        }
    }
}

HoloLens用プロジェクト

namespace HelloWorldPlugin
{
    public class MessageProvider : IMessageProvider
    {
        public string CreateMessage(string name) => $"Hello {name}. HoloLens version.";
    }
}

感想

UnityEditor 用はダミーデータを返すだけで、HoloLens用に本番ロジックをがっつり詰め込むときはこっちのほうが綺麗な気がしますね。