機能追加
Unity MARS アプリケーションは、エディターのシミュレーションから AR デバイスまで、さまざまなコンテキストで動作します。ユーザーコードへの変更をしなくても複数のコンテキストに対応できるように、主要な機能へのアクセスは、Functional Injection (機能追加) と呼ばれるシムレイヤーを介して実現されます。機能追加は、さまざまなデータプロバイダーパッケージへの汎用アクセスを提供する統合パターンです。一対の C# インターフェースによって、特定の機能の API が定義されます。慣例上、一方のインターフェースは IProvides
で始まる名前が付けられます。これは IFunctionalityProvider
を実装し、Unity MARS が使用できるようデータプロバイダークラスで実装する必要があるメソッドやイベントを定義します。もう一方のインターフェースは通常、IUses
で始まる名前が付けられ、IFunctionalitySubscriber<TProvider>
を実装します。このインターフェースは、プロバイダーのメソッドにアクセスする必要のあるクラスによって実装されます。システムが任意のサブスクライバー型のプロバイダーを確実にロードおよびアンロードできるよう、TProvider
は IFunctionalityProvider
を実装する型に制限されます。
機能追加の目的は、ユーザースクリプトに追加の機能をできるだけ簡単に加えられるようにすることです。サブスクライバーインターフェースは、クラス内の他の部分に変更を加えることなく追加したり削除したりできます。機能追加は、XR Management におけるサブシステムとローダーの概念と同様の役割を果たしますが、より粒度の細かい設定機能を備えており、AR プラットフォームの統合に加えて、Unity MARS の多くの機能を公開する目的で使用されます。
Unity MARS で定義されている主要な機能追加のインターフェースは、Packages/Unity MARS/Interfaces
フォルダーのサブフォルダーである Providers
と Subscribers
にあります。以下に、これらのインターフェースの例をいくつか挙げます。
IProvidesPlaneFinding
/ IUsesPlaneFinding
IProvidesFaceTracking
/ IUsesFaceTracking
IProvidesCameraPose
/ IUsesCameraPose
これらのインターフェースとその他のサポート対象の型は、依存関係をできるだけ簡素化するために、Unity MARS の Runtime
アセンブリおよび Editor
アセンブリとは異なるアセンブリに定義されています。場合によっては、サードパーティのアセンブリから参照するのは Unity.MARS.Interfaces
と Unity.XRTools.ModuleLoader.Interfaces
だけでよく、API の変更頻度は Runtime
アセンブリよりもはるかに低くなります。必要なインターフェースのメソッドやイベントをプロバイダーやサブスクライバーが別個に実装できるのであれば、Unity MARS のコードを他に参照する必要はありません。
IFunctionalityProvider
では以下の 3 つのメソッドを定義しています。
void LoadProvider()
void UnloadProvider()
void ConnectSubscriber(object subscriber)
IFunctionalitySubscriber<TProvider>
では、TProvider provider
プロパティを定義しています。このプロパティは、プロバイダーをサブスクライバーに接続する際に使用できます。システムは各サブスクライバーを引数として、すべてのプロバイダーにある ConnectSubscriber
を呼び出すことでこの接続を行います。これにより、IFunctionalitySubscriber<TProvider>
を実装するコードは、コンテキストに応じてランタイムに選択されるプロバイダーに対して暗黙的にアクセスできるようになります。便宜上、IFunctionalitySubscriber<TProvider>
のすべての実装には、TProvider
インターフェースに定義されているメソッドやイベントにマップする拡張メソッドを含んだ補完クラスを定義することが推奨されます。そうすればサブスクライバーのコードから provider
プロパティにアクセスする必要がなくなり、複数のサブスクライバー型を実装するクラスであっても、個々の provider
プロパティを区別するための厄介なキャストが不要になります。
具体的な機能サブスクライバーの例として、MARSPlaneVisualizer
を考えてみましょう。このクラスは IUsesPlaneFinding
を実装しているので、this.GetPlanes(planes)
を呼び出し、イベントをサブスクライブして、AR 平面の外観をレンダリングするゲームオブジェクトを作成することができます。MARSPlaneVisualizer
のインスタンスは、コンテキストに応じてさまざまなプロバイダーから平面を取得します。編集モードの Instant (即時) シミュレーションでは、Unity MARS は、そこに SimulatedPlanesProvider
を接続して、アクティブな環境からすべての平面を提供します。再生モードと編集モードの Continuous (連続) シミュレーションでは、SimulatedDiscoveryPlanesProvider
がデフォルトのプロバイダーとして、特定のセッション中、ある時点で検出されている平面を提供します。Unity MARS AR Foundation Providers パッケージをプロジェクトに組み込んだモバイルプレイヤービルドでは、AR サブシステムを介して公開された平面が、ARFoundationPlanesProvider
によって提供されます。Project Settings > MARS > Simulation
の Simulate In Play Mode
オプションを無効にして、プレイヤービルドでどのプロバイダーが選択されるかをテストすることもできます。各種ビルドターゲットをエミュレートするには、Project Settings > Module Loader
の Override Platform In Playmode
オプションを使用します。
アクティブになっている Functionality Island (機能アイランド) とその中にロードされているプロバイダーは、Assets/Module Loader/Settings/Resources/FunctionalityInjectionModule
にある Functionality Injection モジュールのインスペクターを見て確認できます。このインスペクターには、システムが把握しているすべてのプロバイダー型およびサブスクライバー型の集計リストも一覧表示されます。また、再生モード (シミュレーションなし) とプレイヤービルドにおけるすべてのシーンで使用される Default Island
を変更することもできます。さらに、Advanced
折りたたみグループにある MARSSession
インスペクターの Functionality Island
フィールドを使用して、Default Island
をオーバーライドすることもできます。
新しいプロバイダーの作成
アプリケーションや拡張ワークフローによっては、新しい機能プロバイダーが必要になる場合があります。例えば、サードパーティは、エディターでのライブフェイストラッキングを有効にするために、ウェブカメラやその他のデバイスを使用して頭とランドマークのポーズを Unity MARS に伝える新しいフェイストラッキングプロバイダーを作成できます。そのために必要なのは、対応する IProvides
インターフェース型 (この場合は IProvidesFaceTracking
) を実装する何らかのクラスを作成することだけです。そこからは、実装者の判断で、適切なインターフェースメソッドを定義したり、必要に応じてイベントを呼び出したりできます。既存のライブラリをラップする方法については、com.unity.mars-ar-foundation-providers
にあるプロバイダーが良い例ですが、これをそのまま利用できるかどうかはユースケースによって異なります。Unity MARS サンプル (Package Manager UI からアクセス可能) にフェイストラッキングプロバイダーのコピーがあり、プロバイダーの実装の基礎を確認するには、そちらの方が参考になるかもしれません。Module Loader パッケージにある簡単な IProvidesFoo
サンプルよりも複雑ですが、AR Foundation のプロバイダーほど複雑ではありません。
すべてのプロバイダーは、void LoadProvider()
、void UnloadProvider()
、void ConnectSubscriber(object subscriber)
を実装する必要があります。LoadProvider
とUnloadProvider
は、機能アイランドにプロバイダーを追加したりそこから削除したりするときに、システムによって呼び出されます。UnloadProvider
は、アイランド自体を Functionality Injection モジュールからアンロード (削除) するときに、機能アイランド内のすべてのプロバイダーで呼び出されます。これは、シミュレーションのコンテキストのように、設定コードによって明示的に追加される場合もあれば、サブスクライバーの収集時に何の種類のプロバイダーが必要かをシステムが評価するときに暗黙的に行われる場合もあります。これらのメソッドは MonoBehaviour
型の Awake
および Destroy
と似ています。実装者はこれらを利用して、設定や破棄の処理を行うことができます。
ConnectSubscriber
は、候補となるサブスクライバーオブジェクトに機能を挿入するときに、システムによって呼び出されます。これは、MARSSceneModule
でのシーンのロード時に自動的に行われるほか、InjectFunctionality
メソッド、またはこのメソッドの他のバリアントをユーザーコードで呼び出して発動することもできます。プロバイダーは、受け取ったオブジェクトが対応するサブスクライバーインターフェースを実装しているかどうかを確認したり、サブスクライバーの対応する provider
プロパティを this
に設定したりする処理を担います。これは、プロバイダーが IFunctionalityProvider.TryConnectSubscriber<TProvider>
拡張メソッドを使用して簡単に行うことができます。このメソッドは見落とされやすいので、実装されていることを注意して確認してください。1 つのクラスに 複数の 機能プロバイダーインターフェース型が実装されている場合、実装されている各プロバイダーインターフェースにつき 1 回、TryConnectSubscriber<TProvider>
を呼び出す 必要があります。
プロバイダーの基本クラス
機能プロバイダーの唯一の要件は、IFunctionalityProvider
を実装する型です。つまりプロバイダーは、実質的に任意の型から継承ができます。必要であれば、プロバイダーは MonoBehaviors
であってもかまいません。例えば、実装者がフレームごとに Update
コールバックを利用して何らかの処理を実行したければ、MonoBehaviour
を継承するのは理にかなっています。最も単純なプロバイダー型は、通常の C# クラス
ですが、パラメーターを取らないコンストラクターメソッドがあって、Activator.CreateInstance
でアクティベートできるのであれば、実質的にどの型でも継承できます。MonoBehaviour
プロバイダーの場合、そのプロバイダーが必要になったときにインスタンス化されるプレハブを、Functionality Island
のデフォルトプロバイダーリストに用意することができます。そのプレハブがすでにインスタンス化されている場合、そのコンポーネントの中のどれかが他の機能を備えていれば、そのプレハブが再利用されます。それ以外の場合は、必要な MonoBehavior
プロバイダーごとに新しいゲームオブジェクトが作成されます。
プロバイダーが ScriptableObject
を継承することはシステムにより許可されません。なぜなら、これらのオブジェクトの作成と除去には、特殊なメソッド呼び出しが利用されているためです。シリアル化されたデータを持つアセットを定義する機能がないのであれば、ScriptableObject
を使用することに、通常の C# クラスと比べた場合の優位性はありません。ただし、シリアル化されたフィールドを持つアセットを定義し、そのプロバイダーをシングルトンのように扱うことを実装者が望むのであれば、プロバイダーは com.unity.xrtools.utils
に定義されている ScriptableSettingsBase
を継承することができます。
Unity MARS データベースへのデータの提供
プロバイダーの種類によっては、Unity MARS データベース にデータを公開することが求められます (こちら でも取り上げています)。この機能は、IUsesMARSData<T>
、IUsesMARSTrackableData<T>
、IProvidesTraits<T>
によって公開されます。プロバイダーは通常 IProvidesTraits<T>
を実装する必要があります。また、プロキシで使用できるデータを提供する場合は、いずれかの Data<T>
インターフェースも実装する必要があります。Unity MARS データベースには、C# の型によって整理されたコレクションとしてデータが格納されます。IUsesMARSData<T>
の実装者は、任意の型 T
のデータを追加、更新、削除することができます。IMRTrackable
インターフェースを実装するデータ型には、プロバイダーで IUsesMARSTrackableData<T>
を使用する必要があります。Pose
と MarsTrackableId
を提供できるすべてのデータ型は、IMRTrackable
を実装する必要があります。そうすることで、Unity MARS データベースから提供される ID の代わりに、プラットフォームから提供されるトラッキング可能な ID の使用が可能になります。
プロキシは、個々の Traits
に作用するその条件に基づき、特定のデータ ID に一致します。したがって、IUsesMARSData<T>
経由で追加されたデータが役に立つには、そのデータに関連付けられた Traits をプロバイダーが公開することも必要になります。プロキシがその検出対象として想定する一般的なトレイトの例については、Unity.MARS.Query
名前空間の TraitNames
と TraitDefinitions
を参照してください。AR Foundation Providers パッケージに含まれる機能プロバイダーには、こうしたトレイトの使用例が具体的に示されています。IProvidesTraits<T>
を実装する場合は、提供するトレイトをシステムに伝えるために、static TraitDefinition[] GetStaticProvidedTraits()
と TraitDefinition[] GetProvidedTraits()
を実装する必要があります。
プロバイダーは、Add/Update/Remove パターンに従って Unity MARS データベースと対話します。これらのメソッドは通常、プラットフォームの API に対応しており、例えば平面検索では、平面が追加、更新、削除されます。MRTrackable
データ型には ID が含まれているため、IUsesMARSTrackableData
では Add と Update が AddOrUpdate
に統合されています。プロバイダーは、データ ID を返す AddData
を呼び出し、その ID をデータに関連付ける必要があります。そうすることで、データが変化した際に ID を使用して UpdateData
を呼び出したり、後でデータが利用できなくなった場合に ID を使用して RemoveData
を呼び出したりできます。IUsesMARSTrackableData
の場合、AddOrUpdateData
と RemoveData
から Unity MARS データベース ID (MarsTrackableId とは異なり int
) が返されるので、その ID を使用して、関連付けられているトレイトデータを追加、更新、削除できます。プロバイダーは、IUsesMARSData<T>
メソッドおよび IUsesMARSTrackableData<T>
メソッドの呼び出しと同時に、対応する AddOrUpdateTrait
メソッドまたは RemoveTrait
メソッドを、適切な Unity MARS データベース ID で呼び出す必要があります。Unity MARS データベースと対話する一般的なプロバイダーのデモンストレーションについては、Unity MARS サンプル (Package Manager UI からアクセスできます) にあるフェイストラッキングプロバイダーの例を参照してください。
プロバイダーの選択方法
Unity MARS アプリケーションには、多種多様なインテグレーションが考えられます。Functionality Injection (FI。機能追加) モジュールと機能アイランドは、さまざまなシナリオに対応します。以下はその例です。
- ある特定の種類の機能に対して明確な選択肢を設ける。
- 特定の状況におけるデフォルトを明示的な設定で指定する必要がある場合に、複数の選択肢を設ける。
- どのプロバイダーがどのような状況で、どのゲームオブジェクトに使用されるかを正確に指定する。
シーンモジュールと Functionality Injection モジュールでは、以下に該当するプロバイダーが自動的にロードされます。
- シーン内のゲームオブジェクトのサブスクライバーインターフェースと一致する。
- シーン内の条件によって要求されたトレイトを与える。
プロバイダーインターフェースを実装するシーンゲームオブジェクトは、アクティブな機能アイランドのデフォルトのプロバイダーよりも優先されます。これにより、必要なプロバイダーが存在していて、シーン内で動作しているという確信が得られます。スクリプトを使用すると、ランタイムに同じ種類の別のプロバイダーをアクティブなアイランドに設定できますが、標準的な動作としては、プロバイダーはシーンのロード時に設定され、シーンの実行中ずっと同じままです。
一連のオブジェクトを対象に InjectFunctionality
が呼び出されると (例えば、シミュレーションを設定するときやランタイムに MARS を設定するとき)、システムはシーン内の 機能サブスクライバー に基づいてプロバイダーを選択し、初期化します。ProviderSelectionOptions
属性を使用すると、そのプロバイダー型に追加情報を注釈付けできます。第 1 のパラメーターは Priority
です。これはプロバイダーの選択プロセスでプロバイダーを "昇格" または "降格" させるために使用できます。ある特定のプロバイダーインターフェースを複数の型が実装している場合は、最も高い Priority
値を持つプロバイダーが MARS によって選択されます。同じ Priority
値を持つプロバイダー型が複数存在する場合は、リストの最初の型が任意に選択されてログに警告が記録されます。この場合の動作は未定義であり、予期せず変更される可能性があります。
トレイトの要件 に基づいてプロバイダーを選択する際 (通常は、サブスクライバーの要件と同時に行われます)、システムはまず、IProvidesTraits
を実装していて、必要なトレイトを少なくとも 1 つ提供するプロバイダーをすべて収集します。これは RequireProvidersWithDefaultProviders
を呼び出すことによって行われます。リストに 1 つしかプロバイダーが存在しなければ、それが MARS によって選択されます。それ以外の場合は、プロバイダーが保有しているトレイトとシーンに必要なトレイトの数 ("スコア") の降順で、このプロバイダーリストがソートされます。2 つのプロバイダーのスコアが等しい場合、どちらか一方が機能アイランドにおけるデフォルトのプロバイダーであるかどうかが MARS によって確認され、デフォルトである場合は、そのプロバイダーが優先されます。デフォルトリストに両方のプロバイダーが存在するか、どちらも存在しない場合、それらの優先度が MARS によって比較され、型が降順でソートされます。リストの最初の 2 つのプロバイダー間で "スコア" と優先度が異なる場合は、MARS によって 1 つ目のプロバイダーが選択されます。それ以外の場合も 1 つ目のプロバイダーが選択されますが、警告がログに記録されます。この場合の動作は未定義であり、予期せず変更される可能性があります。
プロバイダーの選択は、機能アイランド を使用して手動でオーバーライドできます。機能アイランドにプロバイダークラスやプレハブを追加しても、それがランタイムに MARS によってロードされる保証はありません。デフォルトは、あくまで SetupDefaultProviders でのプロバイダー選択のあいまいさを解消するために存在します。サブスクライバーの要件に基づいてプロバイダーを選択するとき、機能アイランドのデフォルトが Priority
よりも優先されます。前述のように、トレイトの要件に基づいて選択するときには、デフォルトは Priority
よりも優先されますが、デフォルトが考慮されるのは、プロバイダーの "スコア" が等しい場合だけです。ある特定の型のプロバイダーが複数存在するすべてのケースにおいて、アクティブなアイランドにデフォルトが設定されていることを確認する必要があります。
一般に、プロバイダーパッケージには、機能アイランドアセットのサンプルが出発点として用意されています。ほとんどの場合、アプリケーションは、まず基礎となるプロバイダー (AR Foundation Providers パッケージやフェイストラッカーなど) を利用し、必要に応じて付属の機能アイランドにデフォルトのプロバイダーを追加していくことになります。
MonoBehaviour 機能サブスクライバーの作成
MARS Scene Module は、そのロード時にシーン内のすべての MonoBehaviour の機能を挿入します。ただし、MonoBehaviour
サブスクライバーを持つゲームオブジェクトがインスタンス化されるのは、この時点よりも後になる可能性もあるため、MonoBehaviour の機能の挿入は、必要に応じて MonoBehaviour 自体で行うことをお勧めします。これが該当するのは通常、プロキシがランタイムにインスタンス化されるカスタムの条件またはアクションの場合です。これは、Awake()
で CheckAndInjectFunctionality()
拡張メソッドを呼び出すことによって行います。
public class _MonoBehaviour_Subscriber : MonoBehaviour, IFunctionalitySubscriber<TProvider>
{
IProvidesTProvider IFunctionalitySubscriber<IProvidesTProvider>.provider { get; set; }
void Awake()
{
this.CheckAndInjectFunctionality();
}
}
MonoBehaviour が複数のプロバイダーをサブスクライブする場合、挿入が 2 回実行されるのを防ぐために、拡張メソッドである HasProvider<>() と InjectFunctionality() を使用して別個のステップでプロバイダーを検証、挿入します。
public class _MonoBehaviour_Multi_Subscriber : MonoBehaviour, IFunctionalitySubscriber<TProvider1>, IFunctionalitySubscriber<TProvider2>
{
IProvidesTProvider1 IFunctionalitySubscriber<IProvidesTProvider1>.provider { get; set; }
IProvidesTProvider2 IFunctionalitySubscriber<IProvidesTProvider2>.provider { get; set; }
void Awake()
{
if (!this.HasProvider<IProvidesTProvider1>() || !this.HasProvider<IProvidesTProvider2>())
transform.root.gameObject.InjectFunctionality();
}
}
新しいプロバイダー/サブスクライバーインターフェースの作成
すでにあるインターフェースのペアによって目的の機能が十分に記述されていなかった場合は、どうすればよいのでしょうか。解決方法は非常にシンプルです。すべてのアセンブリは Unity.XRTools.ModuleLoader.Interfaces
アセンブリを参照することで、IFunctionalityProvider
と IFunctionalitySubscriber
にアクセスし、新しい機能を記述するインターフェースのペアを新たに実装できます。システムはリフレクションを利用して、IFunctionalityProvider
と IFunctionalitySubscriber
を継承するすべての型を、それらがどこで使用されていても収集します。例えば、Functionality Island
インスペクターは、デフォルトプロバイダーリストの 1 列目に複数の実装が存在するすべてのプロバイダー型を、それらが定義されている場所にかかわらずリストします。新しい種類の機能を実装する方法の基本的な例については、Module Loader のサンプルを参照してください。
機能アイランド
機能アイランドは、サブスクライバーオブジェクトに機能を提供する目的で使用されるオブジェクトです。これには、サブスクライバーオブジェクトに機能を挿入したりプロバイダーを管理したりするための API やプロバイダーのグループが含まれています。FunctionalityIsland
クラスは ScriptableObject
を継承しているため、デフォルトのプロバイダーを選択してアクティベートする方法についてのシリアル化された設定を備えているアセットでサポートできます。機能アイランドは、異なるデフォルトプロバイダーで複数存在でき、プロジェクト内のアセットとして、またはランタイムに同時に使用することができます。これらは相互接続されないため、サブスクライバーオブジェクトは機能を挿入するために使用されるアイランドの "住人" と考えることができます。
機能アイランドは、Functionality Injection モジュール (FI モジュール) によって管理されます。機能を提供するすべてのモジュールをすべてのアイランドに追加できるようにするには、モジュールがロードを完了する前に、すべてのアイランドが FI モジュールに追加されている必要があります。Unity MARS は、モジュールが常にアクセス可能であることを前提にしています。まだ設定の済んでいないアイランドを使用しようとしたり、ワークフローでアイランドを設定するのが遅すぎた場合、MARS から警告が生成されます。そうすることで、後で混乱を招くようなエラーを発生させないようになっています。ほとんどの場合、アイランドを操作するコードを書く必要はなく、シーンの MARS Session ゲームオブジェクトでアイランドの参照を設定するか、またはアクティブなアイランドを使用してゲームオブジェクトに機能を挿入することになります。
アクティブなアイランドは、MARSSession に設定されているオーバーライドに基づいて、シーンの開始時に MARSSceneModule で設定されます。オーバーライドが設定されていない場合、アクティブなアイランドは常にデフォルトになります。アイランドは、デフォルトのプロバイダーのリストに対するシリアル化された参照を保持します。これらのシリアル化された設定によって、シーンを設定する際の設定固有のデフォルトが定義されます。このステップを担うのは、FunctionalityIsland.SetupDefaultProviders()
メソッドです。このメソッドは、シーンの開始時に MARSSceneModule によって呼び出されます。
機能アイランドに含まれるシリアル化されたデフォルトは、プロバイダーの選択時に考慮されます。どのプロバイダーがどのシーンで、また、どのプラットフォームで使用されるかを制御するには、機能アイランドアセットを作成して編集します。ある特定の型のプロバイダーがプロジェクトに 1 つしかない場合、そのプロバイダーにプレハブが必要な場合を除き、デフォルトを作成する必要はありません。
デフォルトを設定するには、機能アイランドの Inspector ウィンドウに移動して、以下の手順に従います。
- Default Providers リストに新しい行を追加します。
- デフォルトを必要とするプロバイダーインターフェースを選択します。
- デフォルトのプロバイダー型を選択するか、そのプロバイダーのプレハブ参照を設定します。
プレハブを追加すると、そのプレハブ内のすべてのプロバイダーコンポーネントが必要に応じて使用されます。ロードするプロバイダー型ごとにプレハブを指定する必要はありません。プレハブを 2 回指定する必要がある場合でも (特定の重複する状況で生じる可能性があります)、インスタンス化されるのは 1 回だけです。それを含む次の行を MARS が処理する際には、同じインスタンスが再利用されます。