mobile/ebitenmobileview: Implement Android gamepad buttons

This is still work in progress.

Updates #1083
This commit is contained in:
Hajime Hoshi 2020-03-22 19:02:56 +09:00
parent 37a8ae06c5
commit 8fcee54849
7 changed files with 344 additions and 49 deletions

View File

@ -334,49 +334,57 @@ const viewJava = `// Code generated by ebitenmobile. DO NOT EDIT.
package {{.JavaPkg}}.{{.PrefixLower}}; package {{.JavaPkg}}.{{.PrefixLower}};
import android.content.Context; import android.content.Context;
import android.hardware.input.InputManager;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log; import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.InputDevice;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.ViewGroup; import android.view.ViewGroup;
import {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview; import {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;
public class EbitenView extends ViewGroup { public class EbitenView extends ViewGroup implements InputManager.InputDeviceListener {
private double getDeviceScale() { private double getDeviceScale() {
if (deviceScale_ == 0.0) { if (this.deviceScale == 0.0) {
deviceScale_ = getResources().getDisplayMetrics().density; this.deviceScale = getResources().getDisplayMetrics().density;
} }
return deviceScale_; return this.deviceScale;
} }
private double pxToDp(double x) { private double pxToDp(double x) {
return x / getDeviceScale(); return x / getDeviceScale();
} }
private double deviceScale_ = 0.0; private double deviceScale = 0.0;
public EbitenView(Context context) { public EbitenView(Context context) {
super(context); super(context);
initialize(); initialize(context);
} }
public EbitenView(Context context, AttributeSet attrs) { public EbitenView(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
initialize(); initialize(context);
} }
private void initialize() { private void initialize(Context context) {
ebitenSurfaceView_ = new EbitenSurfaceView(getContext()); this.ebitenSurfaceView = new EbitenSurfaceView(getContext());
LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
addView(ebitenSurfaceView_, params); addView(this.ebitenSurfaceView, params);
this.inputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE);
this.inputManager.registerInputDeviceListener(this, null);
for (int id : this.inputManager.getInputDeviceIds()) {
this.onInputDeviceAdded(id);
}
} }
@Override @Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
ebitenSurfaceView_.layout(0, 0, right - left, bottom - top); this.ebitenSurfaceView.layout(0, 0, right - left, bottom - top);
double widthInDp = pxToDp(right - left); double widthInDp = pxToDp(right - left);
double heightInDp = pxToDp(bottom - top); double heightInDp = pxToDp(bottom - top);
Ebitenmobileview.layout(widthInDp, heightInDp); Ebitenmobileview.layout(widthInDp, heightInDp);
@ -384,13 +392,13 @@ public class EbitenView extends ViewGroup {
@Override @Override
public boolean onKeyDown(int keyCode, KeyEvent event) { public boolean onKeyDown(int keyCode, KeyEvent event) {
Ebitenmobileview.onKeyDownOnAndroid(keyCode, event.getUnicodeChar()); Ebitenmobileview.onKeyDownOnAndroid(keyCode, event.getUnicodeChar(), event.getSource(), event.getDeviceId());
return true; return true;
} }
@Override @Override
public boolean onKeyUp(int keyCode, KeyEvent event) { public boolean onKeyUp(int keyCode, KeyEvent event) {
Ebitenmobileview.onKeyUpOnAndroid(keyCode); Ebitenmobileview.onKeyUpOnAndroid(keyCode, event.getSource(), event.getDeviceId());
return true; return true;
} }
@ -405,11 +413,82 @@ public class EbitenView extends ViewGroup {
return true; return true;
} }
// The order must be the same as mobile/ebitenmobileview/input_android.go.
static int[] gamepadButtons = {
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_BUTTON_B,
KeyEvent.KEYCODE_BUTTON_C,
KeyEvent.KEYCODE_BUTTON_X,
KeyEvent.KEYCODE_BUTTON_Y,
KeyEvent.KEYCODE_BUTTON_Z,
KeyEvent.KEYCODE_BUTTON_L1,
KeyEvent.KEYCODE_BUTTON_R1,
KeyEvent.KEYCODE_BUTTON_L2,
KeyEvent.KEYCODE_BUTTON_R2,
KeyEvent.KEYCODE_BUTTON_THUMBL,
KeyEvent.KEYCODE_BUTTON_THUMBR,
KeyEvent.KEYCODE_BUTTON_START,
KeyEvent.KEYCODE_BUTTON_SELECT,
KeyEvent.KEYCODE_BUTTON_MODE,
KeyEvent.KEYCODE_BUTTON_1,
KeyEvent.KEYCODE_BUTTON_2,
KeyEvent.KEYCODE_BUTTON_3,
KeyEvent.KEYCODE_BUTTON_4,
KeyEvent.KEYCODE_BUTTON_5,
KeyEvent.KEYCODE_BUTTON_6,
KeyEvent.KEYCODE_BUTTON_7,
KeyEvent.KEYCODE_BUTTON_8,
KeyEvent.KEYCODE_BUTTON_9,
KeyEvent.KEYCODE_BUTTON_10,
KeyEvent.KEYCODE_BUTTON_11,
KeyEvent.KEYCODE_BUTTON_12,
KeyEvent.KEYCODE_BUTTON_13,
KeyEvent.KEYCODE_BUTTON_14,
KeyEvent.KEYCODE_BUTTON_15,
KeyEvent.KEYCODE_BUTTON_16,
};
@Override
public void onInputDeviceAdded(int deviceId) {
InputDevice inputDevice = this.inputManager.getInputDevice(deviceId);
int sources = inputDevice.getSources();
if ((sources & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD &&
(sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) {
return;
}
boolean[] keyExistences = inputDevice.hasKeys(gamepadButtons);
int buttonNum = gamepadButtons.length - 1;
for (int i = gamepadButtons.length - 1; i >= 0; i--) {
if (keyExistences[i]) {
break;
}
buttonNum--;
}
Ebitenmobileview.onGamepadAdded(deviceId, inputDevice.getName(), buttonNum);
}
@Override
public void onInputDeviceChanged(int deviceId) {
// Do nothing.
}
@Override
public void onInputDeviceRemoved(int deviceId) {
InputDevice inputDevice = this.inputManager.getInputDevice(deviceId);
int sources = inputDevice.getSources();
if ((sources & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD &&
(sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) {
return;
}
Ebitenmobileview.onGamepadRemoved(deviceId);
}
// suspendGame suspends the game. // suspendGame suspends the game.
// It is recommended to call this when the application is being suspended e.g., // It is recommended to call this when the application is being suspended e.g.,
// Activity's onPause is called. // Activity's onPause is called.
public void suspendGame() { public void suspendGame() {
ebitenSurfaceView_.onPause(); this.inputManager.unregisterInputDeviceListener(this);
this.ebitenSurfaceView.onPause();
Ebitenmobileview.suspend(); Ebitenmobileview.suspend();
} }
@ -417,7 +496,8 @@ public class EbitenView extends ViewGroup {
// It is recommended to call this when the application is being resumed e.g., // It is recommended to call this when the application is being resumed e.g.,
// Activity's onResume is called. // Activity's onResume is called.
public void resumeGame() { public void resumeGame() {
ebitenSurfaceView_.onResume(); this.inputManager.registerInputDeviceListener(this, null);
this.ebitenSurfaceView.onResume();
Ebitenmobileview.resume(); Ebitenmobileview.resume();
} }
@ -427,7 +507,8 @@ public class EbitenView extends ViewGroup {
Log.e("Go", e.toString()); Log.e("Go", e.toString());
} }
private EbitenSurfaceView ebitenSurfaceView_; private EbitenSurfaceView ebitenSurfaceView;
private InputManager inputManager;
} }
` `
@ -440,6 +521,7 @@ import android.opengl.GLSurfaceView;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10; import javax.microedition.khronos.opengles.GL10;
@ -499,7 +581,5 @@ class EbitenSurfaceView extends GLSurfaceView {
private void onErrorOnGameUpdate(Exception e) { private void onErrorOnGameUpdate(Exception e) {
((EbitenView)getParent()).onErrorOnGameUpdate(e); ((EbitenView)getParent()).onErrorOnGameUpdate(e);
} }
private double deviceScale_ = 0.0;
} }
` `

File diff suppressed because one or more lines are too long

View File

@ -50,3 +50,5 @@ const (
GamepadButton30 GamepadButton30
GamepadButton31 GamepadButton31
) )
const GamepadButtonNum = 32

View File

@ -26,12 +26,13 @@ type pos struct {
} }
type Input struct { type Input struct {
cursorX int cursorX int
cursorY int cursorY int
keys map[driver.Key]struct{} keys map[driver.Key]struct{}
runes []rune runes []rune
touches map[int]pos touches map[int]pos
ui *UserInterface gamepads []Gamepad
ui *UserInterface
} }
func (i *Input) CursorPosition() (x, y int) { func (i *Input) CursorPosition() (x, y int) {
@ -41,30 +42,78 @@ func (i *Input) CursorPosition() (x, y int) {
} }
func (i *Input) GamepadIDs() []int { func (i *Input) GamepadIDs() []int {
return nil i.ui.m.RLock()
defer i.ui.m.RUnlock()
ids := make([]int, 0, len(i.gamepads))
for _, g := range i.gamepads {
ids = append(ids, g.ID)
}
return ids
} }
func (i *Input) GamepadSDLID(id int) string { func (i *Input) GamepadSDLID(id int) string {
i.ui.m.RLock()
defer i.ui.m.RUnlock()
for _, g := range i.gamepads {
if g.ID != id {
continue
}
return g.SDLID
}
return "" return ""
} }
func (i *Input) GamepadName(id int) string { func (i *Input) GamepadName(id int) string {
i.ui.m.RLock()
defer i.ui.m.RUnlock()
for _, g := range i.gamepads {
if g.ID != id {
continue
}
return g.Name
}
return "" return ""
} }
func (i *Input) GamepadAxisNum(id int) int { func (i *Input) GamepadAxisNum(id int) int {
// TODO: Implement this
return 0 return 0
} }
func (i *Input) GamepadAxis(id int, axis int) float64 { func (i *Input) GamepadAxis(id int, axis int) float64 {
// TODO: Implement this
return 0 return 0
} }
func (i *Input) GamepadButtonNum(id int) int { func (i *Input) GamepadButtonNum(id int) int {
i.ui.m.RLock()
defer i.ui.m.RUnlock()
for _, g := range i.gamepads {
if g.ID != id {
continue
}
return g.ButtonNum
}
return 0 return 0
} }
func (i *Input) IsGamepadButtonPressed(id int, button driver.GamepadButton) bool { func (i *Input) IsGamepadButtonPressed(id int, button driver.GamepadButton) bool {
i.ui.m.RLock()
defer i.ui.m.RUnlock()
for _, g := range i.gamepads {
if g.ID != id {
continue
}
if g.ButtonNum <= int(button) {
return false
}
return g.Buttons[button]
}
return false return false
} }
@ -118,7 +167,7 @@ func (i *Input) IsMouseButtonPressed(key driver.MouseButton) bool {
return false return false
} }
func (i *Input) update(keys map[driver.Key]struct{}, runes []rune, touches []*Touch) { func (i *Input) update(keys map[driver.Key]struct{}, runes []rune, touches []*Touch, gamepads []Gamepad) {
i.ui.m.Lock() i.ui.m.Lock()
defer i.ui.m.Unlock() defer i.ui.m.Unlock()
@ -137,6 +186,9 @@ func (i *Input) update(keys map[driver.Key]struct{}, runes []rune, touches []*To
Y: t.Y, Y: t.Y,
} }
} }
i.gamepads = make([]Gamepad, len(gamepads))
copy(i.gamepads, gamepads)
} }
func (i *Input) ResetForFrame() { func (i *Input) ResetForFrame() {

View File

@ -229,7 +229,7 @@ func (u *UserInterface) appMain(a app.App) {
for _, t := range touches { for _, t := range touches {
ts = append(ts, t) ts = append(ts, t)
} }
u.input.update(keys, runes, ts) u.input.update(keys, runes, ts, nil)
} }
} }
} }
@ -451,6 +451,14 @@ type Touch struct {
Y int Y int
} }
func (u *UserInterface) UpdateInput(keys map[driver.Key]struct{}, runes []rune, touches []*Touch) { type Gamepad struct {
u.input.update(keys, runes, touches) ID int
SDLID string
Name string
Buttons [driver.GamepadButtonNum]bool
ButtonNum int
}
func (u *UserInterface) UpdateInput(keys map[driver.Key]struct{}, runes []rune, touches []*Touch, gamepads []Gamepad) {
u.input.update(keys, runes, touches, gamepads)
} }

View File

@ -27,9 +27,10 @@ type position struct {
} }
var ( var (
keys = map[driver.Key]struct{}{} keys = map[driver.Key]struct{}{}
runes []rune runes []rune
touches = map[int]position{} touches = map[int]position{}
gamepads = map[int]*mobile.Gamepad{}
) )
func updateInput() { func updateInput() {
@ -41,5 +42,11 @@ func updateInput() {
Y: position.y, Y: position.y,
}) })
} }
mobile.Get().UpdateInput(keys, runes, ts)
gs := make([]mobile.Gamepad, 0, len(gamepads))
for _, g := range gamepads {
gs = append(gs, *g)
}
mobile.Get().UpdateInput(keys, runes, ts, gs)
} }

View File

@ -16,8 +16,111 @@ package ebitenmobileview
import ( import (
"unicode" "unicode"
"github.com/hajimehoshi/ebiten/internal/driver"
"github.com/hajimehoshi/ebiten/internal/uidriver/mobile"
) )
// https://developer.android.com/reference/android/view/KeyEvent
const (
keycodeButtonA = 0x00000060
keycodeButtonB = 0x00000061
keycodeButtonC = 0x00000062
keycodeButtonX = 0x00000063
keycodeButtonY = 0x00000064
keycodeButtonZ = 0x00000065
keycodeButtonL1 = 0x00000066
keycodeButtonR1 = 0x00000067
keycodeButtonL2 = 0x00000068
keycodeButtonR2 = 0x00000069
keycodeButtonThumbl = 0x0000006a
keycodeButtonThumbr = 0x0000006b
keycodeButtonStart = 0x0000006c
keycodeButtonSelect = 0x0000006d
keycodeButtonMode = 0x0000006e
keycodeButton1 = 0x000000bc
keycodeButton2 = 0x000000bd
keycodeButton3 = 0x000000be
keycodeButton4 = 0x000000bf
keycodeButton5 = 0x000000c0
keycodeButton6 = 0x000000c1
keycodeButton7 = 0x000000c2
keycodeButton8 = 0x000000c3
keycodeButton9 = 0x000000c4
keycodeButton10 = 0x000000c5
keycodeButton11 = 0x000000c6
keycodeButton12 = 0x000000c7
keycodeButton13 = 0x000000c8
keycodeButton14 = 0x000000c9
keycodeButton15 = 0x000000ca
keycodeButton16 = 0x000000cb
)
// https://developer.android.com/reference/android/view/InputDevice
const (
sourceKeyboard = 0x00000101
sourceGamepad = 0x00000401
sourceJoystick = 0x01000010
)
var androidKeyToGamepadButton = map[int]driver.GamepadButton{
keycodeButtonA: driver.GamepadButton0,
keycodeButtonB: driver.GamepadButton1,
keycodeButtonC: driver.GamepadButton2,
keycodeButtonX: driver.GamepadButton3,
keycodeButtonY: driver.GamepadButton4,
keycodeButtonZ: driver.GamepadButton5,
keycodeButtonL1: driver.GamepadButton6,
keycodeButtonR1: driver.GamepadButton7,
keycodeButtonL2: driver.GamepadButton8,
keycodeButtonR2: driver.GamepadButton9,
keycodeButtonThumbl: driver.GamepadButton10,
keycodeButtonThumbr: driver.GamepadButton11,
keycodeButtonStart: driver.GamepadButton12,
keycodeButtonSelect: driver.GamepadButton13,
keycodeButtonMode: driver.GamepadButton14,
keycodeButton1: driver.GamepadButton15,
keycodeButton2: driver.GamepadButton16,
keycodeButton3: driver.GamepadButton17,
keycodeButton4: driver.GamepadButton18,
keycodeButton5: driver.GamepadButton19,
keycodeButton6: driver.GamepadButton20,
keycodeButton7: driver.GamepadButton21,
keycodeButton8: driver.GamepadButton22,
keycodeButton9: driver.GamepadButton23,
keycodeButton10: driver.GamepadButton24,
keycodeButton11: driver.GamepadButton25,
keycodeButton12: driver.GamepadButton26,
keycodeButton13: driver.GamepadButton27,
keycodeButton14: driver.GamepadButton28,
keycodeButton15: driver.GamepadButton29,
keycodeButton16: driver.GamepadButton30,
}
var (
// deviceIDToGamepadID is a map from Android device IDs to Ebiten gamepad IDs.
// As convention, Ebiten gamepad IDs start with 0, and many applications depend on this fact.
deviceIDToGamepadID = map[int]int{}
)
func gamepadIDFromDeviceID(deviceID int) int {
if id, ok := deviceIDToGamepadID[deviceID]; ok {
return id
}
ids := map[int]struct{}{}
for _, id := range deviceIDToGamepadID {
ids[id] = struct{}{}
}
for i := 0; ; i++ {
if _, ok := ids[i]; ok {
continue
}
deviceIDToGamepadID[deviceID] = i
return i
}
panic("ebitenmobileview: a gamepad ID cannot be determined")
}
func UpdateTouchesOnAndroid(action int, id int, x, y int) { func UpdateTouchesOnAndroid(action int, id int, x, y int) {
switch action { switch action {
case 0x00, 0x05, 0x02: // ACTION_DOWN, ACTION_POINTER_DOWN, ACTION_MOVE case 0x00, 0x05, 0x02: // ACTION_DOWN, ACTION_POINTER_DOWN, ACTION_MOVE
@ -35,23 +138,66 @@ func UpdateTouchesOnIOS(phase int, ptr int64, x, y int) {
panic("ebitenmobileview: updateTouchesOnIOSImpl must not be called on Android") panic("ebitenmobileview: updateTouchesOnIOSImpl must not be called on Android")
} }
func OnKeyDownOnAndroid(keyCode int, unicodeChar int) { func OnKeyDownOnAndroid(keyCode int, unicodeChar int, source int, deviceID int) {
key, ok := androidKeyToDriverKey[keyCode] switch {
if !ok { case source&sourceGamepad == sourceGamepad:
return // A gamepad can be detected as a keyboard. Detect the device as a gamepad first.
if button, ok := androidKeyToGamepadButton[keyCode]; ok {
id := gamepadIDFromDeviceID(deviceID)
if _, ok := gamepads[id]; !ok {
// Can this happen?
gamepads[id] = &mobile.Gamepad{}
}
gamepads[id].Buttons[button] = true
updateInput()
}
case source&sourceJoystick == sourceJoystick:
// TODO: Handle DPAD keys
case source&sourceKeyboard == sourceKeyboard:
if key, ok := androidKeyToDriverKey[keyCode]; ok {
keys[key] = struct{}{}
if r := rune(unicodeChar); r != 0 && unicode.IsPrint(r) {
runes = []rune{r}
}
updateInput()
}
} }
keys[key] = struct{}{}
if r := rune(unicodeChar); r != 0 && unicode.IsPrint(r) {
runes = []rune{r}
}
updateInput()
} }
func OnKeyUpOnAndroid(keyCode int) { func OnKeyUpOnAndroid(keyCode int, source int, deviceID int) {
key, ok := androidKeyToDriverKey[keyCode] switch {
if !ok { case source&sourceGamepad == sourceGamepad:
return // A gamepad can be detected as a keyboard. Detect the device as a gamepad first.
if button, ok := androidKeyToGamepadButton[keyCode]; ok {
id := gamepadIDFromDeviceID(deviceID)
if _, ok := gamepads[id]; !ok {
// Can this happen?
gamepads[id] = &mobile.Gamepad{}
}
gamepads[id].Buttons[button] = false
updateInput()
}
case source&sourceJoystick == sourceJoystick:
// TODO: Handle DPAD keys
case source&sourceKeyboard == sourceKeyboard:
if key, ok := androidKeyToDriverKey[keyCode]; ok {
delete(keys, key)
updateInput()
}
} }
delete(keys, key) }
updateInput()
func OnGamepadAdded(deviceID int, name string, buttonNum int) {
id := gamepadIDFromDeviceID(deviceID)
gamepads[id] = &mobile.Gamepad{
ID: id,
SDLID: "", // TODO: Implement this
Name: name,
ButtonNum: buttonNum,
}
}
func OnGamepadRemoved(deviceID int) {
id := gamepadIDFromDeviceID(deviceID)
delete(gamepads, id)
} }