ゼロから始めるVRアプリ/ゲーム開発の手引き

VRにおけるボタン操作の基本を押さえよう

第4回では、UnityのInput Systemを使ってABXYのボタン入力を取得する方法と、VRコンテンツでのモーションコントロールとボタンの使い分け方を解説します。

VRにおけるボタン操作とは

はじめに、この記事におけるモーションコントロールとボタン操作の違いを説明します。

モーションコントロールは、VRデバイスのコントローラ(この記事ではMeta QuestのTouchコントローラーを前提とします)を手で握って動かしたり振り回したりする操作のことを指します。対してボタン操作は、VRデバイスのコントローラに搭載されている物理的なボタンを指で押しこんだり倒したりする操作を指します。たとえばVRシューター『BONELAB』では敵と格闘したり銃器を扱ったりするときはモーションコントロールで操作しますが、プレイヤーキャラクターがジャンプをしたりメニュー画面を開いたりするときはTouchコントローラーにあるボタンを押しています。

BONELAB - Launch Trailer

VRデバイスが一般的なPCやスマホと比べて特徴的なのはモーションコントロールによっても操作できる点です。ただ、VRだからといってすべての操作をモーションコントロールで完結させる必要はなく、モーションコントロールに合わないことはボタン入力で済ませることも重要です。なお、モーションコントロールはWiiやNintendo Switchをはじめとする家庭用ゲーム機にも実装されていますが、VRは「仮想空間の中に実際に居るような心持になれる」という点で、Nintendo Switchなどとは体験が大きく異なります。くわしくは、連載第1回をご参照ください。

ボタン操作の3つの型

今回の記事ではUnityでTouchコントローラーのボタンを使う方法について解説します。Unityのボタン入力のしくみそのものは、VRに限らず一般的なゲームパッドやキーボード・マウスのしくみと共通しています。そのため、まずは一般的なボタン入力のしくみを学びましょう。

ボタン操作の情報はプログラム上ではほとんどの場合⁠bool⁠⁠、⁠float⁠⁠、⁠Vector2⁠の3つの型で表すことができます。

“bool”

「ボタンを押した瞬間と離した瞬間」を表します。つまり、⁠押しているか押していないか以外の状態が存在しない」ものに用います。マウスのクリック、キーボードのキー入力、ゲームパッドのABXY/○×△□ボタンなどさまざまに使用されます。

“float”

「ボタンをどれくらい押しているか?」を指します。これはゲームパッドのアナログトリガー(L2/R2もしくはLT/RT)で使われ、車のアクセル・ブレーキのペダルのように「どれくらい押し込まれているか?」を0.0~1.0の小数として情報が取れます。ただし、PCのキーボードやマウス、Nintendo Switchのコントローラなど機種によってはアナログトリガーが搭載されていないですし、アナログトリガーがあっても、押したか押していないか(bool)だけを判定することも多いです。

“Vector2”

「アナログスティック(別称:ジョイスティック)を上下左右にどれくらい倒したか」という情報をX軸とY軸で-1.0~1.0、ニュートラルでは0.0の小数として数値が取れます。アナログスティックだけではなく、スマートフォンのタッチパネルやPCのマウスポインタもVector2(X軸とY軸の小数)で表すことができます。なお、近年のゲームパッドのアナログスティックは、上下左右に倒すだけでなくボタンのように押し込むこともできるため、boolとVector2の両方を表現できます。

Touchコントローラーのボタン一覧

それでは、Touchコントローラーのボタンを見てみましょう図1⁠、図2⁠。画像は2019年のMeta Quest初代のものでQuest 2以降のものと形状が異なりますが、ボタンの種類と構成は共通しています(Meta QuestはもともとOculus Questという名前で、コントローラもOculus Touchと呼ばれていました⁠⁠。

図1 Touchコントローラーの上部ボタン
(https://developer.oculus.com/documentation/unreal/unreal-controller-input-mapping-reference/)
図1
図2 Touchコントローラーのトリガーボタン
(https://developer.oculus.com/documentation/unreal/unreal-controller-input-mapping-reference/)
図2

Touchコントローラーは一般的なゲームパッドを左右に分割したようなボタン構成になっています。ゲーム慣れしている人であれば「Nintendo SwitchのJoy-Conから十字キーを取っ払ってABXYを左右に振り分けたもの」という表現がもっとも理解しやすいでしょう。Touchコントローラーのボタン構成は以下表1⁠、表2の通りです。

表1 右手側のボタン
該当するボタン
bool Aボタン、Bボタン、右スティック押し込み、ホームボタン(Oculusボタン)
float 右トリガーボタン、右グリップボタン
Vector2 右スティック
表2 左手側のボタン
該当するボタン
bool Xボタン、Yボタン、左スティック押し込み、メニューボタン
float 左トリガーボタン、左グリップボタン
Vector2 左スティック

Touchコントローラーはさらに、他のゲームパッドとは異なり「ボタンを押していないが、ボタンに触れてはいる」という入力を取得できます。これはTouchコントローラーのボタンが静電容量式を採用しているためです。また、ボタンが存在しない場所に親指を置いている場合に、それを検知して⁠サムレスト/Thumb Rest⁠と呼ばれる入力を取得することもできます。

しかし、これらはたとえば「プレイヤーが親指をどこに置いているかによってプレイヤーアバターの指のアニメーションが変わる」というシステムに使われることはありますが、入力の判定に使われることはありません。⁠Aボタンを押し込んでいないが、Aボタンの表面に触れているときにのみ⁠決定⁠できる」というボタン入力を実装したとして、それをまともに操作できる人はほとんどいないでしょう。

UnityでTouchコントローラーのボタン入力を取る

New Controls.inputactionsを作成する

それでは、いよいよTouchコントローラーのボタン入力をUnityで取得してみましょう。今回はボタン入力の管理機能「Input System」を使った実装について解説します。まずはVRに対応したUnityプロジェクト(これまでの連載で使ったものがよいでしょう)のProjectウィンドウで、Assetsフォルダ直下に「Input」という名称のフォルダを作り、そのフォルダ内を右クリックー>Create からInput Actionを選択してください。New Controls.inputactionsという名前のファイルが作成されます。

Input ActionにTouchコントローラーのボタンマップを登録する

新規作成したNew Controls.inputactionsをダブルクリックすると出るウィンドウから、Action Mapsという、ボタン入力の割り当ての設定を作成できます。この割り当て設定を、一般的にはマップと呼びます。マップは、⁠メインのゲームプレイでのボタンの役割」⁠メニュー画面でのボタンの役割」など、場面に応じてボタンが担う役割を適宜変えられるように、複数作成できるようになっています。今回は使用場面は特に意識せず、TouchコントローラーのABXYのボタン入力を取得できるようにしましょう。

ウィンドウのAction Mapsタブの右にある+ボタンを押すと、新規マップが作成されます。名前は「Test」とでもしておきましょう。そのTestマップを選択し、Actionsタブの右にある+ボタンから、ABXYに相当する4つのアクションを作成していきます。デフォルトだと名前がすべて「New Action」になってしまうため、こちらも分かりやすいように変更します。ここで気を付けたいのは、Unity公式では、Aボタン、XボタンはいずれもprimaryButton、BボタンYボタンはいずれもsecondaryButtonという名称にまとめられていることです。機種によって、ボタンの命名規則が異なることに配慮した結果かと思われます。Touchコントローラーでは「右手側にあるのがAボタン、Bボタン」⁠左側にあるのがXボタン、Yボタン」なので、ここではprimaryButtonR、など左右を名称に入れる形としました図3⁠。くわしくは、Unity公式ドキュメントでTouchコントローラーのボタン対応表を見てみてください。

アクションを作成するとAction Propertiesが表示されます。まず、Action欄にあるAction Typeを設定します。ABXYはいずれもboolのボタンですが、その場合はAction TypeをButtonにしましょう。アナログスティックやアナログトリガーの場合は、Valueに変更します。

図3 Action Typeの変更
図3

次に、作成したActionsの右にあるプラスマークをクリック、もしくは右クリックの⁠Add Binding⁠を選択してBindingを作成しましょう。すると、ウィンドウ右側に⁠Binding Properties⁠が表示されます図4⁠。この欄にあるPathをクリックし、ABXYに相当するボタンを探します表3⁠。

図4 Bindingの設定
図4
表3 ABXYボタンとPath名称の対応
ボタンの一般名称 Pathでの名称
Aボタン <XRController>{RightHand}/primaryButton
Bボタン <XRController>{RightHand}/secondaryButton
Xボタン <XRController>{LeftHand}/primaryButton
Yボタン <XRController>{LeftHand}/secondaryButton

なお、リスト一覧から探すだけでなく、リストの右側にあるTボタンをクリックすることでボタン配置の文字列を直接テキストで入力することもできます。

Input Actionのボタンのマネージャーを作成する

つぎに、Input Actionで取得したTouchコントローラーのボタンのマネージャーを作成しましょう。Unityのシーンでボタン入力の結果を出すために押しているとき/isPressed()押した瞬間/WasPressedThisFrame()離した瞬間/WasReleasedThisFrame()の処理をそれぞれ関数として並べます。

using UnityEngine;
using UnityEngine.InputSystem;

public class InputManagerLR : MonoBehaviour
{
    static InputManagerLR m_instance;

	// あとでInspectorのActionAsset欄にInput Actionを代入する
    [SerializeField]
    InputActionAsset m_actionAsset;

    InputActionMap m_ActionMap;
    InputAction m_PrimaryButtonR;
    InputAction m_PrimaryButtonL;
    InputAction m_SecondaryButtonR;
    InputAction m_SecondaryButtonL;

    private void Awake()
    {
        m_instance = this;
	// InputActionマネージャーをシーンから破棄しないようにする
        GameObject.DontDestroyOnLoad(gameObject);
	// Action Mapsの名前を入れる
        m_ActionMap = m_actionAsset.FindActionMap("Test");
	// Actionsの名前をすべて入れる
        m_PrimaryButtonR = m_ActionMap.FindAction("XR_PrimaryButtonR", throwIfNotFound: true);
        m_PrimaryButtonL = m_ActionMap.FindAction("XR_PrimaryButtonL", throwIfNotFound: true);
        m_SecondaryButtonR = m_ActionMap.FindAction("XR_SecondaryButtonR", throwIfNotFound: true);
        m_SecondaryButtonL = m_ActionMap.FindAction("XR_SecondaryButtonL", throwIfNotFound: true);
    }

    private void OnEnable()
    {
        m_ActionMap?.Enable();
    }

    private void OnDisable()
    {
        m_ActionMap?.Disable();
    }

    public static bool PrimaryButtonR()
    {
        return m_instance.m_PrimaryButtonR.IsPressed();
    }

    public static bool PrimaryButtonR_OnPress()
    {
        return m_instance.m_PrimaryButtonR.WasPressedThisFrame();
    }

    public static bool PrimaryButtonR_OnRelease()
    {
        return m_instance.m_PrimaryButtonR.WasReleasedThisFrame();
    }

// 以下PrimaryButtonL、SecondaryButtonR、SecondaryButtonLでも同様に記載

以上のコードを作成したら、シーンに新規作成した空のゲームオブジェクトにスクリプト(この場合は⁠Input Manager LR⁠⁠)を挿入してプレハブ化しておきます図5⁠。

図5 Input Manager LRをプレハブ化
図5

マネージャーで管理しているボタン入力の状態を別のスクリプトで呼び出す

さきほどマネージャーを作成したため、別のスクリプトからボタン入力の情報を呼び出せるようになりました。ボタン入力の判定はUpdate()で毎フレーム行い、検知した場合にのみ指定の関数を実行するようにします。これにゲームオブジェクトとそのスクリプト・関数を紐づけることで、任意のボタンを押したときに別のゲームオブジェクトで関数を実行できるようになります。

using Unity.VRTemplate;
using UnityEngine;

public class InputTextLR : MonoBehaviour
{
    // ボタン入力をさらに他の関数から呼び出したい場合は、
    // 適宜ゲームオブジェクトの変数を用意してそれらの関数を呼び出す
    // このプロジェクトの場合はスクリプト"GrabProjectile"を有する
    // ゲームオブジェクトのみ代入可能となっている
    public GravProjectile m_gravProjectileR;
    public GravProjectile m_gravProjectileL;

    // Update is called once per frame
    // 毎フレームごとにボタン入力の情報を取得する
    void Update()
    {
        if (InputManagerLR.PrimaryButtonR())
        {
            OnPrimaryButtonR();
        }

        if (InputManagerLR.PrimaryButtonR_OnPress())
        {
            OnPressPrimaryButtonR();
        }

// 以下PrimaryButtonL、SecondaryButtonR、SecondaryButtonLでも同様に記載


    // 以下では、ボタン入力の情報が取得できたときに呼び出す関数

    void OnPrimaryButtonR()
    {
        // ここをコメントアウトしているのは、
        // 毎フレームごとにログテキストが表示されると邪魔になるため
        // UnityEngine.Debug.Log("PrimaryButtonRを押している");
    }

    void OnPressPrimaryButtonR()
    {
        UnityEngine.Debug.Log("PrimaryButtonRを押した瞬間");
        // gameObjectの変数に入れたゲームオブジェクトのうち
        // スクリプトGravProjectileに入っているDropObjRight関数を呼び出している
        m_gravProjectileR.DropObjRight();
        m_gravProjectileL.DropObjRight();
    }

    void OnReleasePrimaryButtonR()
    {
        UnityEngine.Debug.Log("PrimaryButtonRを離した瞬間");
    }

// 以下PrimaryButtonL、SecondaryButtonR、SecondaryButtonLでも同様に記載

コードを作成したら、シーンに新規作成した空のゲームオブジェクトにスクリプト(この場合は⁠Input Test LR⁠⁠)を挿入してプレハブ化しておきます図6⁠。

図6 Input Test LRをプレハブ化
図6

上記のコードは別ゲームオブジェクトに紐づけるサンプルとして、GravProjectile(筆者固有のスクリプト)を呼び出すようになっていますが、GravProjectileに関する記述部分を除去すれば「ボタンを押したときと離したときに、Unityのコンソール欄にログテキストが表示される」コードになります。なお、VRヘッドセットをかぶるとUnityのログテキストは見えなくなってしまうので、片手でヘッドセットの内側にある顔センサーを指で抑えたままもう片方の手でコントローラを操作してモニター上で確認することをオススメします。

レギュレーションが決まっているボタンはそれに合わせよう

押さえておくべきレギュレーションや慣習

さきほどUnityでMeta QuestのコントローラのABXYボタンを取得する方法を解説しました。残るはスティックとトリガーボタン、グリップボタンですが、実のところこれらについてはUnity側のスクリプトできっちり管理されていたり、役割が決まっていることがほとんどです(つまり、これらのボタン配置はオリジナリティが求められることがありません⁠⁠。

これまでの連載でもその規定されている役割に沿って、連載第2回ではスティック入力でプレイヤーが移動する方法について、連載第3回ではトリガーボタン・グリップボタンを使ってオブジェクトをつかんだり使ったりする方法を解説しています。今回の解説を応用してスティックとトリガーボタン・グリップボタンのVector2、floatをログテキストに表示するスクリプトを組んでみるのもよいでしょう。

なお、Unityではなく、Metaのレギュレーションによってもボタンの使い方が決められていることがあります。たとえばVRC.Quest.Input.1ではアプリ内のメニューは左のTouchコントローラーのメニューボタンで起動すること、VRC.Quest.Input.2ではアプリ内でオブジェクトを拾うときはTouchコントローラーのトリガーボタンではなくグリップボタンを使うことが推奨されています。これに沿っていないVRアプリはMetaから修正するように指導が入ったり、最悪リリースできなくなることがあるので、きちんとレギュレーションを遵守するよう心がけましょう。

ほか、Metaのレギュレーションに記載はありませんが、VRゲームにおいては一般的に⁠決定ボタン⁠は、Aボタン/Xボタンを押した時に加えて、Touchコントローラーの人差し指にあたるトリガーボタンを押したときに割り当てられます。そのため、家庭用ゲーム機の慣習の延長線上でAボタン⁠だけ⁠に決定の機能を割り当てると多くのVRゲームのユーザーからひんしゅくを買うことになります。VRコンテンツを作るにあたってVRゲームのマニアになる必要はありませんが、人気のVRゲームがどのようなボタン配置をしているかをよく確認することで開発者とユーザーのすれ違い事故を予防することができます。

EXTRA: VRで有効なボタンの使い方

以下の内容は技術的な解説というよりは補足話となりますが、言及されることは少ないものの重要な話として、VRボタンの課題について少し解説します。

課題1 ボタン配置をユーザーが確認しづらい⁠覚えづらい

VRのコンテンツにおいて、⁠A/B/X/Yボタンを入力してください」などとユーザーに指示したいことがあります。しかしVRは、右手と左手のどちらのコントローラのどのボタンにそれが割り当てられているのかが、プレイ中は分かりづらいのが難点です。Aボタンは右手の親指の一番近くにあるのでまだわかりやすいですが、それ以外は筆者でも今一つおぼろげでさえあります。

これの対応としては、一つ目に「ボタンの役割を左右シンメトリーにする」ことがあります。ボタンの機能をUnityにおけるprimaryButton(右手のAボタンと左手のXボタン)とsecondaryButton(右手のBボタンと左手のYボタン)の2種類とし、A/B/X/Yそれぞれに個別の役割を当てたり呼称したりすることを避ければ「右手/左手の親指の、上にあるボタンと下にあるボタン」という2種類の区分に認知負荷を抑えることができます。

二つ目に、視覚的な対応として「VR内にコントローラをそのまま表示してしまう」ことがあります。これを最大限に活用したVRゲームに『Red Matter 2』Meta Quest/Steam VR/PS VR2があり、VRゲーム内でプレイヤーがガジェットとしてVRコントローラに似せたものを持ち運び、ゲーム内でボタンに対応した機能がそのまま表示される、という意欲的なUIをしています図7⁠。ただし、これを実装するとなるとMeta Quest以外の機種のモデルも用意する必要があるでしょう。

図7 Red Matter 2プレイ画面
図7

課題2 float⁠Vector2の情報を活用する機会が限定的

つぎに、VR固有の課題として、VR上で精密・繊細なボタン入力を要求することが適さない場面が多い、ということがあげられます。VRではモーションコントロールによって現実世界における人間の身体のアナログ(あいまいで連続的)な動きをコンピュータに入力できるため、アナログスティックやアナログトリガーで入力のアナログ性を取り入れる操作、トリガーの押し込み具合やスティックの繊細な傾け方を主役にする意義が生じにくいです。

その他、たとえばAボタンを連打することがテーマのVRゲームを作ろうとしても「それはVRでやる必要があるのだろうか」という問題にぶち当たることでしょう。ただ、⁠不必要⁠の壁をクリエイティブで突破することも時には重要ですので、作りたいと考えているコンテンツがVRに適しているかどうかを病的に気にする必要もありません。

さいごに

本連載は第4回をもって最終回を迎えました。そこで、これまでの記事からポイントを振り返ります。

あらためて、VRで肝心なのは、ヘッドセットのディスプレイによる視覚情報と、モーションコントロールによる身体の動きがセットになることで肉体的な実感を伴う行動ができることです。視覚と身体の動きの両方の特性を考慮することで「ぜひVRで体験したい」⁠VRで体験してよかった」とユーザーが感じられるVRコンテンツになります。

つぎに、VRがユーザーの肉体を入力端末として使う以上はVR酔いに考慮しなければいけません。これはカメラ(プレイヤーの目線)の挙動だけでなく、プレイヤーの移動方法や手元の操作性の細かい手触りの調整の積み重ねがものを言います。また、Metaのガイドラインの確認や、アクセシビリティにまつわる一般的なルールへの対応を常に怠らないようにしてください。レギュレーションや慣習を確認することで車輪の再発明を未然に防いだり、⁠VRの課題⁠から逆算してクリエイティビティを注ぐ新たな箇所を見つけることもできます。

さいごに、開発者である前にユーザーの心を忘れないようにしてください。VRコンテンツを作るのにVRゲームのマニアである必要はありませんが、いちユーザーとしての目線が欠けたまま開発しつづけるとユーザーに響かなかったり理解されなかったりする事態に陥るリスクが高まります。定期的に新しいVRコンテンツに触れたり、家族や友人など適切な方にテストプレイをしてもらい意見を求めたりして視点をリセットしてください。

VRは発展途上ではありますが、Meta Questだけでなく今後さまざまな企業やプラットフォームが立ち上がってさらなる発展を遂げたり、規格競争が生じることでしょう。この先の見えない段階で未知の領域へと踏み込み、新たな発見が連なるのは何事にも代えがたい喜びなのです。

おすすめ記事

記事・ニュース一覧