ebiten/cmd/ebitenmobile/gobind.go
Hajime Hoshi 47558d20c5 internal/gamepaddb: enable the database for Android
Before this fix, the button and axis IDs are from the OS. These
didn't match with the SDL game controller databaes unfortunately.

This fix changes the assignments of the buttons and the axes to match
with the database.

Closes #2312
2022-09-09 22:20:39 +09:00

834 lines
26 KiB
Go

// Copyright 2019 The Ebiten Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build ebitenmobilegobind
// +build ebitenmobilegobind
// gobind is a wrapper of the original gobind. This command adds extra files like a view controller.
package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"golang.org/x/tools/go/packages"
)
var (
lang = flag.String("lang", "", "")
outdir = flag.String("outdir", "", "")
javaPkg = flag.String("javapkg", "", "")
prefix = flag.String("prefix", "", "")
bootclasspath = flag.String("bootclasspath", "", "")
classpath = flag.String("classpath", "", "")
tags = flag.String("tags", "", "")
)
var usage = `The Gobind tool generates Java language bindings for Go.
For usage details, see doc.go.`
func main() {
flag.Parse()
if err := run(); err != nil {
log.Fatal(err)
}
}
func invokeOriginalGobind(lang string) (pkgName string, err error) {
cmd := exec.Command("gobind-original", os.Args[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return "", err
}
cfgtags := strings.Join(strings.Split(*tags, ","), " ")
cfg := &packages.Config{}
switch lang {
case "java":
cfg.Env = append(os.Environ(), "GOOS=android")
case "objc":
cfg.Env = append(os.Environ(), "GOOS=darwin")
if cfgtags != "" {
cfgtags += " "
}
cfgtags += "ios"
}
cfg.BuildFlags = []string{"-tags", cfgtags}
pkgs, err := packages.Load(cfg, flag.Args()[0])
if err != nil {
return "", err
}
return pkgs[0].Name, nil
}
func run() error {
writeFile := func(filename string, content string) error {
if err := ioutil.WriteFile(filepath.Join(*outdir, filename), []byte(content), 0644); err != nil {
return err
}
return nil
}
// Add additional files.
langs := strings.Split(*lang, ",")
for _, lang := range langs {
pkgName, err := invokeOriginalGobind(lang)
if err != nil {
return err
}
prefixLower := *prefix + pkgName
prefixUpper := strings.Title(*prefix) + strings.Title(pkgName)
replacePrefixes := func(content string) string {
content = strings.ReplaceAll(content, "{{.PrefixUpper}}", prefixUpper)
content = strings.ReplaceAll(content, "{{.PrefixLower}}", prefixLower)
content = strings.ReplaceAll(content, "{{.JavaPkg}}", *javaPkg)
return content
}
switch lang {
case "objc":
// iOS
if err := writeFile(filepath.Join("src", "gobind", prefixLower+"ebitenviewcontroller_ios.m"), replacePrefixes(objcM)); err != nil {
return err
}
if err := writeFile(filepath.Join("src", "gobind", prefixLower+"ebitenviewcontroller_ios.go"), `package main
// #cgo CFLAGS: -DGLES_SILENCE_DEPRECATION
import "C"`); err != nil {
return err
}
case "java":
// Android
dir := filepath.Join(strings.Split(*javaPkg, ".")...)
dir = filepath.Join(dir, prefixLower)
if err := writeFile(filepath.Join("java", dir, "EbitenView.java"), replacePrefixes(viewJava)); err != nil {
return err
}
if err := writeFile(filepath.Join("java", dir, "EbitenSurfaceView.java"), replacePrefixes(surfaceViewJava)); err != nil {
return err
}
case "go":
// Do nothing.
default:
panic(fmt.Sprintf("unsupported language: %s", lang))
}
}
return nil
}
const objcM = `// Code generated by ebitenmobile. DO NOT EDIT.
//go:build ios
// +build ios
#import <TargetConditionals.h>
#import <stdint.h>
#import <UIKit/UIKit.h>
#import <GLKit/GLKit.h>
#import "Ebitenmobileview.objc.h"
@interface {{.PrefixUpper}}EbitenViewController : UIViewController<EbitenmobileviewRenderRequester>
@end
@implementation {{.PrefixUpper}}EbitenViewController {
UIView* metalView_;
GLKView* glkView_;
bool started_;
bool active_;
bool error_;
CADisplayLink* displayLink_;
bool explicitRendering_;
}
- (UIView*)metalView {
if (!metalView_) {
metalView_ = [[UIView alloc] init];
metalView_.multipleTouchEnabled = YES;
}
return metalView_;
}
- (GLKView*)glkView {
if (!glkView_) {
glkView_ = [[GLKView alloc] init];
glkView_.multipleTouchEnabled = YES;
}
return glkView_;
}
- (void)viewDidLoad {
[super viewDidLoad];
if (!started_) {
@synchronized(self) {
active_ = true;
}
started_ = true;
}
if (EbitenmobileviewIsGL()) {
self.glkView.delegate = (id<GLKViewDelegate>)(self);
[self.view addSubview: self.glkView];
EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
[self glkView].context = context;
[EAGLContext setCurrentContext:context];
} else {
[self.view addSubview: self.metalView];
EbitenmobileviewSetUIView((uintptr_t)(self.metalView));
}
displayLink_ = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)];
[displayLink_ addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
EbitenmobileviewSetRenderRequester(self);
}
- (void)viewWillLayoutSubviews {
CGRect viewRect = [[self view] frame];
if (EbitenmobileviewIsGL()) {
[[self glkView] setFrame:viewRect];
} else {
[[self metalView] setFrame:viewRect];
}
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
CGRect viewRect = [[self view] frame];
EbitenmobileviewLayout(viewRect.size.width, viewRect.size.height);
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
// TODO: Notify this to Go world?
}
- (void)drawFrame{
@synchronized(self) {
if (!active_) {
return;
}
if (EbitenmobileviewIsGL()) {
[[self glkView] setNeedsDisplay];
} else {
[self updateEbiten];
}
if (explicitRendering_) {
[displayLink_ setPaused:YES];
}
}
}
- (void)glkView:(GLKView*)view drawInRect:(CGRect)rect {
@synchronized(self) {
[self updateEbiten];
}
}
- (void)updateEbiten {
if (error_) {
return;
}
NSError* err = nil;
EbitenmobileviewUpdate(&err);
if (err != nil) {
[self performSelectorOnMainThread:@selector(onErrorOnGameUpdate:)
withObject:err
waitUntilDone:NO];
error_ = true;
}
}
- (void)onErrorOnGameUpdate:(NSError*)err {
NSLog(@"Error: %@", err);
}
- (void)updateTouches:(NSSet*)touches {
for (UITouch* touch in touches) {
if (EbitenmobileviewIsGL()) {
if (touch.view != [self glkView]) {
continue;
}
} else {
if (touch.view != [self metalView]) {
continue;
}
}
CGPoint location = [touch locationInView:touch.view];
EbitenmobileviewUpdateTouchesOnIOS(touch.phase, (uintptr_t)touch, location.x, location.y);
}
}
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
[self updateTouches:touches];
}
- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
[self updateTouches:touches];
}
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
[self updateTouches:touches];
}
- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
[self updateTouches:touches];
}
- (void)suspendGame {
NSAssert(started_, @"suspendGame must not be called before viewDidLoad is called");
@synchronized(self) {
active_ = false;
NSError* err = nil;
EbitenmobileviewSuspend(&err);
if (err != nil) {
[self onErrorOnGameUpdate:err];
}
}
}
- (void)resumeGame {
NSAssert(started_, @"resumeGame must not be called before viewDidLoad is called");
@synchronized(self) {
active_ = true;
NSError* err = nil;
EbitenmobileviewResume(&err);
if (err != nil) {
[self onErrorOnGameUpdate:err];
}
}
}
- (void)setExplicitRenderingMode:(BOOL)explicitRendering {
@synchronized(self) {
explicitRendering_ = explicitRendering;
if (explicitRendering_) {
[displayLink_ setPaused:YES];
}
}
}
- (void)requestRenderIfNeeded {
@synchronized(self) {
if (explicitRendering_) {
// Resume the callback temporarily.
// This is paused again soon in drawFrame.
[displayLink_ setPaused:NO];
}
}
}
@end
`
const viewJava = `// Code generated by ebitenmobile. DO NOT EDIT.
package {{.JavaPkg}}.{{.PrefixLower}};
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import android.content.Context;
import android.hardware.input.InputManager;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.KeyEvent;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.view.WindowManager;
import {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;
public class EbitenView extends ViewGroup implements InputManager.InputDeviceListener {
static class Gamepad {
public int deviceId;
public ArrayList<InputDevice.MotionRange> axes;
public ArrayList<InputDevice.MotionRange> hats;
}
// See https://github.com/libsdl-org/SDL/blob/2df2da11f627299c6e05b7e0aff407c915043372/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L154-L173
static class RangeComparator implements Comparator<InputDevice.MotionRange> {
@Override
public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
int arg0Axis = arg0.getAxis();
int arg1Axis = arg1.getAxis();
if (arg0Axis == MotionEvent.AXIS_GAS) {
arg0Axis = MotionEvent.AXIS_BRAKE;
} else if (arg0Axis == MotionEvent.AXIS_BRAKE) {
arg0Axis = MotionEvent.AXIS_GAS;
}
if (arg1Axis == MotionEvent.AXIS_GAS) {
arg1Axis = MotionEvent.AXIS_BRAKE;
} else if (arg1Axis == MotionEvent.AXIS_BRAKE) {
arg1Axis = MotionEvent.AXIS_GAS;
}
return arg0Axis - arg1Axis;
}
}
private static double pxToDp(double x) {
return x / Ebitenmobileview.deviceScale();
}
public EbitenView(Context context) {
super(context);
initialize(context);
}
public EbitenView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context);
}
private void initialize(Context context) {
this.gamepads = new ArrayList<Gamepad>();
this.ebitenSurfaceView = new EbitenSurfaceView(getContext());
LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
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
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
this.ebitenSurfaceView.layout(0, 0, right - left, bottom - top);
double widthInDp = pxToDp(right - left);
double heightInDp = pxToDp(bottom - top);
Ebitenmobileview.layout(widthInDp, heightInDp);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
Ebitenmobileview.onKeyDownOnAndroid(keyCode, event.getUnicodeChar(), event.getSource(), event.getDeviceId());
return true;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
Ebitenmobileview.onKeyUpOnAndroid(keyCode, event.getSource(), event.getDeviceId());
return true;
}
@Override
public boolean onTouchEvent(MotionEvent e) {
// getActionIndex returns a valid value only for the action whose index is the returned value of getActionIndex (#2220).
// See https://developer.android.com/reference/android/view/MotionEvent#getActionMasked().
// For other pointers, treat their actions as MotionEvent.ACTION_MOVE.
int touchIndex = e.getActionIndex();
for (int i = 0; i < e.getPointerCount(); i++) {
int id = e.getPointerId(i);
int x = (int)e.getX(i);
int y = (int)e.getY(i);
int action = (i == touchIndex) ? e.getActionMasked() : MotionEvent.ACTION_MOVE;
Ebitenmobileview.updateTouchesOnAndroid(action, id, (int)pxToDp(x), (int)pxToDp(y));
}
return true;
}
private Gamepad getGamepad(int deviceId) {
for (Gamepad gamepad : this.gamepads) {
if (gamepad.deviceId == deviceId) {
return gamepad;
}
}
return null;
}
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) {
return super.onGenericMotionEvent(event);
}
if (event.getAction() != MotionEvent.ACTION_MOVE) {
return super.onGenericMotionEvent(event);
}
// See https://github.com/libsdl-org/SDL/blob/2df2da11f627299c6e05b7e0aff407c915043372/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L256-L277
Gamepad gamepad = this.getGamepad(event.getDeviceId());
if (gamepad == null) {
return true;
}
int actionPointerIndex = event.getActionIndex();
for (int i = 0; i < gamepad.axes.size(); i++) {
InputDevice.MotionRange range = gamepad.axes.get(i);
float axisValue = event.getAxisValue(range.getAxis(), actionPointerIndex);
float value = (axisValue - range.getMin()) / range.getRange() * 2.0f - 1.0f;
Ebitenmobileview.onGamepadAxisChanged(gamepad.deviceId, i, value);
}
for (int i = 0; i < gamepad.hats.size() / 2; i++) {
int hatX = Math.round(event.getAxisValue(gamepad.hats.get(2*i).getAxis(), actionPointerIndex));
int hatY = Math.round(event.getAxisValue(gamepad.hats.get(2*i+1).getAxis(), actionPointerIndex));
Ebitenmobileview.onGamepadHatChanged(gamepad.deviceId, i, hatX, hatY);
}
return true;
}
@Override
public void onInputDeviceAdded(int deviceId) {
InputDevice inputDevice = this.inputManager.getInputDevice(deviceId);
// The InputDevice can be null on some deivces (#1342).
if (inputDevice == null) {
return;
}
// A fingerprint reader is unexpectedly recognized as a joystick. Skip this (#1542).
if (inputDevice.getName().equals("uinput-fpc")) {
return;
}
int sources = inputDevice.getSources();
if ((sources & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD &&
(sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) {
return;
}
// See https://github.com/libsdl-org/SDL/blob/2df2da11f627299c6e05b7e0aff407c915043372/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L182-L216
List<InputDevice.MotionRange> ranges = inputDevice.getMotionRanges();
Collections.sort(ranges, new RangeComparator());
Gamepad gamepad = new Gamepad();
gamepad.deviceId = deviceId;
gamepad.axes = new ArrayList<InputDevice.MotionRange>();
gamepad.hats = new ArrayList<InputDevice.MotionRange>();
for (InputDevice.MotionRange range : ranges) {
if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) {
gamepad.hats.add(range);
} else {
gamepad.axes.add(range);
}
}
this.gamepads.add(gamepad);
String descriptor = inputDevice.getDescriptor();
int vendorId = inputDevice.getVendorId();
int productId = inputDevice.getProductId();
// These values are required to calculate SDL's GUID.
int buttonMask = getButtonMask(inputDevice, gamepad.hats.size()/2);
int axisMask = getAxisMask(inputDevice);
Ebitenmobileview.onGamepadAdded(deviceId, inputDevice.getName(), gamepad.axes.size(), gamepad.hats.size()/2, descriptor, vendorId, productId, buttonMask, axisMask);
// Initialize the trigger axes values explicitly, or the initial button values would be 0.5 instead of 0.
if (gamepad.axes.size() >= 6) {
Ebitenmobileview.onGamepadAxisChanged(deviceId, 4, -1);
Ebitenmobileview.onGamepadAxisChanged(deviceId, 5, -1);
}
}
// The implementation is copied from SDL:
// https://github.com/libsdl-org/SDL/blob/0e9560aea22818884921e5e5064953257bfe7fa7/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L308
private static int getButtonMask(InputDevice joystickDevice, int nhats) {
int buttonMask = 0;
int[] keys = new int[] {
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_BUTTON_B,
KeyEvent.KEYCODE_BUTTON_X,
KeyEvent.KEYCODE_BUTTON_Y,
KeyEvent.KEYCODE_BACK,
KeyEvent.KEYCODE_BUTTON_MODE,
KeyEvent.KEYCODE_BUTTON_START,
KeyEvent.KEYCODE_BUTTON_THUMBL,
KeyEvent.KEYCODE_BUTTON_THUMBR,
KeyEvent.KEYCODE_BUTTON_L1,
KeyEvent.KEYCODE_BUTTON_R1,
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_BUTTON_SELECT,
KeyEvent.KEYCODE_DPAD_CENTER,
// These don't map into any SDL controller buttons directly
KeyEvent.KEYCODE_BUTTON_L2,
KeyEvent.KEYCODE_BUTTON_R2,
KeyEvent.KEYCODE_BUTTON_C,
KeyEvent.KEYCODE_BUTTON_Z,
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,
};
int[] masks = new int[] {
(1 << 0), // A -> A
(1 << 1), // B -> B
(1 << 2), // X -> X
(1 << 3), // Y -> Y
(1 << 4), // BACK -> BACK
(1 << 5), // MODE -> GUIDE
(1 << 6), // START -> START
(1 << 7), // THUMBL -> LEFTSTICK
(1 << 8), // THUMBR -> RIGHTSTICK
(1 << 9), // L1 -> LEFTSHOULDER
(1 << 10), // R1 -> RIGHTSHOULDER
(1 << 11), // DPAD_UP -> DPAD_UP
(1 << 12), // DPAD_DOWN -> DPAD_DOWN
(1 << 13), // DPAD_LEFT -> DPAD_LEFT
(1 << 14), // DPAD_RIGHT -> DPAD_RIGHT
(1 << 4), // SELECT -> BACK
(1 << 0), // DPAD_CENTER -> A
(1 << 15), // L2 -> ??
(1 << 16), // R2 -> ??
(1 << 17), // C -> ??
(1 << 18), // Z -> ??
(1 << 20), // 1 -> ??
(1 << 21), // 2 -> ??
(1 << 22), // 3 -> ??
(1 << 23), // 4 -> ??
(1 << 24), // 5 -> ??
(1 << 25), // 6 -> ??
(1 << 26), // 7 -> ??
(1 << 27), // 8 -> ??
(1 << 28), // 9 -> ??
(1 << 29), // 10 -> ??
(1 << 30), // 11 -> ??
(1 << 31), // 12 -> ??
// We're out of room...
0xFFFFFFFF, // 13 -> ??
0xFFFFFFFF, // 14 -> ??
0xFFFFFFFF, // 15 -> ??
0xFFFFFFFF, // 16 -> ??
};
boolean[] hasKeys = joystickDevice.hasKeys(keys);
for (int i = 0; i < keys.length; ++i) {
if (hasKeys[i]) {
buttonMask |= masks[i];
}
}
// https://github.com/libsdl-org/SDL/blob/47f2373dc13b66c48bf4024fcdab53cd0bdd59bb/src/joystick/android/SDL_sysjoystick.c#L360-L367
if (nhats > 0) {
// Add Dpad buttons.
buttonMask |= 1 << 11;
buttonMask |= 1 << 12;
buttonMask |= 1 << 13;
buttonMask |= 1 << 14;
}
return buttonMask;
}
private static int getAxisMask(InputDevice joystickDevice) {
final int SDL_CONTROLLER_AXIS_LEFTX = 0;
final int SDL_CONTROLLER_AXIS_LEFTY = 1;
final int SDL_CONTROLLER_AXIS_RIGHTX = 2;
final int SDL_CONTROLLER_AXIS_RIGHTY = 3;
final int SDL_CONTROLLER_AXIS_TRIGGERLEFT = 4;
final int SDL_CONTROLLER_AXIS_TRIGGERRIGHT = 5;
int naxes = 0;
for (InputDevice.MotionRange range : joystickDevice.getMotionRanges()) {
if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
if (range.getAxis() != MotionEvent.AXIS_HAT_X && range.getAxis() != MotionEvent.AXIS_HAT_Y) {
naxes++;
}
}
}
// The variable is_accelerometer seems always false, then skip the checking:
// https://github.com/libsdl-org/SDL/blob/0e9560aea22818884921e5e5064953257bfe7fa7/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L207
int axisMask = 0;
if (naxes >= 2) {
axisMask |= ((1 << SDL_CONTROLLER_AXIS_LEFTX) | (1 << SDL_CONTROLLER_AXIS_LEFTY));
}
if (naxes >= 4) {
axisMask |= ((1 << SDL_CONTROLLER_AXIS_RIGHTX) | (1 << SDL_CONTROLLER_AXIS_RIGHTY));
}
if (naxes >= 6) {
axisMask |= ((1 << SDL_CONTROLLER_AXIS_TRIGGERLEFT) | (1 << SDL_CONTROLLER_AXIS_TRIGGERRIGHT));
}
return axisMask;
}
@Override
public void onInputDeviceChanged(int deviceId) {
// Do nothing.
}
@Override
public void onInputDeviceRemoved(int deviceId) {
// Do not call inputManager.getInputDevice(), which returns null (#1185).
Ebitenmobileview.onInputDeviceRemoved(deviceId);
this.gamepads.remove(this.getGamepad(deviceId));
}
// suspendGame suspends the game.
// It is recommended to call this when the application is being suspended e.g.,
// Activity's onPause is called.
public void suspendGame() {
this.inputManager.unregisterInputDeviceListener(this);
this.ebitenSurfaceView.onPause();
try {
Ebitenmobileview.suspend();
} catch (final Exception e) {
onErrorOnGameUpdate(e);
}
}
// resumeGame resumes the game.
// It is recommended to call this when the application is being resumed e.g.,
// Activity's onResume is called.
public void resumeGame() {
this.inputManager.registerInputDeviceListener(this, null);
this.ebitenSurfaceView.onResume();
try {
Ebitenmobileview.resume();
} catch (final Exception e) {
onErrorOnGameUpdate(e);
}
}
// onErrorOnGameUpdate is called on the main thread when an error happens when updating a game.
// You can define your own error handler, e.g., using Crashlytics, by overriding this method.
protected void onErrorOnGameUpdate(Exception e) {
Log.e("Go", e.toString());
}
private EbitenSurfaceView ebitenSurfaceView;
private InputManager inputManager;
private ArrayList<Gamepad> gamepads;
}
`
const surfaceViewJava = `// Code generated by ebitenmobile. DO NOT EDIT.
package {{.JavaPkg}}.{{.PrefixLower}};
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.Log;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;
import {{.JavaPkg}}.ebitenmobileview.RenderRequester;
import {{.JavaPkg}}.{{.PrefixLower}}.EbitenView;
class EbitenSurfaceView extends GLSurfaceView implements RenderRequester {
private class EbitenRenderer implements GLSurfaceView.Renderer {
private boolean errored_ = false;
@Override
public void onDrawFrame(GL10 gl) {
if (errored_) {
return;
}
try {
Ebitenmobileview.update();
} catch (final Exception e) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
onErrorOnGameUpdate(e);
}
});
errored_ = true;
}
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
Ebitenmobileview.onContextLost();
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
}
}
public EbitenSurfaceView(Context context) {
super(context);
initialize();
}
public EbitenSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
private void initialize() {
setEGLContextClientVersion(2);
setEGLConfigChooser(8, 8, 8, 8, 0, 0);
setRenderer(new EbitenRenderer());
Ebitenmobileview.setRenderRequester(this);
}
private void onErrorOnGameUpdate(Exception e) {
((EbitenView)getParent()).onErrorOnGameUpdate(e);
}
@Override
public synchronized void setExplicitRenderingMode(boolean explicitRendering) {
if (explicitRendering) {
setRenderMode(RENDERMODE_WHEN_DIRTY);
} else {
setRenderMode(RENDERMODE_CONTINUOUSLY);
}
}
@Override
public synchronized void requestRenderIfNeeded() {
if (getRenderMode() == RENDERMODE_WHEN_DIRTY) {
requestRender();
}
}
}
`