ebiten/cmd/ebitenmobile/gobind.go
Hajime Hoshi 8fcee54849 mobile/ebitenmobileview: Implement Android gamepad buttons
This is still work in progress.

Updates #1083
2020-03-23 01:30:17 +09:00

586 lines
16 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.
// +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 forceGL() bool {
for _, tag := range strings.Split(*tags, ",") {
if tag == "ebitengl" {
return true
}
}
return false
}
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)
f := "0"
if forceGL() {
f = "1"
}
content = strings.ReplaceAll(content, "{{.ForceGL}}", f)
return content
}
switch lang {
case "objc":
// iOS
if err := writeFile(filepath.Join("src", "gobind", prefixLower+"ebitenviewcontroller_ios.m"), replacePrefixes(objcM)); 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.
// +build ios
#import <TargetConditionals.h>
#if TARGET_IPHONE_SIMULATOR || {{.ForceGL}}
#define EBITEN_METAL 0
#else
#define EBITEN_METAL 1
#endif
#import <stdint.h>
#import <UIKit/UIKit.h>
#import <GLKit/GLkit.h>
#import "Ebitenmobileview.objc.h"
@interface {{.PrefixUpper}}EbitenViewController : UIViewController
@end
@implementation {{.PrefixUpper}}EbitenViewController {
UIView* metalView_;
GLKView* glkView_;
bool started_;
bool active_;
bool error_;
}
- (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 EBITEN_METAL
[self.view addSubview: self.metalView];
EbitenmobileviewSetUIView((uintptr_t)(self.metalView));
#else
self.glkView.delegate = (id<GLKViewDelegate>)(self);
[self.view addSubview: self.glkView];
EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
[self glkView].context = context;
[EAGLContext setCurrentContext:context];
#endif
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)viewWillLayoutSubviews {
CGRect viewRect = [[self view] frame];
#if EBITEN_METAL
[[self metalView] setFrame:viewRect];
#else
[[self glkView] setFrame:viewRect];
#endif
}
- (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 EBITEN_METAL
[self updateEbiten];
#else
[[self glkView] setNeedsDisplay];
#endif
}
}
- (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 EBITEN_METAL
if (touch.view != [self metalView]) {
continue;
}
#else
if (touch.view != [self glkView]) {
continue;
}
#endif
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 msut not be called before viewDidLoad is called");
@synchronized(self) {
active_ = false;
EbitenmobileviewSuspend();
}
}
- (void)resumeGame {
NSAssert(started_, @"resumeGame msut not be called before viewDidLoad is called");
@synchronized(self) {
active_ = true;
EbitenmobileviewResume();
}
}
@end
`
const viewJava = `// Code generated by ebitenmobile. DO NOT EDIT.
package {{.JavaPkg}}.{{.PrefixLower}};
import android.content.Context;
import android.hardware.input.InputManager;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.ViewGroup;
import {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;
public class EbitenView extends ViewGroup implements InputManager.InputDeviceListener {
private double getDeviceScale() {
if (this.deviceScale == 0.0) {
this.deviceScale = getResources().getDisplayMetrics().density;
}
return this.deviceScale;
}
private double pxToDp(double x) {
return x / getDeviceScale();
}
private double deviceScale = 0.0;
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.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) {
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);
Ebitenmobileview.updateTouchesOnAndroid(e.getActionMasked(), id, (int)pxToDp(x), (int)pxToDp(y));
}
return true;
}
// The order must be the same as mobile/ebitenmobileview/input_android.go.
static int[] gamepadButtons = {
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_BUTTON_B,
KeyEvent.KEYCODE_BUTTON_C,
KeyEvent.KEYCODE_BUTTON_X,
KeyEvent.KEYCODE_BUTTON_Y,
KeyEvent.KEYCODE_BUTTON_Z,
KeyEvent.KEYCODE_BUTTON_L1,
KeyEvent.KEYCODE_BUTTON_R1,
KeyEvent.KEYCODE_BUTTON_L2,
KeyEvent.KEYCODE_BUTTON_R2,
KeyEvent.KEYCODE_BUTTON_THUMBL,
KeyEvent.KEYCODE_BUTTON_THUMBR,
KeyEvent.KEYCODE_BUTTON_START,
KeyEvent.KEYCODE_BUTTON_SELECT,
KeyEvent.KEYCODE_BUTTON_MODE,
KeyEvent.KEYCODE_BUTTON_1,
KeyEvent.KEYCODE_BUTTON_2,
KeyEvent.KEYCODE_BUTTON_3,
KeyEvent.KEYCODE_BUTTON_4,
KeyEvent.KEYCODE_BUTTON_5,
KeyEvent.KEYCODE_BUTTON_6,
KeyEvent.KEYCODE_BUTTON_7,
KeyEvent.KEYCODE_BUTTON_8,
KeyEvent.KEYCODE_BUTTON_9,
KeyEvent.KEYCODE_BUTTON_10,
KeyEvent.KEYCODE_BUTTON_11,
KeyEvent.KEYCODE_BUTTON_12,
KeyEvent.KEYCODE_BUTTON_13,
KeyEvent.KEYCODE_BUTTON_14,
KeyEvent.KEYCODE_BUTTON_15,
KeyEvent.KEYCODE_BUTTON_16,
};
@Override
public void onInputDeviceAdded(int deviceId) {
InputDevice inputDevice = this.inputManager.getInputDevice(deviceId);
int sources = inputDevice.getSources();
if ((sources & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD &&
(sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) {
return;
}
boolean[] keyExistences = inputDevice.hasKeys(gamepadButtons);
int buttonNum = gamepadButtons.length - 1;
for (int i = gamepadButtons.length - 1; i >= 0; i--) {
if (keyExistences[i]) {
break;
}
buttonNum--;
}
Ebitenmobileview.onGamepadAdded(deviceId, inputDevice.getName(), buttonNum);
}
@Override
public void onInputDeviceChanged(int deviceId) {
// Do nothing.
}
@Override
public void onInputDeviceRemoved(int deviceId) {
InputDevice inputDevice = this.inputManager.getInputDevice(deviceId);
int sources = inputDevice.getSources();
if ((sources & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD &&
(sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) {
return;
}
Ebitenmobileview.onGamepadRemoved(deviceId);
}
// suspendGame suspends the game.
// It is recommended to call this when the application is being suspended e.g.,
// Activity's onPause is called.
public void suspendGame() {
this.inputManager.unregisterInputDeviceListener(this);
this.ebitenSurfaceView.onPause();
Ebitenmobileview.suspend();
}
// 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();
Ebitenmobileview.resume();
}
// 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.
protected void onErrorOnGameUpdate(Exception e) {
Log.e("Go", e.toString());
}
private EbitenSurfaceView ebitenSurfaceView;
private InputManager inputManager;
}
`
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}}.{{.PrefixLower}}.EbitenView;
class EbitenSurfaceView extends GLSurfaceView {
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) {
}
@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());
}
private void onErrorOnGameUpdate(Exception e) {
((EbitenView)getParent()).onErrorOnGameUpdate(e);
}
}
`