テラシュールブログ

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

【Unity】Profilerの表示フレーム数を300から2000に増やす

f:id:tsubaki_t1:20191105000141j:plain

プロファイラーでキャプチャーするには、5秒以内に問題を捉える力が必要…

 UnityのProfilerが保存できるフレーム数は300に限定されていました。これは60FPSで動作するゲームでは約5秒に相当します。つまりProfilerで問題を確認する場合、フレームの問題が起こっている所の5秒をピンポイントでレコードする必要があるという事です。これは中々に難易度が高いです。

 さらにVRのように120FPSで動作する場合、なんと驚きの2.5秒。この状態で処理落ちの瞬間をプロファイラーにキャプチャーするには、ザ・ワールドのようなスタンド能力者が一人必要です。

f:id:tsubaki_t1:20191104235501j:plain
プロファイラーを使うには、問題発見の5秒以内にプロファイラーを止める作業が必要

プロファイラのキャプチャーする範囲を2000フレームに伸ばす

 Unity 2019.3でプロファイラーがキャプチャー出来る時間を2000フレームに伸ばすことが出来るようになりました。60FPSで言えば33.3秒。これなら余裕でしょう。

 やり方は簡単、PreferenceのAnalyticsの項目でフレーム数を300から2000に伸ばすだけです。

f:id:tsubaki_t1:20191105000249j:plain
プロファイラーのフレーム数

 この値を大きくすると「Profiler overhead and memory usage can increase significantly the more frame are kept in the profiler window through the ”Frame Count” setting.(”フレーム数”の設定を高く設定すると、プロファイラーのオーバーヘッドとメモリ使用量が大幅に増加する可能性があります)」という警告が出ますが、エディターのメモリ使用量とかそこまで気にするものかな?というのが個人的な印象。

 実際どの程度大きくなるのかは把握していませんが、エディターが落ちるとか無ければ5秒でキャプチャーする作業より遥かに楽じゃないかな。

Unity 2019.3未満の場合

 プロファイラの結果を300フレームごとに書き出していけば、300フレームの限界は突破出来ます。

 例えばProfilerBinarylogSplitがそれを実現していたはずです。

github.com

【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の種類を増やし続ける作業に終止符が…?

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