MRが楽しい

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

UnityのStarterAssetsのプロジェクトのキャラクターをユニティちゃんに差し替える

本日は Unity の小ネタ枠です。
Unity の StarterAssets のプロジェクトのキャラクターをユニティちゃんに差し替える方法を記事にします。

前提条件

予め StarterAssets とユニティちゃんをプロジェクトに取り込んでおきます。
StarterAssets とユニティちゃんのインポート手順は以下の記事を参照ください。
bluebirdofoz.hatenablog.com
bluebirdofoz.hatenablog.com

三人称視点のサンプルシーンを開きます。

ユニティちゃんをシーンに追加する

キャラクターを差し替えるため、サンプルシーンに設定されているプレイヤーキャラクターの PlayerArmature オブジェクトを無効化します。

代わりに以下のユニティちゃんモデルを Hierarchy にドラッグしてシーンに配置します。

Assets/UnityChan/Prefabs/unitychan.prefab

デフォルトでユニティちゃんに設定されている以下のコンポーネントは不要なので Remove Component で除去します。
・IdleChanger
・FaceUpdate
・AutoBlink

サンプルシーンのキャラクターのアニメーションコントローラをユニティちゃんに設定します。
以下のアニメーションコントローラを Animator コンポーネントの[Controller]にドラッグします。

Assets/StarterAssets/ThirdPersonController/Character/Animations/StarterAssetsThirdPerson.controller

サンプルシーンのキャラクターの操作用コンポーネントをユニティちゃんに追加します。
PlayerArmature オブジェクトに設定されている以下のコンポーネントを Copy Component でコピーして unitychan オブジェクトに Paste Component As New で貼り付けます。



ユニティちゃんモデルに合わせたカスタマイズを行う

ユニティちゃんモデルに合わせてキャラクターの当たり判定を変更します。
Charactor Controler コンポーネントの[Center],[Radius],[Height]で当たり判定を調節します。

次にユニティちゃんモデルのレイヤーを変更します。
デフォルトでは地面判定を行うレイヤーが[Default]に設定されているので、キャラクターはこれ以外のレイヤーである必要があります。

今回は[Player]レイヤーを新規作成してユニティちゃんモデルに割り当てました。
子オブジェクト全てのレイヤーを変更する必要はないので unitychan オブジェクトのみ変更しています。




ユニティちゃんにカメラ追従の設定をする

ユニティちゃんにカメラが追従するようにカメラ設定を行います。
カメラの追従ポイントとして unitychan オブジェクトの頭部辺りに PlayerCameraRoot オブジェクトを追加します。

PlayerFollowCamera オブジェクトを開き、CinemachineVirtualCamera コンポーネントの[Follow]に先ほど追加した PlayerCameraRoot オブジェクトを指定します。

デフォルトでは[Ignore Tag]に[Player]タグが指定されています。
unitychan のタグが[Player]以外の場合、unitychan を障害物と判定してしまい、カメラが正常に三人称視点で動きません。

ユニティちゃんモデルを障害物と判定しないように unitychan オブジェクトの[Tag]を[Player]に変更しておきます。

動作確認

これで操作キャラクターをユニティちゃんに差し替えることができました。
シーンを再生してみます。

各種キーでユニティちゃんを動かすことができ、カメラは三人称視点で追従しました。

MetaQuestProで自作(提供元不明)アプリを起動する

本日は MetaQuestPro の小ネタ枠です。
MetaQuestProで自作(提供元不明)アプリを起動する方法を記事にします。

提供元不明アプリ

MetaQuest の公式ストアを介さずにインストールした自作アプリや第三者の開発アプリは提供元不明として分類されます。
bluebirdofoz.hatenablog.com
bluebirdofoz.hatenablog.com

提供元不明は通常アプリライブラリには表示されません。

提供元不明アプリを起動する

提供元不明のアプリを起動するにはアプリライブラリで検索欄を選択します。

検索ページでは検索条件を指定できるので右上のプルダウンから[提供元不明]を選びます。

提供元不明の一覧にインストールしたアプリが表示されるのでアプリ名をタップするとアプリが起動します。

MetaQuest2にMetaQuestProのコントローラをペアリングする

本日は MetaQuest の環境構築手順枠です。
MetaQuest2にMetaQuestProのコントローラをペアリングしてみたので記事にします。

MetaQuest2にMetaQuestProのコントローラをペアリングする

スマートフォンの Quest アプリを起動し、[デバイス]を開きます。

同一ネットワーク内で MetaQuest2 を起動してデバイスと Quest アプリを接続状態にします。
メニューから[ヘッドセットの設定]を開きます。

一覧から[コントローラ]を選択します。

現在接続中のコントローラが表示されます。(ここではMetaQuest2のコントローラ)
[新しいコントローラをペアリング]を選択します。

接続したいコントローラの種類の選択欄が表示されます。
[MetaQuestPro 左/右 コントローラのペアリング]を選択します。

既に別のコントローラをペアリング済みの場合、ペアリング中のコントローラは解除されます。
[次へ]を選択します。

画面の案内に従ってペアリングするコントローラのボタンを長押しします。

ペアリングが成功すると以下の通り、成功画面が表示されます。

同じ手順で逆側のコントローラもペアリングを行います。

これで MetaQuest2 で MetaQuestPro のコントローラが利用できるようになりました。

SemaphoreSlimクラスをラップしてlockステートメントのように利用する

本日は C# の小ネタ枠です。
SemaphoreSlim クラスをラップしてlockステートメントのように利用する方法を試します。

前回記事

SemaphoreSlim の使い方については以下の記事を参照ください。
bluebirdofoz.hatenablog.com
bluebirdofoz.hatenablog.com

SemaphoreSlim クラスをラップしてlockステートメントのように利用する

SemaphoreSlim は非同期処理を排他制御できる利点がある一方、lock ステートメントのように記述できず、必ずリソースの解放を記述する必要があります。
そこで SemaphoreSlim クラスをラップして lock ステートメントのように記述するクラスの一例が下記ページに掲載されています。
www.hanselman.com

以下がその AsyncLock クラスです。
・AsyncLock.cs

using System;
using System.Threading.Tasks;

public sealed class AsyncLock
{
  private readonly System.Threading.SemaphoreSlim m_semaphore 
    = new System.Threading.SemaphoreSlim(1, 1);
  private readonly Task<IDisposable> m_releaser;

  public AsyncLock()
  {
    m_releaser = Task.FromResult((IDisposable)new Releaser(this));
  }

  public Task<IDisposable> LockAsync()
  {
    var wait = m_semaphore.WaitAsync();
    return wait.IsCompleted ?
            m_releaser :
            wait.ContinueWith(
              (_, state) => (IDisposable)state,
              m_releaser.Result, 
              System.Threading.CancellationToken.None,
              TaskContinuationOptions.ExecuteSynchronously, 
              TaskScheduler.Default
            );
  }
  private sealed class Releaser : IDisposable
  {
    private readonly AsyncLock m_toRelease;
    internal Releaser(AsyncLock toRelease) { m_toRelease = toRelease; }
    public void Dispose() { m_toRelease.m_semaphore.Release(); }
  }
}

実際に前回記事のサンプルスクリプトに組み込んで利用してみます。
以下のように lock ステートメントのようにリソースの解放を記述する必要なく非同期処理の排他制御を記述できます。
・AsyncLockTest.cs

using System.Threading.Tasks;
using UnityEngine;
public class AsyncLockTest : MonoBehaviour
{ 
    // AsyncLock クラスを使用する
    private AsyncLock _asyncLock = new();
    
    public async void ButtonEvent()
    {
        // 排他制御を行いたい部分を以下のコードブロックで記述してスレッドをブロックする
        using (await _asyncLock.LockAsync())
        {
            // 1秒ごとに1~3のカウントダウンをログに出力する
            for (int count = 1; count <= 3; count++)
            {
                Debug.Log(count);
                await Task.Delay(1000);
            }
        }
    }
}

サンプルシーンのコンポーネントを差し替えて動作を確認します。
以下の通り、非同期処理の排他制御ができました。

ただしこちらの記法は using (await _asyncLock.LockAsync()) の部分で await の記述忘れが発生しうるという別の問題もあるとのこと。
qiita.com

lockObjectとSemaphoreSlimの使い分け

本日は C# の小ネタ枠です。
非同期処理での lockObject と SemaphoreSlim の使い分けについて記事にします。

lockObject

lock ステートメントは指定のオブジェクトに対する相互排他ロックを取得することで、ステートメントブロックの排他制御を行います。
lock ステートメントは最大で 1 つだけのスレッドが実行されます。
learn.microsoft.com

    private object _lockObject = new ();
    
    public async void ButtonEvent()
    {
        lock (_lockObject)
        {
            for (int count = 1; count <= 3; count++)
            {
                Debug.Log(count);
            }
        }
    }

lock ステートメントは非同期処理をブロックできません。
以下のように lock ステートメント内に await を記述すると「Cannot await in the body of a lock statemen」エラーになります。

        lock (_lockObject)
        {
            for (int count = 1; count <= 3; count++)
            {
                Debug.Log(count);
                await Task.Delay(1000); // Cannot await in the body of a lock statemen エラー
            }
        }

これは lock ステートメントは同一メソッドで解放される必要があるが、非同期処理では同一メソッドで解放されることが保証されないためです。
lock ステートメントを利用する際は、非同期メソッド内でもブロックが必要な同期処理部分だけを lock する必要があります。

SemaphoreSlim

SemaphoreSlim はリソースに同時にアクセスできるスレッドの数を制限して排他制御を行います。
SemaphoreSlim は任意のスレッド数を指定することができます。
learn.microsoft.com

SemaphoreSlim は以下のように await を含む非同期処理をブロックすることができます。
await を含む処理部分をブロックしたい場合は SemaphoreSlim を利用して排他制御を行うと良いです。

    private SemaphoreSlim _semaphoreSlim = new (1,1);
    
    public async void ButtonEvent()
    {
        await _semaphoreSlim.WaitAsync();
        for(int count = 1; count <= 3; count++) {
            Debug.Log(count);
            await Task.Delay(1000);
        }
        _semaphoreSlim.Release();
    }

C#でSemaphoreSlimとCancellationTokenSourceを使って非同期タスクの制御を行う

本日は C# の小ネタ枠です。
C# で SemaphoreSlim と CancellationTokenSource を使って非同期タスクの制御を行う一例です。

前回記事

以下の記事でスレッドが一定時間待機すると待機をキャンセルする実装を行いました。
bluebirdofoz.hatenablog.com

本記事では後から新しい処理が呼び出されたとき、現在のスレッドをキャンセルして次の処理を実行してみます。

SemaphoreSlim.CurrentCount

現在 SemaphoreSlim で処理が実行されているかスレッドに空きがあるかは SemaphoreSlim.CurrentCount で確認できます。
現在利用可能なスレッド数が返却されます。
learn.microsoft.com

CancellationTokenSource.Cancel

CancellationTokenSource.Cancel を使って Cancel を任意のタイミングで発生させます。
learn.microsoft.com

サンプルシーン

新しい処理が呼び出されたとき、既に実行中の処理をキャンセルするように以下の通り実装を変更しました。
・SemaphoreSlimCancelTest2.cs

using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class SemaphoreSlimCancelTest2 : MonoBehaviour
{ 
    // 同時に1つまでのタスクを許容し、かつ、タスクの数を後から追加できないようにする
    private SemaphoreSlim _semaphoreSlim = new (1,1);
    
    private CancellationTokenSource _cancelTokenSource;
    
    public async void ButtonEvent()
    {
        // 既にタスクが実行されていれば処理をキャンセルする
        if (_semaphoreSlim.CurrentCount == 0) _cancelTokenSource.Cancel();
        try
        {
            // スレッドをブロックする(非同期で行いたい場合は WaitAsync() を使用する)
            await _semaphoreSlim.WaitAsync();
            _cancelTokenSource = new();
            // 1秒ごとに1~3のカウントダウンをログに出力する
            for (int count = 1; count <= 3; count++)
            {
                Debug.Log(count);
                await Task.Delay(1000, _cancelTokenSource.Token);
            }
        }
        catch (OperationCanceledException)
        {
            Debug.Log("待機キャンセルされました");
        }
        finally
        {
            // スレッドを解放する
            _semaphoreSlim.Release();
        }
    }
}

シーンを再生して動作を確認します。
ボタンを連打すると、以下の通り2回目の処理が割り込んだときにその前の処理がキャンセルされました。

C#でCancellationTokenSourceを使って一定時間で非同期タスクのキャンセルを行う

本日は C# の小ネタ枠です。
C# で CancellationTokenSource を使って一定時間で非同期タスクのキャンセルを行う方法についてです。

CancellationTokenSource.CancelAfter

CancellationTokenSource.CancelAfter を使って Cancel の発生タイミングを時間指定で設定できます。
learn.microsoft.com

var cancelTokenSource = new CancellationTokenSource();
cancelTokenSource.CancelAfter(2000);

サンプルシーン

以下の SemaphoreSlim を使ってスレッド制御を行ったサンプルシーンでキャンセル処理を実装してみます。
bluebirdofoz.hatenablog.com

スレッド待機が2秒以上続く場合、待機をキャンセルするように以下の通り実装を変更しました。
・SemaphoreSlimCancelTest1.cs

using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class SemaphoreSlimCancelTest1 : MonoBehaviour
{ 
    // 同時に1つまでのタスクを許容し、かつ、タスクの数を後から追加できないようにする
    private SemaphoreSlim _semaphoreSlim = new (1,1);
    
    public async void ButtonEvent()
    {
        // 2秒以上待機する場合は待機をキャンセルする
        var cancelTokenSource = new CancellationTokenSource();
        cancelTokenSource.CancelAfter(2000);

        try
        {
            // スレッドをブロックする(非同期で行いたい場合は WaitAsync() を使用する)
            await _semaphoreSlim.WaitAsync(cancelTokenSource.Token);
            // 1秒ごとに1~3のカウントダウンをログに出力する
            for (int count = 1; count <= 3; count++)
            {
                Debug.Log(count);
                await Task.Delay(1000);
            }

            // スレッドを解放する
            _semaphoreSlim.Release();
        }
        catch (OperationCanceledException)
        {
            Debug.Log("待機キャンセルされました");
        }
        finally
        {
            cancelTokenSource.Dispose();
        }
    }
}

シーンを再生して動作を確認します。
ボタンを連打すると、以下の通り2回目の待機が実行前にキャンセルされました。