テラシュールブログ

旧テラシュールウェアブログUnity記事。主にUnityのTipsやAR・VR、ニコニコ動画についてのメモを残します。

【Unity】NavMeshのデータを実行時にロードしたい

NavMeshの地形データをランタイムにロードする

 Sceneに紐付いているNavMeshではなく、動的にNavMeshの経路探索に使用するデータをロード・アンロードする方法について考えてみます。

 主な用途としては以下の2つです。自分の場合は前者に当たりました。

  • SceneとnavMeshを紐付けたくない場合
  • 地形が変化するが動的なNavMesh計算はしたくない場合

NavMeshをベイクする

 まずSceneに配置したメッシュの情報を利用してNavMeshデータを構築します。

 もしNavMeshComponentsを導入している場合、NavMeshSurfaceをシーン内に配置しBakeボタンを押します。これでSceneファイル以下にNavMeshDataが構築されます。これにNavMeshのデータが格納されています。

f:id:tsubaki_t1:20191103232738j:plain
NavmeshSurfaceを使用する場合

 一方、NavMeshComponentを使用しない場合、エディターAPIを使ってNavMeshを構築します。使用するAPINavMeshBuilder.CollectSourcesInStageNavMeshBuilder.BuildNavMeshDataです。今回の場合、動的にロードすることが目的なのでResourcesに突っ込んでいます。実際にはAddressableなりAssetBundleなりに格納する形になると思います。

  1. 下のコードを導入
  2. NavMeshを作りたいステージの親オブジェクトにBuildNavMeshを追加
  3. コンテキストメニューからCreateNavmeshを選択
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class BuildNavMesh : MonoBehaviour
{
#if UNITY_EDITOR

    [ContextMenu("CreateNavmesh")]
    void Create()
    {
        var sources = new List<NavMeshBuildSource>();
        var markups = new List<NavMeshBuildMarkup>();
        var settings = NavMesh.GetSettingsByID(0);
        var bounds = new Bounds(Vector3.zero, 1000.0f * Vector3.one);

        // Navmeshをビルドする対象を収集する
        UnityEditor.AI.NavMeshBuilder.CollectSourcesInStage(
            root: transform,                                // nullの場合Scene全体が含まれます。transformを指定した場合はルートとその子のみを考慮します。
            includedLayerMask: ~0,                          // クェリに含めるレイヤー
            geometry: NavMeshCollectGeometry.RenderMeshes,  // 収集するジオメトリを選択します。レンダラーかコライダー
            defaultArea: 0,                                 // 割り当てるエリアタイプ
            markups: markups,                               // 収集方法についてのマークアップリスト(含めないエリアとか色々)
            stageProxy: gameObject.scene,                   // 所属するシーン
            results: sources);                              // ベイクに使用するジオメトリのリスト(out)

        // 実際にビルドする
        var navmesh = NavMeshBuilder.BuildNavMeshData(
            buildSettings: settings,                    // ベイク処理の設定
            sources: sources,                           // ベイクに使用するジオメトリのリスト
            localBounds: bounds,                        // NavMeshを構築する範囲
            position: transform.position,               // NavMeshの原点
            rotation: transform.rotation);              // NavMeshの向き


        var exportPath = $"Assets/Resources/{gameObject.name}.asset";
        UnityEditor.AssetDatabase.CreateAsset(navmesh, exportPath);
        UnityEditor.AssetDatabase.Refresh();
    }

#endif

}

f:id:tsubaki_t1:20191104000122j:plain
コンテキストメニューを利用

f:id:tsubaki_t1:20191103235908j:plain
NavMeshDataが出力できた

NavMeshをロードする

 最後にNavMeshDataを現在のシーンに追加します。NavMeshの追加自体はNavMesh.AddNavMeshData(NavMeshData)でOKです。この時に取得するハンドルは後でNavMeshDataを破棄する際に使用されるので大切に保存しましょう。

using UnityEngine;
using UnityEngine.AI;

public class LoadNavmesh : MonoBehaviour
{
    [SerializeField] string assetname = "GameObject";

    private NavMeshDataInstance instance;

    void OnEnable()
    {
        // NavMeshの登録
        var data = Resources.Load<NavMeshData>(assetname);
        instance = NavMesh.AddNavMeshData(data);
    }

    void OnDisable()
    {
        // NavMeshの破棄
        NavMesh.RemoveNavMeshData(instance);
    }
}

f:id:tsubaki_t1:20191104000530j:plain
動的にNavMeshを追加できた

補足

 NavMeshの動的なロードを行う場合、NavMeshのロードが完了したかを判断するフラグを必ず設定しておくと良いです。NavMeshDataのロード前にNavMesh関連の処理が走るとエラーになります。

【Unity】AssetBundleが使用しているアセット一覧を渡してlink.xmlを生成する

問題:AssetBundleからセットを取得する時にCould not produce class with ID XXXが出る

 AssetBundleを使用している場合、AssetBundleでしか使用していないアセットのコードがビルドからストリップ(削除)されてしまう事があります。特にIL2CPPを利用環境ではよく起こる現象で、iOSAndroidといったプラットフォームを本番向けにビルドすると起こるかもしれません。

 この挙動は特にAssemblyDefenitionFile等でアセンブリが分かれている時に顕著に起こります。ポジティブに言えばAssemblyDefenitionFileを分割すればビルド速度やサイズを削減できると言えますが、悪く言えば非常に簡単にコードがストリップされます。特にUnity 2018から多くのUnity標準機能がpackageに移行しAssemblyDefenitionFileで分割されているので、割と簡単にコードストリップによる動作不全を起こすかもしれません。例えば2D系機能やTimeline等もそれに該当します。

 例えば下の画像では「スプライト画像を表示するTimelineを含むPrefab」をAssetBundleに格納し実行時に生成するというコードを実行した結果です。「Could not produce class with ID 212 (SpriteRendererのコードが無いよ!)」のエラーが発生しています。実はコレを解決しても次にTImelineについてのエラーが出ます。

f:id:tsubaki_t1:20191102202407j:plain
AssetBundleでしか使用していないコンポーネントを使用するとよく見る風景

解決方法:コードがストリップされないようにする

 この解決法は大雑把に3つです。

  1. link.xmlホワイトリストを記述する
  2. ストリップされないコード(AssemblyDefenitionFile等で分割されていないコード)からクラスを参照する
  3. ResourcesやScene等から指定のコンポーネントやアセットを参照する

 正規ルート1です。本来はこちらでやるべきです。 とは言え、正直なところlink.xmlを記述するのは面倒くさいです。ゲームをプレイしてエラーを探す(≒クラッシュするまで色々と試す)は完全に理にかなっていないです。

f:id:tsubaki_t1:20191102204518j:plain
ゲームが使用しているコンポーネントホワイトリスト一覧に記述

 なのでは「他のスクリプトや標準で含まれるアセットから参照されているので消されない」という考えを使用して、とりあえずゲームが使用しているPrefab(データを抜いた物)をResourcesやゲームに含めるSceneにでも放り込むという力技も考えられます。面倒なことに頭を悩ませたくない場合の力技として有効な一手です。ただ「気づいたら含まれている」ケースが多いので、link.xmlで記述するのが正規ルートでしょう。

f:id:tsubaki_t1:20191102204416j:plain
Resourcesやゲームに含めるSceneに入れるでもホワイトリスト入りはする

解決手順:link.xmlホワイトリスト)を自動生成する

 link.xmlを自動生成する方法を考えてみます。内容は単純で「AssetBundleに使用しているアセットが使用しているコンポーネント、及び、AssetBundleに格納したデータをlink.xmlに格納する」というものです。ルールは下の通りなので上辺のコンポーネントとデータを網羅しておけば、概ね問題ないという判断です。

  1. link.xmlに指定されているコンポーネントはストリップ対象にならない
  2. ストリップ対象ではないコンポーネントが使用しているクラスはストリップの対象にならない

手順1:LinkXmlGeneratorの導入

まず下のLinkXmlGeneratorを適当なEditorフォルダ以下に配置します。

AssetBundleNameLinkXMLGenerator.cs · GitHub

手順2:link.xmlを生成

 LinkXmlGeneratorでホワイトリストを作ります。覚えておくべきAPIは2つです。 AddAssetsにAssetBundleが使用しているアセットを全部登録して、Saveを実行。あとはlink.xmlをマージするなり、普通にAsset直下に配置すればOKです。

 AssetBundlenameを使用せずAssetBundleを生成している場合、このAPIでアセットを登録する感じです。

// アセットが使用しているクラスをホワイトリストに登録する
LinkXmlGenerator.AddAssets(string[] assetPaths);

// link.xmlをpathに書き出す
LinkXmlGenerator.Save(string path);

 逆に、AssetBundle nameを使用している場合は下のコードで行けます。
 導入後、メニュー>Assets>CreateLink.xmlでファイルを作ってくれます。

using UnityEditor;
using UnityEngine;

public class AssetBundleChecker 
{
    [MenuItem("Assets/CreateLink.xml")]
    static void Create()
    {
        var generator = new LinkXmlGenerator();
        // UnityEditor.Animations.AnimatorControllerを取得してしまうので、RuntimeAnimatorControllerに変更
        generator.SetTypeConversion(typeof(UnityEditor.Animations.AnimatorController), typeof(RuntimeAnimatorController));

        // AssetBundleNameを持つアセットを全て取得して、LinkXmlGeneratorに登録する。
        foreach( var bundleName in AssetDatabase.GetAllAssetBundleNames())
        {
            var assetPaths = AssetDatabase.GetAssetPathsFromAssetBundle(bundleName);
            generator.AddAssets(assetPaths);
        }

        // link.xmlファイルを保存
        generator.Save("Assets/link.xml");
        AssetDatabase.Refresh();
    }
}

f:id:tsubaki_t1:20191102204148j:plain

補足:Addressableの場合は自動で解決してくれる

 Addressableの場合は自動でlink.xmlを生成してくれます。

tsubakit1.hateblo.jp

 なお以前はlink.xmlをStreamingAssetsに生成して使用していましたが、現在はプロジェクトファイル\Library\com.unity.addressables\StreamingAssetsCopy\aa\プラットフォーム以下に格納されており、これをゲームビルド時にコピーすることで実現しています。

f:id:tsubaki_t1:20191102204217j:plain

関連

 AssetBundleManifestから使用するコンポーネントを確認するアプローチを取りたい場合は下の記事。コンポーネントが使用しているAssemblyファイルを直接探しに行くスタイルです。

tsubakit1.hateblo.jp

【Unity】Genericなクラスをシリアライズで来た

ジェネリックなクラスをシリアライズ

 Unity 2020.1aを試している時に気づいたのですが、どうやらジェネリックなクラスをシリアライズ可能になったみたいです。記憶だとUnity 2019.3だったような気がしましたが、改めて手元で確認するとUnity 2020.1ですので、多分勘違いです。

 今までのUnityではジェネリックなクラスを直接シリアライズすることは出来ませんでした。そのため、一旦ジェネリックなクラスを継承したクラスを定義するという面倒な実装が必要でした。例えば任意の引数を要求するUnityEventを実装したい場合は下のような実装が必要でした。

// 適当なジェネリックなクラス
[Serializable]
public class MyGenericData<T>
{
    public T value;
}
// ジェネリックなクラスを使用する場合、事前に型を定義しておく必要がある
[System.Serializable]
public class UnitySerializeEvent<T> : UnityEvent<T> { }

[System.Serializable]
public class Vector3Data : MyGenericData<Vector3> { }


// 実際にコンポーネントで使用している例
public class MyComponent : MonoBehaviour
{
    [SerializeField] GameObjectEvent gameObjectEv;
    [SerializeField] Vector3Data mydata;
 
    //  コレは使えない
    [SerializeField] MyGenericData<Vector3> vector3Value;
}

f:id:tsubaki_t1:20191101223927j:plain
シリアライズされた値

Unity 2020.1でシリアライズ出来た

 さて、この制限ですがUnity 2020.1で試した所、なんとジェネリックなクラスを継承したクラスを経由せず、直接定義してもシリアライズ出来ました。まぁDictionaryQueueといったクラスは無理でしたが。またシリアライズ出来なかったUnityEventも一度Serializableを設定するクラスを経由すれば使用可能でした。超面倒くさいAssetReferenceTも大本にSerializableを付ければOK。

public class MyComponent : MonoBehaviour
{
    // ジェネリックなクラスを継承したクラスをそのまま定義
    [SerializeField] MyGenericData<Vector3> vector3Value;

    [SerializeField] MyGenericData<string> strValue;

    [SerializeField] MyGenericData<GameObject> objValue;

    [SerializeField] AssetReferenceT<GameObject> assetRefValue;

    [SerializeField] UnitySerializeEvent<GameObject> objEv;
}

// UnityEventにはSerializableが無かったので追加
[System.Serializable]
public class UnitySerializeEvent<T> : UnityEvent<T> { }

f:id:tsubaki_t1:20191101224409j:plain
コンポーネントに直接定義しても行けた

Dictionaryには使えなかった

 これDictionaryにも使えるかなと思いましたが、使えませんでした。

 単純に使うとシリアライズ出来ず、Serializable属性を付けるとInspectorには表示出来ますが値を保持出来ませんでした。

public class MyComponent : MonoBehaviour
{
    [SerializeField] Dictionary<int, string> dic1;

    [SerializeField] MyDicM<int, string> dic2;

    void Reset()
    {
        dic2 = new MyDicM<int, string>();
        dic2.Add(0, "message1");
        dic2.Add(1, "message2");
        dic2.Add(2, "message3");
    }

    void Start() => Debug.Log(dic2.Count); // Reset後なら3が帰るが、シーンを読み直すと0になる
}
[System.Serializable]
public class MyDicM<TKey, TValue> : Dictionary<TKey, TValue> { }

f:id:tsubaki_t1:20191101225643j:plain
Dictionaryの内容が格納されていないか、表示されない

感想

 これでコールバックを用意する度にUnityEventの種類を増やし続ける作業に終止符が…?

 なお検証はアルファ版を使用した為、仕様が変わる可能性があります。

【Unity】SerializeReference、Inspectorウィンドウでinterfaceを使用する

f:id:tsubaki_t1:20191031221044j:plain

 Unity 2019.3で利用可能になった SerializeReference Attributeについて紹介します。

SerializeReference

 SerializeReferenceで任意のInterfaceを継承したクラスや構造体をコンポーネントに格納し、SceneやPrefabにシリアライズしたり、Inspectorで編集したりします。

 正しくは、値としてではなく参照型としてC#シリアライズ出来るようになります。これによりinterfaceをスロットとして任意のクラスを格納することが可能になるみたいです。

interfaceをシリアライズ

 まずはinterfaceとクラスを定義します。interfaceはアクションを行う役割を持っており、処理の進行待機としてコルーチンを使用する予定です。

public interface ITankAction
{
    IEnumerator Process(Transform transform);
}

 処理の中身を実装します。前方(上)に移動する処理で、publicとして処理時間と移動距離が含まれます。

// 前方に移動するアクション
[Serializable]
public class MoveForward : ITankAction
{
    public float time = 1;        // 処理時間
    public float distance = 1;  // 移動距離

    IEnumerator ITankAction.Process(Transform transform)
    {
        float endTime = Time.timeSinceLevelLoad + time;
        Vector3 startPos = transform.position;
        Vector3 endPos = startPos + transform.up * distance;

        while (endTime > Time.timeSinceLevelLoad)
        {
            var diff = (endTime - Time.timeSinceLevelLoad) / time;
            transform.position = Vector3.Lerp(endPos, startPos, diff);
            yield return null;
        }
        transform.position = endPos;
    }
}

 これをコンポーネントで使用してみます。コンポーネントITankAction tankActionと定義し[SerializeReference]Attributeを追加します。またtankActionにはMoveRotateクラスをnewしておきます。MoveForwardクラスはUnityEngine.ObjectMonoBehaviourも継承していないので、そのままnew で構築が可能です。これでTankControllerにはITankActionを継承した何らかのクラスを追加し、それをInspectorで編集出来るようになりました。

public class TankController : MonoBehaviour
{
    [SerializeReference]
    ITankAction tankAction = new MoveRotate();

    IEnumerator Start()
    {
        yield return StartCoroutine( tankAction.Process(transform));
    }
}

f:id:tsubaki_t1:20191031222058j:plain
登録したクラスの中身を編集できる

複数のアクションがある場合にアクションを差し替えられるようにする

 この ITankAction tankAction = new MoveRotate(); ですがシリアライズされてしまうのでコードの変更を無視されるようになります。もし異なる型を注入したい場合、拡張を実装する必要があります。下は複数のアクションがある場合の例です。

 まず、移動・回転・待機のアクションをつけてみました。

[Serializable]
public class MoveForward : ITankAction
{
    public float time = 1;        // 処理時間
    public float distance = 1;  // 移動距離

    IEnumerator ITankAction.Process(Transform transform)
    {
        float endTime = Time.timeSinceLevelLoad + time;
        Vector3 startPos = transform.position;
        Vector3 endPos = startPos + transform.up * distance;

        while (endTime > Time.timeSinceLevelLoad)
        {
            transform.position = Vector3.Lerp(endPos, startPos,  (endTime - Time.timeSinceLevelLoad) / time);
            yield return null;
        }
        transform.position = endPos;
    }
}

[Serializable]
public class MoveRotate : ITankAction
{
    public float time = 1;
    public float angle = 90;

    IEnumerator ITankAction.Process(Transform transform)
    {
        float endTime = Time.timeSinceLevelLoad + time;
        Quaternion startRot = transform.rotation;
        Quaternion endRot = startRot * Quaternion.AngleAxis(angle, -Vector3.forward);

        while (endTime > Time.timeSinceLevelLoad)
        {
            var diff = (endTime - Time.timeSinceLevelLoad) / time;
            transform.rotation = Quaternion.Lerp(endRot, startRot, diff);
            yield return null;
        }
        transform.rotation = endRot;
    }
}

[Serializable]
public class WaitTank : ITankAction
{
    public float sec = 1;
    public IEnumerator Process(Transform transform)
    {
        yield return new WaitForSeconds(sec);
    }
}

 次に使用する側です。こちらは拡張コードを実装しておきます。アクションの注入は「ContextMenu」で行っています。アクションを切り替えるとTankControllerのアクションが利用できるパラメーターが変化していることを確認出来ます。

public class TankController : MonoBehaviour
{
    [SerializeReference]
    ITankAction tankAction = new MoveRotate();

    IEnumerator Start()
    {
        yield return StartCoroutine( tankAction.Process(transform));
    }

    [ContextMenu("Act/Move")] void AddMove() => tankAction = new MoveForward();
    [ContextMenu("Act/Rotate")] void AddRotate() => tankAction = new MoveRotate();
    [ContextMenu("Act/Wait")] void AddWait() => tankAction = new WaitTank();
}

f:id:tsubaki_t1:20191031223523g:plain
モードを切り替わる度に、アクションに使用するプロパティが切り替わる

アクションを複数登録する

 最後にアクションを複数登録できる形にしてみます。アクションの中身は同じですが、TankControllerを少し修正して規定の動きを行うようにする感じです。やっていることはITankActionをリストに変更しているだけです。これで同じインターフェースを継承したクラスを複数登録することが出来ます。

public class TankController : MonoBehaviour
{
    [SerializeReference]
    List<ITankAction> tankActions = new List<ITankAction>();

    IEnumerator Start()
    {
        foreach( var act in tankActions)
            yield return StartCoroutine(act.Process(transform));
    }

    [ContextMenu("Act/Move")] void AddMove() => tankActions.Add(new MoveForward());
    [ContextMenu("Act/Rotate")] void AddRotate() => tankActions.Add(new MoveRotate());
    [ContextMenu("Act/Wait")] void AddWait() => tankActions.Add(new WaitTank());
}

f:id:tsubaki_t1:20191031224701g:plain
アクションを登録する作業風景

f:id:tsubaki_t1:20191031224826g:plain
アクションでタンクを動かす

感想

 正直、任意のinterfaceを継承したMonoBehaviourもしくはScriptableObjectを登録出来るオブジェクトフィールドをInspectorに表示する機能と思ってました。

 今まではアクションを行う処理をSceneもしくはprefabにシリアライズする場合、UnityEngine.Objectを継承しなければいけなかった(つまり複数の継承が出来ない)のが、インターフェース単位デシリアライズ出来るようになったので楽出来る所もあるかなという印象です。ほしかった機能とは違いますが、まぁコレはこれで。

関連

qiita.com