Go で作ったゲームを Android/iOS 対応させた話 (1)

この記事は、拙作 Go 製 2D ゲームライブラリ [Ebiten](http://hajimehoshi.github.io/ebiten/) を Android および iOS に対応させた顛末をまとめたものです。まだ完全な対応の完了はしていないため、逐一編集していく可能性があります。 `git log` のコミットを見て思い出しながら書いた日記形式で綴ります。あくまでも思い出しながらなので、不正確な点もあります。

Ebiten は Go でできたレトロ風な 2D ゲームライブラリで、デスクトップやブラウザで動かすことができます。描画は、幾何行列および色行列による変換機能、オフスクリーンレンダリング機能があります。入力はキーボード、マウス、ゲームパッドに対応。音楽は単純な PCM や OGG を再生することができます。

今回は[おめが氏](https://twitter.com/Omegamega)作成の「[いのべーしょん](http://o-mega.sakura.ne.jp/product/ino.html)」というゲームを[Go に移植したもの](https://github.com/hajimehoshi/go-inovation)を、 Android および iOS で動かすというのを目標しました。結論から言うと、両方で動きました。よかったですね。 [go-inovation](https://github.com/hajimehoshi/go-inovation) リポジトリに Android Studio プロジェクトおよび Xcode プロジェクトが用意してございます。 Chrome か Firefox ならば[ブラウザでも遊べます](http://hajimehoshi.github.io/go-inovation/)。

# Go におけるモバイル対応

Go は [`gomobile`](https://golang.org/x/mobile/cmd/gomobile) というコマンドで Android および iOS 対応のバイナリを生成することができます。 `gomobile` には `gomobile build` というすぐ実行できるアプリケーションを生成するコマンドと、 `gomobile bind` という、 Android/iOS で利用するための共有ライブラリを生成するコマンドがあります。最初は前者を利用しようとしたのですが、途中で後者に鞍替えしました。 `gomobile build` は Go だけでアプリが即作れて非常に便利なのですが、 OpenGL のビュー一個しかないアプリしか作れず、例えば広告を入れたりとかそういう工夫をしたい時に事実上不可能であり、実用性に欠けると判断しました。 `gomobile bind` だと Android/iOS 側にメインループがあり、 Pure Go で書けず複雑になるという欠点があるのですが、その代わりアプリ開発者は何でも出来るという柔軟性があります。

描画に関して、 Ebiten は OpenGL を使用しており、 Android および iOS でも OpenGL (ES 2) が当然動作するので、プラットフォーム固有のもの要件は一見無いように思えます。しかしながらコンパイルして一発で画面が表示されたと言うとそんなことはなく、色々修正する必要がありました。

# 前提

* Mac OS X、 Linux、 Windows は対応済み。
* 一部 Web ブラウザにも対応済み。これは [GopherJS](http://www.gopherjs.org/) によるもので、 Go プログラムを JavaScript に変換することで対応させている。
  * モバイルに対応させるのであれば、 WebView を使って JavaScript に変換したものを動かす、という方法も検討していた。パフォーマンスがかなり劣化するので、やるとしても最終手段である。
* 「いのべーしょん」は Go に移植済み。すでにデスクトップ等では動いている。

# 日記

## 2016-04-23

### モバイル対応作業開始

1.3 という名前のブランチを切り、そのブランチ上でバージョン v1.3.0-rc1 をリリース。このバージョンは Mac とブラウザに加えて Windows と Linux に対応させた stable バージョンである。 1.3 ブランチには致命的な bug fix しか入れない。

同時に master ブランチのバージョンを v1.4.0-alpha に変更。ここから Android/iOS 対応を始める。がんばる。まずは iOS よりは手堅そうな Android からだ。 Android のほうが手堅いのはなぜかというと、 Go におけるモバイル対応は experimental なのだが、 iOS については特に「[Support is not complete](https://godoc.org/golang.org/x/mobile/cmd/gomobile)」と明記されているからである。

## 2016-05-06

### Android Studio のお勉強

ゴールデンウィーク中何をやっていたのかというと、 Android Studio を初めて起動し、 Android アプリをどう作るかを勉強していた。 Android Studio の記録は Git リポジトリには残っていないため、その間のログはごっそり抜けてしまっている。

実質生まれて初めての Android プログラミングである。

## 2016-05-07

### [OpenGL レイヤーの準備](https://github.com/hajimehoshi/ebiten/commit/de541bdd7d8748df4deff2acf6c6b2699afbf785)

OpenGL レイヤーは [golang.org/x/mobile/gl](https://golang.org/x/mobile/gl) を用いる。実はすでにある程度は用意してあったので、若干の修正だけでコンパイルが通った。なぜすでに用意してあったかというと、この golang.org/x/mobile/gl はモバイルだけではなく Mac や Windows にも対応しているという触れ込みだったため、 OpenGL レイヤーをこのライブラリで統一しようと試みた時期があったからである。実際には採用に至らなかったが、それは Windows で [GLFW](http://www.glfw.org/) と一緒に動かすのが不可能であったからである。

ちなみにコンパイルが通っただけで、実際に絵は出ていない。Git のログには現れないが、絵がでない原因を探る試行錯誤がここから始まっている。

余談だが、 golang.org/x/mobile/gl の Windows 実装は [ANGLE](https://chromium.googlesource.com/angle/angle/+/master/README.md) (OpenGL API を DirectX でエミュレートしたもの) になっており、実行時に ANGLE の DLL を落としてくるという大胆な実装になっていた。 golang.org/x/mobile/gl はデスクトップにおいては [golang.org/x/exp/shiny](https://golang.org/x/exp/shiny) という GUI ライブラリと一緒に動かすことを想定しているようであり、他の GUI ライブラリと一緒に使うことは全く想定していないようであった。

## 2016-05-08

### [OpenGL 処理の遅延化](https://github.com/hajimehoshi/ebiten/commit/61ed13e32217722f4307f3520acf560f02e6c758)

モバイルにおいては、 Ebiten ライブラリを呼ぶ時点では OpenGL コンテキストが存在しない可能性があることが発覚。これは、メインループを `Run` で発動させる前や `init` 関数においては OpenGL 関数は一切呼べないことを意味する。 Ebiten において `init` 関数内で `Image` オブジェクトを作る (= テクスチャやフレームバッファを作る) ことが常態化してしまっていたため、安易に禁止にはできなかった。結論としては、 `Image` の生成およびメソッド呼び出しを遅延化し、 OpenGL コンテキストができた時点で今まで遅延化させていた処理を全部実行するというものである。これにより、処理の遅延化が不可能な処理である `Image` の `At` メソッド (指定した座標の色を取ってくる) は `init` 内などで呼べなくなってしまったが、まあよしとする。破壊的変更なので本当は良くない。

この修正の影響により、 `go generate` で Ebiten で画像を生成していたコードが動かなくなってしまっていたので[修正](https://github.com/hajimehoshi/ebiten/commit/64a66c57c0e350135655502f4212043bbc0f9279)。

さらにこの影響で `image_test.go` の大半が動かなくなってしまった。一旦動かないテストのコードを消して後で考えることにする。

## 2016-05-11

### [Android で絵が出るようになる (OpenGL レイヤーバグ修正)](https://github.com/hajimehoshi/ebiten/commit/c07f16d9a474e93b4f42d9847cd6e2a0befc92fb)

なぜか Android で画像がでないんだよなあ、と首をひねっていたが、モバイル用 OpenGL レイヤーが古くて、デスクトップ用実装等と sync されていなかったことが原因と発覚。嫌だ、機能が同じなのに重複するコードをたくさん持つからこういうことになるのだ、なんとかしないといけない。現在デスクトップ、 Web、モバイルの OpenGL レイヤーがそれぞれあるが、統合の目処は未だ立っていない。

この修正により、やっとこさ絵が出るようになる。なおこの時点ではまだ `gomobile build` (apk を直接吐くコマンド) でうごいた感じである。この頃から、 `gomobile bind` (apk ではなく aar を吐くコマンド) の対応も視野に入れつつコードを書いていく感じだったと記憶している。

このデバッグは本当にしんどくて、何がしんどいかというと、 gomobile、 Ebiten、 Android、どのレイヤーに原因があるのかの切り分けが困難だからである。そもそも OpenGL はデバッグしにくく、正しく動くまで暗中模索状態になることがよくある。それに経験の浅い Android やら gomobile やらがあるので難易度はさらに増す。 golang.org/x/mobile/gl は `gldebug` というタグを付加してビルドすることでデバッグモード (すべての GL 関数呼び出しのログを吐き出し、毎回 `glGetError` を呼んでくれる) になるのだが、今回の問題は「ゼロ初期化されたままの値を `glBlendFunc` にそのまま渡していた」であり、 `glBlendFunc` は 0 を受け取って「正しく」処理するので、 `glGetError` で取得できるエラーにはならなかった。今回は OpenGL レイヤーの実装をデスクトップのものと目視で比較した結果、たまたま発見できた。 **OpenGL プログラミングに必要な物は、根性である。**

なお、音が出るのは当分先の話である。

## 2016-05-12

### [デッドロックの修正](https://github.com/hajimehoshi/ebiten/commit/9d569e3e4922b3a11fef2453f775c732f7463559)

これはモバイルとはあまり関係ないのだが、一般的なプログラミングの教訓を得られたのでここに記しておく。 

`ebiten.Image` を生成する関数である `NewImageFromImage` には、 `image.Image` インターフェイスを実装したオブジェクトを渡すことが出来る。ところで、 `ebiten.Image` は `image.Image` インターフェイスを実装している。すなわち、 `NewImageFromImage` に `ebiten.Image` を渡すことができてしまう。

Ebiten におけるあらゆる Image の処理は mutex を使っているが、 `NewImageFromImage` の呼び出しにもロックが掛かる。その関数で渡された `ebiten.Image` オブジェクトに対してメソッドを呼びだそうとすると、そのメソッド内部でまたロックをかけようとしてしまい、デッドロックになってしまう。

得られた教訓は、「**mutex ロック中にインターフェイスにアクセスしてはいけない**」である。インターフェイス実装側でどんなコードが呼ばれるのか全く予見できないからだ。

さらに脱線だが、 Ebiten 内部の実装では goroutine を極力避けて mutex を使用している。これはなぜかというと、 GopherJS が生成する JavaScript は goroutine があると `setTimeout` などを多用してしまい、 60FPS 保ちたいゲームにとっては支障になりかねないからである。

## 2016-05-13

### [`go vet` によるミスの発見と修正](https://github.com/hajimehoshi/ebiten/commit/b3d43c185b60807800764f67ca77a98b9bf69292)

この頃から [Go Report Card](https://goreportcard.com/report/github.com/hajimehoshi/ebiten) というサービスを利用し始める。どうやら `go vet` によってバイト操作ミスが発覚、発見者である Hans Rødtang 氏が直してくれました。ありがたや。

### [リソースの埋め込み](https://github.com/hajimehoshi/ebiten/commit/49d171beb058437454d1fb784316cc528166d98e)

Android において `os.Open` などでファイルアクセスすることは可能なのだが、いかんせんカレントディレクトリがよく分からない。そもそも Web ブラウザに至っては、ファイルという概念がないではないか。デスクトップ、モバイル、ブラウザ全てで同じコードで安全に動くリソース管理は、 [go-bindata](https://github.com/jteeuwen/go-bindata) などでリソースを Go コードとして埋め込んでしまうことだ。これを使うと、あらゆるバイナリを Go の巨大な string リテラルに変換し、 `[]byte` でアクセス可能になる Go コードを生成してくれる。便利なのでぜひどんどん使っていきたい。ということでサンプル用のリソースを go-bindata で置き換え始めたのがこの頃である。

## 2016-05-14

### [`uintptr` 同士の足し算はやってはいけない](https://github.com/hajimehoshi/ebiten/commit/88fc32c07097a699cdaed72451ec9a03aa9ed578)

`go vet` は割と細かいところも見ていて、 `uintptr` 同士の足し算とかも検出して警告する。 `uintptr` の演算は `+` または `-` オペレータの右側が必ず整数定数でなければならない模様。仕方ないのでこのコードを C に移動させた。

あと `go vet` のエラーに従い色々修正。

## 2016-05-15

### [コンテキストロスト対応のための下準備](https://github.com/hajimehoshi/ebiten/commit/9f2b53d24c44004a0536dd07705781a4a49a50f9)

いままで Ebiten はデスクトップおよびブラウザでしか動いておらず、 OpenGL のコンテキストロストは意図的にやらなければ起きることはまずなかった。そのためコンテキストがロストした場合の対策を怠っていた。ひどい話である。

Android においてはプロセスを切り替えたり電源を落としたりするだけで容易にコンテキストが破棄され作りなおされる。そのため、コンテキストロストについて本格的に対応せざるを得なくなった。そのために OpenGL の初期化関数周りを整理したりしていた。

なおブラウザにおいて[意図的にコンテキストロストを起こす API](https://www.khronos.org/registry/webgl/extensions/WEBGL_lose_context/) が存在する。勉強になった。

## 2016-05-16

### [コンテキストロスト対応のための下準備](https://github.com/hajimehoshi/ebiten/commit/46cbd0c4a0b3e127e5e3383b4ecd350ca15d5290)

シェーダプログラムを削除するための関数追加とか。

### [`image_test.go` の復活](https://github.com/hajimehoshi/ebiten/commit/90aeee5144a12ffe920834c76d523d614b02163a)

2015-05-08 で動かなくなっていたテストを復活させた。 Go 1.4 から存在する `TestMain` 関数を使うことで、 Ebiten メインループを発動させ、ループ中にテストを実行することで解決した。

## 2016-05-17

### [コンテキストロスト対策? ピクセルデータ退避](https://github.com/hajimehoshi/ebiten/commit/23863d5a2f34c1f5dff2ea363b19491b1d78401a)

Android において、プロセスが一旦退避する前 (Activity の `onPause` が呼ばれる) に `glReadPixels` を呼んでピクセルデータを退避させ Go の `[]byte` として保存しておく。プロセスが復活するとき (Activity の `onResume` が呼ばれる) に `[]byte` から改めてテクスチャやフレームバッファを生成する。

なお、この方法ではコンテキストロストに対する対策にまるでなっていない。コンテキストロストはいつ起こるのか予見不可能であり、「コンテキストロスト前には必ず `onPause` が呼ばれる」という仮定は間違っているからだ。実際この実装だと、電源を切った時に `onPause` が呼ばれないままビューが作りなおされ、うまく resume できない。この時は自分の知識が足りなかったのである。この問題は後々ちゃんと修正することになる。

### [Weak Pointer の実現](https://github.com/hajimehoshi/ebiten/commit/7ab2efaa3f39d9cb5a0cc25626e6a028d09c31e6)

さて、 `Image` のピクセルデータを退避するためには、今現在存在する `Image` オブジェクトを誰かが掌握している必要がある。ここで weak pointer があるとうってつけなのだが、 Go にはそういうものはない。普通のポインタで参照してしまうと、ゲーム側ではすでに使われなくなった `Image` オブジェクトをいつまでも参照してしまうことになる。ではどうするか。 `Image` オブジェクトの実装を別の構造体 `imageImpl` に分け、 `Image` は `imageImpl` へのポインタをメンバとして持つ。 `Image`/`imageImpl` 生成時に `imageImpl` 構造体のポインタを Ebiten 側がテーブルに追加して保持しておく。ゲーム実装側 (Ebiten 利用側) は変わらず `Image` ポインタを利用する。生成時に `Image` のファイナライザを `runtime.SetFinalizer` で設定し、ファイナライザの中で Ebiten 内の `imageImpl` 参照をテーブルから消す。こうすればゲーム側で参照されなくなった Image オブジェクトは破棄され、それと同時に `imageImpl` ポインタテーブルからも無事抹消されるのである。

----

思ったよりも長くなってしまったので続きはまた今度。

# ライセンス

この記事のライセンスは [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) とします。