MRが楽しい

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

WorldLockingToolsのサンプルシーンを試す

本日は HoloLens2 の技術調査枠です。
WorldLockingTools のサンプルシーンを試す手順を記事にします。

WorldLockingTools

WorldLockingTools ホログラフィックを現実空間にバインドするための座標系をプロジェクトに提供します。
物理的な世界の特徴に関連付けて配置されたホログラムはその特徴を持つ現実空間に固定されます。
補正を行う際はホログラムの位置ではなく座標系を補正するため、ホログラム間の相対座標は保持されます。
microsoft.github.io
f:id:bluebirdofoz:20210723232112j:plain

WorldLockingTools の仕組みやメリットを理解したい場合は以下の記事が分かりやすいです。
www.tattichan.work

本記事では上記の記事で紹介されている WorldLockingBasis のサンプルシーンを試してみます。

サンプル用プロジェクトを作成する

Unity プロジェクトを新規作成します。
WorldLockingTools のパッケージはパスが深いため、一定以上のパスの深さにプロジェクトを作成すると FileNotFound のエラーが発生することがあります。
なるべく作成フォルダとプロジェクト名を短いパスに抑えておくことをお勧めします。
f:id:bluebirdofoz:20210723232210j:plain

Unity プロジェクトを作成したら次はパッケージをインポートします。
f:id:bluebirdofoz:20210723232218j:plain

WorldLockingToolsのインポート

WorldLockingTools は Mixed Reality Feature Tool を使ってインポートできます。
Mixed Reality Feature Tool の利用手順は以下の記事を参照ください。
bluebirdofoz.hatenablog.com

[World Locking Tools]の項目から[WLT Core]と[WLT Sample]にチェックを入れて有効にします。
f:id:bluebirdofoz:20210723232236j:plain

必要であれば MRTK のパッケージも合わせてインポートします。
ただし WorldLockingTools の利用に MRTK のインポートは必須ではありません。
f:id:bluebirdofoz:20210723232249j:plain

パッケージを取り込んだら再び Unity プロジェクトを開きます。
f:id:bluebirdofoz:20210723232257j:plain

今回はここでプラットフォームの変更や XR プラグインの設定を行っておきました。
f:id:bluebirdofoz:20210723232307j:plain

サンプルシーンのインポート

WorldLockingTools のサンプルシーンを利用するには更に UnityPackageManager から Samples を取得します。
メニューから[Window -> PackageManger]を選択し、[PackageManager]ダイアログを開きます。
f:id:bluebirdofoz:20210723232348j:plain

検索先のプルダウンから[In Project]を選択し、[com.microsoft.mixedreality.worldlockingsamples]を検索します。
f:id:bluebirdofoz:20210723232400j:plain

表示された[WLT Samples]を選択し、[Import into Project]ボタンをクリックします。
f:id:bluebirdofoz:20210723232409j:plain

これで WorldLockingTools のサンプルシーンがインポートされます。
以下のフォルダからサンプルシーンを選択できます。
・Assets/Samples/WLT Samples/1.3.5/WorldLockingExampies/Scenes
f:id:bluebirdofoz:20210723232419j:plain

WorldLockingBasis を選択してサンプルシーンを開きます。
f:id:bluebirdofoz:20210723232457j:plain

プロジェクトをビルドして HoloLens2 にインストールします。
インストール手順は以下の記事を参考にしてください。
bluebirdofoz.hatenablog.com

動作確認

HoloLens2 上でアプリを起動すると、起動位置に spongy オブジェクトが配置されます。
空間を歩き回り、元の位置に戻ってきたときに座標系が補正され、ホログラムの配置が現実空間の正しい位置に補正されることを確認できます。
f:id:bluebirdofoz:20210723232513j:plain

Meshのローカル座標とワールド座標のBoundsをそれぞれ取得する

本日は Unity の小ネタ枠です。
Meshのローカル座標とワールド座標のBoundsをそれぞれ取得する手順を記事にします。

MeshのBounds取得

Mesh の Bounds 情報をスクリプトから取得するには MeshFilter と MeshRenderer の2つのアクセス方法があります。
MeshFilter からはローカル座標の Bounds、MeshRenderer からはワールド座標の Bounds 情報が取得可能です。

MeshFilterからのローカル座標のBounds取得

ローカル座標の Bounds は以下のように取得します。

Mesh mesh = GetComponent<MeshFilter>().mesh;
Bounds bounds = mesh.bounds;

docs.unity3d.com
docs.unity3d.com
docs.unity3d.com

MeshRendererからのワールド座標のBounds取得方法

ワールド座標の Bounds は以下のように取得します。

Renderer renderer = GetComponent<Renderer>();
Bounds bounds = renderer.bounds;

docs.unity3d.com
docs.unity3d.com

サンプルスクリプト

以下の記事で作成した Mesh の bounds をもとにワールド座標のスケールを調整するスクリプトを改修してみます。
bluebirdofoz.hatenablog.com

スクリプトは以下の処理を行います。
1.アタッチしたゲームオブジェクトの子オブジェクトを全てチェックする。
2.MeshRenderer が設定されているオブジェクトからメッシュ形状の Bounds を取得する
3.取得したメッシュ形状の Bounds 全てを含むワールド座標系の Bounds を取得する
4.ワールド座標の Bounds の最大長の辺が指定のスケールに収まるようにローカルスケールを調整する
5.アタッチしたゲームオブジェクトのローカル座標系の Bounds を設定する
6.シーンで結果が確認できるように Bounds サイズの BoxCollider を設定する
・RendererControlChildObjBounds.cs

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

public class RendererControlChildObjBounds : MonoBehaviour
{
    /// <summary>
    /// 子オブジェクトの統合Bounds
    /// </summary>
    public Bounds childObjBounds;
    
    void Start()
    {
        // オブジェクトのローカルスケールをオブジェクトが 1m 長に収まるよう調整する
        ChangeWorldBoundsSize(1.0f);

        // Boundsの大きさと形状が見た目に分かるようコライダーを追加する
        BoxCollider collider = this.gameObject.AddComponent<BoxCollider>();
        // 計算されたバウンドボックスに合わせてコライダーの大きさと位置を変更する
        collider.center = childObjBounds.center;
        collider.size = childObjBounds.size;
    }

    /// <summary>
    /// ワールド座標での全体のバウンドサイズを元にローカルスケールを調整する
    /// </summary>
    public void ChangeWorldBoundsSize(float size)
    {
        // ワールド座標のバウンドサイズを計算する
        Bounds objBounds = CalcChildObjWorldBounds(this.gameObject, new Bounds());

        // バウンドの最大長の辺の長さを取得する
        float maxlength = Mathf.Max(objBounds.size.x, objBounds.size.y, objBounds.size.z);

        // スケール調整の係数を取得する
        float coefficient = size / maxlength;

        // ローカルスケールを変更する
        this.transform.localScale = this.transform.localScale * coefficient;

        // ローカル座標でのバウンドサイズを計算する
        childObjBounds = CalcLocalObjBounds(this.gameObject);
    }

    /// <summary>
    /// 現在オブジェクトのローカル座標でのバウンド計算
    /// </summary>
    private Bounds CalcLocalObjBounds(GameObject obj)
    {
        // 指定オブジェクトのワールドバウンドを計算する
        Bounds totalBounds = CalcChildObjWorldBounds(obj, new Bounds());

        // ローカルオブジェクトの相対座標に合わせてバウンドを再計算する
        // オブジェクトのワールド座標とサイズを取得する
        Vector3 ObjWorldPosition = this.transform.position;
        Vector3 ObjWorldScale = this.transform.lossyScale;

        // バウンドのローカル座標とサイズを取得する
        Vector3 totalBoundsLocalCenter = new Vector3(
            (totalBounds.center.x - ObjWorldPosition.x) / ObjWorldScale.x,
            (totalBounds.center.y - ObjWorldPosition.y) / ObjWorldScale.y,
            (totalBounds.center.z - ObjWorldPosition.z) / ObjWorldScale.z);
        Vector3 meshBoundsLocalSize = new Vector3(
            totalBounds.size.x / ObjWorldScale.x,
            totalBounds.size.y / ObjWorldScale.y,
            totalBounds.size.z / ObjWorldScale.z);

        Bounds localBounds = new Bounds(totalBoundsLocalCenter, meshBoundsLocalSize);

        return localBounds;
    }

    /// <summary>
    /// 子オブジェクトのワールド座標でのバウンド計算(再帰処理)
    /// </summary>
    private Bounds CalcChildObjWorldBounds(GameObject obj, Bounds bounds)
    {
        // 指定オブジェクトの全ての子オブジェクトをチェックする
        foreach (Transform child in obj.transform)
        {
            if(!child.gameObject.activeSelf)
            {
                // 無効なゲームオブジェクトは無視する
                continue;
            }

            // メッシュレンダラーの存在確認
            MeshRenderer renderer = child.gameObject.GetComponent<MeshRenderer>();

            if (renderer != null)
            {
                // フィルターのメッシュ情報からバウンドボックスを取得する
                Bounds meshBounds = renderer.bounds;

                // バウンドのワールド座標とサイズを取得する
                Vector3 meshBoundsWorldCenter = meshBounds.center;
                Vector3 meshBoundsWorldSize = meshBounds.size;

                // バウンドの最小座標と最大座標を取得する
                Vector3 meshBoundsWorldMin = meshBoundsWorldCenter - (meshBoundsWorldSize / 2);
                Vector3 meshBoundsWorldMax = meshBoundsWorldCenter + (meshBoundsWorldSize / 2);

                // 取得した最小座標と最大座標を含むように拡大/縮小を行う
                if (bounds.size == Vector3.zero)
                {
                    // 元バウンドのサイズがゼロの場合はバウンドを作り直す
                    bounds = new Bounds(meshBoundsWorldCenter, Vector3.zero);
                }
                bounds.Encapsulate(meshBoundsWorldMin);
                bounds.Encapsulate(meshBoundsWorldMax);
            }

            // 再帰処理
            bounds = CalcChildObjWorldBounds(child.gameObject, bounds);
        }
        return bounds;
    }
}

f:id:bluebirdofoz:20210722231742j:plain

MeshFilter で Bounds 情報を取得していた個所を MeshRenderer で取得することでよりシンプルに記述できました。
指定のスケールサイズに収めたい子オブジェクトを持つオブジェクトに、スクリプトを設定します。
f:id:bluebirdofoz:20210722231756j:plain

実行確認

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

シーンの再生と同時に、子オブジェクトが 1m 立方のサイズに収まるようにオブジェクトが縮小されます。
f:id:bluebirdofoz:20210722232218j:plain

スクリプトからUnityEventに引数有りのリスナーを登録する

本日は UnityEvent の小ネタ枠です。
スクリプトからUnityEventに引数有りのリスナーを登録する方法を記事にします。

前提条件

前回記事の続きです。
今回はスクリプトからの登録となるため、非永続的リスナーでの登録方法となります。
bluebirdofoz.hatenablog.com

引数有りの登録方法

以下のように AddListener に関数を登録することで、動的に引数を指定することができます。

UnityEventInstance.AddListener(() => CallMethod(args));

サンプルスクリプト

文字列リストを基に、引数を指定したリスナーを登録して実行する以下のスクリプトを作成しました。
・CheckUnityEventArgs.cs

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

public class CheckUnityEventArgs : MonoBehaviour
{
    /// <summary>
    /// テスト用UnityEvent
    /// </summary>
    public UnityEvent TestEvent;

    /// <summary>
    /// テスト用引数リスト
    /// </summary>
    public List<string> MessageList;

    /// <summary>
    /// リスナーの登録とイベントの実行
    /// </summary>
    public void AddListenerAndInvoke()
    {
        Debug.Log("AddListenerAndInvoke");

        // リスナーを一旦全て破棄する
        TestEvent.RemoveAllListeners();

        // リストのメッセージを個々に引数に渡すリスナーを登録する
        foreach(string message in MessageList)
        {
            TestEvent.AddListener(() => TestCallMethod(message));
        }

        // 登録リスナーを呼び出す
        TestEvent.Invoke();
    }

    /// <summary>
    /// 試験用呼び出しメソッド
    /// </summary>
    public void TestCallMethod(string message)
    {
        Debug.Log("TestCallMethod Called !!! : " + message);
    }
}

適当なゲームオブジェクトに設定し、引数で渡す文字列リストを作成しておきます。
f:id:bluebirdofoz:20210721233138j:plain

動作確認

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

関数を実行すると、引数にリストの文字列を指定されたイベントが実行されます。
f:id:bluebirdofoz:20210721233200j:plain

UnityEventの永続的リスナーと非永続的リスナーの違い

本日は Unity の小ネタ枠です。
UnityEventの永続リスナーと非永続リスナーの違いについて理解できていなかったので記事にします。
f:id:bluebirdofoz:20210720223636j:plain

UnityEvent

Scene に保存可能なコールバック関数を登録します。
docs.unity3d.com

UnityEvent へのコールバック関数の登録は Inspector ビューから行う方法と AddListener を用いる方法があります。
f:id:bluebirdofoz:20210720223655j:plain

docs.unity3d.com

UnityEvent m_MyEvent = new UnityEvent();
void Start()
{
    // イベントへのリスナー登録
    m_MyEvent.AddListener(MyAction);
}

永続的リスナーと非永続的リスナー

UnityEvent に登録されるリスナーには永続的リスナーと非永続的リスナーの2種類のリスナーが存在します。
Inspector ビューは永続的リスナーを、AddListener は非永続的リスナーを登録します。

サンプルスクリプト

動作を実際に確認するため、以下のサンプルスクリプトを作成しました。
・CheckUnityEvent.cs

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

public class CheckUnityEvent : MonoBehaviour
{
    /// <summary>
    /// テスト用UnityEvent
    /// </summary>
    public UnityEvent TestEvent;

    /// <summary>
    /// UnityEventの実行
    /// </summary>
    public void InvokeEvent()
    {
        // 登録リスナーを呼び出す
        TestEvent.Invoke();
    }

    /// <summary>
    /// 登録リスナー数の確認
    /// </summary>
    public void CheckAddListenerCount()
    {
        Debug.Log("Listener Count : " + TestEvent.GetPersistentEventCount().ToString());
    }

    /// <summary>
    /// スクリプトからのリスナー登録
    /// </summary>
    public void TestAddListener()
    {
        Debug.Log("TestAddListener Called !!!");
        TestEvent.AddListener(TestCallMethod);
    }

    /// <summary>
    /// 試験用呼び出しメソッド
    /// </summary>
    public void TestCallMethod()
    {
        Debug.Log("TestCallMethod Called !!!");
    }
}

永続的リスナー

Inspector ビューから登録したリスナーは永続的リスナーになります。
登録済みの永続的リスナーは Inspector 上から確認できます。
f:id:bluebirdofoz:20210720223724j:plain

永続的リスナーはその登録数を GetPersistentEventCount 関数で取得できます。
f:id:bluebirdofoz:20210720223735j:plain

Invoke で関数呼び出しを実行します。
f:id:bluebirdofoz:20210720223745j:plain

非永続的リスナー

スクリプトから AddListener 関数で登録したリスナーは非永続的リスナーになります。
非永続的リスナーは Inspector 上から確認できません。
f:id:bluebirdofoz:20210720223756j:plain

また、その登録数を GetPersistentEventCount 関数で取得できません。
f:id:bluebirdofoz:20210720223813j:plain

永続的リスナーと同じく Invoke で関数呼び出しを実行します。
f:id:bluebirdofoz:20210720223824j:plain

なお、永続的リスナーと非永続的リスナーは同時に登録可能です。
以下は Inspector からの登録と AddListener の登録を行った場合です。
それぞれの登録関数が実行され、2回メッセージが表示されています。
f:id:bluebirdofoz:20210720223834j:plain

正常に起動できなくなった HoloLens2 を回復モードでリカバリーする

本日は HoloLens2 の小ネタ枠です。
正常に起動できなくなった HoloLens2 を回復モードでリカバリーする手順を記事にします。

回復モードでの手動リカバリ

HoloLens2 が正常に起動しない場合、HoloLens2 の回復モードと Advanced Recovery Companion アプリを使って HoloLens2 をリカバリできます。
docs.microsoft.com

今回は筆者の HoloLens2 を[リセットと回復]で初期化しようとしたところ、延々と再起動を繰り返す事象が発生したため、こちらの手段を利用しました。

Advanced Recovery Companion アプリのインストール

以下の Microsoft Store から Advanced Recovery Companion アプリをインストールします。
www.microsoft.com
f:id:bluebirdofoz:20210719015106j:plain

インストールを行ったらアプリを起動します。
f:id:bluebirdofoz:20210719015117j:plain

HoloLens2の回復モード

[音量+]のボタンを押しながら[電源]ボタンを押して HoloLens2 を起動します。
[電源]ボタンからは手を離し、[音量+]のボタンを15秒間押しっぱなしにしていると HoloLens2 が回復モードになります。

HoloLens2 が回復モードになると 5 つのLED のうち、中央の LED だけが点灯します。
ただしリカバリの実行には HoloLens2 の充電が少なくとも 40% 以上になっている必要があります。
f:id:bluebirdofoz:20210719015129j:plain

AdvancedRecoveryCompanionアプリの更新プロセス

AdvancedRecoveryCompanion アプリを起動すると、PC に接続中のデバイスが自動で検出されます。
回復モードの HoloLens2 を PC にUSB接続して検出されるのを待機します。
f:id:bluebirdofoz:20210719015141j:plain

検出に成功すると、以下のように HoloLens2 のアイコンが一覧に表示されます。
これを選択して HoloLens2 の更新プロセスを開始します。
f:id:bluebirdofoz:20210719015151j:plain

[ソフトウェアのインストール]ボタンをクリックすると、最新の OS イメージがインターネットからダウンロードされてインストールされます。
[手動によるパッケージの選択]を選択して、ローカルの FFU ファイルを OS イメージとして指定することもできます。
f:id:bluebirdofoz:20210719015204j:plain

実行の確認画面が表示されるので[続行]ボタンをクリックすると、リカバリが開始されます。
f:id:bluebirdofoz:20210719015213j:plain

HoloLens2 のリカバリが完了すると以下の完了画面が表示されます。
f:id:bluebirdofoz:20210719015223j:plain

HoloLens2でホロモンアプリを作る その48(NavMeshを使わずにホロモンの追跡処理を行う)

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

今回は NavMesh を使わずにホロモンの追跡処理を行うメモです。

前提条件

以下の記事ではホロモンのプレイヤー追跡に NavMesh を利用しました。
しかし現実空間では HoloLens の認識する空間メッシュが逐次変化するため、リアルタイムの追跡で失敗することが多いです。
bluebirdofoz.hatenablog.com

そこで NavMesh を使わない追跡処理もホロモンアプリに実装してみました。

実装コード

以下の追跡スクリプトを作成しました。

適当なオブジェクトに追加し、プレイヤー追跡の要求から呼び出せるようにします。
f:id:bluebirdofoz:20210718235203j:plain

動作確認

シーンを再生し、動作を確認します。
ホロモンに「おいで」と声をかけてみます。
f:id:bluebirdofoz:20210718235216j:plain

ホロモンがプレイヤーの方に向かって追跡移動を開始します。
f:id:bluebirdofoz:20210718235229j:plain

一定距離まで近づくと、追跡を完了したと判定して止まります。
f:id:bluebirdofoz:20210718235242j:plain

NavMesh と異なり、プレイヤーに向かって真っすぐに進んでくるのみで障害物を避けたりするロジックは実装していません。
一定時間移動できないとスタックしたと判断し、追跡失敗の判定で止まります。
f:id:bluebirdofoz:20210718235253j:plain

HoloLens2でホロモンアプリを作る その47(async/awaitを使ってモーションを組み合わせた時系列の処理を記述する)

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

今回は async/await を使ってモーションを組み合わせた時系列の処理を記述するメモです。
以下の記事の実装のリファクタリングになります。
bluebirdofoz.hatenablog.com

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

async/await を使ってリファクタリングした後の2つの関数例を以下に示します。
・「プレイヤーの方向を向く処理」を単体で行う関数
・「プレイヤーを探す」⇒「プレイヤーの方向を向く処理」⇒「じゃんけん」の3つの処理を時系列に行う関数

/// <summary>
/// 指定ターゲットの方を向くアクションの開始(1モーション例)
/// </summary>
public async UniTask<bool> StartTurnLookTargetAsync(GameObject a_TargetObject)
{
    try
    {
        // アクション名の定義
        HoloMonActionFunction actionFunction = HoloMonActionFunction.TurnLookTarget;

        // ターゲットの方向を向くモードのアクション設定を指定する
        p_HoloMonAction = new HoloMonAction(actionFunction,
            new HoloMonActionSettings(
                new ModeLogicSetting(new ModeLogicTurnTargetData(a_TargetObject)),
                new HeadLogicSetting(new HeadLogicLookAtTargetData(a_TargetObject)),
                new TailLogicSetting(new TailLogicNoOverrideData())
            ));

        // 指定モードのアクションに切り替えて完了を待機する
        ModeLogicResult modeLogicResult =
            await p_HoloMonActUnitChanger.RunActUnitAsync(p_HoloMonAction.ActionSettings);

        return true;
    }
    catch (OperationCanceledException ex)
    {
        Debug.Log(ex.Message);

        return false;
    }
}

/// <summary>
/// じゃんけんで遊ぶアクションの開始(3モーション例)
/// </summary>
public async UniTask<bool> StartJankenTargetAsync()
{
    try
    {
        // アクション名の定義
        HoloMonActionFunction actionFunction = HoloMonActionFunction.JankenTarget;

        // 友人に注目する
        ObjectUnderstandType targetType = ObjectUnderstandType.FriendFace;

        // 指定種別のオブジェクトが見えているか否か
        GameObject targetVisionObject =
            HoloMonFocusOfVisionSingleton.Instance.FindTypeObject(targetType)?.GameObject;

        // モード結果の変数を用意する
        ModeLogicResult modeLogicResult;

        if (targetVisionObject == null)
        {
            // ターゲットが見えていない場合

            // 見回しモードのアクション設定を指定する
            p_HoloMonAction = new HoloMonAction(actionFunction,
                new HoloMonActionSettings(
                    new ModeLogicSetting(new ModeLogicLookAroundData(targetType)),
                    new HeadLogicSetting(new HeadLogicNoOverrideData()),
                    new TailLogicSetting(new TailLogicNoOverrideData())
                ));

            // 指定モードのアクションに切り替えて完了を待機する
            modeLogicResult = await p_HoloMonActUnitChanger.RunActUnitAsync(p_HoloMonAction.ActionSettings);

            // 結果に応じて次のイベントを発生させる
            if (modeLogicResult.FinishModeStatus != HoloMonActionModeStatus.Achievement)
            {
                // ロジックを達成できなかった場合

                // 処理を終了する
                return true;
            }

            // 発見オブジェクトを取得する
            targetVisionObject = modeLogicResult.ModeLogicLookAroundReturn.FindedObject;
        }

        // 振り向きモードのアクション設定を指定する
        p_HoloMonAction = new HoloMonAction(actionFunction,
            new HoloMonActionSettings(
                new ModeLogicSetting(new ModeLogicTurnTargetData(targetVisionObject)),
                new HeadLogicSetting(new HeadLogicLookAtTargetData(targetVisionObject)),
                new TailLogicSetting(new TailLogicNoOverrideData())
            ));

        // 指定モードのアクションに切り替えて完了を待機する
        modeLogicResult =
            await p_HoloMonActUnitChanger.RunActUnitAsync(p_HoloMonAction.ActionSettings);

        // 結果に応じて次のイベントを発生させる
        if (modeLogicResult.FinishModeStatus != HoloMonActionModeStatus.Achievement)
        {
            // ロジックを達成できなかった場合

            // 処理を終了する
            return true;
        }

        // じゃんけんモードのアクション設定を指定する
        p_HoloMonAction = new HoloMonAction(actionFunction,
        new HoloMonActionSettings(
            new ModeLogicSetting(new ModeLogicJankenData()),
            new HeadLogicSetting(new HeadLogicLookAtTargetData(targetVisionObject)),
            new TailLogicSetting(new TailLogicNoOverrideData())
        ));

        // 指定モードのアクションに切り替えて完了を待機する
        modeLogicResult = await p_HoloMonActUnitChanger.RunActUnitAsync(p_HoloMonAction.ActionSettings);

        return true;
    }
    catch (OperationCanceledException ex)
    {
        Debug.Log(ex.Message);

        return false;
    }
}

f:id:bluebirdofoz:20210717225226j:plain

前回の UniRx を利用した記述方法と比べると、処理を入れ子で記述する必要がなく、処理を時系列にかくことができます。
このため分岐を伴う処理はこちらの方が簡潔に記述できます。

動作確認

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

「じゃんけん」の音声を認識すると、ホロモンは最初にプレイヤーを探して周りを見回します。
f:id:bluebirdofoz:20210717225252j:plain

プレイヤーを発見すると見回しのアクションが終わり、ホロモンはこちらを振り返るアクションを開始します。
f:id:bluebirdofoz:20210717225303j:plain

こちらへの振り返り切ると、ジャンケンのアクションが始まります。
f:id:bluebirdofoz:20210717225316j:plain