mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-24 18:58:54 +01:00
parent
0afb6fd22a
commit
7bf822bdb1
101
cmd/ebitenmobile/_files/EbitenSurfaceView.java
Normal file
101
cmd/ebitenmobile/_files/EbitenSurfaceView.java
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
401
cmd/ebitenmobile/_files/EbitenView.java
Normal file
401
cmd/ebitenmobile/_files/EbitenView.java
Normal file
@ -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<InputDevice.MotionRange> axes;
|
||||
public ArrayList<InputDevice.MotionRange> hats;
|
||||
}
|
||||
|
||||
// See https://github.com/libsdl-org/SDL/blob/2df2da11f627299c6e05b7e0aff407c915043372/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L154-L173
|
||||
static class RangeComparator implements Comparator<InputDevice.MotionRange> {
|
||||
@Override
|
||||
public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
|
||||
int arg0Axis = arg0.getAxis();
|
||||
int arg1Axis = arg1.getAxis();
|
||||
if (arg0Axis == MotionEvent.AXIS_GAS) {
|
||||
arg0Axis = MotionEvent.AXIS_BRAKE;
|
||||
} else if (arg0Axis == MotionEvent.AXIS_BRAKE) {
|
||||
arg0Axis = MotionEvent.AXIS_GAS;
|
||||
}
|
||||
if (arg1Axis == MotionEvent.AXIS_GAS) {
|
||||
arg1Axis = MotionEvent.AXIS_BRAKE;
|
||||
} else if (arg1Axis == MotionEvent.AXIS_BRAKE) {
|
||||
arg1Axis = MotionEvent.AXIS_GAS;
|
||||
}
|
||||
return arg0Axis - arg1Axis;
|
||||
}
|
||||
}
|
||||
|
||||
private static double pxToDp(double x) {
|
||||
return x / Ebitenmobileview.deviceScale();
|
||||
}
|
||||
|
||||
public EbitenView(Context context) {
|
||||
super(context);
|
||||
initialize(context);
|
||||
}
|
||||
|
||||
public EbitenView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(context);
|
||||
}
|
||||
|
||||
private void initialize(Context context) {
|
||||
this.gamepads = new ArrayList<Gamepad>();
|
||||
|
||||
this.ebitenSurfaceView = new EbitenSurfaceView(getContext());
|
||||
LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
|
||||
addView(this.ebitenSurfaceView, params);
|
||||
|
||||
this.inputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE);
|
||||
this.inputManager.registerInputDeviceListener(this, null);
|
||||
for (int id : this.inputManager.getInputDeviceIds()) {
|
||||
this.onInputDeviceAdded(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
this.ebitenSurfaceView.layout(0, 0, right - left, bottom - top);
|
||||
double widthInDp = pxToDp(right - left);
|
||||
double heightInDp = pxToDp(bottom - top);
|
||||
Ebitenmobileview.layout(widthInDp, heightInDp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
Ebitenmobileview.onKeyDownOnAndroid(keyCode, event.getUnicodeChar(), event.getSource(), event.getDeviceId());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
Ebitenmobileview.onKeyUpOnAndroid(keyCode, event.getSource(), event.getDeviceId());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent e) {
|
||||
// getActionIndex returns a valid value only for the action whose index is the returned value of getActionIndex (#2220).
|
||||
// See https://developer.android.com/reference/android/view/MotionEvent#getActionMasked().
|
||||
// For other pointers, treat their actions as MotionEvent.ACTION_MOVE.
|
||||
int touchIndex = e.getActionIndex();
|
||||
for (int i = 0; i < e.getPointerCount(); i++) {
|
||||
int id = e.getPointerId(i);
|
||||
int x = (int)e.getX(i);
|
||||
int y = (int)e.getY(i);
|
||||
int action = (i == touchIndex) ? e.getActionMasked() : MotionEvent.ACTION_MOVE;
|
||||
Ebitenmobileview.updateTouchesOnAndroid(action, id, (int)pxToDp(x), (int)pxToDp(y));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Gamepad getGamepad(int deviceId) {
|
||||
for (Gamepad gamepad : this.gamepads) {
|
||||
if (gamepad.deviceId == deviceId) {
|
||||
return gamepad;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) {
|
||||
return super.onGenericMotionEvent(event);
|
||||
}
|
||||
if (event.getAction() != MotionEvent.ACTION_MOVE) {
|
||||
return super.onGenericMotionEvent(event);
|
||||
}
|
||||
|
||||
// See https://github.com/libsdl-org/SDL/blob/2df2da11f627299c6e05b7e0aff407c915043372/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L256-L277
|
||||
Gamepad gamepad = this.getGamepad(event.getDeviceId());
|
||||
if (gamepad == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int actionPointerIndex = event.getActionIndex();
|
||||
for (int i = 0; i < gamepad.axes.size(); i++) {
|
||||
InputDevice.MotionRange range = gamepad.axes.get(i);
|
||||
float axisValue = event.getAxisValue(range.getAxis(), actionPointerIndex);
|
||||
float value = (axisValue - range.getMin()) / range.getRange() * 2.0f - 1.0f;
|
||||
Ebitenmobileview.onGamepadAxisChanged(gamepad.deviceId, i, value);
|
||||
}
|
||||
for (int i = 0; i < gamepad.hats.size() / 2; i++) {
|
||||
int hatX = Math.round(event.getAxisValue(gamepad.hats.get(2*i).getAxis(), actionPointerIndex));
|
||||
int hatY = Math.round(event.getAxisValue(gamepad.hats.get(2*i+1).getAxis(), actionPointerIndex));
|
||||
Ebitenmobileview.onGamepadHatChanged(gamepad.deviceId, i, hatX, hatY);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceAdded(int deviceId) {
|
||||
InputDevice inputDevice = this.inputManager.getInputDevice(deviceId);
|
||||
// The InputDevice can be null on some deivces (#1342).
|
||||
if (inputDevice == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A fingerprint reader is unexpectedly recognized as a joystick. Skip this (#1542).
|
||||
if (inputDevice.getName().equals("uinput-fpc")) {
|
||||
return;
|
||||
}
|
||||
|
||||
int sources = inputDevice.getSources();
|
||||
if ((sources & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD &&
|
||||
(sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) {
|
||||
return;
|
||||
}
|
||||
|
||||
// See https://github.com/libsdl-org/SDL/blob/2df2da11f627299c6e05b7e0aff407c915043372/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L182-L216
|
||||
List<InputDevice.MotionRange> ranges = inputDevice.getMotionRanges();
|
||||
Collections.sort(ranges, new RangeComparator());
|
||||
|
||||
Gamepad gamepad = new Gamepad();
|
||||
gamepad.deviceId = deviceId;
|
||||
gamepad.axes = new ArrayList<InputDevice.MotionRange>();
|
||||
gamepad.hats = new ArrayList<InputDevice.MotionRange>();
|
||||
for (InputDevice.MotionRange range : ranges) {
|
||||
if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) {
|
||||
gamepad.hats.add(range);
|
||||
} else {
|
||||
gamepad.axes.add(range);
|
||||
}
|
||||
}
|
||||
this.gamepads.add(gamepad);
|
||||
|
||||
String descriptor = inputDevice.getDescriptor();
|
||||
int vendorId = inputDevice.getVendorId();
|
||||
int productId = inputDevice.getProductId();
|
||||
|
||||
// These values are required to calculate SDL's GUID.
|
||||
int buttonMask = getButtonMask(inputDevice, gamepad.hats.size()/2);
|
||||
int axisMask = getAxisMask(inputDevice);
|
||||
|
||||
Ebitenmobileview.onGamepadAdded(deviceId, inputDevice.getName(), gamepad.axes.size(), gamepad.hats.size()/2, descriptor, vendorId, productId, buttonMask, axisMask);
|
||||
|
||||
// Initialize the trigger axes values explicitly, or the initial button values would be 0.5 instead of 0.
|
||||
if (gamepad.axes.size() >= 6) {
|
||||
Ebitenmobileview.onGamepadAxisChanged(deviceId, 4, -1);
|
||||
Ebitenmobileview.onGamepadAxisChanged(deviceId, 5, -1);
|
||||
}
|
||||
}
|
||||
|
||||
// The implementation is copied from SDL:
|
||||
// https://github.com/libsdl-org/SDL/blob/0e9560aea22818884921e5e5064953257bfe7fa7/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L308
|
||||
private static int getButtonMask(InputDevice joystickDevice, int nhats) {
|
||||
int buttonMask = 0;
|
||||
int[] keys = new int[] {
|
||||
KeyEvent.KEYCODE_BUTTON_A,
|
||||
KeyEvent.KEYCODE_BUTTON_B,
|
||||
KeyEvent.KEYCODE_BUTTON_X,
|
||||
KeyEvent.KEYCODE_BUTTON_Y,
|
||||
KeyEvent.KEYCODE_BACK,
|
||||
KeyEvent.KEYCODE_BUTTON_MODE,
|
||||
KeyEvent.KEYCODE_BUTTON_START,
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBL,
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBR,
|
||||
KeyEvent.KEYCODE_BUTTON_L1,
|
||||
KeyEvent.KEYCODE_BUTTON_R1,
|
||||
KeyEvent.KEYCODE_DPAD_UP,
|
||||
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||
KeyEvent.KEYCODE_DPAD_LEFT,
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT,
|
||||
KeyEvent.KEYCODE_BUTTON_SELECT,
|
||||
KeyEvent.KEYCODE_DPAD_CENTER,
|
||||
|
||||
// These don't map into any SDL controller buttons directly
|
||||
KeyEvent.KEYCODE_BUTTON_L2,
|
||||
KeyEvent.KEYCODE_BUTTON_R2,
|
||||
KeyEvent.KEYCODE_BUTTON_C,
|
||||
KeyEvent.KEYCODE_BUTTON_Z,
|
||||
KeyEvent.KEYCODE_BUTTON_1,
|
||||
KeyEvent.KEYCODE_BUTTON_2,
|
||||
KeyEvent.KEYCODE_BUTTON_3,
|
||||
KeyEvent.KEYCODE_BUTTON_4,
|
||||
KeyEvent.KEYCODE_BUTTON_5,
|
||||
KeyEvent.KEYCODE_BUTTON_6,
|
||||
KeyEvent.KEYCODE_BUTTON_7,
|
||||
KeyEvent.KEYCODE_BUTTON_8,
|
||||
KeyEvent.KEYCODE_BUTTON_9,
|
||||
KeyEvent.KEYCODE_BUTTON_10,
|
||||
KeyEvent.KEYCODE_BUTTON_11,
|
||||
KeyEvent.KEYCODE_BUTTON_12,
|
||||
KeyEvent.KEYCODE_BUTTON_13,
|
||||
KeyEvent.KEYCODE_BUTTON_14,
|
||||
KeyEvent.KEYCODE_BUTTON_15,
|
||||
KeyEvent.KEYCODE_BUTTON_16,
|
||||
};
|
||||
int[] masks = new int[] {
|
||||
(1 << 0), // A -> A
|
||||
(1 << 1), // B -> B
|
||||
(1 << 2), // X -> X
|
||||
(1 << 3), // Y -> Y
|
||||
(1 << 4), // BACK -> BACK
|
||||
(1 << 5), // MODE -> GUIDE
|
||||
(1 << 6), // START -> START
|
||||
(1 << 7), // THUMBL -> LEFTSTICK
|
||||
(1 << 8), // THUMBR -> RIGHTSTICK
|
||||
(1 << 9), // L1 -> LEFTSHOULDER
|
||||
(1 << 10), // R1 -> RIGHTSHOULDER
|
||||
(1 << 11), // DPAD_UP -> DPAD_UP
|
||||
(1 << 12), // DPAD_DOWN -> DPAD_DOWN
|
||||
(1 << 13), // DPAD_LEFT -> DPAD_LEFT
|
||||
(1 << 14), // DPAD_RIGHT -> DPAD_RIGHT
|
||||
(1 << 4), // SELECT -> BACK
|
||||
(1 << 0), // DPAD_CENTER -> A
|
||||
(1 << 15), // L2 -> ??
|
||||
(1 << 16), // R2 -> ??
|
||||
(1 << 17), // C -> ??
|
||||
(1 << 18), // Z -> ??
|
||||
(1 << 20), // 1 -> ??
|
||||
(1 << 21), // 2 -> ??
|
||||
(1 << 22), // 3 -> ??
|
||||
(1 << 23), // 4 -> ??
|
||||
(1 << 24), // 5 -> ??
|
||||
(1 << 25), // 6 -> ??
|
||||
(1 << 26), // 7 -> ??
|
||||
(1 << 27), // 8 -> ??
|
||||
(1 << 28), // 9 -> ??
|
||||
(1 << 29), // 10 -> ??
|
||||
(1 << 30), // 11 -> ??
|
||||
(1 << 31), // 12 -> ??
|
||||
// We're out of room...
|
||||
0xFFFFFFFF, // 13 -> ??
|
||||
0xFFFFFFFF, // 14 -> ??
|
||||
0xFFFFFFFF, // 15 -> ??
|
||||
0xFFFFFFFF, // 16 -> ??
|
||||
};
|
||||
boolean[] hasKeys = joystickDevice.hasKeys(keys);
|
||||
for (int i = 0; i < keys.length; ++i) {
|
||||
if (hasKeys[i]) {
|
||||
buttonMask |= masks[i];
|
||||
}
|
||||
}
|
||||
// https://github.com/libsdl-org/SDL/blob/47f2373dc13b66c48bf4024fcdab53cd0bdd59bb/src/joystick/android/SDL_sysjoystick.c#L360-L367
|
||||
if (nhats > 0) {
|
||||
// Add Dpad buttons.
|
||||
buttonMask |= 1 << 11;
|
||||
buttonMask |= 1 << 12;
|
||||
buttonMask |= 1 << 13;
|
||||
buttonMask |= 1 << 14;
|
||||
}
|
||||
return buttonMask;
|
||||
}
|
||||
|
||||
private static int getAxisMask(InputDevice joystickDevice) {
|
||||
final int SDL_CONTROLLER_AXIS_LEFTX = 0;
|
||||
final int SDL_CONTROLLER_AXIS_LEFTY = 1;
|
||||
final int SDL_CONTROLLER_AXIS_RIGHTX = 2;
|
||||
final int SDL_CONTROLLER_AXIS_RIGHTY = 3;
|
||||
final int SDL_CONTROLLER_AXIS_TRIGGERLEFT = 4;
|
||||
final int SDL_CONTROLLER_AXIS_TRIGGERRIGHT = 5;
|
||||
|
||||
int naxes = 0;
|
||||
for (InputDevice.MotionRange range : joystickDevice.getMotionRanges()) {
|
||||
if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
|
||||
if (range.getAxis() != MotionEvent.AXIS_HAT_X && range.getAxis() != MotionEvent.AXIS_HAT_Y) {
|
||||
naxes++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The variable is_accelerometer seems always false, then skip the checking:
|
||||
// https://github.com/libsdl-org/SDL/blob/0e9560aea22818884921e5e5064953257bfe7fa7/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L207
|
||||
int axisMask = 0;
|
||||
if (naxes >= 2) {
|
||||
axisMask |= ((1 << SDL_CONTROLLER_AXIS_LEFTX) | (1 << SDL_CONTROLLER_AXIS_LEFTY));
|
||||
}
|
||||
if (naxes >= 4) {
|
||||
axisMask |= ((1 << SDL_CONTROLLER_AXIS_RIGHTX) | (1 << SDL_CONTROLLER_AXIS_RIGHTY));
|
||||
}
|
||||
if (naxes >= 6) {
|
||||
axisMask |= ((1 << SDL_CONTROLLER_AXIS_TRIGGERLEFT) | (1 << SDL_CONTROLLER_AXIS_TRIGGERRIGHT));
|
||||
}
|
||||
return axisMask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceChanged(int deviceId) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceRemoved(int deviceId) {
|
||||
// Do not call inputManager.getInputDevice(), which returns null (#1185).
|
||||
Ebitenmobileview.onInputDeviceRemoved(deviceId);
|
||||
this.gamepads.remove(this.getGamepad(deviceId));
|
||||
}
|
||||
|
||||
// suspendGame suspends the game.
|
||||
// It is recommended to call this when the application is being suspended e.g.,
|
||||
// Activity's onPause is called.
|
||||
public void suspendGame() {
|
||||
this.inputManager.unregisterInputDeviceListener(this);
|
||||
this.ebitenSurfaceView.onPause();
|
||||
try {
|
||||
Ebitenmobileview.suspend();
|
||||
} catch (final Exception e) {
|
||||
onErrorOnGameUpdate(e);
|
||||
}
|
||||
}
|
||||
|
||||
// resumeGame resumes the game.
|
||||
// It is recommended to call this when the application is being resumed e.g.,
|
||||
// Activity's onResume is called.
|
||||
public void resumeGame() {
|
||||
this.inputManager.registerInputDeviceListener(this, null);
|
||||
this.ebitenSurfaceView.onResume();
|
||||
try {
|
||||
Ebitenmobileview.resume();
|
||||
} catch (final Exception e) {
|
||||
onErrorOnGameUpdate(e);
|
||||
}
|
||||
}
|
||||
|
||||
// onErrorOnGameUpdate is called on the main thread when an error happens when updating a game.
|
||||
// You can define your own error handler, e.g., using Crashlytics, by overriding this method.
|
||||
protected void onErrorOnGameUpdate(Exception e) {
|
||||
Log.e("Go", e.toString());
|
||||
}
|
||||
|
||||
private EbitenSurfaceView ebitenSurfaceView;
|
||||
private InputManager inputManager;
|
||||
private ArrayList<Gamepad> gamepads;
|
||||
}
|
33
cmd/ebitenmobile/_files/EbitenViewController.h
Normal file
33
cmd/ebitenmobile/_files/EbitenViewController.h
Normal file
@ -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 <UIKit/UIKit.h>
|
||||
|
||||
@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
|
224
cmd/ebitenmobile/_files/EbitenViewController.m
Normal file
224
cmd/ebitenmobile/_files/EbitenViewController.m
Normal file
@ -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 <TargetConditionals.h>
|
||||
|
||||
#import <stdint.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <GLKit/GLKit.h>
|
||||
|
||||
#import "Ebitenmobileview.objc.h"
|
||||
|
||||
@interface {{.PrefixUpper}}EbitenViewController : UIViewController<EbitenmobileviewRenderRequester>
|
||||
@end
|
||||
|
||||
@implementation {{.PrefixUpper}}EbitenViewController {
|
||||
UIView* metalView_;
|
||||
GLKView* glkView_;
|
||||
bool started_;
|
||||
bool active_;
|
||||
bool error_;
|
||||
CADisplayLink* displayLink_;
|
||||
bool explicitRendering_;
|
||||
}
|
||||
|
||||
- (UIView*)metalView {
|
||||
if (!metalView_) {
|
||||
metalView_ = [[UIView alloc] init];
|
||||
metalView_.multipleTouchEnabled = YES;
|
||||
}
|
||||
return metalView_;
|
||||
}
|
||||
|
||||
- (GLKView*)glkView {
|
||||
if (!glkView_) {
|
||||
glkView_ = [[GLKView alloc] init];
|
||||
glkView_.multipleTouchEnabled = YES;
|
||||
}
|
||||
return glkView_;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
if (!started_) {
|
||||
@synchronized(self) {
|
||||
active_ = true;
|
||||
}
|
||||
started_ = true;
|
||||
}
|
||||
|
||||
if (EbitenmobileviewIsGL()) {
|
||||
self.glkView.delegate = (id<GLKViewDelegate>)(self);
|
||||
[self.view addSubview: self.glkView];
|
||||
|
||||
EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
|
||||
[self glkView].context = context;
|
||||
|
||||
[EAGLContext setCurrentContext:context];
|
||||
} else {
|
||||
[self.view addSubview: self.metalView];
|
||||
EbitenmobileviewSetUIView((uintptr_t)(self.metalView));
|
||||
}
|
||||
|
||||
displayLink_ = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)];
|
||||
[displayLink_ addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
|
||||
EbitenmobileviewSetRenderRequester(self);
|
||||
}
|
||||
|
||||
- (void)viewWillLayoutSubviews {
|
||||
CGRect viewRect = [[self view] frame];
|
||||
if (EbitenmobileviewIsGL()) {
|
||||
[[self glkView] setFrame:viewRect];
|
||||
} else {
|
||||
[[self metalView] setFrame:viewRect];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidLayoutSubviews {
|
||||
[super viewDidLayoutSubviews];
|
||||
CGRect viewRect = [[self view] frame];
|
||||
|
||||
EbitenmobileviewLayout(viewRect.size.width, viewRect.size.height);
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning {
|
||||
[super didReceiveMemoryWarning];
|
||||
// Dispose of any resources that can be recreated.
|
||||
// TODO: Notify this to Go world?
|
||||
}
|
||||
|
||||
- (void)drawFrame{
|
||||
@synchronized(self) {
|
||||
if (!active_) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (EbitenmobileviewIsGL()) {
|
||||
[[self glkView] setNeedsDisplay];
|
||||
} else {
|
||||
[self updateEbiten];
|
||||
}
|
||||
|
||||
@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
|
@ -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 <TargetConditionals.h>
|
||||
|
||||
#import <stdint.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <GLKit/GLKit.h>
|
||||
|
||||
#import "Ebitenmobileview.objc.h"
|
||||
|
||||
@interface {{.PrefixUpper}}EbitenViewController : UIViewController<EbitenmobileviewRenderRequester>
|
||||
@end
|
||||
|
||||
@implementation {{.PrefixUpper}}EbitenViewController {
|
||||
UIView* metalView_;
|
||||
GLKView* glkView_;
|
||||
bool started_;
|
||||
bool active_;
|
||||
bool error_;
|
||||
CADisplayLink* displayLink_;
|
||||
bool explicitRendering_;
|
||||
}
|
||||
|
||||
- (UIView*)metalView {
|
||||
if (!metalView_) {
|
||||
metalView_ = [[UIView alloc] init];
|
||||
metalView_.multipleTouchEnabled = YES;
|
||||
}
|
||||
return metalView_;
|
||||
}
|
||||
|
||||
- (GLKView*)glkView {
|
||||
if (!glkView_) {
|
||||
glkView_ = [[GLKView alloc] init];
|
||||
glkView_.multipleTouchEnabled = YES;
|
||||
}
|
||||
return glkView_;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
if (!started_) {
|
||||
@synchronized(self) {
|
||||
active_ = true;
|
||||
}
|
||||
started_ = true;
|
||||
}
|
||||
|
||||
if (EbitenmobileviewIsGL()) {
|
||||
self.glkView.delegate = (id<GLKViewDelegate>)(self);
|
||||
[self.view addSubview: self.glkView];
|
||||
|
||||
EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
|
||||
[self glkView].context = context;
|
||||
|
||||
[EAGLContext setCurrentContext:context];
|
||||
} else {
|
||||
[self.view addSubview: self.metalView];
|
||||
EbitenmobileviewSetUIView((uintptr_t)(self.metalView));
|
||||
}
|
||||
|
||||
displayLink_ = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)];
|
||||
[displayLink_ addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
|
||||
EbitenmobileviewSetRenderRequester(self);
|
||||
}
|
||||
|
||||
- (void)viewWillLayoutSubviews {
|
||||
CGRect viewRect = [[self view] frame];
|
||||
if (EbitenmobileviewIsGL()) {
|
||||
[[self glkView] setFrame:viewRect];
|
||||
} else {
|
||||
[[self metalView] setFrame:viewRect];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidLayoutSubviews {
|
||||
[super viewDidLayoutSubviews];
|
||||
CGRect viewRect = [[self view] frame];
|
||||
|
||||
EbitenmobileviewLayout(viewRect.size.width, viewRect.size.height);
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning {
|
||||
[super didReceiveMemoryWarning];
|
||||
// Dispose of any resources that can be recreated.
|
||||
// TODO: Notify this to Go world?
|
||||
}
|
||||
|
||||
- (void)drawFrame{
|
||||
@synchronized(self) {
|
||||
if (!active_) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (EbitenmobileviewIsGL()) {
|
||||
[[self glkView] setNeedsDisplay];
|
||||
} else {
|
||||
[self updateEbiten];
|
||||
}
|
||||
|
||||
@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<InputDevice.MotionRange> axes;
|
||||
public ArrayList<InputDevice.MotionRange> hats;
|
||||
}
|
||||
|
||||
// See https://github.com/libsdl-org/SDL/blob/2df2da11f627299c6e05b7e0aff407c915043372/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L154-L173
|
||||
static class RangeComparator implements Comparator<InputDevice.MotionRange> {
|
||||
@Override
|
||||
public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
|
||||
int arg0Axis = arg0.getAxis();
|
||||
int arg1Axis = arg1.getAxis();
|
||||
if (arg0Axis == MotionEvent.AXIS_GAS) {
|
||||
arg0Axis = MotionEvent.AXIS_BRAKE;
|
||||
} else if (arg0Axis == MotionEvent.AXIS_BRAKE) {
|
||||
arg0Axis = MotionEvent.AXIS_GAS;
|
||||
}
|
||||
if (arg1Axis == MotionEvent.AXIS_GAS) {
|
||||
arg1Axis = MotionEvent.AXIS_BRAKE;
|
||||
} else if (arg1Axis == MotionEvent.AXIS_BRAKE) {
|
||||
arg1Axis = MotionEvent.AXIS_GAS;
|
||||
}
|
||||
return arg0Axis - arg1Axis;
|
||||
}
|
||||
}
|
||||
|
||||
private static double pxToDp(double x) {
|
||||
return x / Ebitenmobileview.deviceScale();
|
||||
}
|
||||
|
||||
public EbitenView(Context context) {
|
||||
super(context);
|
||||
initialize(context);
|
||||
}
|
||||
|
||||
public EbitenView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(context);
|
||||
}
|
||||
|
||||
private void initialize(Context context) {
|
||||
this.gamepads = new ArrayList<Gamepad>();
|
||||
|
||||
this.ebitenSurfaceView = new EbitenSurfaceView(getContext());
|
||||
LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
|
||||
addView(this.ebitenSurfaceView, params);
|
||||
|
||||
this.inputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE);
|
||||
this.inputManager.registerInputDeviceListener(this, null);
|
||||
for (int id : this.inputManager.getInputDeviceIds()) {
|
||||
this.onInputDeviceAdded(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
this.ebitenSurfaceView.layout(0, 0, right - left, bottom - top);
|
||||
double widthInDp = pxToDp(right - left);
|
||||
double heightInDp = pxToDp(bottom - top);
|
||||
Ebitenmobileview.layout(widthInDp, heightInDp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
Ebitenmobileview.onKeyDownOnAndroid(keyCode, event.getUnicodeChar(), event.getSource(), event.getDeviceId());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
Ebitenmobileview.onKeyUpOnAndroid(keyCode, event.getSource(), event.getDeviceId());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent e) {
|
||||
// getActionIndex returns a valid value only for the action whose index is the returned value of getActionIndex (#2220).
|
||||
// See https://developer.android.com/reference/android/view/MotionEvent#getActionMasked().
|
||||
// For other pointers, treat their actions as MotionEvent.ACTION_MOVE.
|
||||
int touchIndex = e.getActionIndex();
|
||||
for (int i = 0; i < e.getPointerCount(); i++) {
|
||||
int id = e.getPointerId(i);
|
||||
int x = (int)e.getX(i);
|
||||
int y = (int)e.getY(i);
|
||||
int action = (i == touchIndex) ? e.getActionMasked() : MotionEvent.ACTION_MOVE;
|
||||
Ebitenmobileview.updateTouchesOnAndroid(action, id, (int)pxToDp(x), (int)pxToDp(y));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Gamepad getGamepad(int deviceId) {
|
||||
for (Gamepad gamepad : this.gamepads) {
|
||||
if (gamepad.deviceId == deviceId) {
|
||||
return gamepad;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) {
|
||||
return super.onGenericMotionEvent(event);
|
||||
}
|
||||
if (event.getAction() != MotionEvent.ACTION_MOVE) {
|
||||
return super.onGenericMotionEvent(event);
|
||||
}
|
||||
|
||||
// See https://github.com/libsdl-org/SDL/blob/2df2da11f627299c6e05b7e0aff407c915043372/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L256-L277
|
||||
Gamepad gamepad = this.getGamepad(event.getDeviceId());
|
||||
if (gamepad == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int actionPointerIndex = event.getActionIndex();
|
||||
for (int i = 0; i < gamepad.axes.size(); i++) {
|
||||
InputDevice.MotionRange range = gamepad.axes.get(i);
|
||||
float axisValue = event.getAxisValue(range.getAxis(), actionPointerIndex);
|
||||
float value = (axisValue - range.getMin()) / range.getRange() * 2.0f - 1.0f;
|
||||
Ebitenmobileview.onGamepadAxisChanged(gamepad.deviceId, i, value);
|
||||
}
|
||||
for (int i = 0; i < gamepad.hats.size() / 2; i++) {
|
||||
int hatX = Math.round(event.getAxisValue(gamepad.hats.get(2*i).getAxis(), actionPointerIndex));
|
||||
int hatY = Math.round(event.getAxisValue(gamepad.hats.get(2*i+1).getAxis(), actionPointerIndex));
|
||||
Ebitenmobileview.onGamepadHatChanged(gamepad.deviceId, i, hatX, hatY);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceAdded(int deviceId) {
|
||||
InputDevice inputDevice = this.inputManager.getInputDevice(deviceId);
|
||||
// The InputDevice can be null on some deivces (#1342).
|
||||
if (inputDevice == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A fingerprint reader is unexpectedly recognized as a joystick. Skip this (#1542).
|
||||
if (inputDevice.getName().equals("uinput-fpc")) {
|
||||
return;
|
||||
}
|
||||
|
||||
int sources = inputDevice.getSources();
|
||||
if ((sources & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD &&
|
||||
(sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) {
|
||||
return;
|
||||
}
|
||||
|
||||
// See https://github.com/libsdl-org/SDL/blob/2df2da11f627299c6e05b7e0aff407c915043372/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L182-L216
|
||||
List<InputDevice.MotionRange> ranges = inputDevice.getMotionRanges();
|
||||
Collections.sort(ranges, new RangeComparator());
|
||||
|
||||
Gamepad gamepad = new Gamepad();
|
||||
gamepad.deviceId = deviceId;
|
||||
gamepad.axes = new ArrayList<InputDevice.MotionRange>();
|
||||
gamepad.hats = new ArrayList<InputDevice.MotionRange>();
|
||||
for (InputDevice.MotionRange range : ranges) {
|
||||
if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) {
|
||||
gamepad.hats.add(range);
|
||||
} else {
|
||||
gamepad.axes.add(range);
|
||||
}
|
||||
}
|
||||
this.gamepads.add(gamepad);
|
||||
|
||||
String descriptor = inputDevice.getDescriptor();
|
||||
int vendorId = inputDevice.getVendorId();
|
||||
int productId = inputDevice.getProductId();
|
||||
|
||||
// These values are required to calculate SDL's GUID.
|
||||
int buttonMask = getButtonMask(inputDevice, gamepad.hats.size()/2);
|
||||
int axisMask = getAxisMask(inputDevice);
|
||||
|
||||
Ebitenmobileview.onGamepadAdded(deviceId, inputDevice.getName(), gamepad.axes.size(), gamepad.hats.size()/2, descriptor, vendorId, productId, buttonMask, axisMask);
|
||||
|
||||
// Initialize the trigger axes values explicitly, or the initial button values would be 0.5 instead of 0.
|
||||
if (gamepad.axes.size() >= 6) {
|
||||
Ebitenmobileview.onGamepadAxisChanged(deviceId, 4, -1);
|
||||
Ebitenmobileview.onGamepadAxisChanged(deviceId, 5, -1);
|
||||
}
|
||||
}
|
||||
|
||||
// The implementation is copied from SDL:
|
||||
// https://github.com/libsdl-org/SDL/blob/0e9560aea22818884921e5e5064953257bfe7fa7/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L308
|
||||
private static int getButtonMask(InputDevice joystickDevice, int nhats) {
|
||||
int buttonMask = 0;
|
||||
int[] keys = new int[] {
|
||||
KeyEvent.KEYCODE_BUTTON_A,
|
||||
KeyEvent.KEYCODE_BUTTON_B,
|
||||
KeyEvent.KEYCODE_BUTTON_X,
|
||||
KeyEvent.KEYCODE_BUTTON_Y,
|
||||
KeyEvent.KEYCODE_BACK,
|
||||
KeyEvent.KEYCODE_BUTTON_MODE,
|
||||
KeyEvent.KEYCODE_BUTTON_START,
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBL,
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBR,
|
||||
KeyEvent.KEYCODE_BUTTON_L1,
|
||||
KeyEvent.KEYCODE_BUTTON_R1,
|
||||
KeyEvent.KEYCODE_DPAD_UP,
|
||||
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||
KeyEvent.KEYCODE_DPAD_LEFT,
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT,
|
||||
KeyEvent.KEYCODE_BUTTON_SELECT,
|
||||
KeyEvent.KEYCODE_DPAD_CENTER,
|
||||
|
||||
// These don't map into any SDL controller buttons directly
|
||||
KeyEvent.KEYCODE_BUTTON_L2,
|
||||
KeyEvent.KEYCODE_BUTTON_R2,
|
||||
KeyEvent.KEYCODE_BUTTON_C,
|
||||
KeyEvent.KEYCODE_BUTTON_Z,
|
||||
KeyEvent.KEYCODE_BUTTON_1,
|
||||
KeyEvent.KEYCODE_BUTTON_2,
|
||||
KeyEvent.KEYCODE_BUTTON_3,
|
||||
KeyEvent.KEYCODE_BUTTON_4,
|
||||
KeyEvent.KEYCODE_BUTTON_5,
|
||||
KeyEvent.KEYCODE_BUTTON_6,
|
||||
KeyEvent.KEYCODE_BUTTON_7,
|
||||
KeyEvent.KEYCODE_BUTTON_8,
|
||||
KeyEvent.KEYCODE_BUTTON_9,
|
||||
KeyEvent.KEYCODE_BUTTON_10,
|
||||
KeyEvent.KEYCODE_BUTTON_11,
|
||||
KeyEvent.KEYCODE_BUTTON_12,
|
||||
KeyEvent.KEYCODE_BUTTON_13,
|
||||
KeyEvent.KEYCODE_BUTTON_14,
|
||||
KeyEvent.KEYCODE_BUTTON_15,
|
||||
KeyEvent.KEYCODE_BUTTON_16,
|
||||
};
|
||||
int[] masks = new int[] {
|
||||
(1 << 0), // A -> A
|
||||
(1 << 1), // B -> B
|
||||
(1 << 2), // X -> X
|
||||
(1 << 3), // Y -> Y
|
||||
(1 << 4), // BACK -> BACK
|
||||
(1 << 5), // MODE -> GUIDE
|
||||
(1 << 6), // START -> START
|
||||
(1 << 7), // THUMBL -> LEFTSTICK
|
||||
(1 << 8), // THUMBR -> RIGHTSTICK
|
||||
(1 << 9), // L1 -> LEFTSHOULDER
|
||||
(1 << 10), // R1 -> RIGHTSHOULDER
|
||||
(1 << 11), // DPAD_UP -> DPAD_UP
|
||||
(1 << 12), // DPAD_DOWN -> DPAD_DOWN
|
||||
(1 << 13), // DPAD_LEFT -> DPAD_LEFT
|
||||
(1 << 14), // DPAD_RIGHT -> DPAD_RIGHT
|
||||
(1 << 4), // SELECT -> BACK
|
||||
(1 << 0), // DPAD_CENTER -> A
|
||||
(1 << 15), // L2 -> ??
|
||||
(1 << 16), // R2 -> ??
|
||||
(1 << 17), // C -> ??
|
||||
(1 << 18), // Z -> ??
|
||||
(1 << 20), // 1 -> ??
|
||||
(1 << 21), // 2 -> ??
|
||||
(1 << 22), // 3 -> ??
|
||||
(1 << 23), // 4 -> ??
|
||||
(1 << 24), // 5 -> ??
|
||||
(1 << 25), // 6 -> ??
|
||||
(1 << 26), // 7 -> ??
|
||||
(1 << 27), // 8 -> ??
|
||||
(1 << 28), // 9 -> ??
|
||||
(1 << 29), // 10 -> ??
|
||||
(1 << 30), // 11 -> ??
|
||||
(1 << 31), // 12 -> ??
|
||||
// We're out of room...
|
||||
0xFFFFFFFF, // 13 -> ??
|
||||
0xFFFFFFFF, // 14 -> ??
|
||||
0xFFFFFFFF, // 15 -> ??
|
||||
0xFFFFFFFF, // 16 -> ??
|
||||
};
|
||||
boolean[] hasKeys = joystickDevice.hasKeys(keys);
|
||||
for (int i = 0; i < keys.length; ++i) {
|
||||
if (hasKeys[i]) {
|
||||
buttonMask |= masks[i];
|
||||
}
|
||||
}
|
||||
// https://github.com/libsdl-org/SDL/blob/47f2373dc13b66c48bf4024fcdab53cd0bdd59bb/src/joystick/android/SDL_sysjoystick.c#L360-L367
|
||||
if (nhats > 0) {
|
||||
// Add Dpad buttons.
|
||||
buttonMask |= 1 << 11;
|
||||
buttonMask |= 1 << 12;
|
||||
buttonMask |= 1 << 13;
|
||||
buttonMask |= 1 << 14;
|
||||
}
|
||||
return buttonMask;
|
||||
}
|
||||
|
||||
private static int getAxisMask(InputDevice joystickDevice) {
|
||||
final int SDL_CONTROLLER_AXIS_LEFTX = 0;
|
||||
final int SDL_CONTROLLER_AXIS_LEFTY = 1;
|
||||
final int SDL_CONTROLLER_AXIS_RIGHTX = 2;
|
||||
final int SDL_CONTROLLER_AXIS_RIGHTY = 3;
|
||||
final int SDL_CONTROLLER_AXIS_TRIGGERLEFT = 4;
|
||||
final int SDL_CONTROLLER_AXIS_TRIGGERRIGHT = 5;
|
||||
|
||||
int naxes = 0;
|
||||
for (InputDevice.MotionRange range : joystickDevice.getMotionRanges()) {
|
||||
if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
|
||||
if (range.getAxis() != MotionEvent.AXIS_HAT_X && range.getAxis() != MotionEvent.AXIS_HAT_Y) {
|
||||
naxes++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The variable is_accelerometer seems always false, then skip the checking:
|
||||
// https://github.com/libsdl-org/SDL/blob/0e9560aea22818884921e5e5064953257bfe7fa7/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L207
|
||||
int axisMask = 0;
|
||||
if (naxes >= 2) {
|
||||
axisMask |= ((1 << SDL_CONTROLLER_AXIS_LEFTX) | (1 << SDL_CONTROLLER_AXIS_LEFTY));
|
||||
}
|
||||
if (naxes >= 4) {
|
||||
axisMask |= ((1 << SDL_CONTROLLER_AXIS_RIGHTX) | (1 << SDL_CONTROLLER_AXIS_RIGHTY));
|
||||
}
|
||||
if (naxes >= 6) {
|
||||
axisMask |= ((1 << SDL_CONTROLLER_AXIS_TRIGGERLEFT) | (1 << SDL_CONTROLLER_AXIS_TRIGGERRIGHT));
|
||||
}
|
||||
return axisMask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceChanged(int deviceId) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceRemoved(int deviceId) {
|
||||
// Do not call inputManager.getInputDevice(), which returns null (#1185).
|
||||
Ebitenmobileview.onInputDeviceRemoved(deviceId);
|
||||
this.gamepads.remove(this.getGamepad(deviceId));
|
||||
}
|
||||
|
||||
// suspendGame suspends the game.
|
||||
// It is recommended to call this when the application is being suspended e.g.,
|
||||
// Activity's onPause is called.
|
||||
public void suspendGame() {
|
||||
this.inputManager.unregisterInputDeviceListener(this);
|
||||
this.ebitenSurfaceView.onPause();
|
||||
try {
|
||||
Ebitenmobileview.suspend();
|
||||
} catch (final Exception e) {
|
||||
onErrorOnGameUpdate(e);
|
||||
}
|
||||
}
|
||||
|
||||
// resumeGame resumes the game.
|
||||
// It is recommended to call this when the application is being resumed e.g.,
|
||||
// Activity's onResume is called.
|
||||
public void resumeGame() {
|
||||
this.inputManager.registerInputDeviceListener(this, null);
|
||||
this.ebitenSurfaceView.onResume();
|
||||
try {
|
||||
Ebitenmobileview.resume();
|
||||
} catch (final Exception e) {
|
||||
onErrorOnGameUpdate(e);
|
||||
}
|
||||
}
|
||||
|
||||
// onErrorOnGameUpdate is called on the main thread when an error happens when updating a game.
|
||||
// You can define your own error handler, e.g., using Crashlytics, by overriding this method.
|
||||
protected void onErrorOnGameUpdate(Exception e) {
|
||||
Log.e("Go", e.toString());
|
||||
}
|
||||
|
||||
private EbitenSurfaceView ebitenSurfaceView;
|
||||
private InputManager inputManager;
|
||||
private ArrayList<Gamepad> gamepads;
|
||||
}
|
||||
`
|
||||
|
||||
const surfaceViewJava = `// Code generated by ebitenmobile. DO NOT EDIT.
|
||||
|
||||
package {{.JavaPkg}}.{{.PrefixLower}};
|
||||
|
||||
import android.content.Context;
|
||||
import android.opengl.GLSurfaceView;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
import {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;
|
||||
import {{.JavaPkg}}.ebitenmobileview.RenderRequester;
|
||||
import {{.JavaPkg}}.{{.PrefixLower}}.EbitenView;
|
||||
|
||||
class EbitenSurfaceView extends GLSurfaceView implements RenderRequester {
|
||||
|
||||
private class EbitenRenderer implements GLSurfaceView.Renderer {
|
||||
|
||||
private boolean errored_ = false;
|
||||
|
||||
@Override
|
||||
public void onDrawFrame(GL10 gl) {
|
||||
if (errored_) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Ebitenmobileview.update();
|
||||
} catch (final Exception e) {
|
||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onErrorOnGameUpdate(e);
|
||||
}
|
||||
});
|
||||
errored_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
|
||||
Ebitenmobileview.onContextLost();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceChanged(GL10 gl, int width, int height) {
|
||||
}
|
||||
}
|
||||
|
||||
public EbitenSurfaceView(Context context) {
|
||||
super(context);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public EbitenSurfaceView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
setEGLContextClientVersion(2);
|
||||
setEGLConfigChooser(8, 8, 8, 8, 0, 0);
|
||||
setRenderer(new EbitenRenderer());
|
||||
Ebitenmobileview.setRenderRequester(this);
|
||||
}
|
||||
|
||||
private void onErrorOnGameUpdate(Exception e) {
|
||||
((EbitenView)getParent()).onErrorOnGameUpdate(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setExplicitRenderingMode(boolean explicitRendering) {
|
||||
if (explicitRendering) {
|
||||
setRenderMode(RENDERMODE_WHEN_DIRTY);
|
||||
} else {
|
||||
setRenderMode(RENDERMODE_CONTINUOUSLY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void requestRenderIfNeeded() {
|
||||
if (getRenderMode() == RENDERMODE_WHEN_DIRTY) {
|
||||
requestRender();
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 <UIKit/UIKit.h>
|
||||
|
||||
@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}}
|
||||
|
Loading…
Reference in New Issue
Block a user