This topic covers how to implement mouse input for Google Play Games on PC for games where input translation mode doesn't provide an ideal player experience.
PC players typically have a keyboard and mouse rather than a touchscreen, making it important to consider whether your game accomodates mouse input. By default, Google Play Games on PC converts any left-click mouse event into a single virtual tap event. This is known as "input translation mode".
Although this mode makes your game functional with few changes, it does not provide PC players with a native-feeling experience. For that, we recommend that you implement the following:
- Hover states for context menus rather than press and hold actions
- Right-click for alternative actions that happen on long press or in a context menu
- Mouselook for first or third person action games rather than a press and drag event
In order to support UI patterns that are common on PCs, you must disable input translation mode.
Input handling for Google Play Games on PC is identical to that of ChromeOS. The changes that support PCs also improve your game for all Android players.
Disable input translation mode
In your AndroidManifest.xml
file,
declare the
android.hardware.type.pc
feature.
This indicates that your game uses PC hardware and disables input translation
mode. In addition, adding required="false"
helps ensure that your game can
still be installed on phones and tablets without a mouse. For example:
<manifest ...>
<uses-feature
android:name="android.hardware.type.pc"
android:required="false" />
...
</manifest>
The production version of Google Play Games on PC switches to the correct mode when a game launches. When running in the developer emulator, you need to right-click the task bar icon, select Developer Options, and then PC mode(KiwiMouse) to receive raw mouse input.
After you do this, the mouse movement is reported by View.onGenericMotionEvent with the source SOURCE_MOUSE
indicating that it's a mouse event.
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; });
For details on handling mouse input, see the ChromeOS documentation.
Handling mouse movement
To detect mouse movement, listen to the ACTION_HOVER_ENTER
, ACTION_HOVER_EXIT
, and
ACTION_HOVER_MOVE
events.
This is best used to detect the user hovering over buttons or objects in a game, giving you a chance to display a hint box or implement a mouseover state to highlight what a player is about to select. For example:
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; });
Handling mouse buttons
PCs have long had both left and right mouse buttons, giving interactive elements both primary and secondary actions. In a game, tap actions like tapping on a button are best mapped to left-click where touch & hold actions feel most natural with right-click. In real time strategy games you might also use left-click to select and right-click to move. First person shooters might assign primary and secondary fire-to-left and right-click. An infinite runner might use left-click to jump and right-click to dash. We have not added support for the middle-click event.
To handle button presses, use ACTION_DOWN
and ACTION_UP
. Then use
getActionButton
to determine which button triggered the action or
getButtonState
to get the state of all the buttons.
In this example, an enum is used to help display the result of
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; } } }
In this example, the action is handled similar to the hover events:
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; });
Handle mousewheel scrolling
We recommend that you use the mouse scroll wheel in place of pinch to zoom gestures or touch and drag scroll areas in your game.
To read scroll wheel values, listen for the ACTION_SCROLL
event. The delta
since the last frame can be retrieved using getAxisValue
with AXIS_VSCROLL
for vertical offset and AXIS_HSCROLL
for horizontal offset. For example:
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; });
Capture mouse input
Some games need to take full control of the mouse cursor such as first or third
person action games that map mouse movement to camera movement. To take
exclusive control of the mouse, invoke View.requestPointerCapture()
.
requestPointerCapture()
only works when the view hierarchy containing your
view has focus. For this reason, you cannot acquire pointer capture in the
onCreate
callback. You should either wait for player interaction to capture
the mouse pointer, such as when interacting with the main menu, or use the
onWindowFocusChanged
callback. For example:
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(); } }
Events captured by requestPointerCapture()
are dispatched to the focusable view that registered
OnCapturedPointerListener
. For example:
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; });
In order to release exclusive mouse capture, such as to allow players to
interact with a pause menu, invoke View.releasePointerCapture()
.