ntop
Posted on August 18, 2018
Months ago, @hajimehoshi wrote a post Go Packages we developed for our games, he use gomobile bind
to port Go games to Android in his engine - ebiten. But here, I'll use gomobile build
, gomobile build
can pack .apk
directly and need no other dependencies.
I have written a game engine to implement some basic game logic. But I won't talk about it (for anyone interested: https://korok.io) today, I'll talk about the problems we meet when using 'gomobile build' command.
You can get it on itch.io: The 2048 Game
How to make a fullscreen/NoTitleBar screen?
The first problem is to make a full screen, gomobile
does't provide any configuration to do this. After reading the gomobile wiki, I know that I can create an AndroidManifest.xml
file at the root directory in my project. gomobile
will include it in android APK file.
This is the manifest file I used in my project:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1" android:versionName="1.0" package="io.korok.ad2048">
<application android:label="2048 Game">
<activity android:label="2048 Game"
android:name="org.golang.app.GoNativeActivity"
android:configChanges="keyboardHidden|orientation"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:screenOrientation="portrait">
<meta-data android:name="android.app.lib_name" android:value="2048" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
In the file, android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
will make a full screen.
Where to store my game data ?
In my 2048 game, I need to store the score value in local storage. As an Android developer once, I known there are three ways to store data in Android:
- File
- SharedPreferences
- SQLite
There is no wrapper method in Go. But, if I can find a writable file directory, I can use File system. After reading the source code of android.go
, I see that gomobile
has a environment value -- 'TMPDIR', which represent a temporary file directory(only works on Android):
var filepath = os.Getenv("TMPDIR")
Then, I can create file and store data.
How to get the system language?
If you installed my 2048 games, You'll find that it supports multi-language(Chinese and English), I implemented this by getting the system language. Getting system language is harder than getting a writable file path, it needs writing glue method: Go -> Cgo -> Java. I'll also lean JNI to call java method from C, fortunately, JNI is like java reflection, very easy!
Here is the code to get system language:
/*
#cgo LDFLAGS: -landroid
#include <jni.h>
#include <stdlib.h>
#include <string.h>
// Equivalent to:
// String lan = Locale.getDefault().getLanguage();
char* kk_getLanguage(uintptr_t java_vm, uintptr_t jni_env, jobject ctx) {
JavaVM* vm = (JavaVM*)java_vm;
JNIEnv* env = (JNIEnv*)jni_env;
jclass locale_clazz = (*env)->FindClass(env, "java/util/Locale");
jmethodID getdft_id = (*env)->GetStaticMethodID(env, locale_clazz, "getDefault", "()Ljava/util/Locale;");
jobject locale = (*env)->CallStaticObjectMethod(env, locale_clazz, getdft_id);
jmethodID getlang_id = (*env)->GetMethodID(env, locale_clazz, "getLanguage", "()Ljava/lang/String;");
jobject lang = (*env)->CallObjectMethod(env, locale, getlang_id);
const char* str = (*env)->GetStringUTFChars(env, (jstring) lang, NULL);
char * retString = strdup(str);
(*env)->ReleaseStringUTFChars(env, (jstring)lang, str);
return retString;
}
*/
import "C"
import (
"golang.org/x/mobile/app"
"unsafe"
)
func Language() string {
return deviceAttr.Lang(func() string {
var ret string
app.RunOnJVM(func(vm, jniEnv, ctx uintptr) error {
cstring := C.kk_getLanguage(C.uintptr_t(vm), C.uintptr_t(jniEnv), C.jobject(ctx))
ret = C.GoString(cstring)
C.free(unsafe.Pointer(cstring))
return nil
})
return ret
})
}
Note: app.RunOnJVM
method is a useful method to access JNI, the latest version of gomobile has this method.
The Back Key!
When I installed my game on an Android device, I found that it'll just exit if I press the Back key. It's not a normal behavior when I'm playing and touch the Back accidently. Most Android games will have a 'Double Click to Exit' mechanism to ensure you really want to exit the game.
The Good news is you can gets the key event from gomobile, the Bad news is gomobile just ignores the Back key. There is a function called convAndroidKeyCode
in android.go
which maps the Back key as Unknown. To solve the problem, I have to changes the code in gomobile, I did this in a new branch(thanks git!!).
// event/key/key.go
// generate different Codes (but the same Rune).
Code Code
+ // Origin is the original value of the key code.
+ Origin int
+
// Modifiers is a bitmask representing a set of modifier keys: ModShift,
// ModAlt, etc.
Modifiers Modifiers
// app/android.go
k := key.Event{
Rune: rune(C.getKeyRune(env, e)),
Code: convAndroidKeyCode(int32(C.AKeyEvent_getKeyCode(e))),
+ Origin: int(C.AKeyEvent_getKeyCode(e)),
}
switch C.AKeyEvent_getAction(e) {
case C.AKEY_STATE_DOWN:
Adding a new filed 'Origin', I can use it to get the original key code value.
case key.Event:
if e.Origin == 4 {
// do something
}
Note: The Back key's value is 4 on Android. You need also rebuild the gomobile
tools to make it works.
I can get the Back key event, but the game still exit. To change the default Back key behavior on Android, need to override the OnBackPressed
method in Acitivty
class.
There is a java
file in gomobile package called GoNativeActivity.java
, this is where the Activity declared. Add the following method:
@Override
public void onBackPressed() {
Log.e("Go", "Override Back key");
}
Note: You need also regenerate the dex.go
file with go run gendex.go
, then rebuild the gomobile
again.
How to Exit the app, manually?
Now, we override the default Back key event, and can get Back key event, what should we do next? A simple way to do this is call panic
method, just kill the process with an exception. Or we can call the Activity.finish()
method with glue method, I have written some JNI method, it not that hard.
/*
#cgo LDFLAGS: -landroid
#include <jni.h>
#include <stdlib.h>
#include <string.h>
// Equivalent to:
// Activity.Finish()
void kk_finish(uintptr_t java_vm, uintptr_t jni_env, jobject ctx) {
JavaVM* vm = (JavaVM*)java_vm;
JNIEnv* env = (JNIEnv*)jni_env;
jclass clazz = (*env)->GetObjectClass(env, ctx);
jmethodID finish_id = (*env)->GetMethodID(env, clazz, "finish", "()V");
(*env)->CallVoidMethod(env, ctx, finish_id);
}
*/
import "C"
import (
"golang.org/x/mobile/app"
"unsafe"
)
func Quit() {
app.RunOnJVM(func(vm, jniEnv, ctx uintptr) error {
C.kk_finish(C.uintptr_t(vm), C.uintptr_t(jniEnv), C.jobject(ctx))
return nil
})
}
I also make a 'Double Click Exit':
Preserve eglContext!!
If I pressed the Home button on an Android device, the system'll bring my game to back. Then click the game icon again, the system'll bring my game to front, but, it's a white screen!!! Nothing is showing!!! I have searched a lot about this bugly behavior, it seems that Android apps will lost eglContext when paused. Ebiten has some code to restore the eglContext, reload all shaders/texutres... But I think it's too complicated to implement this, SDL2 use setPreserveEGLContextOnPause method, I can implement the same logic on gomobile, too.
In android.c
file, add a new global variable EGLContext context = NULL;
, then reuses it if it's valid or creates it if it's invalid.
// app/android.c
EGLDisplay display = NULL;
EGLSurface surface = NULL;
+EGLContext context = NULL;
- const EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE };
- context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
+ if (context == NULL) {
+ const EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE };
+ context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
+ }
These changes works very well. Now, my game can correctly paused and resumed.
Note: You need to rebuild the gomobile
tools again!
How to set 'targetSdkVersion '?
I thought I have fixed all the bug and successfully build an APK file. But when I upload the APK file to GooglePlay, it always fails with an error message -"you need to target Android version 26"!!
As an experienced Android developer, I know how to fix it, just add 'targetSdkVersion=26' in 'AndroidManifest.xml' will works. But, unfortunately, it failed, gomoile complains that:
manual declaration of uses-sdk in AndroidManifest.xml not supported
Gomobile must did some weird logic when building APK file. After digging into the build_androidapp.go
and binres.go
file, I found that gombile checks the 'uses-sdk' element and fails the build process:
case "uses-sdk":
return nil, fmt.Errorf("manual declaration of uses-sdk in AndroidManifest.xml not supported")
case "manifest":
I also found where 'gomobile' set the 'minSdkVersion', but, it never set the targetSdkVersion
, so I add some code:
if !skipSynthesize {
s := xml.StartElement{
Name: xml.Name{
Space: "",
Local: "uses-sdk",
},
Attr: []xml.Attr{
xml.Attr{
Name: xml.Name{
Space: androidSchema,
Local: "minSdkVersion",
},
Value: fmt.Sprintf("%v", MinSDK),
},
xml.Attr{
Name: xml.Name{
Space: androidSchema,
Local: "targetSdkVersion",
},
Value: fmt.Sprintf("%v", 26),
},
},
}
Finally, I have to say, it's a long way, I fixed all the problem, and upload it to GooglePlay. It's welcomed to download my 2048 games, it's 99% Golang、no ads、clean design、smooth animation... Yeah, just install, I'm really happy to hear that.
Here is the link to itch.io: The 2048 Game
I also create serval issues in Github, but with no respond now:
Posted on August 18, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.