MRが楽しい

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

HoloLens2でホロモンアプリを作る その50(待機中、視界内にオブジェクトが検出されるとよそ見をする)

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

今回は待機中、視界内にオブジェクトが検出されるとよそ見をするメモです。

視界ロジック

以前の記事で、ホロモンの視界ロジックを作成しました。
ホロモンから見えている物体を判別し、その優先度(興味)の高さで注目オブジェクトを決定します。
bluebirdofoz.hatenablog.com
bluebirdofoz.hatenablog.com

待機中の注視判定

今回はこの優先度判定を待機ロジック内に組み込む形にリファクタリングしました。
ロジックごとに注視判定を行うことで行動ごとにホロモンが注目するオブジェクトを管理できるようになりました。
以下の待機中ロジックでは普段は興味のあるオブジェクトを注視し続け、視界に検出されたオブジェクトは 1 秒だけよそ見をします。
・HoloMonModeLogicStandby.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

using Cysharp.Threading.Tasks;
using UniRx;

using HoloMonApp.CommonSpace;
using HoloMonApp.DataFormatSpace;
using HoloMonApp.HeadLogicSpace;
using HoloMonApp.FieldOfVisionSpace;

namespace HoloMonApp.ModeLogicSpace
{
    public class HoloMonModeLogicStandby : MonoBehaviour, HoloMonModeLogicInterface
    {
        /// <summary>
        /// モードロジック共通情報
        /// </summary>
        [SerializeField, Tooltip("モードロジック共通情報")]
        private HoloMonModeLogicCommon p_ModeLogicCommon = new HoloMonModeLogicCommon();

        /// <summary>
        /// 現在の実行待機中フラグ
        /// </summary>
        /// <returns></returns>
        public bool CurrentRunAwaitFlg()
        {
            return p_ModeLogicCommon.RunAwaitFlg;
        }

        /// <summary>
        /// モード実行(async/await制御)
        /// </summary>
        public async UniTask<ModeLogicResult> RunModeAsync(ModeLogicSetting a_ModeLogicSetting)
        {
            // 設定アクションデータを保持する
            p_ModeLogicCommon.SaveCommonSetting(a_ModeLogicSetting);

            // 開始処理を行う
            EnableSetting();

            // モードを開始して完了を待機する
            HoloMonActionModeStatus status = await p_ModeLogicCommon.RunModeAsync();

            // 終了状態を返却する
            return new ModeLogicResult(status, new ModeLogicStandbyReturn());
        }

        /// <summary>
        /// モードキャンセル
        /// </summary>
        public void CancelMode()
        {
            // 停止処理を行う
            DisableSetting();

            // キャンセル処理を行う
            p_ModeLogicCommon.CancelMode();
        }

        /// <summary>
        /// モード内部停止
        /// </summary>
        private void StopMode(HoloMonActionModeStatus a_StopModeStatus)
        {
            // 停止処理を行う
            DisableSetting();

            // 停止状態を設定する
            p_ModeLogicCommon.StopMode(a_StopModeStatus);
        }

        /// <summary>
        /// 割込み通知
        /// </summary>
        public bool TransmissionInterrupt(InterruptInformation a_InterruptInfo)
        {
            bool isProcessed = false;

            // 処理対象の割込み処理のみ記述
            switch (a_InterruptInfo.HoloMonInterruptType)
            {
                case HoloMonInterruptType.FindVision:
                    {
                        // 視界オブジェクト発見の割込み
                        InterruptFindVisionData interrupttFindVisionData = a_InterruptInfo.InterruptFindVisionData;
                        // 視界オブジェクトが検出された場合はよそ見をする
                        isProcessed = Lookaway(interrupttFindVisionData.ObjectWrap.Object);
                    }
                    break;
                case HoloMonInterruptType.LostVision:
                    {
                        // 改めて最も優先度の高い注目オブジェクトを注視する
                        LookObjectByPriority();
                    }
                    break;
                default:
                    break;
            }

            return isProcessed;
        }


        /// <summary>
        /// 固有アクションデータの参照
        /// </summary>
        private ModeLogicStandbyData p_Data => p_ModeLogicCommon.ModeLogicSetting.ModeLogicStandbyData;


        /// <summary>
        /// よそ見実行のトリガー
        /// </summary>
        IDisposable p_LookawayTrigger;


        void Update()
        {
            // モードが実行中かチェックする
            if (p_ModeLogicCommon.ModeLogicStatus == HoloMonActionModeStatus.Runtime)
            {
            }
        }


        /// <summary>
        /// モードの設定を有効化する
        /// </summary>
        private bool EnableSetting()
        {
            // アニメーションを待機モードにする
            p_ModeLogicCommon.ReferenceAnimation.ReturnStandbyMode();

            // 頭部追従ロジックを設定する
            p_ModeLogicCommon.ReferenceLimbControl.Head.ChangeHeadLogic(
                    new HeadLogicSetting(new HeadLogicNoOverrideData())
                );

            // 最も優先度の高い注目オブジェクトを注視する
            LookObjectByPriority();

            return true;
        }

        /// <summary>
        /// モードの設定を無効化する
        /// </summary>
        private bool DisableSetting()
        {
            // アニメーションを待機モードにする
            p_ModeLogicCommon.ReferenceAnimation.ReturnStandbyMode();

            // 頭部追従ロジックを解除する
            p_ModeLogicCommon.ReferenceLimbControl.Head.ChangeHeadLogic(
                    new HeadLogicSetting(new HeadLogicNoOverrideData())
                );

            return true;
        }

        /// <summary>
        /// 最も優先度の高い注目オブジェクトを注視する
        /// </summary>
        private void LookObjectByPriority()
        {
            // 視界内オブジェクトから現在最も優先度の高い注目オブジェクトを取得する
            VisionObjectWrap priorityObjectWrap = p_ModeLogicCommon.ReferenceFieldOfVision.CheckCollectionByTypePriority();

            if (priorityObjectWrap != null)
            {
                // 優先注目オブジェクトが取得できていれば注視リアクションを行う
                ReactionLookObject(priorityObjectWrap.Object);
            }
        }

        /// <summary>
        /// 一定時間よそ見をする
        /// </summary>
        /// <param name="a_LookObject"></param>
        private bool Lookaway(GameObject a_LookObject)
        {
            bool isProcessed = false;

            // 指定のオブジェクトを見る
            p_ModeLogicCommon.ReferenceLimbControl.Head.ChangeHeadLogic(
                    new HeadLogicSetting(new HeadLogicLookAtTargetData(a_LookObject))
                );

            // トリガーを設定済みの場合は一旦破棄する
            p_LookawayTrigger?.Dispose();

            // 1.0秒後によそ見キャンセルのトリガーを実行する
            p_LookawayTrigger = Observable
                .Timer(TimeSpan.FromSeconds(1.0f))
                .SubscribeOnMainThread()
                .Subscribe(x =>
                {
                    // 再び最も優先度の高い注目オブジェクトを注視する
                    LookObjectByPriority();
                })
                .AddTo(this);

            isProcessed = true;

            return isProcessed;
        }

        /// <summary>
        /// 注視リアクションを行う
        /// </summary>
        private void ReactionLookObject(GameObject a_LookObject)
        {
            // 頭部追従ロジックを設定する
            p_ModeLogicCommon.ReferenceLimbControl.Head.ChangeHeadLogic(
                (a_LookObject == null) ?
                new HeadLogicSetting(new HeadLogicNoOverrideData()) :
                new HeadLogicSetting(new HeadLogicLookAtTargetData(a_LookObject))
                );
        }
    }
}

視界オブジェクトを管理するクラスは様々な優先度や条件に応じて、現在見えているオブジェクトから優先して注視すべきオブジェクトを返します。
・HoloMonFieldOfVisionSingleton.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using UniRx;

using HoloMonApp.CommonSpace;
using HoloMonApp.DataFormatSpace;

namespace HoloMonApp.FieldOfVisionSpace
{
    public class HoloMonFieldOfVisionSingleton : MonoBehaviour
    {
        /// <summary>
        /// 単一インスタンス
        /// </summary>
        private static HoloMonFieldOfVisionSingleton ps_instance;

        /// <summary>
        /// 参照用インスタンス
        /// </summary>
        public static HoloMonFieldOfVisionSingleton Instance
        {
            get
            {
                if (ps_instance == null && ps_searchForInstance)
                {
                    ps_searchForInstance = false;
                    var ps_instances = FindObjectsOfType<HoloMonFieldOfVisionSingleton>();
                    if (ps_instances.Length == 1)
                    {
                        ps_instance = ps_instances[0];
                    }
                    else if (ps_instances.Length > 1)
                    {
                        Debug.LogErrorFormat("Expected exactly 1 {0} but found {1}.", ps_instance.GetType().ToString(), ps_instances.Length);
                    }
                }
                return ps_instance;
            }
        }

        private static bool ps_searchForInstance = true;


        /// <summary>
        /// 視界内でホロモンが見つけたもの
        /// </summary>
        [SerializeField, Tooltip("視界内でホロモンが見つけたもの")]
        private VisionObjectWrapReactiveProperty p_FindObjectWrap
            = new VisionObjectWrapReactiveProperty();

        /// <summary>
        /// 視界内でホロモンの見つけたもののReadOnlyReactivePropertyの保持変数
        /// </summary>
        private IReadOnlyReactiveProperty<VisionObjectWrap> p_IReadOnlyReactivePropertyFindObjectWrap;

        /// <summary>
        /// 視界内でホロモンの見つけたもののReadOnlyReactivePropertyの参照変数
        /// </summary>
        public IReadOnlyReactiveProperty<VisionObjectWrap> IReadOnlyReactivePropertyFindObjectWrap
            => p_IReadOnlyReactivePropertyFindObjectWrap
            ?? (p_IReadOnlyReactivePropertyFindObjectWrap = p_FindObjectWrap.ToSequentialReadOnlyReactiveProperty());



        /// <summary>
        /// 視界内でホロモンが見失ったもの
        /// </summary>
        [SerializeField, Tooltip("視界内でホロモンが見失ったもの")]
        private StringReactiveProperty p_LostObjectName
            = new StringReactiveProperty();

        /// <summary>
        /// 視界内でホロモンが見失ったもののReadOnlyReactivePropertyの保持変数
        /// </summary>
        private IReadOnlyReactiveProperty<string> p_IReadOnlyReactivePropertyLostObjectName;

        /// <summary>
        /// 視界内でホロモンが見失ったもののReadOnlyReactivePropertyの参照変数
        /// </summary>
        public IReadOnlyReactiveProperty<string> IReadOnlyReactivePropertyLostObjectName
            => p_IReadOnlyReactivePropertyLostObjectName
            ?? (p_IReadOnlyReactivePropertyLostObjectName = p_LostObjectName.ToSequentialReadOnlyReactiveProperty());


        /// <summary>
        /// 見つけたオブジェクトを設定する
        /// </summary>
        /// <returns></returns>
        private void ApplyFindObjectWrap(VisionObjectWrap a_VisionObjectWrap)
        {
            p_FindObjectWrap.SetValueAndForceNotify(a_VisionObjectWrap);
        }

        /// <summary>
        /// 見失ったオブジェクトを設定する
        /// </summary>
        /// <returns></returns>
        private void ApplyLostObjectName(string a_ObjectName)
        {
            p_LostObjectName.SetValueAndForceNotify(a_ObjectName);
        }


        /// <summary>
        /// 視界オブジェクトコレクションの参照
        /// </summary>
        [SerializeField, Tooltip("視界オブジェクトコレクションの参照")]
        private FieldOfVisionCollection p_Collection;

        /// <summary>
        /// 距離チェック用の基準トランスフォーム
        /// </summary>
        [SerializeField, Tooltip("距離チェック用の基準トランスフォーム")]
        private Transform p_CheckDistancePosition;

        /// <summary>
        /// オブジェクト理解種別ごとの優先度リスト
        /// </summary>
        [SerializeField, Tooltip("オブジェクト理解種別ごとの優先度リスト")]
        private List<ObjectUnderstandTypePriority> p_ObjectUnderstandTypePriorityList;


        private void OnEnable()
        {
            // コレクションの追加、削除イベントを追加
            p_Collection.FindObjectWrapEvent += ApplyFindObjectWrap;
            p_Collection.LostObjectNameEvent += ApplyLostObjectName;
        }

        private void OnDisable()
        {
            // コレクションの追加、削除イベントを削除
            p_Collection.FindObjectWrapEvent -= ApplyFindObjectWrap;
            p_Collection.LostObjectNameEvent -= ApplyLostObjectName;
        }


        /// <summary>
        /// 視界オブジェクトコレクションから指定条件が一致する
        /// かつ、距離が最も近いオブジェクトを探す
        /// </summary>
        public VisionObjectWrap CheckCollectionByNearDistance(
            string a_ObjectName = null,
            float a_NearDistance = -1.0f,
            float a_InsideAngle = -1.0f,
            ObjectUnderstandType a_ObjectUnderstandType = ObjectUnderstandType.Nothing
            )
        {
            // 選択オブジェクトの参照
            VisionObjectWrap targetVisionObjectWrap = null;

            // 選択オブジェクトの距離
            float targetDistance = 0.0f;

            foreach (VisionObjectWrap checkVisionObjectWrap in p_Collection.ValueList())
            {
                // 各条件を満たすかチェックする
                if (!CheckObjectName(a_ObjectName, checkVisionObjectWrap)) continue;
                if (!CheckObjectNearDistance(a_NearDistance, checkVisionObjectWrap)) continue;
                if (!CheckObjectInsideAngle(a_InsideAngle, checkVisionObjectWrap)) continue;
                if (!CheckObjectUnderstandType(a_ObjectUnderstandType, checkVisionObjectWrap)) continue;

                // オブジェクトまでの距離を取得する
                float checkDistance = Vector3.Distance(
                    p_CheckDistancePosition.position,
                    checkVisionObjectWrap.Object.transform.position
                    );

                // 1つ目はチェック無し
                if (targetVisionObjectWrap == null)
                {
                    // 選択状態を保存する
                    targetVisionObjectWrap = checkVisionObjectWrap;
                    targetDistance = checkDistance;

                    continue;
                }

                // 2つ目以降は比較して距離の近い方を選択する
                if (checkDistance < targetDistance)
                {
                    // 選択状態を保存する
                    targetVisionObjectWrap = checkVisionObjectWrap;
                    targetDistance = checkDistance;

                    continue;
                }
            }

            return targetVisionObjectWrap;
        }

        /// <summary>
        /// 視界オブジェクトコレクションから指定条件が一致する
        /// かつ、優先度が最も近いオブジェクトを探す
        /// 優先度が同じ場合は距離の近いものを選択する
        /// </summary>
        public VisionObjectWrap CheckCollectionByTypePriority(
            string a_ObjectName = null,
            float a_NearDistance = -1.0f,
            float a_InsideAngle = -1.0f
            )
        {
            // 選択オブジェクトの参照
            VisionObjectWrap targetVisionObjectWrap = null;

            // 選択オブジェクトの距離
            float targetDistance = 0.0f;

            // 選択オブジェクトの優先度
            int targetPriority = 0;

            foreach (VisionObjectWrap checkVisionObjectWrap in p_Collection.ValueList())
            {
                // 各条件を満たすかチェックする
                if (!CheckObjectName(a_ObjectName, checkVisionObjectWrap)) continue;
                if (!CheckObjectNearDistance(a_NearDistance, checkVisionObjectWrap)) continue;
                if (!CheckObjectInsideAngle(a_InsideAngle, checkVisionObjectWrap)) continue;

                // チェック対象のオブジェクト理解種別を取得する
                ObjectUnderstandType checkType = checkVisionObjectWrap.CurrentFeatures().ObjectUnderstandType;

                // 優先度が設定されていれば優先度の値を取得する
                int checkPriority = 0;
                foreach (ObjectUnderstandTypePriority typePriority in p_ObjectUnderstandTypePriorityList)
                {
                    if (checkType == typePriority.ObjectUnderstandType)
                    {
                        checkPriority = typePriority.Priority;
                        break;
                    }
                }

                // オブジェクトまでの距離を取得する
                float checkDistance = Vector3.Distance(
                    p_CheckDistancePosition.position,
                    checkVisionObjectWrap.Object.transform.position
                    );

                // 1つ目はチェック無し
                if (targetVisionObjectWrap == null)
                {
                    // 選択状態を保存する
                    targetVisionObjectWrap = checkVisionObjectWrap;
                    targetDistance = checkDistance;
                    targetPriority = checkPriority;

                    continue;
                }

                // 2つ目以降は比較して優先度が高ければ選択する
                if (checkPriority > targetPriority)
                {
                    // 選択状態を保存する
                    targetVisionObjectWrap = checkVisionObjectWrap;
                    targetDistance = checkDistance;
                    targetPriority = checkPriority;

                    continue;
                }

                // もし優先度が同じならば近い方を選択する
                if (checkPriority == targetPriority)
                {
                    if (checkDistance < targetDistance)
                    {
                        // 選択状態を保存する
                        targetVisionObjectWrap = checkVisionObjectWrap;
                        targetDistance = checkDistance;
                        targetPriority = checkPriority;

                        continue;
                    }
                }
            }

            return targetVisionObjectWrap;
        }

        /// <summary>
        /// オブジェクト名をチェックする
        /// 条件の指定がない場合は True が返る
        /// </summary>
        private bool CheckObjectName(string a_CheckName, VisionObjectWrap a_CheckObjectWrap)
        {
            if (a_CheckName == null) return true;
            if (a_CheckName != a_CheckObjectWrap.CurrentName()) return false;
            return true;
        }

        /// <summary>
        /// オブジェクト種別をチェックする
        /// 条件の指定がない場合は True が返る
        /// </summary>
        private bool CheckObjectUnderstandType(ObjectUnderstandType a_CheckUnderstandType, VisionObjectWrap a_CheckObjectWrap)
        {
            if (a_CheckUnderstandType == ObjectUnderstandType.Nothing) return true;
            if (a_CheckUnderstandType != a_CheckObjectWrap.CurrentFeatures().ObjectUnderstandType) return false;
            return true;
        }

        /// <summary>
        /// 距離が指定距離より近いかチェックする
        /// 条件の指定がない場合は True が返る
        /// </summary>
        private bool CheckObjectNearDistance(float a_NearDistance, VisionObjectWrap a_CheckObjectWrap)
        {
            if (a_NearDistance < 0.0f) return true;
            if (a_NearDistance < Vector3.Distance(p_CheckDistancePosition.position, a_CheckObjectWrap.Object.transform.position)) return false;
            return true;
        }

        /// <summary>
        /// 距離が指定距離より遠いかチェックする
        /// 条件の指定がない場合は True が返る
        /// </summary>
        private bool CheckObjectFarDistance(float a_FarDistance, VisionObjectWrap a_CheckObjectWrap)
        {
            if (a_FarDistance < 0.0f) return true;
            if (a_FarDistance > Vector3.Distance(p_CheckDistancePosition.position, a_CheckObjectWrap.Object.transform.position)) return false;
            return true;
        }

        /// <summary>
        /// 角度が指定角度より内側かチェックする
        /// 条件の指定がない場合は True が返る
        /// </summary>
        private bool CheckObjectInsideAngle(float a_InsideAngle, VisionObjectWrap a_CheckObjectWrap)
        {
            if (a_InsideAngle < 0.0f) return true;

            // 視界原点とオブジェクト間のベクトルを算出し、方向ベクトルを取得する
            Vector3 betweenDirection = (a_CheckObjectWrap.Object.transform.position - p_CheckDistancePosition.position).normalized;

            // 視界原点の方向ベクトルを取得する
            Vector3 headDirection = p_CheckDistancePosition.rotation * Vector3.forward;

            // 2つの方向ベクトルの角度差(0°~360°)を取得する
            float diffAngle = Vector3.Angle(headDirection, betweenDirection);

            if (a_InsideAngle < diffAngle) return false;
            return true;
        }

    }
}

f:id:bluebirdofoz:20210801224440j:plain

動作確認

シーンを再生して動作を確認します。
f:id:bluebirdofoz:20210801224139j:plain

ホロモンが待機中、視界内にオブジェクトが現れると、そのオブジェクトの方をよそ見をします。
その後はボールなどの興味の高いオブジェクトを優先して注視し、興味が同じオブジェクトの場合には距離が近いオブジェクトを注視します。
f:id:bluebirdofoz:20210801224153j:plain

「ホロモン」と呼びかけると、プレイヤーの注視ロジックに入ります。
このときは視界内にオブジェクトが現れるとよそ見をするもの、それ以外はプレイヤーの顔をずっと注視し続けます。
f:id:bluebirdofoz:20210801224205j:plain