MRが楽しい

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

UnityのコードエディターにVisualStudioCodeを利用する

本日は VisualStudioCode の小ネタ枠です。
UnityのコードエディターにVisualStudioCodeを利用する手順を記事にします。
f:id:bluebirdofoz:20210507011115j:plain

前提条件

前提条件として Unity と VisualStudioCode はインストール済みからの手順になります。
bluebirdofoz.hatenablog.com

VisualStudioCodeの拡張機能の追加

C# の開発を行ったり、Unity のデバッグを実行できるようにするため、VisualStudioCode の拡張機能をインストールします。
VisualStudioCode を起動し、Extensions を開きます。
f:id:bluebirdofoz:20210507011145j:plain

以下の3つの拡張機能を検索し、インストールを実行します。
C#(C#開発ツール)
f:id:bluebirdofoz:20210507011227j:plain

・Debugger for Unity(Unity連携のデバッグ機能)
f:id:bluebirdofoz:20210507011240j:plain

・MonoBehaviour Snippets(Unity関連のコード補間)
f:id:bluebirdofoz:20210507011259j:plain

C# を利用するには .NET Framework 4.5 Targeting Pack をインストールしておく必要があるようです。
インストールが未実施の場合は VisualStudio Installer からインストール可能です。
qiita.com

Unityで利用するエディタの指定

拡張機能を追加したら Unity で利用するエディタを VisualStudioCode に指定します。
Unity を起動し、メニューから[Edit -> Preferences..]を開きます。
f:id:bluebirdofoz:20210507011321j:plain

ダイアログが開くので[External Tools]タブを開きます。
[External Scripts Editor]の項目から[Visual Studio Code]を選択します。
f:id:bluebirdofoz:20210507011333j:plain

これで Unity のコードエディターに VisualStudioCode を利用することができました。
f:id:bluebirdofoz:20210507011344j:plain

参考ページ

qiita.com

メモ

その他、インストールしておくと便利な拡張機能を以下にメモしておきます。
C# XML Documentation Comments(XMLドキュメントコメントの自動生成)
f:id:bluebirdofoz:20210507011403j:plain

Visual Studio Codeのインストール手順

本日は環境構築枠です。
Windows 環境における Visual Studio Code のインストール手順を記録します。
f:id:bluebirdofoz:20210506233007j:plain

VisualStuioCodeのインストール

Visual Studio Codeインストーラを以下のページの[Download]ボタンから取得します。
code.visualstudio.com
f:id:bluebirdofoz:20210506233028j:plain

ダウンロードした VSCodeUserSetup-xXX-X.XX.X.exeを実行します。
f:id:bluebirdofoz:20210506233040j:plain

セットアップ画面が開きます。
最初に使用許諾画面が表示されるので、「同意する」を選択して「次へ」をクリックします。
f:id:bluebirdofoz:20210506233051j:plain

インストール先の指定画面が表示されます。
デフォルトまたは任意のインストール先を設定して「次へ」をクリックします。
f:id:bluebirdofoz:20210506233105j:plain

スタートメニューフォルダの指定画面が表示されます。
ショートカットを作成する場合はそのまま「次へ」をクリックします。
f:id:bluebirdofoz:20210506233114j:plain

追加タスクの選択画面が表示されます。
そのままでも問題ありません。好みがあればカスタマイズ設定を行い、「次へ」をクリックします。
f:id:bluebirdofoz:20210506233126j:plain

最後に、インストール準備画面が表示されます。
「インストール」をクリックしてインストールを実行します。
f:id:bluebirdofoz:20210506233137j:plain

以上で Visual Studio Code のインストールは完了です。
f:id:bluebirdofoz:20210506233154j:plain
f:id:bluebirdofoz:20210506233207j:plain

HoloLens2でホロモンアプリを作る その39(NavMeshAgentの追跡先を足元の空間認識レイヤー上の位置に設定する)

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

今回は NavMeshAgent の追跡先を足元の空間認識レイヤー上の位置に設定するメモです。

NavMeshAgent の追跡先を足元の空間認識レイヤー上の位置に設定する

前回記事では追跡ターゲットとの距離を測り、追跡成功と失敗を判定するスクリプトを作成しました。
bluebirdofoz.hatenablog.com

しかし、実際に現実空間で人の位置を追跡対象とした場合、HoloLens2 の位置は頭部の位置になります。
このため、NavMesh で追跡を行う空間認識レイヤーからは身長分高い位置に追跡ターゲットが存在します。

距離で追跡成功を正確に判定する場合、追跡対象も空間認識レイヤー上に追跡ターゲットが存在する必要があります。
そこで以下のように真下方向にレイを飛ばし、空間認識レイヤーが存在する場合、その位置をポイントするスクリプトを作成しました。
・FootPositionTracking.cs

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

// CoreSystemへのアクセスのため
using Microsoft.MixedReality.Toolkit;
// 空間認識情報の取得のため
using Microsoft.MixedReality.Toolkit.SpatialAwareness;


namespace HMProject.HoloMonMoveAgent
{
    public class FootPositionTracking : MonoBehaviour
    {
        /// <summary>
        /// 追跡跡オブジェクトのトランスフォーム
        /// </summary>
        [SerializeField, Tooltip("追跡オブジェクトのトランスフォーム")]
        private Transform p_TrackingTransform;

        /// <summary>
        /// 高さのオフセット
        /// </summary>
        [SerializeField, Tooltip("高さのオフセット")]
        private float p_YAxisOffset = 0.1f;

        /// <summary>
        /// 足元までの上限距離
        /// </summary>
        [SerializeField, Tooltip("足元までの上限距離")]
        private float p_HeightMax = 2.0f;

        /// <summary>
        /// 空間認識レイヤー(ヒット判定レイヤー)
        /// </summary>
        [SerializeField, Tooltip("空間認識レイヤー(ヒット判定レイヤー)")]
        int p_SpatialAwarenessLayer;


        /// <summary>
        /// 開始処理
        /// </summary>
        void Start()
        {
            // 空間認識のオブザーバを取得する
            IMixedRealitySpatialAwarenessMeshObserver SpatialAwarenessMeshObserver =
                CoreServices.GetSpatialAwarenessSystemDataProvider<IMixedRealitySpatialAwarenessMeshObserver>();

            // オブザーバからレイヤー番号を取得する
            p_SpatialAwarenessLayer = SpatialAwarenessMeshObserver.MeshPhysicsLayer;

        }

        /// <summary>
        /// 定期処理
        /// </summary>
        void LateUpdate()
        {
            if(p_TrackingTransform == null)
            {
                return;
            }

            // 追跡の正否
            bool isTracking = false;

            // レイキャストを追跡対象の位置から真下に落とす
            Ray ray = new Ray(p_TrackingTransform.position, -Vector3.up);
            RaycastHit hitInfo = new RaycastHit();

            // 空間認識レイヤー用マスク
            int layerMask = 1 << p_SpatialAwarenessLayer;

            // レイキャストのヒット情報を取得する
            if (Physics.Raycast(ray, out hitInfo, Mathf.Infinity, layerMask))
            {
                // ヒット位置が上限距離を超えているかチェックする
                if (hitInfo.distance < p_HeightMax)
                {
                    // 超えていなければヒット位置を足元として自身の位置を移動する
                    this.transform.position = new Vector3(
                        hitInfo.point.x,
                        hitInfo.point.y + p_YAxisOffset,
                        hitInfo.point.z);
                    // 追跡の成功
                    isTracking = true;
                }
            }
            if (isTracking == false)
            {
                // ヒットに失敗した場合は追跡対象の真下かつ上限距離の位置に自身の位置を移動する
                this.transform.position = new Vector3(
                    p_TrackingTransform.position.x,
                    p_TrackingTransform.position.y - p_HeightMax,
                    p_TrackingTransform.position.z);
            }
        }

        public void SetTrackingTransform(Transform a_TargetTransform)
        {
            p_TrackingTransform = a_TargetTransform;
        }

        public void ClearTrackingTransform()
        {
            p_TrackingTransform = null;
        }
    }
}

f:id:bluebirdofoz:20210505183229j:plain

例えば本スクリプトの p_TrackingTransform にカメラのトランスフォームを設定すると、プレイヤーの足元位置にオブジェクトが追従するようになります。
レイキャストのレイヤーマスクの設定方法の詳細は以下を参照ください。
docs.unity3d.com

シーンを再生して動作を確認してみます。
ホロモンに「おいで」と言って、プレイヤーを追跡させます。
f:id:bluebirdofoz:20210505183250j:plain

すると以下の通り、NavMesh の追跡先はプレイヤーの足元位置に設定されるようになり、頭部が高い位置にあっても追跡成功と判定されます。
f:id:bluebirdofoz:20210505183259j:plain

空間認識レイヤーを基に足元の位置を決定しているため、以下のようにホロモンが登れない高い場所に上った場合はちゃんと追跡失敗と判定されます。
f:id:bluebirdofoz:20210505183310j:plain

HoloLens2でホロモンアプリを作る その38(NavMeshAgentによる追跡結果の成否を判定する)

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

今回はNavMeshAgentによる追跡結果の成否を判定するメモです。

NavMeshAgentによる追跡結果の成否を判定する

ホロモンアプリでは NavMeshAgent を使って現実空間でホロモンが人を追跡できるようにしています。
bluebirdofoz.hatenablog.com

ただし、リアルタイムに計測した現実空間のメッシュでは想定外の障害物やメッシュの抜けなどが発生するため、目標の位置に辿り着けないことも多発します。
NavMeshAgent はその状況で到達可能な位置に到達した時点で追跡を完了するので、実際に目標位置に到達できたかは別に判定する必要があります。

以下の通り、移動が完了した時点でターゲットのポジションまでの直線距離を計算して到達できたか判定するコードを追加しました。
・HoloMonModeLogicTargetTracking

    // ----- 前略 -----

                // 現在の速度を取得する
                float agentSpeed = HoloMonComponentsSingleton.Instance.GetNavMeshAgent().velocity.magnitude;

                // 移動中か否か
                if (agentSpeed <= 0.0f)
                {
                    // 移動していないならカウントアップ
                    StopCheckCount++;

                    // 敷居値回数以上停止していれば停止と判定する
                    if (StopCheckCount > StopCheckThreshold)
                    {
                        // 現時点のターゲットまでの距離
                        float targetDistance = Vector3.Distance(
                            p_TargetPoint.position,                                                  // ターゲットの座標
                            HoloMonComponentsSingleton.Instance.GetNavMeshAgent().transform.position // Agentの現在座標
                            );

                        // ターゲットに到達したと判定する距離(ここではNavMeshAgentに指定した停止距離をそのまま利用)
                        float stoppingDistance = HoloMonComponentsSingleton.
                            Instance.GetNavMeshAgent().stoppingDistance;

                        // 移動していなければ追跡を停止し、到達したか否かの判定を行う
                        if (targetDistance <= stoppingDistance)
                        {
                            // 到達場所が停止距離以内であれば追跡達成と判定する
                            Debug.Log("Tracking Achievement");
                            DisableSetting(HoloMonModeStatus.Achievement);
                        }
                        else
                        {
                            // 到達場所が停止距離以遠であれば追跡失敗と判定する
                            Debug.Log("Tracking Missing");
                            DisableSetting(HoloMonModeStatus.Missing);
                        }
                    }
                }

    // ----- 後略 -----

f:id:bluebirdofoz:20210504233410j:plain

NavMeshAgent の destination は到達可能な範囲での座標を指してしまうため、距離の計算には直接ターゲットとしているトランスフォームを指定しています。
docs.unity3d.com

シーンを再生して動作を確認してみます。
ホロモンに「おいで」と言って、プレイヤーを追跡させます。
f:id:bluebirdofoz:20210504233437j:plain

間に障害物などがなく、目の前まで追跡できた場合は追跡達成と判定されました。
f:id:bluebirdofoz:20210504233449j:plain

次に障害物があったり、メッシュが抜けていてプレイヤーに到達できないパターンを試してみます。
この場合は追跡失敗と判定されました。
f:id:bluebirdofoz:20210504233503j:plain

HoloLens2でホロモンアプリを作る その37(UniRxを使って1つの関数でモーションを組み合わせた時系列の処理を記述する)

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

今回はUniRxを使って1つの関数でモーションを組み合わせた時系列の処理を記述するメモです。

1つの関数でモーションを組み合わせた時系列の処理を記述する

今回はじゃんけんのアクションの初めに、プレイヤーの方向を向く処理を追加します。
ホロモンがこちらを向く処理は既に実装済みなので、これを活用します。
bluebirdofoz.hatenablog.com

このためには本処理が完了した後に、じゃんけんのモーションを開始する必要があります。
今回は UniRx を使って時系列の処理を1つの関数に以下のように記述してみました。

        /// <summary>
        /// 継続実行用トリガー
        /// </summary>
        IDisposable p_ContinueActionTrigger;

        /// <summary>
        /// ターゲットの方を向くアクションの要求(1モーション例)
        /// </summary>
        public void ActTurnLookPlayer(GameObject a_TargetObject, HoloMonActionOption a_ActionOption)
        {
            // プレイヤーの方向を向くモーション
            HoloMonModeStatusSingleton.Instance.ChangeModeTargetTurn(a_TargetObject.transform);

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

            // 完了判定のトリガーを登録する
            p_ContinueActionTrigger = HoloMonModeStatusSingleton.Instance
                .IObservableHoloMonModeEveryValueChanged
                .ObserveOnMainThread()
                .Subscribe(holoMonMode => {
                    // モード変更を検知すればアクションは終了
                    CheckHoloMonMode(holoMonMode);
                })
                .AddTo(this);
        }

        /// <summary>
        /// じゃんけんで遊ぶアクションの要求(2モーション例)
        /// </summary>
        public void ActPlayJanken(GameObject a_TargetObject, HoloMonActionOption a_ActionOption)
        {
            // プレイヤーの方向を向く
            HoloMonModeStatusSingleton.Instance.ChangeModeTargetTurn(a_TargetObject.transform);


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

            // スタンバイになったタイミングで次のイベントを発生させる
            p_ContinueActionTrigger = HoloMonModeStatusSingleton.Instance.IObservableHoloMonModeEveryValueChanged
                .First(mode => mode == HoloMonMode.Standby)
                .ObserveOnMainThread()
                .Subscribe(mode => {
                    // じゃんけんモードに切り替える
                    HoloMonModeStatusSingleton.Instance.ChangeModeRockPaperScissors();

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

                    // 完了判定のトリガーを登録する
                    p_ContinueActionTrigger = HoloMonModeStatusSingleton.Instance
                        .IObservableHoloMonModeEveryValueChanged
                        .ObserveOnMainThread()
                        .Subscribe(holoMonMode => {
                            // モード変更を検知すればアクションは終了
                            CheckHoloMonMode(holoMonMode);
                        })
                        .AddTo(this);
                })
                .AddTo(this);
        }

f:id:bluebirdofoz:20210503224821j:plain

UniRx を利用して、任意のタイミングで次のアクションを実行させるトリガーを入れ子に記述しています。
これにより、1つの関数で次のアクションが起こる条件と順序を全て記載しておくことができ、処理の流れがわかりやすくなります。
また、別のアクションが割込みで呼ばれた場合はトリガーが破棄されます。

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

「ジャンケン」と言うとホロモンが最初にこちらを向くモーションを行い、それが完了してからジャンケンを行うようになりました。
f:id:bluebirdofoz:20210503224916j:plain

HoloLens2でホロモンアプリを作る その36(見えないプレイヤーに呼ばれたときに周りを見渡して探す)

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

今回は見えないプレイヤーに呼ばれたときに周りを見渡して探すメモです。

見えないプレイヤーに呼ばれたときに周りを見渡して探す

前回記事でホロモンが周りを見渡すことで物を発見できるようにしました。
bluebirdofoz.hatenablog.com

今回はこのアクションを利用して、ホロモンがプレイヤーに呼ばれたときに周りを見渡してプレイヤーを発見するようにします。
視界に人のオブジェクトが含まれているかの判定は以下の記事で作成したスクリプトを利用します。
bluebirdofoz.hatenablog.com

プレイヤーに呼ばれた場合、視界スクリプトにプレイヤーが検出されているか確認し、プレイヤーが検出されていなければ周りを見渡すアクションを行うスクリプトを作成しました。
・HoloMonAICenter.cs

        public void RequestTurnLookPlayer()
        {
            // 目的を設定する
            p_HoloMonPurpose = new HoloMonPurpose(HoloMonPurposeType.TurnTo, HoloMonPurposeTarget.Friend);

            // 現在プレイヤーが見えているか否か
            string friendName = HoloMonFieldOfVisionSingleton.Instance.FindFriend();

            if(friendName != "")
            {
                // アクションのオプションを設定する
                GameObject targetObject = Camera.main.gameObject;
                HoloMonActionOption actionOption = new HoloMonActionOption(
                    HoloMonActionHead.LookAtTarget, targetObject, HoloMonActionTail.Nothing);

                // プレイヤーが見えていればプレイヤーの方を向く
                p_HoloMonActionController.ActTurnLookPlayer(targetObject, actionOption);
            }
            else
            {
                // アクションのオプションを設定する
                HoloMonActionOption actionOption = new HoloMonActionOption(
                    HoloMonActionHead.Nothing, null, HoloMonActionTail.Nothing);

                // プレイヤーが見えていなければ周囲を見回す
                p_HoloMonActionController.ActLookAround(actionOption);
            }
        }

f:id:bluebirdofoz:20210502234025j:plain

視界に人の特徴を持つオブジェクトが検出された場合、それを呼びかけた人と判断して再度振り向き処理を行います。
ホロモンの行動目的とアクションを参照することで、複数のアクションを組み合わせて目的を達成します。
・HoloMonAIVisionStudy.cs

        /// <summary>
        /// 視界内オブジェクト発見状態の変化時のアクションを実行する
        /// </summary>
        private void FieldOfVisionObject()
        {
            // 見てるもののオブジェクト情報を取得する
            HoloMonFieldOfVisionObject visionObject = HoloMonFieldOfVisionSingleton.
                Instance.IReadOnlyReactivePropertyHoloMonVision.Value;

            // 見てるものがあるか
            bool isVision = visionObject.Vision;

            // 現在のホロモンの行動目的を取得する
            HoloMonPurpose currentHolomonPurpose = p_HoloMonAI.GetCurrentPurpose();

            // 現在のホロモンのアクション種別を取得する
            HoloMonActionTag currentHolomonActionType = p_HoloMonAI.GetCurrentActionTag();

            switch (currentHolomonPurpose.PurposeType)
            {
                case HoloMonPurposeType.Nothing:
                case HoloMonPurposeType.LookAt:
                    // 目的無し、または、何らかの注視中、物探し中は反応する
                    if (isVision)
                    {
                        switch (visionObject.UnderstandType)
                        {
                            case ObjectUnderstandType.NewSpawn:
                                // 新発見オブジェクトが見えている場合はオブジェクトの方を見てオブジェクト種別をチェックする
                                p_HoloMonAI.RequestLookingNewSpawn(visionObject.GameObject);
                                break;
                            case ObjectUnderstandType.FriendFace:
                                // 友達オブジェクトが見えている場合はオブジェクトの方を見る
                                p_HoloMonAI.RequestLookingFriend(visionObject.GameObject);
                                break;
                            case ObjectUnderstandType.FriendRightHand:
                                break;
                            case ObjectUnderstandType.FriendLeftHand:
                                break;
                            case ObjectUnderstandType.Food:
                                break;
                            case ObjectUnderstandType.Shit:
                                break;
                            case ObjectUnderstandType.Attention:
                                // 注目オブジェクトが見えている場合はオブジェクトの方を見る
                                p_HoloMonAI.RequestLookingAttention(visionObject.GameObject);
                                break;
                            default:
                                // 注視したいオブジェクトが存在しない場合は待機アクションに戻る
                                p_HoloMonAI.RequestStandby();
                                break;
                        }
                    }
                    else
                    {
                        // 視界にオブジェクトが存在しない場合は待機アクションに戻る
                        p_HoloMonAI.RequestStandby();
                        break;
                    }
                    break;
                case HoloMonPurposeType.LookingFor:
                    break;
                case HoloMonPurposeType.TurnTo:
                    if (currentHolomonActionType == HoloMonActionTag.LookAround)
                    {
                        if (isVision)
                        {
                            switch (visionObject.UnderstandType)
                            {
                                case ObjectUnderstandType.NewSpawn:
                                    // 新発見オブジェクトが見えている場合はそのままオブジェクト種別をチェックする
                                    p_HoloMonAI.RequestCheckNewSpawn(visionObject.GameObject);
                                    break;
                                case ObjectUnderstandType.FriendFace:
                                    if (currentHolomonPurpose.PurposeTarget == HoloMonPurposeTarget.Friend)
                                    {
                                        // 友達オブジェクトを探していた場合はターン処理を再実行する
                                        p_HoloMonAI.RequestTurnLookPlayer();
                                    }
                                    break;
                                default:
                                    break;
                            }
                        }
                    }
                    break;
                case HoloMonPurposeType.TrackingFor:
                    break;
                case HoloMonPurposeType.StopWaiting:
                    break;
                case HoloMonPurposeType.Eating:
                    break;
                case HoloMonPurposeType.Playing:
                    break;
                case HoloMonPurposeType.Sleeping:
                    break;
                case HoloMonPurposeType.ShitPutout:
                    break;
                default:
                    break;
            }

f:id:bluebirdofoz:20210502234041j:plain

シーンを再生して動作を確認します。
ホロモンがこちらを発見しているときに「ホロモン」と声をかけてみます。
f:id:bluebirdofoz:20210502234056j:plain

このときはホロモンがそのままこちらを向きます。
f:id:bluebirdofoz:20210502234110j:plain

次にホロモンがこちらを見失っているときに「ホロモン」と声をかけてみます。
f:id:bluebirdofoz:20210502234124j:plain

ホロモンが顔を振って周りを見渡し、こちらを探します。
f:id:bluebirdofoz:20210502234139j:plain

こちらを視界に収めて発見したときに、こちらを向いてくれました。
f:id:bluebirdofoz:20210502234152j:plain

HoloLens2でホロモンアプリを作る その35(ホロモンが周りを見渡して物を発見する)

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

今回はホロモンが周りを見渡して物を発見するメモです。

ホロモンが周りを見渡して物を発見する

以下の記事でホロモンの頭部ボーンを基に視界範囲を判定するスクリプトを作成しました。
bluebirdofoz.hatenablog.com

今回はこのロジックを活用して、ホロモンが周りを見渡して物を発見できるようにしてみます。
ホロモンのアニメーションに新たに首を左右に大きく振るアニメーションを追加作成します。
f:id:bluebirdofoz:20210501231043j:plain

fbx ファイルでデータを出力して Unity に取り込みます。
この際、以下の手順を実行すると、アニメーションデータのみを取り出すことができます。
bluebirdofoz.hatenablog.com

作成したアニメーションを新たなモーションとして追加します。
f:id:bluebirdofoz:20210501231104j:plain

再生モーションを指定してシーンを再生します。
プレイヤーが背後にいるときは、ホロモンはプレイヤーを発見できません。
f:id:bluebirdofoz:20210501231118j:plain

ホロモンが周りを見渡すモーションを始めると、頭部トランスフォームに合わせて視野が動きます。
これでホロモン自身が周りを見渡すことでプレイヤーを発見することができました。
f:id:bluebirdofoz:20210501231132j:plain