loading...

Just port a Golang game to Android

ntoooop profile image ntop Updated on ・7 min read

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.

Here is 2048 game I made:
2048

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:

  1. File
  2. SharedPreferences
  3. 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':
Click Back Again to Quit!

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:

  1. x/mobile: manual declaration of uses-sdk in AndroidManifest.xml not supported
  2. x/mobile: Can't get the Back key event on Android
  3. x/mobile: preserve eglContext when activity paused on Android
  4. x/mobile: lifecycle.StageDead never reach on Android

Discussion

pic
Editor guide
Collapse
krusenas profile image
Karolis

I actually explored similar strategy once and it was "okay" but it was far from a good android app.
Recently I have tried a bit different approach and was super happy with it:

  • App frontend written in Dart + Flutter. Dart seemed very familiar after Go and didn't even have to read language docs to actually write it. Flutter was also familiar after working with React/Vuejs (state management).
  • App backend (logic, storage) was written in Go as I am most comfortable writing GO.
  • Connecting frontend to backend via a JSON RPC, basically got a thin shim between the languages written in Java that I could write once and not modify ever. For example have a look at this: github.com/adieu/flutter_go.

Pros:

  • Flutter is a superior framework to anything I have ever seen for mobile.
  • Writing backend in Go seems safe and nice

Cons:

  • I would probably just go all-in and write the app fully in Dart but since I already had lots of code in Go that I could reuse for the app, it didn't make sense for me at that time.
Collapse
natoboram profile image
Nato Boram

Those are pretty critical issues you submitted - no SDK target, no back button, context not preserved after home button, stageDead not reported. I'm not sure I want to use GoMobile after reading these, that's like the basics to get an app to work. I wonder if there's actual people who use GoMobile. I would love to create Android apps in pure Go, but those issues are impossible to miss.

Collapse
ntoooop profile image
ntop Author

Yeah, I think there is no one really use gomobile build to build a workable APK, the author of ebiten engine @hajimehoshi use gomobile bind to work around those issues, he is the one I know that has published serval Golang games. GoMobile has hardcoded too much things. But those issues can be fixed easily, If anyone fixes the issues it'll be a good start!