diff --git a/cmd/ebitenmobile/gobind.go b/cmd/ebitenmobile/gobind.go index 077679e04..f50c7319e 100644 --- a/cmd/ebitenmobile/gobind.go +++ b/cmd/ebitenmobile/gobind.go @@ -334,49 +334,57 @@ const viewJava = `// Code generated by ebitenmobile. DO NOT EDIT. package {{.JavaPkg}}.{{.PrefixLower}}; import android.content.Context; +import android.hardware.input.InputManager; import android.os.Handler; import android.os.Looper; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; +import android.view.InputDevice; import android.view.MotionEvent; import android.view.ViewGroup; import {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview; -public class EbitenView extends ViewGroup { +public class EbitenView extends ViewGroup implements InputManager.InputDeviceListener { private double getDeviceScale() { - if (deviceScale_ == 0.0) { - deviceScale_ = getResources().getDisplayMetrics().density; + if (this.deviceScale == 0.0) { + this.deviceScale = getResources().getDisplayMetrics().density; } - return deviceScale_; + return this.deviceScale; } private double pxToDp(double x) { return x / getDeviceScale(); } - private double deviceScale_ = 0.0; + private double deviceScale = 0.0; public EbitenView(Context context) { super(context); - initialize(); + initialize(context); } public EbitenView(Context context, AttributeSet attrs) { super(context, attrs); - initialize(); + initialize(context); } - private void initialize() { - ebitenSurfaceView_ = new EbitenSurfaceView(getContext()); + private void initialize(Context context) { + this.ebitenSurfaceView = new EbitenSurfaceView(getContext()); 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 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 heightInDp = pxToDp(bottom - top); Ebitenmobileview.layout(widthInDp, heightInDp); @@ -384,13 +392,13 @@ public class EbitenView extends ViewGroup { @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - Ebitenmobileview.onKeyDownOnAndroid(keyCode, event.getUnicodeChar()); + Ebitenmobileview.onKeyDownOnAndroid(keyCode, event.getUnicodeChar(), event.getSource(), event.getDeviceId()); return true; } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { - Ebitenmobileview.onKeyUpOnAndroid(keyCode); + Ebitenmobileview.onKeyUpOnAndroid(keyCode, event.getSource(), event.getDeviceId()); return true; } @@ -405,11 +413,82 @@ public class EbitenView extends ViewGroup { 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. // It is recommended to call this when the application is being suspended e.g., // Activity's onPause is called. public void suspendGame() { - ebitenSurfaceView_.onPause(); + this.inputManager.unregisterInputDeviceListener(this); + this.ebitenSurfaceView.onPause(); 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., // Activity's onResume is called. public void resumeGame() { - ebitenSurfaceView_.onResume(); + this.inputManager.registerInputDeviceListener(this, null); + this.ebitenSurfaceView.onResume(); Ebitenmobileview.resume(); } @@ -427,7 +507,8 @@ public class EbitenView extends ViewGroup { 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.Looper; import android.util.AttributeSet; +import android.util.Log; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; @@ -499,7 +581,5 @@ class EbitenSurfaceView extends GLSurfaceView { private void onErrorOnGameUpdate(Exception e) { ((EbitenView)getParent()).onErrorOnGameUpdate(e); } - - private double deviceScale_ = 0.0; } ` diff --git a/cmd/ebitenmobile/gobind.src.go b/cmd/ebitenmobile/gobind.src.go index 2d7af33aa..099c07563 100644 --- a/cmd/ebitenmobile/gobind.src.go +++ b/cmd/ebitenmobile/gobind.src.go @@ -3,4 +3,4 @@ package main -var gobindsrc = []byte("// Copyright 2019 The Ebiten Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// +build ebitenmobilegobind\n\n// gobind is a wrapper of the original gobind. This command adds extra files like a view controller.\npackage main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"golang.org/x/tools/go/packages\"\n)\n\nvar (\n\tlang = flag.String(\"lang\", \"\", \"\")\n\toutdir = flag.String(\"outdir\", \"\", \"\")\n\tjavaPkg = flag.String(\"javapkg\", \"\", \"\")\n\tprefix = flag.String(\"prefix\", \"\", \"\")\n\tbootclasspath = flag.String(\"bootclasspath\", \"\", \"\")\n\tclasspath = flag.String(\"classpath\", \"\", \"\")\n\ttags = flag.String(\"tags\", \"\", \"\")\n)\n\nvar usage = `The Gobind tool generates Java language bindings for Go.\n\nFor usage details, see doc.go.`\n\nfunc main() {\n\tflag.Parse()\n\tif err := run(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\nfunc invokeOriginalGobind(lang string) (pkgName string, err error) {\n\tcmd := exec.Command(\"gobind-original\", os.Args[1:]...)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcfgtags := strings.Join(strings.Split(*tags, \",\"), \" \")\n\tcfg := &packages.Config{}\n\tswitch lang {\n\tcase \"java\":\n\t\tcfg.Env = append(os.Environ(), \"GOOS=android\")\n\tcase \"objc\":\n\t\tcfg.Env = append(os.Environ(), \"GOOS=darwin\")\n\t\tif cfgtags != \"\" {\n\t\t\tcfgtags += \" \"\n\t\t}\n\t\tcfgtags += \"ios\"\n\t}\n\tcfg.BuildFlags = []string{\"-tags\", cfgtags}\n\tpkgs, err := packages.Load(cfg, flag.Args()[0])\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn pkgs[0].Name, nil\n}\n\nfunc forceGL() bool {\n\tfor _, tag := range strings.Split(*tags, \",\") {\n\t\tif tag == \"ebitengl\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc run() error {\n\twriteFile := func(filename string, content string) error {\n\t\tif err := ioutil.WriteFile(filepath.Join(*outdir, filename), []byte(content), 0644); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Add additional files.\n\tlangs := strings.Split(*lang, \",\")\n\tfor _, lang := range langs {\n\t\tpkgName, err := invokeOriginalGobind(lang)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tprefixLower := *prefix + pkgName\n\t\tprefixUpper := strings.Title(*prefix) + strings.Title(pkgName)\n\t\treplacePrefixes := func(content string) string {\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.PrefixUpper}}\", prefixUpper)\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.PrefixLower}}\", prefixLower)\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.JavaPkg}}\", *javaPkg)\n\n\t\t\tf := \"0\"\n\t\t\tif forceGL() {\n\t\t\t\tf = \"1\"\n\t\t\t}\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.ForceGL}}\", f)\n\t\t\treturn content\n\t\t}\n\n\t\tswitch lang {\n\t\tcase \"objc\":\n\t\t\t// iOS\n\t\t\tif err := writeFile(filepath.Join(\"src\", \"gobind\", prefixLower+\"ebitenviewcontroller_ios.m\"), replacePrefixes(objcM)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"java\":\n\t\t\t// Android\n\t\t\tdir := filepath.Join(strings.Split(*javaPkg, \".\")...)\n\t\t\tdir = filepath.Join(dir, prefixLower)\n\t\t\tif err := writeFile(filepath.Join(\"java\", dir, \"EbitenView.java\"), replacePrefixes(viewJava)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := writeFile(filepath.Join(\"java\", dir, \"EbitenSurfaceView.java\"), replacePrefixes(surfaceViewJava)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"go\":\n\t\t\t// Do nothing.\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unsupported language: %s\", lang))\n\t\t}\n\t}\n\n\treturn nil\n}\n\nconst objcM = `// Code generated by ebitenmobile. DO NOT EDIT.\n\n// +build ios\n\n#import \n\n#if TARGET_IPHONE_SIMULATOR || {{.ForceGL}}\n#define EBITEN_METAL 0\n#else\n#define EBITEN_METAL 1\n#endif\n\n#import \n#import \n#import \n\n#import \"Ebitenmobileview.objc.h\"\n\n@interface {{.PrefixUpper}}EbitenViewController : UIViewController\n@end\n\n@implementation {{.PrefixUpper}}EbitenViewController {\n UIView* metalView_;\n GLKView* glkView_;\n bool started_;\n bool active_;\n bool error_;\n}\n\n- (UIView*)metalView {\n if (!metalView_) {\n metalView_ = [[UIView alloc] init];\n metalView_.multipleTouchEnabled = YES;\n }\n return metalView_;\n}\n\n- (GLKView*)glkView {\n if (!glkView_) {\n glkView_ = [[GLKView alloc] init];\n glkView_.multipleTouchEnabled = YES;\n }\n return glkView_;\n}\n\n- (void)viewDidLoad {\n [super viewDidLoad];\n\n if (!started_) {\n @synchronized(self) {\n active_ = true;\n }\n started_ = true;\n }\n\n#if EBITEN_METAL\n [self.view addSubview: self.metalView];\n EbitenmobileviewSetUIView((uintptr_t)(self.metalView));\n#else\n self.glkView.delegate = (id)(self);\n [self.view addSubview: self.glkView];\n\n EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];\n [self glkView].context = context;\n\t\n [EAGLContext setCurrentContext:context];\n#endif\n\n CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)];\n [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];\n}\n\n- (void)viewWillLayoutSubviews {\n CGRect viewRect = [[self view] frame];\n#if EBITEN_METAL\n [[self metalView] setFrame:viewRect];\n#else\n [[self glkView] setFrame:viewRect];\n#endif\n}\n\n- (void)viewDidLayoutSubviews {\n [super viewDidLayoutSubviews];\n CGRect viewRect = [[self view] frame];\n\n EbitenmobileviewLayout(viewRect.size.width, viewRect.size.height);\n}\n\n- (void)didReceiveMemoryWarning {\n [super didReceiveMemoryWarning];\n // Dispose of any resources that can be recreated.\n // TODO: Notify this to Go world?\n}\n\n- (void)drawFrame{\n @synchronized(self) {\n if (!active_) {\n return;\n }\n\n#if EBITEN_METAL\n [self updateEbiten];\n#else\n [[self glkView] setNeedsDisplay];\n#endif\n }\n}\n\n- (void)glkView:(GLKView*)view drawInRect:(CGRect)rect {\n @synchronized(self) {\n [self updateEbiten];\n }\n}\n\n- (void)updateEbiten {\n if (error_) {\n return;\n }\n NSError* err = nil;\n EbitenmobileviewUpdate(&err);\n if (err != nil) {\n [self performSelectorOnMainThread:@selector(onErrorOnGameUpdate:)\n withObject:err\n waitUntilDone:NO];\n error_ = true;\n }\n}\n\n- (void)onErrorOnGameUpdate:(NSError*)err {\n NSLog(@\"Error: %@\", err);\n}\n\n- (void)updateTouches:(NSSet*)touches {\n for (UITouch* touch in touches) {\n#if EBITEN_METAL\n if (touch.view != [self metalView]) {\n continue;\n }\n#else\n if (touch.view != [self glkView]) {\n continue;\n }\n#endif\n CGPoint location = [touch locationInView:touch.view];\n EbitenmobileviewUpdateTouchesOnIOS(touch.phase, (uintptr_t)touch, location.x, location.y);\n }\n}\n\n- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)suspendGame {\n NSAssert(started_, @\"suspendGame msut not be called before viewDidLoad is called\");\n\n @synchronized(self) {\n active_ = false;\n EbitenmobileviewSuspend();\n }\n}\n\n- (void)resumeGame {\n NSAssert(started_, @\"resumeGame msut not be called before viewDidLoad is called\");\n\n @synchronized(self) {\n active_ = true;\n EbitenmobileviewResume();\n }\n}\n\n@end\n`\n\nconst viewJava = `// Code generated by ebitenmobile. DO NOT EDIT.\n\npackage {{.JavaPkg}}.{{.PrefixLower}};\n\nimport android.content.Context;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.util.AttributeSet;\nimport android.util.Log;\nimport android.view.KeyEvent;\nimport android.view.MotionEvent;\nimport android.view.ViewGroup;\n\nimport {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;\n\npublic class EbitenView extends ViewGroup {\n private double getDeviceScale() {\n if (deviceScale_ == 0.0) {\n deviceScale_ = getResources().getDisplayMetrics().density;\n }\n return deviceScale_;\n }\n\n private double pxToDp(double x) {\n return x / getDeviceScale();\n }\n\n private double deviceScale_ = 0.0;\n\n public EbitenView(Context context) {\n super(context);\n initialize();\n }\n\n public EbitenView(Context context, AttributeSet attrs) {\n super(context, attrs);\n initialize();\n }\n\n private void initialize() {\n ebitenSurfaceView_ = new EbitenSurfaceView(getContext());\n LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);\n addView(ebitenSurfaceView_, params);\n }\n\n @Override\n protected void onLayout(boolean changed, int left, int top, int right, int bottom) {\n ebitenSurfaceView_.layout(0, 0, right - left, bottom - top);\n double widthInDp = pxToDp(right - left);\n double heightInDp = pxToDp(bottom - top);\n Ebitenmobileview.layout(widthInDp, heightInDp);\n }\n\n @Override\n public boolean onKeyDown(int keyCode, KeyEvent event) {\n Ebitenmobileview.onKeyDownOnAndroid(keyCode, event.getUnicodeChar());\n return true;\n }\n\n @Override\n public boolean onKeyUp(int keyCode, KeyEvent event) {\n Ebitenmobileview.onKeyUpOnAndroid(keyCode);\n return true;\n }\n\n @Override\n public boolean onTouchEvent(MotionEvent e) {\n for (int i = 0; i < e.getPointerCount(); i++) {\n int id = e.getPointerId(i);\n int x = (int)e.getX(i);\n int y = (int)e.getY(i);\n Ebitenmobileview.updateTouchesOnAndroid(e.getActionMasked(), id, (int)pxToDp(x), (int)pxToDp(y));\n }\n return true;\n }\n\n // suspendGame suspends the game.\n // It is recommended to call this when the application is being suspended e.g.,\n // Activity's onPause is called.\n public void suspendGame() {\n ebitenSurfaceView_.onPause();\n Ebitenmobileview.suspend();\n }\n\n // resumeGame resumes the game.\n // It is recommended to call this when the application is being resumed e.g.,\n // Activity's onResume is called.\n public void resumeGame() {\n ebitenSurfaceView_.onResume();\n Ebitenmobileview.resume();\n }\n\n // onErrorOnGameUpdate is called on the main thread when an error happens when updating a game.\n // You can define your own error handler, e.g., using Crashlytics, by overwriting this method.\n protected void onErrorOnGameUpdate(Exception e) {\n Log.e(\"Go\", e.toString());\n }\n\n private EbitenSurfaceView ebitenSurfaceView_;\n}\n`\n\nconst surfaceViewJava = `// Code generated by ebitenmobile. DO NOT EDIT.\n\npackage {{.JavaPkg}}.{{.PrefixLower}};\n\nimport android.content.Context;\nimport android.opengl.GLSurfaceView;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.util.AttributeSet;\n\nimport javax.microedition.khronos.egl.EGLConfig;\nimport javax.microedition.khronos.opengles.GL10;\n\nimport {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;\nimport {{.JavaPkg}}.{{.PrefixLower}}.EbitenView;\n\nclass EbitenSurfaceView extends GLSurfaceView {\n\n private class EbitenRenderer implements GLSurfaceView.Renderer {\n\n private boolean errored_ = false;\n\n @Override\n public void onDrawFrame(GL10 gl) {\n if (errored_) {\n return;\n }\n try {\n Ebitenmobileview.update();\n } catch (final Exception e) {\n new Handler(Looper.getMainLooper()).post(new Runnable() {\n @Override\n public void run() {\n onErrorOnGameUpdate(e);\n }\n });\n errored_ = true;\n }\n }\n\n @Override\n public void onSurfaceCreated(GL10 gl, EGLConfig config) {\n }\n\n @Override\n public void onSurfaceChanged(GL10 gl, int width, int height) {\n }\n }\n\n public EbitenSurfaceView(Context context) {\n super(context);\n initialize();\n }\n\n public EbitenSurfaceView(Context context, AttributeSet attrs) {\n super(context, attrs);\n initialize();\n }\n\n private void initialize() {\n setEGLContextClientVersion(2);\n setEGLConfigChooser(8, 8, 8, 8, 0, 0);\n setRenderer(new EbitenRenderer());\n }\n\n private void onErrorOnGameUpdate(Exception e) {\n ((EbitenView)getParent()).onErrorOnGameUpdate(e);\n }\n\n private double deviceScale_ = 0.0;\n}\n`\n") +var gobindsrc = []byte("// Copyright 2019 The Ebiten Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// +build ebitenmobilegobind\n\n// gobind is a wrapper of the original gobind. This command adds extra files like a view controller.\npackage main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"golang.org/x/tools/go/packages\"\n)\n\nvar (\n\tlang = flag.String(\"lang\", \"\", \"\")\n\toutdir = flag.String(\"outdir\", \"\", \"\")\n\tjavaPkg = flag.String(\"javapkg\", \"\", \"\")\n\tprefix = flag.String(\"prefix\", \"\", \"\")\n\tbootclasspath = flag.String(\"bootclasspath\", \"\", \"\")\n\tclasspath = flag.String(\"classpath\", \"\", \"\")\n\ttags = flag.String(\"tags\", \"\", \"\")\n)\n\nvar usage = `The Gobind tool generates Java language bindings for Go.\n\nFor usage details, see doc.go.`\n\nfunc main() {\n\tflag.Parse()\n\tif err := run(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\nfunc invokeOriginalGobind(lang string) (pkgName string, err error) {\n\tcmd := exec.Command(\"gobind-original\", os.Args[1:]...)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcfgtags := strings.Join(strings.Split(*tags, \",\"), \" \")\n\tcfg := &packages.Config{}\n\tswitch lang {\n\tcase \"java\":\n\t\tcfg.Env = append(os.Environ(), \"GOOS=android\")\n\tcase \"objc\":\n\t\tcfg.Env = append(os.Environ(), \"GOOS=darwin\")\n\t\tif cfgtags != \"\" {\n\t\t\tcfgtags += \" \"\n\t\t}\n\t\tcfgtags += \"ios\"\n\t}\n\tcfg.BuildFlags = []string{\"-tags\", cfgtags}\n\tpkgs, err := packages.Load(cfg, flag.Args()[0])\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn pkgs[0].Name, nil\n}\n\nfunc forceGL() bool {\n\tfor _, tag := range strings.Split(*tags, \",\") {\n\t\tif tag == \"ebitengl\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc run() error {\n\twriteFile := func(filename string, content string) error {\n\t\tif err := ioutil.WriteFile(filepath.Join(*outdir, filename), []byte(content), 0644); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Add additional files.\n\tlangs := strings.Split(*lang, \",\")\n\tfor _, lang := range langs {\n\t\tpkgName, err := invokeOriginalGobind(lang)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tprefixLower := *prefix + pkgName\n\t\tprefixUpper := strings.Title(*prefix) + strings.Title(pkgName)\n\t\treplacePrefixes := func(content string) string {\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.PrefixUpper}}\", prefixUpper)\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.PrefixLower}}\", prefixLower)\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.JavaPkg}}\", *javaPkg)\n\n\t\t\tf := \"0\"\n\t\t\tif forceGL() {\n\t\t\t\tf = \"1\"\n\t\t\t}\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.ForceGL}}\", f)\n\t\t\treturn content\n\t\t}\n\n\t\tswitch lang {\n\t\tcase \"objc\":\n\t\t\t// iOS\n\t\t\tif err := writeFile(filepath.Join(\"src\", \"gobind\", prefixLower+\"ebitenviewcontroller_ios.m\"), replacePrefixes(objcM)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"java\":\n\t\t\t// Android\n\t\t\tdir := filepath.Join(strings.Split(*javaPkg, \".\")...)\n\t\t\tdir = filepath.Join(dir, prefixLower)\n\t\t\tif err := writeFile(filepath.Join(\"java\", dir, \"EbitenView.java\"), replacePrefixes(viewJava)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := writeFile(filepath.Join(\"java\", dir, \"EbitenSurfaceView.java\"), replacePrefixes(surfaceViewJava)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"go\":\n\t\t\t// Do nothing.\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unsupported language: %s\", lang))\n\t\t}\n\t}\n\n\treturn nil\n}\n\nconst objcM = `// Code generated by ebitenmobile. DO NOT EDIT.\n\n// +build ios\n\n#import \n\n#if TARGET_IPHONE_SIMULATOR || {{.ForceGL}}\n#define EBITEN_METAL 0\n#else\n#define EBITEN_METAL 1\n#endif\n\n#import \n#import \n#import \n\n#import \"Ebitenmobileview.objc.h\"\n\n@interface {{.PrefixUpper}}EbitenViewController : UIViewController\n@end\n\n@implementation {{.PrefixUpper}}EbitenViewController {\n UIView* metalView_;\n GLKView* glkView_;\n bool started_;\n bool active_;\n bool error_;\n}\n\n- (UIView*)metalView {\n if (!metalView_) {\n metalView_ = [[UIView alloc] init];\n metalView_.multipleTouchEnabled = YES;\n }\n return metalView_;\n}\n\n- (GLKView*)glkView {\n if (!glkView_) {\n glkView_ = [[GLKView alloc] init];\n glkView_.multipleTouchEnabled = YES;\n }\n return glkView_;\n}\n\n- (void)viewDidLoad {\n [super viewDidLoad];\n\n if (!started_) {\n @synchronized(self) {\n active_ = true;\n }\n started_ = true;\n }\n\n#if EBITEN_METAL\n [self.view addSubview: self.metalView];\n EbitenmobileviewSetUIView((uintptr_t)(self.metalView));\n#else\n self.glkView.delegate = (id)(self);\n [self.view addSubview: self.glkView];\n\n EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];\n [self glkView].context = context;\n\t\n [EAGLContext setCurrentContext:context];\n#endif\n\n CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)];\n [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];\n}\n\n- (void)viewWillLayoutSubviews {\n CGRect viewRect = [[self view] frame];\n#if EBITEN_METAL\n [[self metalView] setFrame:viewRect];\n#else\n [[self glkView] setFrame:viewRect];\n#endif\n}\n\n- (void)viewDidLayoutSubviews {\n [super viewDidLayoutSubviews];\n CGRect viewRect = [[self view] frame];\n\n EbitenmobileviewLayout(viewRect.size.width, viewRect.size.height);\n}\n\n- (void)didReceiveMemoryWarning {\n [super didReceiveMemoryWarning];\n // Dispose of any resources that can be recreated.\n // TODO: Notify this to Go world?\n}\n\n- (void)drawFrame{\n @synchronized(self) {\n if (!active_) {\n return;\n }\n\n#if EBITEN_METAL\n [self updateEbiten];\n#else\n [[self glkView] setNeedsDisplay];\n#endif\n }\n}\n\n- (void)glkView:(GLKView*)view drawInRect:(CGRect)rect {\n @synchronized(self) {\n [self updateEbiten];\n }\n}\n\n- (void)updateEbiten {\n if (error_) {\n return;\n }\n NSError* err = nil;\n EbitenmobileviewUpdate(&err);\n if (err != nil) {\n [self performSelectorOnMainThread:@selector(onErrorOnGameUpdate:)\n withObject:err\n waitUntilDone:NO];\n error_ = true;\n }\n}\n\n- (void)onErrorOnGameUpdate:(NSError*)err {\n NSLog(@\"Error: %@\", err);\n}\n\n- (void)updateTouches:(NSSet*)touches {\n for (UITouch* touch in touches) {\n#if EBITEN_METAL\n if (touch.view != [self metalView]) {\n continue;\n }\n#else\n if (touch.view != [self glkView]) {\n continue;\n }\n#endif\n CGPoint location = [touch locationInView:touch.view];\n EbitenmobileviewUpdateTouchesOnIOS(touch.phase, (uintptr_t)touch, location.x, location.y);\n }\n}\n\n- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)suspendGame {\n NSAssert(started_, @\"suspendGame msut not be called before viewDidLoad is called\");\n\n @synchronized(self) {\n active_ = false;\n EbitenmobileviewSuspend();\n }\n}\n\n- (void)resumeGame {\n NSAssert(started_, @\"resumeGame msut not be called before viewDidLoad is called\");\n\n @synchronized(self) {\n active_ = true;\n EbitenmobileviewResume();\n }\n}\n\n@end\n`\n\nconst viewJava = `// Code generated by ebitenmobile. DO NOT EDIT.\n\npackage {{.JavaPkg}}.{{.PrefixLower}};\n\nimport android.content.Context;\nimport android.hardware.input.InputManager;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.util.AttributeSet;\nimport android.util.Log;\nimport android.view.KeyEvent;\nimport android.view.InputDevice;\nimport android.view.MotionEvent;\nimport android.view.ViewGroup;\n\nimport {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;\n\npublic class EbitenView extends ViewGroup implements InputManager.InputDeviceListener {\n private double getDeviceScale() {\n if (this.deviceScale == 0.0) {\n this.deviceScale = getResources().getDisplayMetrics().density;\n }\n return this.deviceScale;\n }\n\n private double pxToDp(double x) {\n return x / getDeviceScale();\n }\n\n private double deviceScale = 0.0;\n\n public EbitenView(Context context) {\n super(context);\n initialize(context);\n }\n\n public EbitenView(Context context, AttributeSet attrs) {\n super(context, attrs);\n initialize(context);\n }\n\n private void initialize(Context context) {\n this.ebitenSurfaceView = new EbitenSurfaceView(getContext());\n LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);\n addView(this.ebitenSurfaceView, params);\n\n this.inputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE);\n this.inputManager.registerInputDeviceListener(this, null);\n for (int id : this.inputManager.getInputDeviceIds()) {\n this.onInputDeviceAdded(id);\n }\n }\n\n @Override\n protected void onLayout(boolean changed, int left, int top, int right, int bottom) {\n this.ebitenSurfaceView.layout(0, 0, right - left, bottom - top);\n double widthInDp = pxToDp(right - left);\n double heightInDp = pxToDp(bottom - top);\n Ebitenmobileview.layout(widthInDp, heightInDp);\n }\n\n @Override\n public boolean onKeyDown(int keyCode, KeyEvent event) {\n Ebitenmobileview.onKeyDownOnAndroid(keyCode, event.getUnicodeChar(), event.getSource(), event.getDeviceId());\n return true;\n }\n\n @Override\n public boolean onKeyUp(int keyCode, KeyEvent event) {\n Ebitenmobileview.onKeyUpOnAndroid(keyCode, event.getSource(), event.getDeviceId());\n return true;\n }\n\n @Override\n public boolean onTouchEvent(MotionEvent e) {\n for (int i = 0; i < e.getPointerCount(); i++) {\n int id = e.getPointerId(i);\n int x = (int)e.getX(i);\n int y = (int)e.getY(i);\n Ebitenmobileview.updateTouchesOnAndroid(e.getActionMasked(), id, (int)pxToDp(x), (int)pxToDp(y));\n }\n return true;\n }\n\n // The order must be the same as mobile/ebitenmobileview/input_android.go.\n static int[] gamepadButtons = {\n KeyEvent.KEYCODE_BUTTON_A,\n KeyEvent.KEYCODE_BUTTON_B,\n KeyEvent.KEYCODE_BUTTON_C,\n KeyEvent.KEYCODE_BUTTON_X,\n KeyEvent.KEYCODE_BUTTON_Y,\n KeyEvent.KEYCODE_BUTTON_Z,\n KeyEvent.KEYCODE_BUTTON_L1,\n KeyEvent.KEYCODE_BUTTON_R1,\n KeyEvent.KEYCODE_BUTTON_L2,\n KeyEvent.KEYCODE_BUTTON_R2,\n KeyEvent.KEYCODE_BUTTON_THUMBL,\n KeyEvent.KEYCODE_BUTTON_THUMBR,\n KeyEvent.KEYCODE_BUTTON_START,\n KeyEvent.KEYCODE_BUTTON_SELECT,\n KeyEvent.KEYCODE_BUTTON_MODE,\n KeyEvent.KEYCODE_BUTTON_1,\n KeyEvent.KEYCODE_BUTTON_2,\n KeyEvent.KEYCODE_BUTTON_3,\n KeyEvent.KEYCODE_BUTTON_4,\n KeyEvent.KEYCODE_BUTTON_5,\n KeyEvent.KEYCODE_BUTTON_6,\n KeyEvent.KEYCODE_BUTTON_7,\n KeyEvent.KEYCODE_BUTTON_8,\n KeyEvent.KEYCODE_BUTTON_9,\n KeyEvent.KEYCODE_BUTTON_10,\n KeyEvent.KEYCODE_BUTTON_11,\n KeyEvent.KEYCODE_BUTTON_12,\n KeyEvent.KEYCODE_BUTTON_13,\n KeyEvent.KEYCODE_BUTTON_14,\n KeyEvent.KEYCODE_BUTTON_15,\n KeyEvent.KEYCODE_BUTTON_16,\n };\n\n @Override\n public void onInputDeviceAdded(int deviceId) {\n InputDevice inputDevice = this.inputManager.getInputDevice(deviceId);\n int sources = inputDevice.getSources();\n if ((sources & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD &&\n (sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) {\n return;\n }\n boolean[] keyExistences = inputDevice.hasKeys(gamepadButtons);\n int buttonNum = gamepadButtons.length - 1;\n for (int i = gamepadButtons.length - 1; i >= 0; i--) {\n if (keyExistences[i]) {\n break;\n }\n buttonNum--;\n }\n Ebitenmobileview.onGamepadAdded(deviceId, inputDevice.getName(), buttonNum);\n }\n\n @Override\n public void onInputDeviceChanged(int deviceId) {\n // Do nothing.\n }\n\n @Override\n public void onInputDeviceRemoved(int deviceId) {\n InputDevice inputDevice = this.inputManager.getInputDevice(deviceId);\n int sources = inputDevice.getSources();\n if ((sources & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD &&\n (sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) {\n return;\n }\n Ebitenmobileview.onGamepadRemoved(deviceId);\n }\n\n // suspendGame suspends the game.\n // It is recommended to call this when the application is being suspended e.g.,\n // Activity's onPause is called.\n public void suspendGame() {\n this.inputManager.unregisterInputDeviceListener(this);\n this.ebitenSurfaceView.onPause();\n Ebitenmobileview.suspend();\n }\n\n // resumeGame resumes the game.\n // It is recommended to call this when the application is being resumed e.g.,\n // Activity's onResume is called.\n public void resumeGame() {\n this.inputManager.registerInputDeviceListener(this, null);\n this.ebitenSurfaceView.onResume();\n Ebitenmobileview.resume();\n }\n\n // onErrorOnGameUpdate is called on the main thread when an error happens when updating a game.\n // You can define your own error handler, e.g., using Crashlytics, by overwriting this method.\n protected void onErrorOnGameUpdate(Exception e) {\n Log.e(\"Go\", e.toString());\n }\n\n private EbitenSurfaceView ebitenSurfaceView;\n private InputManager inputManager;\n}\n`\n\nconst surfaceViewJava = `// Code generated by ebitenmobile. DO NOT EDIT.\n\npackage {{.JavaPkg}}.{{.PrefixLower}};\n\nimport android.content.Context;\nimport android.opengl.GLSurfaceView;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.util.AttributeSet;\nimport android.util.Log;\n\nimport javax.microedition.khronos.egl.EGLConfig;\nimport javax.microedition.khronos.opengles.GL10;\n\nimport {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;\nimport {{.JavaPkg}}.{{.PrefixLower}}.EbitenView;\n\nclass EbitenSurfaceView extends GLSurfaceView {\n\n private class EbitenRenderer implements GLSurfaceView.Renderer {\n\n private boolean errored_ = false;\n\n @Override\n public void onDrawFrame(GL10 gl) {\n if (errored_) {\n return;\n }\n try {\n Ebitenmobileview.update();\n } catch (final Exception e) {\n new Handler(Looper.getMainLooper()).post(new Runnable() {\n @Override\n public void run() {\n onErrorOnGameUpdate(e);\n }\n });\n errored_ = true;\n }\n }\n\n @Override\n public void onSurfaceCreated(GL10 gl, EGLConfig config) {\n }\n\n @Override\n public void onSurfaceChanged(GL10 gl, int width, int height) {\n }\n }\n\n public EbitenSurfaceView(Context context) {\n super(context);\n initialize();\n }\n\n public EbitenSurfaceView(Context context, AttributeSet attrs) {\n super(context, attrs);\n initialize();\n }\n\n private void initialize() {\n setEGLContextClientVersion(2);\n setEGLConfigChooser(8, 8, 8, 8, 0, 0);\n setRenderer(new EbitenRenderer());\n }\n\n private void onErrorOnGameUpdate(Exception e) {\n ((EbitenView)getParent()).onErrorOnGameUpdate(e);\n }\n}\n`\n") diff --git a/internal/driver/gamepadbutton.go b/internal/driver/gamepadbutton.go index bbc6399e4..e8c4fc677 100644 --- a/internal/driver/gamepadbutton.go +++ b/internal/driver/gamepadbutton.go @@ -50,3 +50,5 @@ const ( GamepadButton30 GamepadButton31 ) + +const GamepadButtonNum = 32 diff --git a/internal/uidriver/mobile/input.go b/internal/uidriver/mobile/input.go index 73e39533a..cdf381d14 100644 --- a/internal/uidriver/mobile/input.go +++ b/internal/uidriver/mobile/input.go @@ -26,12 +26,13 @@ type pos struct { } type Input struct { - cursorX int - cursorY int - keys map[driver.Key]struct{} - runes []rune - touches map[int]pos - ui *UserInterface + cursorX int + cursorY int + keys map[driver.Key]struct{} + runes []rune + touches map[int]pos + gamepads []Gamepad + ui *UserInterface } func (i *Input) CursorPosition() (x, y int) { @@ -41,30 +42,78 @@ func (i *Input) CursorPosition() (x, y 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 { + i.ui.m.RLock() + defer i.ui.m.RUnlock() + + for _, g := range i.gamepads { + if g.ID != id { + continue + } + return g.SDLID + } return "" } 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 "" } func (i *Input) GamepadAxisNum(id int) int { + // TODO: Implement this return 0 } func (i *Input) GamepadAxis(id int, axis int) float64 { + // TODO: Implement this return 0 } 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 } 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 } @@ -118,7 +167,7 @@ func (i *Input) IsMouseButtonPressed(key driver.MouseButton) bool { 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() 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, } } + + i.gamepads = make([]Gamepad, len(gamepads)) + copy(i.gamepads, gamepads) } func (i *Input) ResetForFrame() { diff --git a/internal/uidriver/mobile/ui.go b/internal/uidriver/mobile/ui.go index 865432056..65267d650 100644 --- a/internal/uidriver/mobile/ui.go +++ b/internal/uidriver/mobile/ui.go @@ -229,7 +229,7 @@ func (u *UserInterface) appMain(a app.App) { for _, t := range touches { 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 } -func (u *UserInterface) UpdateInput(keys map[driver.Key]struct{}, runes []rune, touches []*Touch) { - u.input.update(keys, runes, touches) +type Gamepad struct { + 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) } diff --git a/mobile/ebitenmobileview/input.go b/mobile/ebitenmobileview/input.go index 486878f98..299570409 100644 --- a/mobile/ebitenmobileview/input.go +++ b/mobile/ebitenmobileview/input.go @@ -27,9 +27,10 @@ type position struct { } var ( - keys = map[driver.Key]struct{}{} - runes []rune - touches = map[int]position{} + keys = map[driver.Key]struct{}{} + runes []rune + touches = map[int]position{} + gamepads = map[int]*mobile.Gamepad{} ) func updateInput() { @@ -41,5 +42,11 @@ func updateInput() { 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) } diff --git a/mobile/ebitenmobileview/input_android.go b/mobile/ebitenmobileview/input_android.go index b6f781256..e726e2c34 100644 --- a/mobile/ebitenmobileview/input_android.go +++ b/mobile/ebitenmobileview/input_android.go @@ -16,8 +16,111 @@ package ebitenmobileview import ( "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) { switch action { 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") } -func OnKeyDownOnAndroid(keyCode int, unicodeChar int) { - key, ok := androidKeyToDriverKey[keyCode] - if !ok { - return +func OnKeyDownOnAndroid(keyCode int, unicodeChar int, source int, deviceID int) { + switch { + case source&sourceGamepad == sourceGamepad: + // 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) { - key, ok := androidKeyToDriverKey[keyCode] - if !ok { - return +func OnKeyUpOnAndroid(keyCode int, source int, deviceID int) { + switch { + case source&sourceGamepad == sourceGamepad: + // 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) }