Java
Android
Go
OpenAL

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

More than 1 year has passed since last update.

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

これを書いている最中に Ebiten 製「いのべーしょん」の Android 版がリリースされました。パチパチパチ。ダウンロードはこちらから:

Get it on Google Play

日記

2016-05-18

ゲームループ周りの整理

単なるリファクタリング。

Travis CI 上で JavaScript バージョンのテストを諦める

いつのまにやら gl モジュールが使えなくなってしまった。 node で Ebiten 使う人なんて皆無だと思われるのでコメントアウトして放置。

Travis CI テストといえば、 GLFW 版でも Travis 上でうまく動かないんだよな。 X のエラーが出てしまう。いつか直さないといけない。

2016-05-19

mobile モジュールの作成

とうとうモバイル用の API を追加した。今回は gomobile build (アプリケーションを作る) ではなく gomobile bind (共有ライブラリを作る) のがターゲットである。よって Go だけで完結したアプリではなく、 Go の API を Java/Objective-C 用に公開することになる。 Go API が他言語向けにどのようにエクスポートされるかについては、 gobind コマンドのページを参考のこと。大雑把には、 intstring などのプリミティブな型、 byte のスライス、およびそれのみで構成される関数、構造体やインターフェイスが対象となる。チャネルや関数ポインタ、それで構成される関数などは対象にならない。エクスポート対象にならない関数などを定義しても、 gomobile bind によってガン無視されるだけである。

今回そのモバイル用 API として、 Ebiten に mobile モジュールというのを新設した。 gomobile bind で作成したライブラリの公開 API としての使用を想定している。実際には、これらの API は gomobile bind で指定したモジュール (= ゲーム) 側でいちいち Proxy してやる必要がある。たとえば mobile モジュールで Update という関数を定義してあったとして、ゲーム側でそれを呼ぶだけの Update という関数を別途定義してやる必要がある。実際いのべーしょんではそのような Proxy 的なコードがいくつかある

API としては入力等を除くと次の 2 つが基本である:

  • Start: ゲームを開始する。 Android の GLSurfaceViewonLayout や iOS の GLKViewviewDidLayout/viewDidLayoutSubviews で呼ばれることを想定している。なおいずれもプロセスが生きている間に複数回呼ばれる可能性があるため、 Start が複数回呼ばれてしまわないように (もしくは呼ばれても大丈夫なように) 工夫する必要がある。今回は IsRunning というメソッドを追加して呼び出し側が条件分岐するようにした。
  • Update: ゲームを更新する。Android の RendereronFrame や iOS の GLKViewglkView:drawInRect: で呼ばれることを想定している。過去の名前は Render だった。

前述のとおりメインループが Go 側ではなく Java/Objective-C 側にあるため、毎フレーム Update を呼ばなければならない。また後述するが、入力に関しても別途 API を生やす必要がある。

2016-05-23

タッチイベントのハンドラー追加

mobile モジュールの API にタッチ関係の API を追加した。いずれも Android ならば onTouchEvent で呼ばれることを想定している。とりあえずタッチイベントがないとゲームが始まらないので、 API を生やすのは一時的な措置のつもりであったが、 Go 側でタッチ情報を取る方法がどうにもないらしく、仕方がなく現在に至る。 gomobile bind ではなく gomobile build ならば、このような余計な API は一切必要ないのだが…。

構造体導入による API 削減 (後に撤廃)

グローバル関数がたくさんある API より、構造体 1 個とそれにメソッドが生えていたほうが色々楽じゃないか、という発想で、 EventDispatcher という構造体を導入した。

これは後に撤廃した。ゲーム側では gomobile bind でエクスポートするために当然 type EventDispatcher mobile.EventDispatcher のように再定義し直す (もしくは interface を定義する) 必要があるが、 Objective-C 向け gomobile bind ではこのように構造体型を定義してもなぜか export されないためである。 Android/Java 用ではうまくいったのだが。おそらくこの挙動はバグなのだろう。そもそも API がたくさんあるのを構造体のメソッドにしてしまうのは、ある種の甘えというか、その前に関数を出来る限り減らすべきではないかと考えを改めたため、 EventDispatcher は撤廃することにした。

2016-05-24

mobile モジュールに PauseResume 関数の追加 (後に撤廃)

コンテキストがロストしたり復活した時に Android/iOS 側から呼んでもらう関数を追加した。後に、これらの関数は不要であることに気づく。外部から情報を伝達せずとも、毎フレーム適当なテクスチャについて glIsTexture が真を返すかどうかで、コンテキストが生きているのかどうか分かるのでは、と気づいた。そして実際にそれがうまくいったのだった。外部に晒す API は少なければ少ないほどよい。

x/mobile/exp/audio/al の利用を試みるが断念

Android で音を鳴らすために、 OpenAL を利用するライブラリ golang.org/x/mobile/exp/audio/al を使おうとしたのだが、失敗した。 al: cannot load libopenal.so というエラーが出てどうしてもなくせなかった。同じバグは過去に報告はあったものの、 Issue 報告者はこのバグがすでに直ったと言っており、自分の環境でこの問題がなぜ起きるのか不明なままだ。この Issue では別の問題が議論中であり、このモジュールはまだ Android では当分安定して使えるものではなさそうだ。

そもそも Ebiten ではオーディオのドライバ部分は PCM が鳴らせる実装であれば良くて、 OpenAL のような 3D オーディオ機能は不要である。

以上から、 golang.org/x/mobile/exp/audio/al ではなく、 Android 向けに独自のオーディオ実装を行うことにした。

余談だが、OpenAL は Android にはデフォルトに入っておらず、共有ライブラリを動的に落としてくる形になるのだが、その際 OpenAL のライブラリは LGPL で、ライセンスを明記する必要がある。golang.org/x/mobile/exp/audio にも以下のように明記されている:

When compiled for Android, this package uses OpenAL Soft as a backend. Please add its license file to the open source notices of your application. OpenAL Soft's license file could be found at http://repo.or.cz/w/openal-soft.git/blob/HEAD:/COPYING.

2016-05-25

Android オーディオ実装

Ebiten における音の実装は、プラットフォーム非依存なレイヤーとプラットフォーム依存なレイヤー (ドライバ) に大まかに分かれている。プラットフォーム非依存のレイヤーがよしなにミキシングなどを担当し、ドライバレイヤーは単に PCM を鳴らす実装をつくればよいようにできている。今回は Android ドライバを実装するのだが、前述の要件より、 AudioTrack を使えば十分だ。今回は Go から JNI を利用して、 AudioTrack を叩く実装をした。

Go から JNI を利用するにはどうしたらよいか。単に cgo を利用するだけで良さそうなのだが、実は C が書けるだけではなくて肝心の「現在の JavaVM*」を取ってこなければならない。 Android では 実は JavaVM* 型の変数は x/mobile/internal/mobileinit/ctx_android.go に定義されている。 JNI を利用する際にはこの値をとってこないといけない。モジュールが internal なので他モジュールからは触れられなさそうだ。とおもいきや、変数自体は C の変数であり、 external linkage である (他ファイルから参照できる)。今回は ctx_android.go の実装をコピーし、 RunOnJVM 関数を作成した。これで無事 JNI を叩く準備が整った。

あとは単純に AudioTrack を叩くだけだ。 JNI は今回が初めてだったが、型チェックがものすごく貧弱になるので、なるべく書きたくないなあと思った。

なおこの時点ではまだ実装にバグが有り、音がなるに至っていない。

オーディオ初期化の遅延 (init 関数の罠の回避)

gomobile bind は共有ライブラリが作られるのだが、エントリーポイントとなる main 関数はない。 Go には init 関数というのがあり、これらは main よりも前に呼ばれる。では main のない共有ライブラリで init 関数があった場合、これらはいつ呼ばれるだろうか。正解は「外部からそのライブラリの関数どれでも、一番最初に呼び出した時」である。つまりちょっとでも共有ライブラリの関数に外部から触れると、その関数呼び出し前に init 関数が呼び出される。 init 関数以外の関数は全部 init 関数が呼ばれ済みであることを期待している、と考えると至極当然に思える。逆に共有ライブラリの関数を誰も呼んでないと、 init 関数が呼ばれることはない。

さてここで困ったことが起きた。 init 関数内で Ebiten のオーディオ関係の初期化を行っていたのだが、内部でドライバの初期化も行っており、前述のとおり Go から JNI を叩いているので、この時点で JavaVM* 型の current_vm 変数が初期化されている必要がある。しかし今回 init が呼ばれるタイミングでは、この変数はまだ未初期化状態のようなのだ。 current_vm は後から値を設定しているので、そういった事態が起きても不思議ではない。

解決法としては単純で、 init でそういった処理はせずに、必要になるギリギリまでオーディオ初期化を単に遅延させただけである。

今回このバグの原因究明に若干苦労した。なぜなら「呼びだそうとしている関数の前に init 関数が呼ばれる」ということに気づかないと、ありえもしないところで落ちているように見えるからである。

2016-05-26

オーディオのミス修正

Android でやっと音が鳴るようになった。原因は凡ミスの積み重ね。 JNI つらい。

2016-05-28

マルチタッチ対応

いままで Ebiten は 1 個のタッチにしか対応していなかったのをマルチタッチにするように修正。アクションゲームとかだと、マルチタッチないととてもゲームにならないので、必須である。


これで一通り「いのべーしょん」に必要な機能はそろった。身内に「5 月末までにいのべーしょんを Android で動かせるようにするぞ」という公約をしていたのだが、ギリギリ達成したのだった。しかしこの時点ではまだ Nexus 5x で悲しいことに 30FPS ほどしか出ず、目標の 60FPS を達成するまでひたすらチューニングを続けることになる。まだまだ続く。

ライセンス

この記事のライセンスは CC BY 4.0 とします。