Unity を検索

Unity エディターを拡張するためのヒント – Project MARS の教訓

2019年4月3日 カテゴリ: テクノロジー | 11 分 で読めます
取り上げているトピック
シェア

Is this article helpful for you?

Thank you for your feedback!

Unity Labs のオーサリングツールグループでは、Project MARS(Mixed and Augmented Reality Studio)という、Unity で強力な AR 体験を構築、テストできる拡張機能を開発しています。このブログ記事では、エディターの拡張について、MARS の開発から得られた知見とヒントを紹介します。ただし、広範に適用できるものもあれば、特定のユースケースにしか適用できないものもあることに注意してください。その多くは、SuperScience GitHub リポジトリで対応するサンプルが公開されています。

編集モードで実行

一般的な MARS シーンは、実世界データが満たさなければならない条件と、それらの条件が満たされたときに表示されるデジタルコンテンツから構成されます。このワークフローをサポートするために、MARS にはシミュレーションビューがあります。このビューを利用して、ユーザーはさまざまな環境で設定をテストし、それらの環境のコンテキストに合わせて条件とコンテンツを調整できます。シミュレーションビューで行った調整はシーンに保存する必要があるので、シミュレーションは編集モードで行う必要があります。

MonoBehaviour を編集モードで実行する方法はいくつかあります。ExecuteInEditModeExecuteAlways という属性があり、これらを使用すると MonoBehaviour のすべてのインスタンスを編集モードで実行できます。しかし、この方法は私たちのユースケースには適していません。というのも、実行したいのはシミュレーションに関連するインスタンスだけだからです。そのため、代わりに runInEditMode プロパティーを使用します(ExecuteAlways は Prefab モードをサポートしていますが、ExecuteInEditMode はサポートしていないことに注意してください。2018.3 では、runInEditMode も Prefab モードをサポートしています)。

MonoBehaviour の runInEditMode プロパティーを true に設定すると、初回のみ再生モードと同じライフサイクルが実行されます(Awake、Start など)。その後に runInEditMode を false に設定すると、オブジェクトがアップデートを受け取らなくなりますが、それによって OnDisable がトリガーされることはありません。オブジェクトが(すでに有効化されている場合)無効化されて、再度有効化されるのは、次に runInEditMode が true に設定されたときです。MARS では(すでに有効になっている)MonoBehaviour を無効にし、runInEditMode を false に設定した後、同じ MonoBehaviour を再度有効にする MonoBehaviour 拡張メソッド StopRunInEditMode を作成すると便利であることがわかりました。また、一貫性のために StartRunInEditMode を使用していますが、これは runInEditMode を true に設定するだけです。これは、MonoBehaviour が有効になると OnEnable が常にトリガーされるためです。

デフォルトでは、編集モードで実行されるオブジェクトが更新を受け取るのは、シーン内の何かが変更されたときだけですが、EditorApplication.QueuePlayerLoopUpdate を使用することで更新を強制することもできます。編集モードでオブジェクトを継続的に更新したい場合には、QueuePlayerLoopUpdate をデリゲート EditorApplication.update にフックします。

実際に runInEditMode がどのように機能するかを確認するには、RunInEditHelper を使用します。このエディターウィンドウでは、実行中のオブジェクトを変更したり、PlayerLoop をトグルしたりできます。

シーン内の変更への反応

シミュレーションビューには、ユーザーがシーンの状態を変更すると、シミュレーションプロセスが再開され、新しい状況設定がどのように機能するのかについてユーザーがすぐにフィードバックを得ることができるユーティリティーがあります。これにより、AR シーンの設定とテストにかかるイテレーション回数が改善します。MARS では、Undo.postprocessModifications と Undo.undoRedoPerformed をフックすることによって、ユーザーがシーンに変更を加えたことを検出しています。

postprocessModifications コールバックは UndoPropertyModification の配列を受け取ります。そのため、特定の種類の変更のみを検出したい場合は、それぞれの UndoPropertyModification の currentValue フィールドを確認します。たとえば、MARS では、currentValue.target の種類を確認して、MARS 固有のコンポーネントに関する変更のみに反応するようにしています。

しかし、Undo.undoRedoPerformed はパラメーターを何も受け取らないため、何が変更されたかを正確に知るのは困難です。このコールバックが発生すると、変更されたオブジェクトには Selection.activeGameObject または Selection.gameObjects が含まれると考えられますが、それが保証されるのは、ロックされていないインスペクターを通じてオブジェクトが変更された場合のみです。また、SerializedProperty など、任意のユーザーコードによって変更が行われる可能性も常にあります。

応答の遅延

変更に対する Undo コールバックによる応答のなかには、すぐに発生してほしくないものがあるケースも多いはずです。たとえば、ユーザーがインスペクターでスライダーをドラッグすると、Undo.postprocessModifications の呼び出しが多数トリガーされますが、それらの呼び出しが行われるたびに MARS でシミュレーションプロセスを再起動したくはありません。ユーザーが一連の変更を「完了」したときにのみ応答をトリガーしたい場合に合理的な対策は、変更を検出したらリセットされて起動する短いタイマー(約 0.3 秒)を用意し、タイマーが終了したときにのみ応答をトリガーすることです。

以下に、このパターンの実例を示します。対応する実装はこちらで確認できます。

シーンメタデータの保存

それぞれの MARS シーンについて、そのシーンに必要な実世界データの種類といった一定の情報をメタデータとして保存する必要があります。シーンに必要なメタデータを保存する方法はいくつかあり、それぞれに長所と短所があります。

  1. シーンごとに 1 つの ScriptableObject アセットを使用する
    • 長所
      • シーンを開かなくてもメタデータにアクセスできる。
      • エディター専用のメタデータをビルドから隔離しておくことができる(エディターのメタデータをランタイムのメタデータとは別のアセットとしている場合)。
      • シーンと同じディレクトリーに保存している場合、見つけるのが簡単。
    • 短所
      • 確実にシーンと同期させる必要がある。*
      • シーンの複製を確認し、メタデータも複製されるようにする必要がある。
      • (新しいアセットが必要なシーンがあるほど)プロジェクトのサイズが大きくなる。
  2. 1 つのマスター ScriptableObject アセットにすべてのシーンのメタデータを保存する
    • 長所
      • シーンを開かなくてもメタデータにアクセスできる。
      • エディター専用のメタデータをビルドから隔離しておくことができる(エディターのメタデータをランタイムのメタデータとは別のアセットとしている場合)。
      • シーンごとに 1 つのアセットを用意するのではなく、1 つのプロジェクトに 1 つのメタデータアセットでよい。
    • 短所
      • 確実にシーンと同期させる必要がある。*
      • シーンの複製を確認し、メタデータも複製されるようにする必要がある。
      • バージョン管理がより難しくなる。1 つのシーンに対する変更がマスターアセット全体に影響する。
      • すべてのシーンのメタデータが 1 か所に保存されているため、シーンをさまざまなパッケージに分散させることができない。
  3. そのシーン自体で MonoBehaviour を使用する
    • 長所
      • シーンとの同期を維持するのが簡単。シーンを保存するとメタデータも保存される。
      • シーンを複製するとメタデータも複製される。
      • シーン内にメタデータがあるので見つけるのが簡単。
    • 短所
      • シーンを開かないとメタデータにアクセスできない。
      • ユーザー体験に悪影響を及ぼす可能性がある(「なぜこのオブジェクトがシーン内にあるのか?」)。
        • ユースケースによっては、ヒエラルキー内のメタデータゲームオブジェクトまたはコンポーネントを非表示にして、メタデータの表示/編集用にカスタムウィンドウを用意してもよい。

MARS では当初、1 つのマスター ScriptableObject を使用するアプローチを採用しました。その後、MonoBehaviour を使用するアプローチに変えました。というのも、マスターアセットを使用するアプローチの短所が長所よりも大きかったからです(特に、マージの競合とメタデータの同期状態の維持)。また、別の理由によって、その時点ですでにシーン内に MARS 固有のコンポーネントを必要としていたからでもあります。

*ScriptableObject を使うアプローチを採用する場合は、メタデータとシーンを同期させるタイミングに注意が必要です。シーンを保存していない状態でメタデータアセットをダーティーにすると、シーンの前にそのメタデータが保存されてしまう可能性があります。というのも、プロジェクトを保存しても、開いているシーンが保存されるわけではないからです。シーンが保存されるまでメタデータアセットをダーティーにするタイミングを遅らせた場合には、シーンを保存するときに一緒にメタデータも保存する必要があります。これを実現するには、OnWillSaveAssets を使用します。OnWillSaveAssets を使うと、所定のパスにシーンのパスが含まれるかどうかを確認し、含まれる場合はメタデータアセットをダーティーにして、返す文字列配列にそのパスを含めることができます。こちらに、その実行方法の例を示します。

アセンブリ定義

拡張を作成する際は、その拡張が他の拡張やユーザーコードとうまく連携するよう注意することが大切です。アセンブリ定義を使用すると、Unity でコンパイルするたびにユーザーが拡張を再コンパイルする必要がなくなります。また、ユーザーがコードの依存関係を定義するのも簡単になります。

パッケージやエディター拡張には、3 つの標準アセンブリがあります。

  1. ランタイム
    1. プレイヤーのビルドに含める必要のあるコードが存在すれば、それをすべて含みます。エディター専用拡張では、ランタイムアセンブリが必要ないこともあります。
    2. シーンオブジェクトに追加する必要があるコンポーネントは、ビルドに含めるかどうかにかかわらず、ランタイムに含める必要があります。
    3. ランタイムアセンブリは、エディターアセンブリを参照することはできません。アセンブリ定義を含まない Editor フォルダーのスクリプトと同様です。
  2. エディター
    1. すべてのカスタムインスペクターコードと、エディターにのみ必要なコンポーネントでない C# コードを含める必要があります。
    2. エディターアセンブリの定義は、ターゲットプラットフォームをエディターだけにする必要があります。
    3. エディターアセンブリは、ほぼ確実にランタイムアセンブリを参照します。
  3. テスト
    1. 拡張のテストだけを目的とするすべてのコードを含める必要があります。他のユーザーが拡張を変更できるようにするのでない限り、配布する際にこのフォルダーに拡張を含める必要はありませんが、パッケージによってはこのフォルダーが含まれていることがあります。
    2. 編集モードでのテストと再生モードでのテストの両方を行う場合には、2 つの異なるアセンブリが必要です。
      1. Extension.Tests.Editor(編集モード)
      2. Extension.Tests.Runtime(再生モード)

ランタイムアセンブリの名前は、パッケージの名前です。
他のアセンブリ定義はそれぞれ、PackageName.{suffix} というような名前にする必要があります。

MARS では、それぞれ MARS、MARS.Editor、MARS.Tests となっています。拡張のコードの最上位名前空間は、アセンブリ定義のプレフィックスと一致する必要があります。

ランタイムでエディターアセンブリのコードを使用する

場合によっては、ランタイムアセンブリでエディターのコードの参照が必要になることもあります。たとえば、MonoBehaviour が編集時の機能のためだけに存在しているものの、ルールによりエディターアセンブリ内に MonoBehaviour を含めることが禁じられているためにランタイムアセンブリ内に含める必要がある場合です。
この場合、#if UNTY_EDITOR ディレクティブ内にいくつかの静的デリゲートフィールドを定義すると役立ちます。すると、Editor クラスがそれらのデリゲートに独自のメソッドを割り当てて、ランタイムアセンブリ内のそのクラス自体に対するアクセスを提供します。

このパターンの例は、SuperScience リポジトリで確認できます。EditorWindow、Runtime アセンブリ内の Editor デリゲートを使用するクラス、およびそれらのデリゲートを使用する MonoBehaviour があります。

ライフサイクルイベントと拡張性

ユーザーのプロジェクトの他の側面と拡張を統合できるようにするために私たちが従っている重要なパターンがあります。それは、比較的小規模なシステムの重要な状態変化を知らせるためのイベントを提供することです。システムやオブジェクトに重要な変更が加えられた場合に発生するイベントは、一般的に、ライフサイクルイベントと呼ばれます。

大切なのは、あなたのツールと統合するためにユーザーが行う必要があるであろう、あらゆることに対応することはできないと覚えておくことです。そのため、理想的にはイベントシグネチャは厳しくしすぎないようにしましょう。通常は void を返すようにします。

たとえば、MARS でシミュレーションのためにシーンを開いたり閉じたりする場合に、ユーザーのプロジェクトで必要なカスタムの setup や teardown を使用できるようにイベントを用意しています。ユーザーはこれを使用して、MARS シミュレーションに独自の機能を追加できます。こうしておくことで、ユーザー固有のユースケースに合わせてユーザーが行うであろうあらゆることについて、私たちが対処する必要がなくなりました(そもそも対処は不可能です)。それでいて、ユーザーは非常に柔軟に、必要な機能を実装できるのです。

この場合、私たちが用意したのは非常にシンプルなライフサイクルです。作成イベントと破壊イベントを、引数なしでイベント関数に渡します。

class SimulationSceneModule
{
    public static event Action simulationSceneCreated;
    public static event Action simulationSceneDestroyed;
}

class SimulationLifecycleEventUser : MonoBehaviour
{
   public void OnEnable()
   {
       SimulationSceneModule.simulationSceneCreated += Setup;
       SimulationSceneModule.simulationSceneDestroyed += TearDown;
   }

   public void OnDisable()
   {
       SimulationSceneModule.simulationSceneCreated -= Setup;
       SimulationSceneModule.simulationSceneDestroyed -= TearDown;
   }

   void Setup() { /* プロジェクト固有の初期設定コードをここに書く */ }
   void TearDown() { /* プロジェクト固有の終了時実行コードをここに書く */ }
}

応答する必要がなくなったら、イベントのサブスクライブを解除することを忘れないでください。そうしないと、そのイベントを使用するオブジェクトが破壊された後も、そのイベントが発生する可能性があります。

ライフサイクルイベントは、多くの場合、何らかの状態変更データをエンドユーザーに渡す必要があります。一連のコンポーネントが破壊または変更されたことを伝える必要があれば、次のようなイベントを用意します。

class ComponentEvents
{
    public event Action<List<Component>> componentsChanged;
}

上記の例では Action を使用していますが、イベントをインスペクターで接続できるようにしたい場合は、Action を UnityEvent に置き換えます。

まとめ

私たちは、コミュニティの皆さんがエディターに追加するさまざまな面白い機能を楽しみにしています。開発者が開発者の成功を可能にするのを見るのはとても元気づけられるものです。オーサリングツールグループの使命は、クリエイターが 3D の未来を形作る手助けをすることであり、そのために、今後も知識を積極的に共有していく予定です。質問やご意見がございましたら、labs@unity3d.com までご連絡ください。

2019年4月3日 カテゴリ: テクノロジー | 11 分 で読めます

Is this article helpful for you?

Thank you for your feedback!

取り上げているトピック