loading...

Port a Golang game to iOS

ntoooop profile image ntop ・4 min read

These days, I'm porting our Android game Shoot Stack to iOS. It's a game written in Golang with our own game engine. This article will give some tips and our experiences in poring Golang game to iOS. If you have ever used the GoMobile you know that gomobile build can build .app and install directly to an iOS device. But if you want to integrate third-party SDK, like ads, analytics, it's impossible with gomobile build. We need a way to build library, then we can use it in xcode and gomobile bind just do this.

Here is our project structure:

✗ tree .
.
├── arena.go
├── audio.go
├── build.md
├── color.go
├── file.go
├── flash_message.go
├── gameover.go
├── input_glfw.go
├── input_mob.go
├── main.go
├── particle.go
├── ready.go
└── welcome.go

yeah, it's all in a main package. Using gomobile bind command, we can build .framework file for xcode. But we still need a way to invoke the main method of our lib. All xcode project has a main.h file:

int main(int argc, char * argv[]) {
@autoreleasepool {
    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

So we can invoke our lib's main method here. As I said above, all my code stay in a main package, but GoMobile cann't bind file in main(it's just not supported). So I do have to change a little to my project:

✗ tree .
.
├── ios
│   └── ios.go
└── main
    ├── game
    │   ├── arena.go
    │   ├── audio.go
    │   ├── build.md
    │   ├── color.go
    │   ├── file.go
    │   ├── flash_message.go
    │   ├── gameover.go
    │   ├── input_glfw.go
    │   ├── input_mob.go
    │   ├── main.go
    │   ├── particle.go
    │   ├── ready.go
    │   └── welcome.go
    └── main.go

I put the source code in main/game package, then I can still use gomobile build to build Desktop and Android. Then I created ios/ios.go file, it's not a main file, so I can build it with gomobile bind, the content:

package ios

import (
   "korok.io/korok/gfx/dbg"
   "korok.io/korok"
   "korok.io/korok/math/f32"
   "main/game"
)

func Run() {
   dbg.DEBUG = dbg.None
   option := korok.Options{
      Width:360,
      Height:640,
      Clear:f32.Vec4{0,0,0,1},
   }
   korok.Run(&option, &game.StartScene{})
}

The method Run is same as the main.main():

func main() {
    dbg.DEBUG = dbg.None
    option := korok.Options{
        Width:360,
        Height:640,
        Clear:f32.Vec4{0,0,0,1},
    }
    korok.Run(&option, &StartScene{})
}

It's just created to easy the bind process. Now, we can use gomobile bind -target=ios ios to build a .framework. If all is OK, next, we can integrate the .framework to our xcode project(just drag it to xcode). Modify the main.h file as following:

#import "Ios/Ios.h"

int main(int argc, char * argv[]) {
    IosRun();
}

Build & Run, now the project (with Go inside) works.

How to add ads SDK?

Now, we have successfully created a library and used it in xcode project. If we want to integrate Ad SDK, we still need add some lifecycle hook in the original .m file used by GoMobile. In the package x/mobile/app, we can find a file darwin_ios.m, this is the file GoMobile used to build iOS application, there are two @interface defined here:

@interface GoAppAppController : GLKViewController<UIContentContainer, GLKViewDelegate>
@end

@interface GoAppAppDelegate : UIResponder<UIApplicationDelegate>
@end

I will hook the lifecycle method, so that I can use it in the xcode project:

@implementation GoAppAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    ...
    [self kkDidFinishLaunching];
    return YES;
}
- (void)viewDidLoad {
    ...
    [self kkViewDidLoad];
}

Then create a file (ad.m) in the xcode project, add the following code:

@implementation GoAppAppDelegate(AD)
-(void)kkDidFinishLaunching {
    NSLog(@"hi, implemenmt method");
}
@end

@interface GoAppAppController(AD)
@end

@implementation GoAppAppController(AD)

- (void)kkViewDidLoad {
    NSLog(@"view did load2..");
@end

Here we used the Category in Obj-C language. It'll implement the method we used in 'darwin_ios.h'.

Build & Run, you should see the log info printed in Console.

Here is a snapshot of our game (with ad, but I'll remove it when release):

Golang game with admob

Other problem

Life is not easy, in fact, it takes days to build a actual game and run it on my iPhone6. There is a problem I just don't know how to solve it, the problem is when the Application suspended and reactive to the foreground the App will freeze. I have tested the basic example provide by GoMobile, it also have the problem, it's bug. In the basic example, it uses switch e.Crosses(lifecycle. StageVisible) to manage app lifecycle, if you change the lifecycle. StageVisible to lifecycle. StageAlive, it'll freeze.

I have digged into the problem for serval days. It seems that GoMobile use a custom loop to draw GL command, and call:

[EAGLContext setCurrentContext:ctx];
[ctx presentRenderbuffer:GL_RENDERBUFFER];

to swap buffer, but this code will fail if the app suspended and activated again. I tried serval ways to solve the problem, but it's not that easy as I thought, at last, I given up and created an issue in Github: application freeze after resumed from suspend state on iOS . But I still need a way to work around the bug, after some research in the git commit, I found that the old implementation --- just use the system loop instead of Go loop. So I rollback to the old implementation:

self.glview.enableSetNeedsDisplay = NO;
CADisplayLink* displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(render:)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

- (void)render:(CADisplayLink*)displayLink {
    [self.glview display];
}

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
    drawloop();
}
//export drawloop
func drawloop() {
    runtime.LockOSThread()
    defer runtime.UnlockOSThread()

    for workAvailable := theApp.worker.WorkAvailable();;{
        select {
        case <-workAvailable:
            theApp.worker.DoWork()
        case <-theApp.publish:
            theApp.publishResult <- PublishResult{}
            return
        case <-time.After(50 * time.Millisecond): // incase the method blocked!!
            return
        }
    }
}

I also found the same(mostly) implementation in Cocos2DX and Ebiten. Finally, all the problem solved!! I'm uploading the Game to Apple Store Connect, you can download it in several days or weeks(you know AppStore review is not easy, too).

Discussion

pic
Editor guide