MRが楽しい

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

HoloLens2でホロモンアプリを作る その21(ReactivePropertyに構造体を指定する)

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

今回は ReactiveProperty に構造体を指定するメモです。

ReactiveProperty に構造体を指定する

ホロモンの空腹度、ご機嫌度、眠さなどの情報を前回作成した以下のクラスで合わせて管理させるようにしました。
bluebirdofoz.hatenablog.com

これらのステータスを保持する一つの構造体を作成し、ReactiveProperty としました。
以下のように、ステータスの変更を行うスクリプトと参照するスクリプトを作成します。
・HoloMonLifeStatus.cs

using UnityEngine;
using System;
using UniRx;

namespace HMProject.HoloMon
{
    /// <summary>
    /// ホロモンのライフコンディションの構造定義
    /// </summary>
    public struct HoloMonLifeStatus
    {
        /// <summary>
        /// ホロモンの空腹度
        /// </summary>
        public int HungryPercent;

        /// <summary>
        /// ホロモンの機嫌度
        /// </summary>
        public int HappyPercent;

        /// <summary>
        /// ホロモンの眠り度
        /// </summary>
        public HoloMonSleepinessLevel SleepinessLevel;
    }

    /// <summary>
    /// ホロモンの眠り度を表す定義
    /// </summary>
    public enum HoloMonSleepinessLevel
    {
        Nothing = 0,
        Little = 1,
        Sleepy = 2
    }

    /// <summary>
    /// ホロモンのライフコンディションを表すReactiveProperty
    /// </summary>
    [Serializable]
    public class HoloMonLifeStatusReactiveProperty : ReactiveProperty<HoloMonLifeStatus>
    {
        public HoloMonLifeStatusReactiveProperty()
        {
        }
        public HoloMonLifeStatusReactiveProperty(HoloMonLifeStatus a_HoloMonLifeStatus) : base(a_HoloMonLifeStatus)
        {
        }
    }

    public class HoloMonConditionLifeSingleton : MonoBehaviour
    {
        /// <summary>
        /// 単一インスタンス
        /// </summary>
        private static HoloMonConditionLifeSingleton ps_instance;

        /// <summary>
        /// 参照用インスタンス
        /// </summary>
        public static HoloMonConditionLifeSingleton Instance
        {
            get
            {
                if (ps_instance == null && ps_searchForInstance)
                {
                    ps_searchForInstance = false;
                    var ps_instances = FindObjectsOfType<HoloMonConditionLifeSingleton>();
                    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 HoloMonLifeStatusReactiveProperty p_HoloMonLifeStatus
            = new HoloMonLifeStatusReactiveProperty();

        /// <summary>
        /// ホロモンのライフコンディションのReadOnlyReactivePropertyの保持変数
        /// </summary>
        private IReadOnlyReactiveProperty<HoloMonLifeStatus> p_IReadOnlyReactivePropertyHoloMonLifeStatus;

        /// <summary>
        /// ホロモンのライフコンディションのReadOnlyReactivePropertyの参照変数
        /// </summary>
        public IReadOnlyReactiveProperty<HoloMonLifeStatus> IReadOnlyReactivePropertyHoloMonLifeStatus
            => p_IReadOnlyReactivePropertyHoloMonLifeStatus
            ?? (p_IReadOnlyReactivePropertyHoloMonLifeStatus = p_HoloMonLifeStatus.ToReadOnlyReactiveProperty());

        

        /// <summary>
        /// 時刻判定(分刻み)のトリガー
        /// </summary>
        IDisposable p_MinuteTimeTrigger;


        /// <summary>
        /// 眠さの変化
        /// </summary>
        /// <param name="a_Sleepiness"></param>
        private void ReceptionSleepiness(HoloMonSleepinessLevel a_Sleepiness)
        {
            // 変数の登録を行う
            // 非変更時は通知が発生しない
            HoloMonLifeStatus status = p_HoloMonLifeStatus.Value;
            status.SleepinessLevel = a_Sleepiness;
            p_HoloMonLifeStatus.Value = status;
        }


        /// <summary>
        /// 開始処理
        /// </summary>
        void Start()
        {
            // ライフコンディションを初期化
            InitializeLifeStatus();

            // 1分毎にトリガーを実行する
            p_MinuteTimeTrigger = Observable
                .Timer(TimeSpan.FromSeconds(60.0f - DateTime.Now.Second), TimeSpan.FromMinutes(1.0f))
                .SubscribeOnMainThread()
                .Subscribe(x => {
                    ChangeOverTimeCondition(DateTime.Now);
                })
                .AddTo(this);
        }

        /// <summary>
        /// 定期処理
        /// </summary>
        void Update()
        {
        }

        /// <summary>
        /// ライフコンディションを初期化する
        /// </summary>
        private void InitializeLifeStatus()
        {
            // ライフコンディションを初期化する
            //  Todo : 再起動時の読み込み処理
            HoloMonLifeStatus status = p_HoloMonLifeStatus.Value;

            status.HungryPercent = 0;
            status.HappyPercent = 0;
            status.SleepinessLevel = HoloMonSleepinessLevel.Nothing;

            p_HoloMonLifeStatus.Value = status;
        }

        /// <summary>
        /// 時間経過によるコンディション変化を管理する
        /// </summary>
        /// <param name="a_DateTime"></param>
        private void ChangeOverTimeCondition(DateTime a_DateTime)
        {
            Debug.Log("ChangeOverTimeCondition : " + a_DateTime.ToString());

            // 睡眠時刻をチェックする
            CheckSleepTime(a_DateTime);
        }


        /// <summary>
        /// 睡眠時刻をチェックする
        /// </summary>
        /// <param name="a_DataTime"></param>
        private void CheckSleepTime(DateTime a_DataTime)
        {
            // 現在時刻から時刻情報だけを取得する
            TimeSpan timeOfDay = DateTime.Now.TimeOfDay;

            // 21:00 ~ 06:00 の範囲をチェックする
            TimeSpan startTime_am = new TimeSpan(0, 0, 0);
            TimeSpan endTime_am = new TimeSpan(5, 59, 59);
            TimeSpan startTime_pm = new TimeSpan(21, 0, 0);
            TimeSpan endTime_pm = new TimeSpan(23, 59, 59);

            // 時間の範囲内か
            if (((startTime_am <= timeOfDay) && (timeOfDay <= endTime_am)) ||
                ((startTime_pm <= timeOfDay) && (timeOfDay <= endTime_pm)))
            {
                // 範囲内なら眠さ度の変更を行う
                ReceptionSleepiness(HoloMonSleepinessLevel.Sleepy);
            }
            else
            {
                // 範囲外なら眠さ度の変更を行う
                ReceptionSleepiness(HoloMonSleepinessLevel.Nothing);
            }
        }
    }
}

f:id:bluebirdofoz:20210327234346j:plain

・HoloMonAISingleton.cs

using UnityEngine;
using UniRx;

using HMProject.HoloMonLogic;

namespace HMProject.HoloMon
{
    public class HoloMonAISingleton : MonoBehaviour
    {
        // --- 中略 ---
        
        /// <summary>
        /// 開始処理
        /// </summary>
        void Start()
        {
            // 音声聞き取り時の処理を設定する
            HoloMonListenSingleton.Instance.IReadOnlyReactivePropertyHoloMonListenWord
                .ObserveOnMainThread()
                .Subscribe(word => {
                    ListenWord(word);
                })
                .AddTo(this);

            // 眠り状態の変化時の処理を設定する
            HoloMonConditionLifeSingleton.Instance.IReadOnlyReactivePropertyHoloMonLifeStatus
                .ObserveOnMainThread()
                .Subscribe(status =>
                {
                    SleepinessLevel(status.SleepinessLevel);
                })
                .AddTo(this);
        }
        
        // --- 中略 ---
        
        /// <summary>
        /// 眠り度の変化に合わせたアクションを実行する
        /// </summary>
        /// <param name="a_SleepinessLevel"></param>
        private void SleepinessLevel(HoloMonSleepinessLevel a_SleepinessLevel)
        {
            Debug.Log("Change SleepinessLevel");
            switch(a_SleepinessLevel)
            {
                case HoloMonSleepinessLevel.Nothing:
                    // 眠気がない場合かつ眠り状態であれば待機状態にして起こす
                    if(HoloMonModeStatusSingleton.Instance.IReadOnlyReactivePropertyHoloMonMode.Value == HoloMonMode.Sleep)
                    {
                        RequestStandby();
                    }
                    break;
                case HoloMonSleepinessLevel.Little:
                    // 少し眠い状態では何も行わない
                    break;
                case HoloMonSleepinessLevel.Sleepy:
                    // 眠い状態なら睡眠の開始要求を行う
                    RequestStartSleep();
                    break;
            }
        }
}

f:id:bluebirdofoz:20210327234357j:plain

シーンを再生して、ホロモンの睡眠時間にステータスの変更が発生することを確認します。
ReactiveProperty に構造体を指定した場合でも、変更が行われたタイミングでのみ通知が行われます。
f:id:bluebirdofoz:20210327233901j:plain

HoloLensアプリでVideoPlayerを使って動的に動画ファイルを読み込んで再生する

本日は HoloLens の技術調査枠です。
HoloLensアプリでVideoPlayerを使って動的に動画ファイルを読み込んで再生してみます。
f:id:bluebirdofoz:20210326084111j:plain

VideoPlayer

Unity のシーン上で動画を再生する場合、VideoPlayer コンポーネントを利用します。
docs.unity3d.com
docs.unity3d.com

サンプルプロジェクトの作成

動画を再生するシーンを作成します。
以下の記事に従って MRTK をインポートしたサンプルプロジェクトを作成します。
bluebirdofoz.hatenablog.com
f:id:bluebirdofoz:20210326084150j:plain

VideoPlayer コンポーネントを追加するための Panel オブジェクトを追加します。
f:id:bluebirdofoz:20210326084201j:plain
f:id:bluebirdofoz:20210326084211j:plain

[Add Component]ボタンで Panel オブジェクトに[Video Player]コンポーネントを追加します。
f:id:bluebirdofoz:20210326084220j:plain

Video Player へ動的に動画ファイルを設定する以下のスクリプトを作成しました。
・VideoPlayerTest.cs

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

// 必須コンポーネントの指定
[RequireComponent(typeof(VideoPlayer))]
public class VideoPlayerTest : MonoBehaviour
{
    /// <summary>
    /// 起動処理
    /// </summary>
    void Start()
    {
        // VideoPlayerの参照を取得する
        VideoPlayer videoPlayer = this.gameObject.GetComponent<VideoPlayer>();

        // 自動再生OFF
        videoPlayer.playOnAwake = false;
        
        // ループON
        videoPlayer.isLooping = true;

        // 指定ディレクトリ配下の HoloMonLife.mp4 を再生する
        string filename = string.Format(@"HoloMonLife.mp4");
        string filePath = System.IO.Path.Combine(VideoFileDirectoryPath(), filename);
        
        // パスを指定する
        videoPlayer.url = filePath;

        // 動画を再生する
        videoPlayer.Play();
    }

    /// <summary>
    /// 定期処理
    /// </summary>
    void Update()
    {
        
    }
    /// <summary>
    /// ビデオ保存ディレクトリパスの取得
    /// 実行環境によって参照ディレクトリを変更する
    /// </summary>
    static private string VideoFileDirectoryPath()
    {
        string directorypath = "";
#if WINDOWS_UWP
        // HoloLens上での動作の場合、LocalAppData/AppName/LocalStateフォルダを参照する
        directorypath = Windows.Storage.ApplicationData.Current.LocalFolder.Path;
#else
        // Unity上での動作の場合、Assets/StreamingAssetsフォルダを参照する
        directorypath = UnityEngine.Application.streamingAssetsPath;
#endif
        return directorypath;
    }
}

f:id:bluebirdofoz:20210326084234j:plain

作成したスクリプトを Panel オブジェクトに追加します。
f:id:bluebirdofoz:20210326084243j:plain

エディター上での動作確認

エディター上で動作確認を行います。
Assets/StreamingAssets フォルダに参照する動画ファイルを配置します。
f:id:bluebirdofoz:20210326084251j:plain

シーンを再生すると Panel オブジェクトに動画が再生されます。
f:id:bluebirdofoz:20210326084303j:plain

HoloLens2上での動作確認

次に HoloLens2 上での動作確認を行います。
HoloLens2 ではアプリの LocalState フォルダを参照するように設定しているので、ここに動画ファイルを配置します。
f:id:bluebirdofoz:20210326084312j:plain

アプリを起動して、動画が再生されれば成功です。
f:id:bluebirdofoz:20210326084322j:plain

hololensアプリでXML設定ファイルの操作を行う その3(暗号化と復号化)

本日は hololens の技術調査枠です。
アプリの情報を暗号化して書き出し、復号化して読み込みます。

前提条件

以下の前回記事の続きになります。
bluebirdofoz.hatenablog.com
bluebirdofoz.hatenablog.com

暗号化と復号化

今回は以下のページを参考に暗号化と復号化を実施してみました。
docs.microsoft.com
docs.microsoft.com

前回記事のクラスに暗号化して情報を書き出す関数と、復号化を行って情報を読み込む関数を追加します。
・DataSaveManager.cs

HoloLens2 にアプリを展開して Save/Load を行い、暗号化と復号化を介してデータの書き込みと読み込みができることを確認しました。
f:id:bluebirdofoz:20210325232423j:plain

HoloLens2でホロモンアプリを作る その20(お肉を正面方向に投げるように登場させる)

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

今回はお肉を正面方向に投げるようにスポーンさせるメモです。

正面方向に慣性を設定する

ボタンを押したとき、指定のオブジェクトから正面方向にお肉を投げるように登場させます。
Rigidbody の AddForce と Transform の forward を利用します。
docs.unity3d.com
docs.unity3d.com

以下のような自身のオブジェクトを指定位置から正面方向に登場させるスクリプトを作成しました。
・ItemFoodController.cs

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

namespace HMProject.Item
{
    public class ItemFoodController : MonoBehaviour
    {
        /// <summary>
        /// 投擲基準位置
        /// </summary>
        [SerializeField, Tooltip("投擲基準位置")]
        private Transform p_ThrowPosition;

        /// <summary>
        /// NavMesh追跡ポイント
        /// </summary>
        [SerializeField, Tooltip("NavMesh追跡ポイント")]
        private Transform p_NavMeshTrackingPoint;

        /// <summary>
        /// デフォルト座標
        /// </summary>
        [SerializeField, Tooltip("デフォルト座標")]
        private Vector3 p_DefaultWorldPosition;

        /// <summary>
        /// 開始処理
        /// </summary>
        void Start()
        {
            // デフォルト座標を保存
            p_DefaultWorldPosition = this.transform.position;

            // 初期状態は無効とする
            this.gameObject.SetActive(false);
        }

        /// <summary>
        /// 定期処理
        /// </summary>
        void Update()
        {

        }

        /// <summary>
        /// オブジェクトリセット
        /// </summary>
        public void RestObject()
        {
            // 慣性をリセットする
            this.gameObject.GetComponent<Rigidbody>().velocity = Vector3.zero;

            // デフォルト位置に戻す
            this.transform.position = p_DefaultWorldPosition;

            // オブジェクトを無効化する
            this.gameObject.SetActive(false);
        }

        /// <summary>
        /// 投げ渡す
        /// </summary>
        public void ThrowObject()
        {
            // オブジェクトを有効化する
            this.gameObject.SetActive(true);

            // 投擲基準位置にオブジェクトを移動する
            this.transform.position = p_ThrowPosition.position + (p_ThrowPosition.up * 0.2f);

            // 一旦、慣性をリセットする
            this.gameObject.GetComponent<Rigidbody>().velocity = Vector3.zero;

            // 投擲位置の正面方向を取得して慣性の設定を行う
            this.gameObject.GetComponent<Rigidbody>().AddForce(p_ThrowPosition.forward);
        }
    }
}

f:id:bluebirdofoz:20210324231623j:plain

スクリプトをお肉オブジェクトに追加してボタン実行時に ThrowObject 関数を呼び出すよう設定します。
f:id:bluebirdofoz:20210324231635j:plain

シーンを再生して確認します。
ボタンを押したとき、お肉が正面方向に投げるように登場するようになりました。
f:id:bluebirdofoz:20210324231643j:plain

UnityのAssets内のファイルをファイル名が省略されないように一覧表示する

本日は Unity の小ネタ枠です。
UnityのAssets内のファイルをファイル名が省略されないように一覧表示する方法です。
f:id:bluebirdofoz:20210323230954j:plain

Assetsのファイル表示サイズを調整する

Assets のファイルはデフォルトでアイコンで表示されます。
このとき、長いファイル名は省略されてしまうため、先頭部分が同じファイルの区別が難しい場合があります。
f:id:bluebirdofoz:20210323231004j:plain

アイコンの表示を変更したい場合、[Project]画面の右下の摘みで Assets 内のアイコンサイズを変更できます。
f:id:bluebirdofoz:20210323231016j:plain

この摘みを左端の最小サイズに寄せると、ファイルが一覧で表示されるようになります。
これでファイル名が省略されなくなりました。
f:id:bluebirdofoz:20210323231026j:plain

Tokyo HoloLens ミートアップ vol.25での発表資料

Tokyo HoloLens ミートアップ vol.25 での発表資料です。
発表内容は「HoloLens2でデジタルなモンスターと遊ぶ」です。
f:id:bluebirdofoz:20210322085454j:plain

以下に資料をアップしています。

www.slideshare.net

イベント情報

イベント情報は以下になります。
hololens.connpass.com

各技術に関する記事

おいかけっこの経路探査

bluebirdofoz.hatenablog.com

じゃんけんの手の形状判定

bluebirdofoz.hatenablog.com

ダンス時の目線検知

bluebirdofoz.hatenablog.com

時間経過によるホロモンの生活

bluebirdofoz.hatenablog.com

HoloLens2でホロモンアプリを作る その19(HandJointから手のポーズを判定してじゃんけんする)

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

今回は HandJoint から手のポーズを判定してじゃんけんするメモです。

HandJointから手のポーズを判定してじゃんけんする

ホロモンアプリにじゃんけんの機能を追加しました。
HoloLens2 の装着者の手の形を見て、ホロモンがじゃんけんのグー・チョキ・パーを見分けます。
f:id:bluebirdofoz:20210321231916j:plain

手の形状を取得するには MRTK の IMixedRealityHandJointHandler を利用します。
docs.microsoft.com
docs.microsoft.com

今回は以下のようなじゃんけんの手の形を判定するスクリプトを作成しました。
・HandRockPaperScissorsGesture

会社の先輩の協力に伝授して頂いた関節の内積を使って指の曲がり具合を判定する方法を使用しています。
詳細は以下の資料を参照ください。他にも様々な手の形を判定するサンプルがあります。

www2.slideshare.net

更に以下のランダム値を利用してホロモンにランダムなジャンケンの手を出させます。
bluebirdofoz.hatenablog.com

自身の出した手と HoloLens2 の装着者の手の形を比較すると、ホロモンがじゃんけんの勝敗を知ることができます。
f:id:bluebirdofoz:20210321232012j:plain