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