From 7bf822bdb109ee5951ce9dd693376af0f80bdaf2 Mon Sep 17 00:00:00 2001 From: Artem Yadelskyi Date: Fri, 4 Nov 2022 10:20:21 +0200 Subject: [PATCH] cmd/ebitenmobile: use `go:embed` (#2435) Closes #2410 --- .../_files/EbitenSurfaceView.java | 101 +++ cmd/ebitenmobile/_files/EbitenView.java | 401 ++++++++++ .../_files/EbitenViewController.h | 33 + .../_files/EbitenViewController.m | 224 ++++++ cmd/ebitenmobile/gobind.go | 706 +----------------- cmd/ebitenmobile/gomobile.go | 22 + cmd/ebitenmobile/main.go | 27 +- 7 files changed, 795 insertions(+), 719 deletions(-) create mode 100644 cmd/ebitenmobile/_files/EbitenSurfaceView.java create mode 100644 cmd/ebitenmobile/_files/EbitenView.java create mode 100644 cmd/ebitenmobile/_files/EbitenViewController.h create mode 100644 cmd/ebitenmobile/_files/EbitenViewController.m diff --git a/cmd/ebitenmobile/_files/EbitenSurfaceView.java b/cmd/ebitenmobile/_files/EbitenSurfaceView.java new file mode 100644 index 000000000..931e4a5cf --- /dev/null +++ b/cmd/ebitenmobile/_files/EbitenSurfaceView.java @@ -0,0 +1,101 @@ +// Copyright 2022 The Ebitengine 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. + +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(); + } + } +} diff --git a/cmd/ebitenmobile/_files/EbitenView.java b/cmd/ebitenmobile/_files/EbitenView.java new file mode 100644 index 000000000..20a2b1b33 --- /dev/null +++ b/cmd/ebitenmobile/_files/EbitenView.java @@ -0,0 +1,401 @@ +// Copyright 2022 The Ebitengine 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. + +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 axes; + public ArrayList 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 { + @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(); + + 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 ranges = inputDevice.getMotionRanges(); + Collections.sort(ranges, new RangeComparator()); + + Gamepad gamepad = new Gamepad(); + gamepad.deviceId = deviceId; + gamepad.axes = new ArrayList(); + gamepad.hats = new ArrayList(); + 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 gamepads; +} diff --git a/cmd/ebitenmobile/_files/EbitenViewController.h b/cmd/ebitenmobile/_files/EbitenViewController.h new file mode 100644 index 000000000..9ed51617b --- /dev/null +++ b/cmd/ebitenmobile/_files/EbitenViewController.h @@ -0,0 +1,33 @@ +// Copyright 2022 The Ebitengine 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. + +#import + +@interface {{.PrefixUpper}}EbitenViewController : UIViewController + +// 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 overwriting this method. +- (void)onErrorOnGameUpdate:(NSError*)err; + +// suspendGame suspends the game. +// It is recommended to call this when the application is being suspended e.g., +// UIApplicationDelegate's applicationWillResignActive is called. +- (void)suspendGame; + +// resumeGame resumes the game. +// It is recommended to call this when the application is being resumed e.g., +// UIApplicationDelegate's applicationDidBecomeActive is called. +- (void)resumeGame; + +@end diff --git a/cmd/ebitenmobile/_files/EbitenViewController.m b/cmd/ebitenmobile/_files/EbitenViewController.m new file mode 100644 index 000000000..8ca0b77f7 --- /dev/null +++ b/cmd/ebitenmobile/_files/EbitenViewController.m @@ -0,0 +1,224 @@ +// Copyright 2022 The Ebitengine 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. + +#import + +#import +#import +#import + +#import "Ebitenmobileview.objc.h" + +@interface {{.PrefixUpper}}EbitenViewController : UIViewController +@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)(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]; + } + + @synchronized(self) { + if (explicitRendering_) { + [displayLink_ setPaused:YES]; + } + } +} + +- (void)glkView:(GLKView*)view drawInRect:(CGRect)rect { + [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 diff --git a/cmd/ebitenmobile/gobind.go b/cmd/ebitenmobile/gobind.go index c36dc85b8..db23da703 100644 --- a/cmd/ebitenmobile/gobind.go +++ b/cmd/ebitenmobile/gobind.go @@ -18,6 +18,7 @@ package main import ( + _ "embed" "flag" "fmt" "log" @@ -29,6 +30,15 @@ import ( "golang.org/x/tools/go/packages" ) +//go:embed _files/EbitenViewController.m +var objcM string + +//go:embed _files/EbitenView.java +var viewJava string + +//go:embed _files/EbitenSurfaceView.java +var surfaceViewJava string + var ( lang = flag.String("lang", "", "") outdir = flag.String("outdir", "", "") @@ -133,699 +143,3 @@ import "C"`); err != nil { return nil } - -const objcM = `// Code generated by ebitenmobile. DO NOT EDIT. - -#import - -#import -#import -#import - -#import "Ebitenmobileview.objc.h" - -@interface {{.PrefixUpper}}EbitenViewController : UIViewController -@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)(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]; - } - - @synchronized(self) { - if (explicitRendering_) { - [displayLink_ setPaused:YES]; - } - } -} - -- (void)glkView:(GLKView*)view drawInRect:(CGRect)rect { - [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 axes; - public ArrayList 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 { - @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(); - - 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 ranges = inputDevice.getMotionRanges(); - Collections.sort(ranges, new RangeComparator()); - - Gamepad gamepad = new Gamepad(); - gamepad.deviceId = deviceId; - gamepad.axes = new ArrayList(); - gamepad.hats = new ArrayList(); - 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 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(); - } - } -} -` diff --git a/cmd/ebitenmobile/gomobile.go b/cmd/ebitenmobile/gomobile.go index fe3660448..a6f0d8d98 100644 --- a/cmd/ebitenmobile/gomobile.go +++ b/cmd/ebitenmobile/gomobile.go @@ -30,6 +30,15 @@ import ( //go:embed gobind.go var gobind_go []byte +//go:embed _files/EbitenViewController.m +var objcM []byte + +//go:embed _files/EbitenView.java +var viewJava []byte + +//go:embed _files/EbitenSurfaceView.java +var surfaceViewJava []byte + func runCommand(command string, args []string, env []string) error { if buildX || buildN { for _, e := range env { @@ -169,6 +178,19 @@ import ( return tmp, err } + if err := os.Mkdir(filepath.Join("src", "_files"), 0755); err != nil { + return tmp, err + } + if err := os.WriteFile(filepath.Join("src", "_files", "EbitenViewController.m"), objcM, 0644); err != nil { + return tmp, err + } + if err := os.WriteFile(filepath.Join("src", "_files", "EbitenView.java"), viewJava, 0644); err != nil { + return tmp, err + } + if err := os.WriteFile(filepath.Join("src", "_files", "EbitenSurfaceView.java"), surfaceViewJava, 0644); err != nil { + return tmp, err + } + if err := runGo("build", "-o", exe(filepath.Join("bin", "gobind")), "-tags", "ebitenmobilegobind", filepath.Join("src", "gobind.go")); err != nil { return tmp, err } diff --git a/cmd/ebitenmobile/main.go b/cmd/ebitenmobile/main.go index 563810370..2f981f7f5 100644 --- a/cmd/ebitenmobile/main.go +++ b/cmd/ebitenmobile/main.go @@ -21,6 +21,7 @@ package main import ( + _ "embed" "flag" "fmt" "log" @@ -37,6 +38,9 @@ const ( ebitenmobileCommand = "ebitenmobile" ) +//go:embed _files/EbitenViewController.h +var objcH string + func init() { flag.Usage = func() { // This message is copied from `gomobile bind -h` @@ -287,29 +291,6 @@ func doBind(args []string, flagset *flag.FlagSet, buildOS string) error { return nil } -const objcH = `// Code generated by ebitenmobile. DO NOT EDIT. - -#import - -@interface {{.PrefixUpper}}EbitenViewController : UIViewController - -// 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 overwriting this method. -- (void)onErrorOnGameUpdate:(NSError*)err; - -// suspendGame suspends the game. -// It is recommended to call this when the application is being suspended e.g., -// UIApplicationDelegate's applicationWillResignActive is called. -- (void)suspendGame; - -// resumeGame resumes the game. -// It is recommended to call this when the application is being resumed e.g., -// UIApplicationDelegate's applicationDidBecomeActive is called. -- (void)resumeGame; - -@end -` - var iosModuleMapTmpl = template.Must(template.New("iosmmap").Parse(`framework module "{{.Module}}" { {{range .Headers}} header "{{.}}" {{end}}