マウス入力

このトピックでは、入力変換モードが理想的なプレーヤー エクスペリエンスを提供しないゲームで PC 版 Google Play Games のマウス入力を実装する方法について説明します。

PC のプレーヤーは通常、タッチスクリーンではなくキーボードとマウスを利用するため、ゲームがマウス入力に対応するかどうかを検討することが大切です。デフォルトでは、PC 版 Google Play Games は左クリックのマウスイベントをシングル バーチャル タップイベントに変換します。これを「入力変換モード」と呼びます。

このモードの場合、ゲームを動作させるにはわずかな変更で済みますが、PC のプレーヤーにはネイティブ感のエクスペリエンスが提供されません。そのため、以下を実装することをおすすめします。

  • コンテキスト メニューには、長押しアクションではなくホバー状態を実装する
  • 長押しやコンテキスト メニューで生じる代替アクションには右クリックを実装する
  • ファースト パーソン アクション ゲームやサードパーソン アクション ゲームには、押してドラッグのイベントではなく、マウスルックを実装する

PC で一般的な UI パターンをサポートするためには、入力変換モードを無効にする必要があります。

PC 版 Google Play Games の入力処理は、ChromeOS の場合と同じです。PC をサポートする変更を加えると、すべての Android プレーヤーを対象とするゲームも改善されます。

入力変換モードを無効にする

AndroidManifest.xml ファイルで、android.hardware.type.pc 機能を宣言します。これは、ゲームが PC ハードウェアを使用し、入力変換モードを無効にすることを示します。さらに、required="false" を追加すると、マウスのないスマートフォンやタブレットにもゲームをインストールできます。次に例を示します。

<manifest ...>
  <uses-feature
      android:name="android.hardware.type.pc"
      android:required="false" />
  ...
</manifest>

製品版の PC 版 Google Play Games は、ゲームの起動時に正しいモードに切り替わります。デベロッパー エミュレータで実行する場合は、タスクバー アイコンを右クリックし、[Developer Options]、[PC モード(KiwiMouse)] の順に選択して未加工のマウス入力を受け取る必要があります。

コンテキスト メニューで [PC モード(KiwiMouse)] を選択した場合のスクリーンショット

この操作を行うと、View.onGenericMotionEvent によってマウスの移動が報告され、ソース SOURCE_MOUSE でマウスイベントであることが示されます。

Kotlin

gameView.setOnGenericMotionListener { _, motionEvent ->
    var handled = false
    if (motionEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
        // handle the mouse event here
        handled = true
    }
    handled
}

Java

gameView.setOnGenericMotionListener((view, motionEvent) -> {
    if (motionEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
        // handle the mouse event here
        return true;
    }
    return false;
});

マウス入力の処理方法について詳しくは、Chrome OS のドキュメントをご覧ください。

マウス移動の処理

マウスの移動を検出するには、ACTION_HOVER_ENTERACTION_HOVER_EXITACTION_HOVER_MOVE のイベントをリッスンします。

これは、ユーザーがゲーム内のボタンやオブジェクトにカーソルを合わせたことを検出するのにおすすめの方法で、ヒントボックスを表示したり、マウスオーバー状態を実装してプレーヤーが選択しようとしている項目をハイライト表示したりするのに役立ちます。次に例を示します。

Kotlin

gameView.setOnGenericMotionListener { _, motionEvent ->
   var handled = false
   if (motionEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
       when(motionEvent.action) {
           MotionEvent.ACTION_HOVER_ENTER -> Log.d("MA", "Mouse entered at ${motionEvent.x}, ${motionEvent.y}")
           MotionEvent.ACTION_HOVER_EXIT -> Log.d("MA", "Mouse exited at ${motionEvent.x}, ${motionEvent.y}")
           MotionEvent.ACTION_HOVER_MOVE -> Log.d("MA", "Mouse hovered at ${motionEvent.x}, ${motionEvent.y}")
       }
       handled = true
   }

   handled
}

Java

gameView.setOnGenericMotionListener((view, motionEvent) -> {
    if (motionEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
        switch (motionEvent.getAction()) {
            case MotionEvent.ACTION_HOVER_ENTER:
                Log.d("MA", "Mouse entered at " + motionEvent.getX() + ", " + motionEvent.getY());
                break;
            case MotionEvent.ACTION_HOVER_EXIT:
                Log.d("MA", "Mouse exited at " + motionEvent.getX() + ", " + motionEvent.getY());
                break;
            case MotionEvent.ACTION_HOVER_MOVE:
                Log.d("MA", "Mouse hovered at " + motionEvent.getX() + ", " + motionEvent.getY());
                break;
        }
        return true;
    }
    return false;
});

マウスボタンの処理

PC には昔から左右両方のマウスボタンが搭載され、インタラクティブな要素にプライマリ アクションとセカンダリ アクションの両方が割り当てられます。ゲームでは、ボタンのタップなどのタップ アクションは、左クリックにマッピングするのが最適で、長押しアクションは、右クリックで行うのが最も自然です。リアルタイム戦略ゲームでは、左クリックで選択し、右クリックで移動することもできます。ファースト パーソン シューティング ゲームでは、プライマリ ファイアを左クリック、セカンダリ ファイアを右クリックに割り当てることができます。エンドレス ランナーでは、左クリックでジャンプ、右クリックでダッシュを行えます。ミドルクリック イベントのサポートは追加されていません。

ボタンの押下を処理するには、ACTION_DOWNACTION_UP を使用します。次に、getActionButton を使用してアクションをトリガーしたボタンを特定するか、getButtonState を使用してすべてのボタンの状態を取得します。

この例では、列挙型を使用して getActionButton の結果を表示します。

Kotlin

enum class MouseButton {
   LEFT,
   RIGHT,
   UNKNOWN;
   companion object {
       fun fromMotionEvent(motionEvent: MotionEvent): MouseButton {
           return when (motionEvent.actionButton) {
               MotionEvent.BUTTON_PRIMARY -> LEFT
               MotionEvent.BUTTON_SECONDARY -> RIGHT
               else -> UNKNOWN
           }
       }
   }
}

Java

enum MouseButton {
    LEFT,
    RIGHT,
    MIDDLE,
    UNKNOWN;
    static MouseButton fromMotionEvent(MotionEvent motionEvent) {
        switch (motionEvent.getActionButton()) {
            case MotionEvent.BUTTON_PRIMARY:
                return MouseButton.LEFT;
            case MotionEvent.BUTTON_SECONDARY:
                return MouseButton.RIGHT;
            default:
                return MouseButton.UNKNOWN;
        }
    }
}

この例では、アクションはホバーイベントと同じように処理されます。

Kotlin

// Handle the generic motion event
gameView.setOnGenericMotionListener { _, motionEvent ->
   var handled = false
   if (motionEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
       when (motionEvent.action) {
           MotionEvent.ACTION_BUTTON_PRESS -> Log.d(
               "MA",
               "${MouseButton.fromMotionEvent(motionEvent)} pressed at ${motionEvent.x}, ${motionEvent.y}"
           )
           MotionEvent.ACTION_BUTTON_RELEASE -> Log.d(
               "MA",
               "${MouseButton.fromMotionEvent(motionEvent)} released at ${motionEvent.x}, ${motionEvent.y}"
           )
       }
       handled = true
   }

   handled
}

Java

gameView.setOnGenericMotionListener((view, motionEvent) -> {
    if (motionEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
        switch (motionEvent.getAction()) {
            case MotionEvent.ACTION_BUTTON_PRESS:
                Log.d("MA", MouseButton.fromMotionEvent(motionEvent) + " pressed at " + motionEvent.getX() + ", " + motionEvent.getY());
                break;
            case MotionEvent.ACTION_BUTTON_RELEASE:
                Log.d("MA", MouseButton.fromMotionEvent(motionEvent) + " released at " + motionEvent.getX() + ", " + motionEvent.getY());
                break;
        }
        return true;
    }
    return false;
});

マウスホイール スクロールの処理

ゲーム内のスクロール エリアには、ピンチしてズームの操作やタップしてドラッグの代わりにマウス スクロール ホイールを使用することをおすすめします。

スクロール ホイールの値を読み取るには、ACTION_SCROLL イベントをリッスンします。最後のフレーム以降のデルタは、getAxisValue を使用し、垂直オフセットには AXIS_VSCROLL、水平オフセットには AXIS_HSCROLL を指定して取得できます。次に例を示します。

Kotlin

gameView.setOnGenericMotionListener { _, motionEvent ->
   var handled = false
   if (motionEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
       when (motionEvent.action) {
           MotionEvent.ACTION_SCROLL -> {
               val scrollX = motionEvent.getAxisValue(MotionEvent.AXIS_HSCROLL)
               val scrollY = motionEvent.getAxisValue(MotionEvent.AXIS_VSCROLL)
               Log.d("MA", "Mouse scrolled $scrollX, $scrollY")
           }
       }
       handled = true
   }
   handled
}

Java

gameView.setOnGenericMotionListener((view, motionEvent) -> {
    if (motionEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
        switch (motionEvent.getAction()) {
            case MotionEvent.ACTION_SCROLL:
                float scrollX = motionEvent.getAxisValue(MotionEvent.AXIS_HSCROLL);
                float scrollY = motionEvent.getAxisValue(MotionEvent.AXIS_VSCROLL);
                Log.d("MA", "Mouse scrolled " + scrollX + ", " + scrollY);
                break;
        }
        return true;
    }
    return false;
});

マウス入力のキャプチャ

マウスの移動をカメラの移動にマッピングするファースト パーソン アクション ゲームやサードパーソン アクション ゲームなど、一部のゲームではマウスカーソルを完全に制御する必要があります。マウスの排他制御には、View.requestPointerCapture() を呼び出します。

requestPointerCapture() は、ビューを含むビュー階層にフォーカスがある場合にのみ動作します。このため、onCreate コールバックでポインタのキャプチャを取得することはできません。メインメニューを操作するときなど、プレーヤーの操作によってマウスポインタがキャプチャされるまで待つか、onWindowFocusChanged コールバックを使用する必要があります。次に例を示します。

Kotlin

override fun onWindowFocusChanged(hasFocus: Boolean) {
   super.onWindowFocusChanged(hasFocus)

   if (hasFocus) {
       gameView.requestPointerCapture()
   }
}

Java

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);

    if (hasFocus) {
        View gameView = findViewById(R.id.game_view);
        gameView.requestPointerCapture();
    }
}

requestPointerCapture() によってキャプチャされたイベントは、OnCapturedPointerListener を登録したフォーカス可能なビューにディスパッチされます。次に例を示します。

Kotlin

gameView.focusable = View.FOCUSABLE
gameView.setOnCapturedPointerListener { _, motionEvent ->
    Log.d("MA", "${motionEvent.x}, ${motionEvent.y}, ${motionEvent.actionButton}")
    true
}

Java

gameView.setFocusable(true);
gameView.setOnCapturedPointerListener((view, motionEvent) -> {
    Log.d("MA", motionEvent.getX() + ", " + motionEvent.getY() + ", " + motionEvent.getActionButton());
    return true;
});

プレーヤーが一時停止メニューを操作できるようにするなど、排他的なマウス キャプチャを解放するためには、View.releasePointerCapture() を呼び出します。