しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

Unity + MRTK の HoloLens アプリで指を立てたら反応するようにする

※ これは 2018/01/08 Unity 2017.3.0f3、Mixed Reality Toolkit 2017.2.1.3 Hot Fix 時点の情報です

最新版では動作が異なる可能性がありますのでご注意ください

現実世界に降り立ったにゃんこ、今回は Mixed Reality Toolkit を利用して待機中は毛づくろい状態にして指を立てたらこちらを向く、という動作をつけてみたいと思います

f:id:matatabi_ux:20180315221356p:plain

とりあえずアニメーションコントローラーを修正

f:id:matatabi_ux:20180401205508p:plain

Looking という State を追加して Idle から IsLooking の bool = true の場合に遷移し、false になったら Idle に戻るようにしました

f:id:matatabi_ux:20180401213502p:plain

今まで待機中に利用していたアニメは Looking 状態で再生するようにし、Idle 状態では毛づくろいをするアニメに変更

次にねこモデルにアタッチしているスクリプトを修正します

using HoloToolkit.Unity.InputModule;
using UnityEngine;

[RequireComponent(typeof(CharacterController))]
[RequireComponent(typeof(Animator))]
public class CatWalker : MonoBehaviour, ISourceStateHandler
{
    /// <summary>
    /// 顔の部分のボーン
    /// </summary>
    [SerializeField]
    private Transform headBone = null;

    /// <summary>
    /// キャッシュ用カメラ Transform
    /// </summary>
    private Transform camra;

    private Animator animator;
    private CharacterController controller;
    private uint sourceId = 0;

    /// <summary>
    /// 状態の列挙型
    /// </summary>
    internal enum State
    {
        /// <summary>
        /// 待機状態
        /// </summary>
        Idle,

        /// <summary>
        /// 歩行中
        /// </summary>
        Walk,

        /// <summary>
        /// こちらを向く
        /// </summary>
        Look,
    }

    /// <summary>
    /// 現在の状態
    /// </summary>
    private State state;

    /// <summary>
    /// 歩行状態
    /// </summary>
    public bool IsWalking
    {
        get { return this.animator.GetBool("IsWalking"); }
        set
        {
            // 変更があった場合のみ反映
            if (value != this.IsWalking)
            {
                this.animator.SetBool("IsWalking", value);
                this.state = value ? State.Walk : State.Idle;
            }
        }
    }

    /// <summary>
    /// 顔向き状態
    /// </summary>
    public bool IsLooking
    {
        get { return this.animator.GetBool("IsLooking"); }
        set
        {
            var newState = value && !this.IsWalking;

            // 変更があった場合のみ反映
            if (newState != this.IsLooking)
            {
                this.animator.SetBool("IsLooking", newState);
                this.state = newState ? State.Look : this.IsWalking ? State.Walk : State.Idle;
            }
        }
    }

    /// <summary>
    /// 初期処理
    /// </summary>
    public void Start()
    {
        this.camra = Camera.main.transform;
        this.animator = this.GetComponent<Animator>();
        this.controller = this.GetComponent<CharacterController>();
        this.state = State.Idle;

        InputManager.Instance.AddGlobalListener(this.gameObject);
    }

    /// <summary>
    /// 破棄時の処理
    /// </summary>
    public void OnDestroy()
    {
        if (this != null && InputManager.Instance != null)
        {
            InputManager.Instance.RemoveGlobalListener(this.gameObject);
        }
    }

    /// <summary>
    /// 可変毎フレームごとの処理
    /// </summary>
    public void Update()
    {
        // 距離の判定に高低差を考慮しない
        var cameraPosition = this.camra.position;
        cameraPosition.y = this.transform.position.y;

        var distance = Vector3.Distance(cameraPosition, this.transform.position);
        switch (this.state)
        {
            case State.Idle:
            case State.Look:
                this.IsWalking = distance > 2f;
                break;

            case State.Walk:
                this.IsWalking = distance > 1f;
                break;
        }
    }

    /// <summary>
    /// 固定毎フレームの処理
    /// </summary>
    public void FixedUpdate()
    {
        var cameraPosition = this.camra.position;

        switch (this.state)
        {
            case State.Walk:
                // 身体全体をカメラに向ける(Y軸の回転)
                cameraPosition.y = this.transform.position.y;
                this.transform.LookAt(cameraPosition);

                var distance = Vector3.Distance(cameraPosition, this.transform.position);
                // カメラに向かって移動
                var delta = Mathf.Clamp(distance - 0.9f, 0f, 0.01f);
                this.controller.Move(Vector3.MoveTowards(this.transform.position, cameraPosition, delta) - this.transform.position);

                break;

            case State.Look:
                // 身体全体をカメラに向ける(Y軸の回転)
                cameraPosition.y = this.transform.position.y;
                this.transform.LookAt(cameraPosition);
                break;
        }

        // 重力をかける
        this.controller.Move(Physics.gravity);
    }

    /// <summary>
    /// 遅延フレームの処理
    /// </summary>
    public void LateUpdate()
    {
        switch (this.state)
        {
            case State.Look:
                var cameraPosition = this.camra.position;

                // 目の高さに合わせる
                cameraPosition.y -= 0.3f;

                // 顔だけカメラに向かせる(X軸の回転)
                this.headBone.LookAt(cameraPosition);
                var angle = this.headBone.rotation.eulerAngles.x;
                angle = angle > 180f ? angle - 360f : angle;
                this.headBone.localRotation = Quaternion.Euler(0f + Mathf.Clamp(angle, -30f, 30f), 0f, 0f);

                break;
        }
    }

    /// <summary>
    /// 入力ソース認識時のイベントハンドラ
    /// </summary>
    /// <param name="eventData">イベント情報</param>
    public void OnSourceDetected(SourceStateEventData eventData)
    {
        InteractionSourceInfo info;
        if (eventData.InputSource.TryGetSourceKind(eventData.SourceId, out info))
        {
            switch (info)
            {
                case InteractionSourceInfo.Hand:
                    this.sourceId = eventData.SourceId;
                    this.IsLooking = true;
                    break;
            }
        }
    }

    /// <summary>
    /// 入力ソースの認識が解除時のイベントハンドラ
    /// </summary>
    /// <param name="eventData">イベント情報</param>
    public void OnSourceLost(SourceStateEventData eventData)
    {
        if (eventData.SourceId == this.sourceId)
        {
            this.IsLooking = false;
        }
    }
}

Mixed Reality Toolkit で指を立てるというジェスチャーは HoloLens が手を認識したことを表す SourceDetected という状態になります

この状態をねこモデル側で検知するために、ISourceStateHandler を実装し、OnSourceDetected と OnSourceLost イベントハンドラを追加しました

OnSourceDetected で Look 状態にし、OnSourceLost 状態で解除することで、指を立てた時だけ顔を向けるようにするという感じ

これだけだとねこモデルをフォーカス(見つめて)していないと検知できないため、Start() メソッドの中で下記を実行しています

        InputManager.Instance.AddGlobalListener(this.gameObject);

このコードにより InputManager のイベント通知先にねこモデルを追加することができます

この場合はグローバルリスナーとして追加しているため、どんな状況であっても通知されます

この他にもモーダルハンドラ、フォールバックハンドラへの追加によっても通知されますがグローバルリスナーとは動作が少し変わります

        // モーダルハンドラ(下位のハンドラを無効にして検知)
        InputManager.Instance.PushModalInputHandler(this.gameObject);

        // フォールバックハンドラ(モーダルハンドラ、フォーカスハンドラが有効でない場合だけ検知)
        InputManager.Instance.PushFallbackInputHandler(this.gameObject);

モーダルハンドラに追加した場合、グローバルリスナーとは異なり他のフォーカスした通知先やフォールバックハンドラへは通知されません

また、フォールバックハンドラはモーダルハンドラや他のフォーカスした通知先がない場合のみ通知されます

今回は特に他の通知先への制限はしたくのと、他から制限を受けたくないのでグローバルリスナーに追加しました

グローバルリスナーへの登録はねこモデルが破棄されるときに解除したいので下記のようなメソッドも追加しています

    /// <summary>
    /// 破棄時の処理
    /// </summary>
    public void OnDestroy()
    {
        if (this != null && InputManager.Instance != null)
        {
            InputManager.Instance.RemoveGlobalListener(this.gameObject);
        }
    }

さて、これで UnityEditor 上で試してみました

f:id:matatabi_ux:20180401213011g:plain

できました!

Mixed Reality Toolkit のおかげで HoloLens の実機でなくても試せるのが楽ですね