Mouse input

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.

Screenshot of the "PC mode(KiwiMouse)" selected in the context menu

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().