MRが楽しい

MRやVRについて学習したことを書き残す

インタフェースを指定してSerializeFieldにコンポーネントの参照を設定する その3(CustomPropertyDrawerで型指定のカスタムプロパティを作成する)

本日は Unity の小ネタ枠です。
ホロモンアプリ実装時にインタフェースを指定してSerializeFieldにコンポーネントの参照を設定したいことがあったので実装を試してみました。
今回は CustomPropertyDrawer を利用して実装を試してみました。

前回記事

以下の記事の続きです。
bluebirdofoz.hatenablog.com

CustomPropertyDrawerで型指定のカスタムプロパティを作成する

PropertyAttribute を派生してカスタムアトリビュートを作成し、更にそのカスタムアトリビュートのインスペクター上での動作を CustomPropertyDrawer で制御します。
これにより指定の型を実装しているコンポーネントのみ登録可能なカスタムプロパティを作成可能です。

以下の記事を参考にしました。
qiita.com

カスタムアトリビュートの実装は特に変更が必要な箇所がないため、解説コメントを追記した以外はそのまま引用しています。

指定の型を位置指定パラメータで受け取るカスタムアトリビュート

・ComponentRestrictionAttribute.cs

using System;
using UnityEngine;

// AttributeUsage:適用可能なプログラム要素を指定する
// Inherited:派生したクラスによって継承可能か
// AllowMultiple:属性の複数のインスタンスが存在できるか
// https://learn.microsoft.com/ja-jp/dotnet/standard/attributes/writing-custom-attributes
[System.AttributeUsage(System.AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
// PropertyAttributeでカスタムプロパティを定義する
// https://docs.unity3d.com/ja/current/ScriptReference/PropertyAttribute.html
public class ComponentRestrictionAttribute : PropertyAttribute
{
    /// <summary>
    /// 位置指定パラメータ
    /// https://learn.microsoft.com/ja-jp/dotnet/csharp/programming-guide/concepts/attributes/creating-custom-attributes
    /// </summary>
    public Type type;
    public ComponentRestrictionAttribute(Type type)
    {
        this.type = type;
    }
}
アトリビュートのインスペクター上での動作を制御するカスタムプロパティ

・ComponentRestrictionDrawer.cs

using UnityEngine;
using UnityEditor;

// CustomPropertyDrawerでインスぺクター上の描画をカスタマイズする
// https://docs.unity3d.com/ja/current/ScriptReference/CustomPropertyDrawer.html
[CustomPropertyDrawer(typeof(ComponentRestrictionAttribute))]
public class ComponentRestrictionDrawer : PropertyDrawer
{
    // OnGUIで描画時の処理をカスタマイズする
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        var restriction = (ComponentRestrictionAttribute)attribute;

        // UnityEngine.Objectから派生した型か否か(SerializedPropertyType.ObjectReference)
        // https://docs.unity3d.com/ja/current/ScriptReference/SerializedPropertyType.ObjectReference.html
        if (property.propertyType == SerializedPropertyType.ObjectReference)
        {
            // ComponentRestrictionAttributeで指定した型のみ割り当てを許可する
            // https://docs.unity3d.com/ja/current/ScriptReference/EditorGUI.ObjectField.html
            EditorGUI.ObjectField(position, property, restriction.type);
        }
        else
        {
            EditorGUI.PropertyField(position, property);
        }
    }
}

スクリプトは Unity のエディター拡張なので Editor フォルダ配下に作成する必要があります。

利用方法

型を指定したいプロパティに ComponentRestriction アトリビュートを設定します。
・WorldItemAccesserExample04.cs

using UnityEngine;
namespace HoloMonApp.Content.Character.WorldItem.Common
{
    public class WorldItemAccesserExample04 : MonoBehaviour
    {
        // ComponentRestrictionでWorldItemSharingModuleIF型を指定する
        [SerializeField, ComponentRestriction(typeof(WorldItemSharingModuleIF))]
        private Component p_WorldItemSharingModuleIFComponent;
        private WorldItemSharingModuleIF p_WorldItemSharingModuleIF => p_WorldItemSharingModuleIFComponent as WorldItemSharingModuleIF;
    }
}

このプロパティには指定の型を実装したコンポーネントのみが登録できるようになります。

一度カスタムプロパティを作成してしまえばその後の型指定がとても楽になります。

インタフェースを指定してSerializeFieldにコンポーネントの参照を設定する その2(OnValidateでチェックする)

本日は Unity の小ネタ枠です。
ホロモンアプリ実装時にインタフェースを指定してSerializeFieldにコンポーネントの参照を設定したいことがあったので実装を試してみました。
今回は OnValidate を利用して実装を試してみました。

前回記事

以下の記事の続きです。
bluebirdofoz.hatenablog.com

OnValidateを使ってSerializeFieldへの登録を制限する

以下のように OnValidate を使って Inspector ビューで値が更新されたタイミングでインタフェースが実装されているかチェックを行う実装にしてみました。
対象のオブジェクトに指定のインタフェースが実装されたコンポーネントがない場合は値を空にすることで登録させません。
・WorldItemAccesserExample03.cs

using UnityEngine;
namespace HoloMonApp.Content.Character.WorldItem.Common
{
    public class WorldItemAccesserExample03 : MonoBehaviour
    {
        [SerializeField]
        private Component p_WorldItemSharingModuleIFComponent;
        private WorldItemSharingModuleIF p_WorldItemSharingModuleIF => p_WorldItemSharingModuleIFComponent as WorldItemSharingModuleIF;

        /// <summary>
        /// インスペクター上でスクリプトが読み込まれたとき
        /// または値が変更されたときに呼び出される
        /// </summary>
        private void OnValidate()
        {
            bool IsFound = false;
            Component[] components = p_WorldItemSharingModuleIFComponent.GetComponents<Component>();
            foreach (Component component in components)
            {
                // 指定オブジェクトの中でWorldItemSharingModuleIFを実装しているComponentを登録する
                WorldItemSharingModuleIF control = component as WorldItemSharingModuleIF;
                if (control != null)
                {
                    p_WorldItemSharingModuleIFComponent = component;
                    IsFound = true;
                }
            }
            if (!IsFound)
            {
                // 見つからなければ値を外す
                p_WorldItemSharingModuleIFComponent = null;
            }
        }
    }
}

こちらの方法であればドラッグ操作で登録が行えますし、後からインタフェースが実装されていない値への差し替えもできません。
ただ登録に関する処理でコードが冗長になるのが悩みどころです。

HoloLens2でホロモンアプリを作る その111(再生時にアイテムの管理コンポーネントを追加する)

本日はアプリ作成枠です。
HoloLens2でホロモンアプリを作る進捗を書き留めていきます。

再生時にアイテムの管理コンポーネントを追加する

前回のアイテムの管理方法の見直しに合わせてアイテムの管理に必要なコンポーネントを再生時に追加する形にしました。
例えばホロモンに認識させたいオブジェクトがある場合は、以下の WorldItemAPI コンポーネントのみをルートオブジェクトに設定します。

このコンポーネントはホロモンがアイテムを認識するのに必要な情報を Reset 関数で取得します。
・WorldItemAPI.cs

using HoloMonApp.Content.Character.Data.Knowledge.Objects;
using HoloMonApp.Content.Character.WorldItem.Common;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;

namespace HoloMonApp.Content.Character.WorldItem
{
    [RequireComponent(typeof(Collider))]
    [RequireComponent(typeof(Rigidbody))]
    public class WorldItemAPI : MonoBehaviour
    {
        [Header("デフォルト定義")]

        [SerializeField, Tooltip("アイテムの初期設定")]
        private WorldItemDefaultSettings p_WorldItemDefaultSettings;


        [Header("再生時自動生成")]

        [SerializeField, Tooltip("アクセサ参照")]
        private WorldItemAccesserAPI p_WorldItemAccesserAPI;
        public WorldItemAccesserAPI Accesser => p_WorldItemAccesserAPI;


        private void Reset()
        {
            // アイテムの初期設定を作成する
            p_WorldItemDefaultSettings = new WorldItemDefaultSettings(
                new WorldItemDefaultFeatureSettings(gameObject, GetComponents<Collider>().ToList(), GetComponent<Rigidbody>()),
                new WorldItemDefaultLogicalSettings(GetComponents<Collider>().ToList())
                );
        }

        private void Awake()
        {
            // 再生時アイテム操作に必要なコンポーネントを保持する子オブジェクトを作成する
            GameObject logicObject = Instantiate(new GameObject("[Spawn] ItemLogic"), p_WorldItemDefaultSettings.Feature.ItemObject.transform);
            p_WorldItemAccesserAPI = logicObject.AddComponent<WorldItemAccesserAPI>();
            p_WorldItemAccesserAPI.Initialize(p_WorldItemDefaultSettings);
        }
    }
}

アプリが再生されると WorldItemAPI によってアイテム操作に必要なコンポーネントを設定した子オブジェクトが生成されます。

この実装方式の理由

WorldItemAPI コンポーネントのみをルートオブジェクトに設定するのは様々なプロジェクトに簡単にホロモンアプリの仕組みを移植できるようにするためです。
例えば、MRTK のハンドインタラクションのサンプルシーンでも対象オブジェクトに WorldItemAPI を追加するだけでホロモンが対象アイテムを認識できるようになります。
www.youtube.com

ただし再生時にコンポーネントを設定した子オブジェクトを生成するのはコード管理の観点からはあまり適切な手法ではないかもしれません。
基本的には MonoBehavier を利用しない方がコードの流用性やテストのしやすさが向上するためです。
今回の場合、各種スクリプトが MonoBehavier を利用していなければ子オブジェクトを生成する必要もなく、初期化タイミングの問題に悩まされることもなくなります。

ただホロモンアプリにおいては UnityEditor 上の Inspector ビューで各コンポーネントのステータスがリアルタイムに見れることを重視してこの方式を取りました。
ホロモンの行動はそのときホロモンが何を見ているか何を考えているか見てるアイテムがどういう状態かなど様々な要因に左右されるため、各種ステータスをリアルタイムに見れないとホロモンの行動の妥当性が分からないためです。
両方の課題を同時に解決できる方法を模索中です。

インタフェースを指定してSerializeFieldにコンポーネントの参照を設定する

本日は Unity の小ネタ枠です。
ホロモンアプリ実装時にインタフェースを指定してSerializeFieldにコンポーネントの参照を設定したいことがあったので実装を試してみました。

SerializeFieldにインタフェースを指定する

インタフェースはシリアライズできないので、インタフェースを SerializeField に指定しても Inspector に表示されません。
・WorldItemAccesserExample01.cs

using UnityEngine;
namespace HoloMonApp.Content.Character.WorldItem.Common
{
    public class WorldItemAccesserExample01 : MonoBehaviour
    {
        // インタフェースをSerializeFieldに指定する
        [SerializeField]
        private WorldItemSharingModuleIF p_WorldItemSharingModuleIF;
    }
}

参照を同一インタフェースを実装したもので差し替えれる形で実装したかったので、代替策として以下の実装を試しました。
コンポーネントを取得し、指定のインタフェースを実装したものであれば参照に追加しています。
・WorldItemAccesserExample02.cs

using UnityEngine;
namespace HoloMonApp.Content.Character.WorldItem.Common
{
    public class WorldItemAccesserExample02 : MonoBehaviour
    {
        // ComponentなのでSerializeFieldに指定できる
        [SerializeField]
        private Component p_WorldItemSharingModuleIFComponent;

        private WorldItemSharingModuleIF p_WorldItemSharingModuleIF => p_WorldItemSharingModuleIFComponent as WorldItemSharingModuleIF;

        private void Reset()
        {
            Component[] components = GetComponents<Component>();
            foreach (Component component in components)
            {
                // WorldItemSharingModuleIFを実装しているComponentを登録する
                WorldItemSharingModuleIF control = component as WorldItemSharingModuleIF;
                if (control != null) p_WorldItemSharingModuleIFComponent = component;
            }
        }
    }
}

ただこのやり方だと後からドラッグ操作でインタフェースを実装していないコンポーネントには参照を差し替えたりできてしまいます。
他に良い手法がないか調査中です。

HoloLens2でホロモンアプリを作る その110(アイテムの管理方法の変更)

本日はアプリ作成枠です。
HoloLens2でホロモンアプリを作る進捗を書き留めていきます。
今回はアイテムの管理方法の変更です。

アイテムの管理方法の変更

シェアリングを実装するに当たってアイテムの管理スクリプトを見直すことにしました。
例えば、これまでは食べ物とそれが出てくる台座は一つのアイテムとしてアクセス用のコンポーネントを作成していました。

ただこの方法の場合、アイテムごとにシェアリングを行うオブジェクトの構成がバラバラであるため、シェアリングで共有すべきトランスフォームが分かりづらくなりそうです。
よって以下のようにトランスフォームを共有すべきオブジェクトごとにアクセス用のコンポーネントを割り当てる形に修正することにしました。

結構大がかりなリファクタリングになりつつある上、この修正自体は特に機能追加がある訳ではありませんが今後の保守性を高めるため改修していきます。

LayoutElementを使ってUI要素の大きさを隙間を詰めるように変化させる

本日は Unity の小ネタ枠です。
LayoutElementを使ってUI要素の大きさを隙間を詰めるように変化させる方法です。

LayoutElement

LayoutElement はレイアウト要素の最小サイズ、最大サイズなどをオーバーライドする際に利用します。
docs.unity3d.com

サンプル

以下の前回記事のプロジェクトを流用して画面の隙間を埋めるように大きさが変化する UI を作成してみます。
bluebirdofoz.hatenablog.com

以下のようにスクロールコンテンツの下に追加でパネルオブジェクトを配置しました。

ボタンを選択したときだけパネルが表示され、ボタンが非選択の場合はパネルが表示されずスクロールコンテンツが画面の上下いっぱいに表示されるようにしてみます。
・パネル表示時

・パネル非表示時

大きさを調整したいスクロールオブジェクトに LayoutElement コンポーネントを設定します。

追加した LayoutElement の[Flexible Height]を 1 に設定します。
Flexible 変数は要素を 0 ~ 1 の比率で変更可能な最大サイズを設定します。
これでスクロールオブジェクトは他の要素がない時、可能な最大の高さまで拡大されるようになります。

同様にパネルオブジェクトにも LayoutElement コンポーネントを設定します。
こちらは[min Height]を 200 に設定します。
これでパネルオブジェクトは高さ 200 以下には縮小されなくなります。

最後に高さ方向のレイアウトを自動調整するため、これらの親オブジェクトに VerticalLayoutGroup コンポーネントを設定します。
[Control Clild Size]の[Height]にチェックを入れます。これで子オブジェクトの高さが自動でオーバライドされるようになります。

動作確認

パネルオブジェクトを有効/無効化して UI の変化を確認します。
パネルオブジェクトが無効化されたときにスクロールオブジェクトが隙間を埋めるように拡大され、有効化したときはパネルオブジェクトの分だけ縮小されれば成功です。

HoloLens2でホロモンアプリを作る その109(ホロモンの位置のシェアリング)

本日はアプリ作成枠です。
HoloLens2でホロモンアプリを作る進捗を書き留めていきます。

ホロモンの位置のシェアリング

ホロモンの位置を複数の HoloLens で共有するため、ホロモンの座標をシェアリングする仕組みを実装しました。
シェアリングの仕組みとホロモンの動作ロジックを切り分けやすくするため、別のオブジェクト群からホロモンのトランスフォームを監視して変更があればシェアリングする仕組みにしています。

2台のPC間で Git push と Git pull を利用して同じプロジェクトを動作させてシェアリングの確認を実施しています。
以下の画像ではクライアント側のPCではホロモンの動作ロジックがクライアントモード(ロジック無し)に切り替わり、ホスト側のホロモンと同じ座標に追従しています。