かずきのBlog@hatena

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

HoloLens や Windows MR で NuGet のライブラリ使いたい

便利なライブラリが沢山ある NuGet ですが Unity で開発する HoloLens や Windows MR では簡単には使え無さそう? UWP のプロジェクトをビルドで吐いたところに手動で NuGet 追加すれば使えるけど、リポジトリにはビルドで出したプロジェクトは入れないからクローンするたびとかに手動で追加しないといけないよね?(認識違ってたら教えてください)

めんどくさいですね。

Unity から出力されるプロジェクトの形

とりあえずシーンを追加して、適当なスクリプトを作成してシーンのカメラあたりにアタッチしました。 この状態で UWP のプロジェクトを出力すると以下のような形のものが出てきます。

f:id:okazuki:20180413093123p:plain

project.json を使った少し前までの形のプロジェクトみたいですね。この形のプロジェクトの場合は NuGet で参照を追加すると project.json に定義が追加されます。試しに Json.NET の 9.0.4 を追加してみました。(最新のだとエラーになったので)

Assembly-CSharp プロジェクトの project.json

{
  "dependencies": {
    "Microsoft.NETCore.UniversalWindowsPlatform": "5.0.0",
    "Newtonsoft.Json": "9.0.1"
  },
  "frameworks": {
    "uap10.0.10240": {}
  },
  "runtimes": {
    "win10-arm": {},
    "win10-arm-aot": {},
    "win10-x86": {},
    "win10-x86-aot": {},
    "win10-x64": {},
    "win10-x64-aot": {}
  }
}

メインのプロジェクト(今回は NuGetApp という名前で作ったので NuGetApp プロジェクト)の project.json

{
  "dependencies": {
    "Microsoft.ApplicationInsights": "1.0.0",
    "Microsoft.ApplicationInsights.PersistenceChannel": "1.0.0",
    "Microsoft.ApplicationInsights.WindowsApps": "1.0.0",
    "Microsoft.NETCore.UniversalWindowsPlatform": "5.0.0",
    "Newtonsoft.Json": "9.0.1"
  },
  "frameworks": {
    "uap10.0.10240": {}
  },
  "runtimes": {
    "win10-arm": {},
    "win10-arm-aot": {},
    "win10-x86": {},
    "win10-x86-aot": {},
    "win10-x64": {},
    "win10-x64-aot": {}
  }
}

これで Json.NET が使えます。こんな感じで。

using Newtonsoft.Json;
using UnityEngine;

public class Sample : MonoBehaviour
{

    // Use this for initialization
    void Start()
    {
        var jsonText = JsonConvert.SerializeObject(new Person
        {
            Name = "tanaka",
        });
    }

    // Update is called once per frame
    void Update()
    {

    }
}

public class Person
{
    public string Name { get; set; }
}

問題点 1 ビルドエラーになる

この状態で、Unityから再ビルドするとエラーになります。何個かエラーが出ますが、恐らくこれが原因でしょう。

Assets/Scripts/Sample.cs(1,7): error CS0246: The type or namespace name `Newtonsoft' could not be found. Are you missing an assembly reference?

まぁ当然ですよね。Unity 側からしたらプラットフォーム固有のビルド用に出力した先で追加されたライブラリのことなんて知ったこっちゃないので。

#if ディレクティブで回避

エラー自体は #if で回避できます。UNITY_UWP で括れば OK です。

先日教えてもらったのですが、WINDOWS_UWPとか何個か定義される定数があるのですが、ものによって定義されるタイミングが違うらしいのですよね。WINDOWS_UWP だと Unity から UWP のプロジェクト出力するタイミングのビルドで評価されるらしいので UWP プロジェクトの出力段階でエラーになります。

UNITY_UWP だと大丈夫みたいです。 ということで以下のようなコードにしておけば OK です。

#if UNITY_UWP
using Newtonsoft.Json;
#endif
using UnityEngine;

public class Sample : MonoBehaviour
{

    // Use this for initialization
    void Start()
    {
#if UNITY_UWP
        var jsonText = JsonConvert.SerializeObject(new Person
        {
            Name = "tanaka",
        });
#endif
    }

    // Update is called once per frame
    void Update()
    {

    }
}

public class Person
{
    public string Name { get; set; }
}

問題点 2 再ビルド時に手動で NuGet 追加

これでなんとか開発出来そうなのですが、出力されたプロジェクトを削除して Unity から再ビルドすると当然ながら project.json が新規に作られて NuGet の定義が足りなくなるので、手動で追加しないといけません。

1 つ 2 つならいいのですが数が増えてくるとやってられませんよね。

ということで自動で追加してもらいましょう。Unity にはビルド後に任意のスクリプトを動かす方法があります。

Unity - Scripting API: PostProcessBuildAttribute

いけそうですね。ということで Editor/AddNuGetPackagePostProcess.cs を作って以下のように書きます。

using UnityEditor;
using UnityEditor.Callbacks;
using UnityEngine;

public class AddNuGetPackagePostProcessor
{
    [PostProcessBuild]

    public static void OnPostProcessBuild(BuildTarget target, string pathToBuildProject)
    {
        Debug.Log(pathToBuildProject);
    }
}

ビルドすると以下のようなログが出ます。

C:/Projects/NuGetApp/UWP
UnityEngine.Debug:Log(Object)
AddNuGetPackagePostProcessor:OnPostProcessBuild(BuildTarget, String) (at Assets/Editor/AddNuGetPackagePostProcessor.cs:11)
UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr)

いけそうですね。あとはゴリゴリ書くだけ。

project.json では Dictionary<string, string> を扱いたかったのですが JsonUtility ではサポートされてないので泣く泣く MiniJSON を使うことにしました。

Unity3D: MiniJSON Decodes and encodes simple JSON strings. Not intended for use with massive JSON strings, probably < 32k preferred. Handy for parsing JSON from inside Unity3d. · GitHub

ということで Editor フォルダに MiniJSON.cs ファイルを作って上記コードをコピペします。 そして、AddNuGetPackagePostProcessor.cs を以下のようにします。単純に指定した定義を project.json に追加してるだけです。

using MiniJSON;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEngine;

public class AddNuGetPackagePostProcessor
{
    [PostProcessBuild]

    public static void OnPostProcessBuild(BuildTarget target, string pathToBuildProject)
    {
        Debug.Log(pathToBuildProject);

        var targetProjectJsonFiles = new[]
        {
            Path.Combine(pathToBuildProject, "NuGetApp",  "project.json"),
            Path.Combine(pathToBuildProject, "GeneratedProjects", "UWP", "Assembly-CSharp", "project.json"),
        };
        var libraries = new Dictionary<string, string>
        {
            { "Newtonsoft.Json", "9.0.1" },
        };

        foreach (var projectJsonPath in targetProjectJsonFiles)
        {
            AddNuGetReference(projectJsonPath, libraries);
        }
    }

    private static void AddNuGetReference(string projectJsonPath, Dictionary<string, string> libraries)
    {
        var projectJson = Json.Deserialize(File.ReadAllText(projectJsonPath)) as Dictionary<string, object>;
        var dependencies = (Dictionary<string, object>)projectJson["dependencies"];
        foreach (var library in libraries.Where(x => !IsAlreadyDefined(dependencies, x)))
        {
            dependencies[library.Key] = library.Value;
        }

        File.WriteAllText(projectJsonPath, Json.Serialize(projectJson));
    }

    private static bool IsAlreadyDefined(Dictionary<string, object> dependencies, KeyValuePair<string, string> library)
    {
        if (!dependencies.ContainsKey(library.Key))
        {
            return false;
        }

        return ((string)dependencies[library.Key]) == library.Value;
    }
}

UWP のプロジェクトを一旦消して Unity から再出力すると以下のように project.json のインデントが死んだ状態ですが Json.NET の定義が追加されてることがわかります。

AddNuGetApp プロジェクト

{"dependencies":{"Microsoft.ApplicationInsights":"1.0.0","Microsoft.ApplicationInsights.PersistenceChannel":"1.0.0","Microsoft.ApplicationInsights.WindowsApps":"1.0.0","Microsoft.NETCore.UniversalWindowsPlatform":"5.0.0","Newtonsoft.Json":"9.0.1"},"frameworks":{"uap10.0":{}},"runtimes":{"win10-arm":{},"win10-arm-aot":{},"win10-x86":{},"win10-x86-aot":{},"win10-x64":{},"win10-x64-aot":{}}}

Assebmly-CSharp プロジェクト

{"dependencies":{"Microsoft.NETCore.UniversalWindowsPlatform":"5.0.0","Newtonsoft.Json":"9.0.1"},"frameworks":{"uap10.0":{}},"runtimes":{"win10-arm":{},"win10-arm-aot":{},"win10-x86":{},"win10-x86-aot":{},"win10-x64":{},"win10-x64-aot":{}}}

確認

生成されたソリューションを Visual Studio で開いてビルドすると参照のところに、ちゃんと Json.NET が追加されていることが確認できます。

f:id:okazuki:20180413113612p:plain

f:id:okazuki:20180413113624p:plain

まとめ

ということでまとめです。

  • NuGet から入手したライブラリに関する処理は #if UNITY_UWP ~ #endif で括ろう
  • NuGet パッケージの手動追加がメンドクサイ場合は PostProcessBuild で project.json を書き換えよう

ちなみに、多分ですが近い将来きっと Unity が出力するプロジェクトは project.json の存在しない形になると思うので、そのタイミングでこの PostBuildProcess は使えなくなります。 その時は、csproj ファイルに NuGet のリファレンスが書き足される形になると思うので、同じ要領で PostProcessBuild で csproj を書き換えてしまいましょう。