2016年11月22日火曜日

【12/01動画追加】【HoloLens開発】ユニティちゃんとHoloLensで戯れる - ゲームパッド編 -

今回は前回実装したSpatial Mappingで作成された空間をXbox Oneコントローラーを使ってユニティちゃんを走らせてみたいと思います。


連載目次
Part 6: 【HoloLens開発】ユニティちゃんとHoloLensで戯れる - ゲームパッド編 - ←本記事
Part 7: 【HoloLens開発】ユニティちゃんとHoloLensで戯れる - シェアリング編 - (予定)

2016/12/01 追記

動画を撮影しました。


-------------------------------------

【HoloLens+ゲームパッド】
ゲームパッドはゲームにとって不可欠な存在といっても過言ではありません。HoloLensでも2016年7月のAnniversary UpdateでBluetooth搭載のXbox Oneコントローラーをサポートしました。
ゲームパッドを使うことでアプリをインタラクティブに操作することができ、よりいろいろなものを作れるものと思います。
HoloLensでゲームパッドを使うにはBluetoothペアリングするのみでOKです。ペアリングはHoloLensのSettings > Devicesから行います。

ちなみに、今回使用したXbox Oneコントローラーこちらです。

【ゲームパッド操作の実装】
HoloLensでゲームパッドのイベントを取得するには、UnityのInputクラスでは行えず、WindowsランタイムAPIである、Windows.Gaming.Input APIを利用する必要があります。Windows.Gaming.Inputについては公式リファレンスをご確認ください。
また、UnityスクリプトからWindowsランタイムAPIを扱うには、Player Settingsの設定とdefineによるコードの書き分けが必要です。Player Settingsの変更はPublishing Settingsのcompilation overridesを「Use Net Core」もしくは「Use Net Core Partially」に設定すればよいです。

define設定はHoloLensアプリはUniversal 10アプリであるため、「WINDOWS_UWP」で定義すればよいです。これらの詳細についてはUnityのリファレンスに記載があります。

今回ゲームパッドで実装した機能は以下の通りです。
  ・左スティック:ユニティちゃんの移動操作
  ・Yボタン:ユニティちゃんのジャンプ操作
  ・Aボタン:ユニティちゃんの休憩操作
  ・LB:ユニティちゃんの移動操作方法変更
  ・RB:認識空間のテクスチャオンオフ

ユニティちゃんの移動モーションのためにunitychanプレハブのAnimatorをAssets > UnityChan > Animatorsの「UnityChanLocomotions」に変更します。

UnityChanLocomotions用の元々のスクリプトはAssets > UnityChan > Scripts > UnityChanControlScriptWithRgidBodyですが、これを参考にXboxOneControllerスクリプトを作成しました。このスクリプトをunitychanプレハブのコンポーネントとして追加します。

XboxOneControllerの実際のソースコードは以下のものです。

using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using HoloToolkit.Unity;
#if WINDOWS_UWP
using Windows.Gaming.Input;
#endif

// 必要なコンポーネントの列記
[RequireComponent(typeof(Animator))]
[RequireComponent(typeof(CapsuleCollider))]
[RequireComponent(typeof(Rigidbody))]

public class XboxOneController : MonoBehaviour {
    public Material Wireframe;                  // Unity側からリソースを選択する必要あり
    public Material Occlusion;                  // Unity側からリソースを選択する必要あり
    public bool showMesh = false;

    public SpatialMappingManager manager;       // メッシュ切り替え用に必要

    public float animSpeed = 1.5f;              // アニメーション再生速度設定
    public float lookSmoother = 3.0f;           // a smoothing setting for camera motion
    public bool useCurves = true;               // Mecanimでカーブ調整を使うか設定する
                                                // このスイッチが入っていないとカーブは使われない
    public float useCurvesHeight = 0.5f;        // カーブ補正の有効高さ(地面をすり抜けやすい時には大きくする)

    // 以下キャラクターコントローラ用パラメタ
    // 前進速度
    public float forwardSpeed = 3.5f;
    // 後退速度
    public float backwardSpeed = 1.0f;
    // 旋回速度
    public float rotateSpeed = 2.0f;
    // ジャンプ威力
    public float jumpPower = 3.0f;
    // キャラクターコントローラ(カプセルコライダ)の参照
    private CapsuleCollider col;
    private Rigidbody rb;
    // キャラクターコントローラ(カプセルコライダ)の移動量
    private Vector3 velocity;
    // CapsuleColliderで設定されているコライダのHeight、Centerの初期値を収める変数
    private float orgColHeight;
    private Vector3 orgVectColCenter;
    private Vector3 moveDirection;

    private Animator anim;                          // キャラにアタッチされるアニメーターへの参照
    private AnimatorStateInfo currentBaseState;         // base layerで使われる、アニメーターの現在の状態の参照

    private GameObject cameraObject;    // メインカメラへの参照

    // アニメーター各ステートへの参照
    static int idleState = Animator.StringToHash("Base Layer.Idle");
    static int locoState = Animator.StringToHash("Base Layer.Locomotion");
    static int jumpState = Animator.StringToHash("Base Layer.Jump");
    static int restState = Animator.StringToHash("Base Layer.Rest");

    // 移動操作モード
    static string MODE_A = "mode_a";                // 元々の操作方法
    static string MODE_B = "mode_b";                // カメラ方向を基準に移動
    private string inputMode = MODE_B;

#if WINDOWS_UWP
    public Gamepad controller;
    // FixedUpdateでボタンのフラグを見るだけだと長押し判定になってしまう可能性が高いので、
    // 前回のフラグを保持しておき、チェックして単押しされたかどうか確認する
    private bool beforeYFlag = false;
    private bool beforeAFlag = false;
    private bool beforeLBFlag = false;
    private bool beforeRBFlag = false;
#endif

    // Use this for initialization
    void Start () {
        // Animatorコンポーネントを取得する
        anim = GetComponent();
        // CapsuleColliderコンポーネントを取得する(カプセル型コリジョン)
        col = GetComponent();
        rb = GetComponent();
        //メインカメラを取得する
        cameraObject = GameObject.FindWithTag("MainCamera");
        // CapsuleColliderコンポーネントのHeight、Centerの初期値を保存する
        orgColHeight = col.height;
        orgVectColCenter = col.center;
#if WINDOWS_UWP
        // Gamepadを探す
        if(Gamepad.Gamepads.Count > 0) {
            Debug.Log("Gamepad found.");
            controller = Gamepad.Gamepads.First();
        } else
        {
            Debug.Log("Gamepad not found.");
        }
        // ゲームパッド追加時イベント処理を追加
        Gamepad.GamepadAdded += Gamepad_GamepadAdded;
#endif
        manager = GameObject.Find("SpatialMapping").GetComponent<SpatialMappingManager>();

    }

    // Update is called once per frame
    void FixedUpdate () {
        float h = 0f;
        float v = 0f;
#if WINDOWS_UWP
        GamepadReading reading;
        if(controller != null)
        {
            // ゲームパッドの現在の状態を取得する
            reading = controller.GetCurrentReading();
            h = (float)reading.LeftThumbstickX;     // 左スティックのX方向(左右)をhで定義
            v = (float)reading.LeftThumbstickY;     // 左スティックのY方向(上下)をvで定義
        }
#else
        h = Input.GetAxis("Horizontal");              // 入力デバイスの水平軸をhで定義
        v = Input.GetAxis("Vertical");                // 入力デバイスの垂直軸をvで定義
#endif
        currentBaseState = anim.GetCurrentAnimatorStateInfo(0); // 参照用のステート変数にBase Layer (0)の現在のステートを設定する
        rb.useGravity = true;//ジャンプ中に重力を切るので、それ以外は重力の影響を受けるようにする
        anim.speed = animSpeed;                             // Animatorのモーション再生速度に animSpeedを設定する
        if (inputMode.Equals(MODE_A))
        {   // 元々の移動操作処理
            anim.SetFloat("Speed", v);                          // Animator側で設定している"Speed"パラメタにvを渡す
            anim.SetFloat("Direction", h);                      // Animator側で設定している"Direction"パラメタにhを渡す

            // 以下、キャラクターの移動処理
            velocity = new Vector3(0, 0, v);        // 上下のキー入力からZ軸方向の移動量を取得
                                                    // キャラクターのローカル空間での方向に変換
            velocity = transform.TransformDirection(velocity);
            //以下のvの閾値は、Mecanim側のトランジションと一緒に調整する
            if (v > 0.1)
            {
                velocity *= forwardSpeed;       // 移動速度を掛ける
            }
            else if (v < -0.1)
            {
                velocity *= backwardSpeed;  // 移動速度を掛ける
            }

            // 上下のキー入力でキャラクターを移動させる
            transform.position += velocity * Time.fixedDeltaTime;

            // 左右のキー入力でキャラクタをY軸で旋回させる
            transform.Rotate(0, h * rotateSpeed, 0);

        }
        else
        {   // 追加した移動操作処理
            float speed = Mathf.Sqrt(v * v + h * h);        // vとhから左スティックの傾き具合を算出
            if (speed > 0.1)
            {
                anim.SetFloat("Speed", speed);

                if (Camera.main != null)
                {
                    // メインカメラ(HoloLensの視線)方向を取得
                    Vector3 forward = Camera.main.transform.TransformDirection(Vector3.forward);
                    Vector3 right = Camera.main.transform.TransformDirection(Vector3.right);
                    moveDirection = h * right + v * forward;
                    velocity = moveDirection * forwardSpeed;

                    Vector3 AnimDir = velocity;
                    AnimDir.y = 0;
                    if (AnimDir.sqrMagnitude > 0.001)
                    {
                        // 前進する方向に旋回
                        Vector3 newDir = Vector3.RotateTowards(transform.forward, AnimDir, 10f * Time.deltaTime, 0f);
                        transform.rotation = Quaternion.LookRotation(newDir);
                    }

                    // 移動処理
                    transform.position += velocity * Time.fixedDeltaTime;
                }
            }
            else
            {
                anim.SetFloat("Speed", 0);
            }
        }

#if WINDOWS_UWP
        if (controller != null)
        {
            reading = controller.GetCurrentReading();
            bool yFlag = reading.Buttons.HasFlag(GamepadButtons.Y);
            if (yFlag && !beforeYFlag)
            {   // Yボタンを押すと
                //アニメーションのステートがLocomotionの最中のみジャンプできる
                if (currentBaseState.fullPathHash == locoState)
                {
                    //ステート遷移中でなかったらジャンプできる
                    if (!anim.IsInTransition(0))
                    {
                        rb.AddForce(Vector3.up * jumpPower, ForceMode.VelocityChange);
                        anim.SetBool("Jump", true);     // Animatorにジャンプに切り替えるフラグを送る
                    }
                }
            }

            beforeYFlag = yFlag;
            bool lbFlag = reading.Buttons.HasFlag(GamepadButtons.LeftShoulder);
            if (lbFlag && !beforeLBFlag)
            {   // LBを押すと移動操作モードを変更する
                if (inputMode.Equals(MODE_A))
                {
                    inputMode = MODE_B;
                }
                else
                {
                    inputMode = MODE_A;
                }
            }
            beforeLBFlag = lbFlag;
        }
        else
        {

        }

#else
        if (Input.GetButtonDown("Jump"))
        {   // スペースキーを入力したら

            //アニメーションのステートがLocomotionまたはIdleの場合ジャンプできる
            if (currentBaseState.fullPathHash == locoState || currentBaseState.fullPathHash == idleState)
            {
                //ステート遷移中でなかったらジャンプできる
                if (!anim.IsInTransition(0))
                {
                    rb.AddForce(Vector3.up * jumpPower, ForceMode.VelocityChange);
                    anim.SetBool("Jump", true);     // Animatorにジャンプに切り替えるフラグを送る
                }
            }
        }
#endif

        // 以下、Animatorの各ステート中での処理
        // Locomotion中
        // 現在のベースレイヤーがlocoStateの時
        if (currentBaseState.fullPathHash == locoState)
        {
            //カーブでコライダ調整をしている時は、念のためにリセットする
            if (useCurves)
            {
                resetCollider();
            }
        }
        // JUMP中の処理
        // 現在のベースレイヤーがjumpStateの時
        else if (currentBaseState.fullPathHash == jumpState)
        {
            // ステートがトランジション中でない場合
            if (!anim.IsInTransition(0))
            {

                // 以下、カーブ調整をする場合の処理
                if (useCurves)
                {
                    // 以下JUMP00アニメーションについているカーブJumpHeightとGravityControl
                    // JumpHeight:JUMP00でのジャンプの高さ(0〜1)
                    // GravityControl:1⇒ジャンプ中(重力無効)、0⇒重力有効
                    float jumpHeight = anim.GetFloat("JumpHeight");
                    float gravityControl = anim.GetFloat("GravityControl");
                    if (gravityControl > 0)
                        rb.useGravity = false;  //ジャンプ中の重力の影響を切る

                    // レイキャストをキャラクターのセンターから落とす
                    Ray ray = new Ray(transform.position + Vector3.up, -Vector3.up);
                    RaycastHit hitInfo = new RaycastHit();
                    // 高さが useCurvesHeight 以上ある時のみ、コライダーの高さと中心をJUMP00アニメーションについているカーブで調整する
                    if (Physics.Raycast(ray, out hitInfo))
                    {
                        if (hitInfo.distance > useCurvesHeight)
                        {
                            col.height = orgColHeight - jumpHeight;          // 調整されたコライダーの高さ
                            float adjCenterY = orgVectColCenter.y + jumpHeight;
                            col.center = new Vector3(0, adjCenterY, 0); // 調整されたコライダーのセンター
                        }
                        else
                        {
                            // 閾値よりも低い時には初期値に戻す(念のため)     
                            resetCollider();
                        }
                    }
                }
                // Jump bool値をリセットする(ループしないようにする)    
                anim.SetBool("Jump", false);
            }
        }
        // IDLE中の処理
        // 現在のベースレイヤーがidleStateの時
        else if (currentBaseState.fullPathHash == idleState)
        {
            //カーブでコライダ調整をしている時は、念のためにリセットする
            if (useCurves)
            {
                resetCollider();
            }
#if WINDOWS_UWP
            if (controller != null)
            {   // Aボタンを押すとRest状態になる
                reading = controller.GetCurrentReading();
                bool aFlag = reading.Buttons.HasFlag(GamepadButtons.A);
                if (aFlag && !beforeAFlag) {
                    anim.SetBool("Rest", true);
                }
                beforeAFlag = aFlag;
            }
#else
            // スペースキーを入力したらRest状態になる
            if (Input.GetButtonDown("Jump"))
            {
                anim.SetBool("Rest", true);
            }
#endif
        }
        // REST中の処理
        // 現在のベースレイヤーがrestStateの時
        else if (currentBaseState.fullPathHash == restState)
        {
            //cameraObject.SendMessage("setCameraPositionFrontView");  // カメラを正面に切り替える
            // ステートが遷移中でない場合、Rest bool値をリセットする(ループしないようにする)
            if (!anim.IsInTransition(0))
            {
                anim.SetBool("Rest", false);
            }
        }

#if WINDOWS_UWP
        if (controller != null)
        {   // RBを押すとSpatialMappingのメッシュのオンオフを切り替える
            reading = controller.GetCurrentReading();
            bool rbFlag = reading.Buttons.HasFlag(GamepadButtons.RightShoulder);
            if (rbFlag && !beforeRBFlag)
            {
                showMesh = !showMesh;
            }
            beforeRBFlag = rbFlag;
        }
#endif

        if (showMesh)
        {
            // Wireframeを表示
            manager.SetSurfaceMaterial(Wireframe);
        }
        else
        {
            // Occlusionを表示
            manager.SetSurfaceMaterial(Occlusion);
        }
    }

    // キャラクターのコライダーサイズのリセット関数
    void resetCollider()
    {
        // コンポーネントのHeight、Centerの初期値を戻す
        col.height = orgColHeight;
        col.center = orgVectColCenter;
    }

#if WINDOWS_UWP
    // ゲームパッド追加時のイベント処理
    private void Gamepad_GamepadAdded(object sender, Gamepad e)
    {
        controller = e;
        Debug.Log("Gamepad added");
    }
#endif
}

ユニティちゃんの移動操作方法について、元々の方式は左スティックを上に倒すとユニティちゃんが向いている方向に進み、左右で旋回、下で後退というもの(一般的なFPSの移動操作)ですが、操作しづらいので、カメラ方向を基準にスティックを倒した方向に進むというもの(一般的なアクションゲームやRPGの移動操作)を追加しました。
カメラ方向を使うため、カメラオブジェクトを探す必要がありますが、そのためにMainCameraのタグをMainCameraに指定します。

OnStart()内でGamepad.Gamepadsの取得を試みますが、基本的にこのタイミングだと取れないようなので、ゲームパッド追加時イベントの処理に取得処理を記述して、Gamepad.GamepadAddedにイベントを追加します。

ちなみに元々のソースは移動中しかジャンプできないようになっていましたがAnimatorに手を入れることでアイドル状態でもジャンプできるように修正しています。

次回は12月上旬くらいにHoloLensを2つ使って空間共有を実装した記事を出したいと思います。
140 180 HoloLens , Unity , Xbox One , ゲームパッド , コントローラー

記載されている会社名、および商品名等は、各社の商標または登録商標です。

0 コメント:

コメントを投稿

Related Posts Plugin for WordPress, Blogger...