Beginning Android Games(Android ゲームプログラミング A to Z)その 1

Android を始めとするマルチプラットフォームの Java 用ゲーム開発フレームワークlibGDX の創始者である Mario Zechner 氏の著書“Beginning Android Games”(邦題は『Android ゲームプログラミング A to Z』)だが、原著は 3 版が 2016 年、初版の日本語訳版は 2011 年に出版されたきりであり、いずれにしても、最新の Android API からは隔絶した内容のものとなってしまっている。

初版は 10 年を経過(!)しているので、Android API 以外の内容にも古さは否めないが、とはいえ原著の 3 版にしても、ごく一部の Android API に関する部分を 2016 年の時点に合わせて修正したのみで、本の構成内容自体は全く更新されていない。特に、初版の時点では、OpenGL ES の 2.0 が出始めたばかりで普及しておらず、OpenGL ES 1.x を対象にしたのはわかるが、版を重ねても、OpenGL 2.0 以降に対応させてはいない。1.x と 2.0 以降では大きな違いがあるので、そこを変えるとなると大幅な書き直しになってしまうからだろう。また、本を読む初心者にとっても、2.0 以降のプログラマブルシェーダーについての学習のハードルが加わることになり、Android ゲーム開発全般をテーマとする本書のスケールを上回ることになる。

この本の良さはそういった Android プログラミングで直接使える技術の情報源として以外の部分にあるので、依然として一読の価値ある本だと思う。Amazon ではほとんど送料だけみたいな価格で古本が売られていたりするので、興味が湧いたら是非、一冊入手してみることをお勧めする。

自己流ゲームライブラリーの構築

本書を参考にして、自分流のゲームライブラリーを構築してみようと思う。最新の Android API 対応は当然として、その他の要点は次の通り:

  1. 2D
  2. OpenGL ES 2.0+
  3. Kotlin
  4. プラットフォームは Android 専用とし、インターフェース化してマルチプラットフォーム化を意識した設計にするようなことはしない。例えば、ファイル入出力なども、直接 Android API を駆使し、ゲームライブラリー化の対象としない。

本では、単に Android 用のゲームライブラリーを構築していくという目的だけでなく、Java を使ったマルチプラットフォーム化を視野に入れたものとなっている性質上、各種命令をインターフェースとして設計し、それぞれの命令の処理内容を Android API によって実装するという、2 段構成を取っている。利便性の反面、Android 専用として考えるとコードの見通しは悪くなるので、そこはザックリと除外してしまうことにする。そうすると、本書でインターフェース化の対象となっているファイル I/O 等の各命令は、ゲーム開発に限定されない、単なる Android API の一般的なノウハウとなり、本書の(古い)情報をアテにする必要はなくなる。

この本から学ぶべき、肝となる部分は、OpenGL を使ったグラフィックス処理に関する部分のライブラリー化についての部分である。本書は 6 章までで SurfaceView を使ったライブラリー化を一通り完成させ、7 〜 9 章でコアとなるグラフィックス部分を SurfaceView から OpenGL ES 1.x 化する(ただし 2D)。さらに 10 〜 12 章では OpenGL 化の真骨頂である、3D 化を行ってゴールとしている。

自分は 3D は除外して、2D で OpenGL ES の恩恵を受けたいと思っているので、2D 化までで十分だが、一方で原著では扱っていない、OpenGL ES 2.0 に対応させるつもりである。

もちろん、近年の Android 開発の環境に合わせて、言語は Kotlin を採用する。


Zechner 流フレームワークの構造

先述したように、マルチプラットフォーム化のための「インターフェースと実装」という 2 段構成を解消し、さらに、サウンドやタッチ、ファイル I/O 等のグラフィックス以外の周辺部分を(マルチプラットフォーム化のための)フレームワーク化対象から除外すると、グラフィックス部分のみをどのようにフレームワーク化しているかという話に絞ることができる。

すると、Mario Zechner 氏のフレームワークの根幹アイデアは、GameScreen と名付けられたクラスの構造に帰結する。

まず、Game は、Activity を拡張したもので、グラフィックスに加え、サウンドやタッチ、ファイル I/O 等の各パーツをインスタンス化し、フィールド変数として格納して、Screen から参照可能なように整えるために用意されている。どちらかというと、フレームワークを利用するゲームプログラマーにとっては、Screen がゲームプログラムのコンテンツである。Game はフレームワーク側にとってのエンジン部分であり、フレームワークを利用して製作されるゲームプログラムにとっては、Screen(の集合体)がゲームソフトそのものなのである。Game が、ファミコンやゲームボーイ、プレイステーションのゲーム機本体だとしたら、Screen(の集合体)がファミコンのカセットやゲームボーイのカートリッジ、プレイステーションの CD-ROM に相当する。

さらに言うと、Game は個々のゲームプログラムに応じてカスタム可能なように、抽象クラスとなっている。

そして、Screen。これこそが Mario Zechner 氏のフレームワークの肝である。一つの Screen は、ゲーム中の一つの画面に相当し、状況に応じて、複数の Screen 間を遷移するような使い方が想定されている。例えば、メイン画面から、ヘルプ画面へと遷移する等。これをプログラム構造的にどのように実現しているかというと、ゲームエンジン側の毎フレームのレンダリングされる部分で Screen 側のレンダー用メソッドを呼び出すようにしている。現在有効な Screen は、Game クラスでポインターを管理しているので、Screen が変更されたら、その Screen のレンダー用メソッドが毎フレーム呼び出されるだけである。そんな感じで、Screen 毎にプログラムし、必要に応じて Screen 間で遷移させるというのが、このフレームワークの特徴となっている。

よって、自己流のフレームワークの構築にあたって、まずは、GLSurfaceView を使った、Game/Screen 構成のスケルトンを用意してみた。

MainActivity
Zechner 版と違い、「Activity を拡張して GameActivity として抽象クラス化する」ことはせず、シングルトン(Game)と Renderer に各役割を分離する。Acitivity は、ゲーム用途以外に、広告の実装等、Android アプリとしての様々なコードが関わることになるため、極力 Game 用フレームワークとコード記述を癒着させないようにしたかったため。
Game
ゲーム全体を通じて各 Screen から参照する値を格納するシングルトンオブジェクト。Zechner 版と違い、Activity を拡張して Game クラスとしての機能を持たせることはせず、純粋なシングルトンオブジェクトとして分離した。Java に比して Kotlin はシングルトンオブジェクトが簡潔に表現できる。
Renderer
GLSurfaceView.Renderer を実装するクラス。これも Activity に統合はせず、単独のクラスとして分離した。
Screen
これは当然、抽象クラスであり、各メソッドの内容は個別の Screen で実装する。また、サウンドやタッチ、ファイル I/O 等のグラフィックス以外の機能は特にフレームワーク化の対象ではなく、通常の Android アプリとして自前で処理することになるため、Context を保持させている(メモリーリークの予防のため、WeakReference を使って保持している)。

以上はあくまでも Android アプリとしての OpenGL プログラミングを行うための「お膳立て」の部分であり、とはいえ、ゲーム用のフレームワークとしては、ファミコンの本体部分の仕様に関する部分の話でもある。これ以降からやっと、OpenGL プログラミングそのものへと突入していくことになる(プログラム的には、Screen クラス内でのコーディング作業となる)。

レンダリングパイプライン

このセクションで「OpenGL (ES) とは、巨大なステートマシンである」と Zechner 先生は断じているが、蓋し名言である。

自分はそもそも「ステートマシン」という言葉の意味について、よく耳(目)にしたことはある一方、よく考えたことがなく、きっちりした定義についても知らなかった。それで、OpenGL については、何となく、サーバー=クライアントモデルのようで、HTTP を通じてサーバーにリクエストを送るような、遠隔操作的な印象を抱いていた。

「OpenGL (ES) とは、巨大なステートマシンである」この一言で、OpenGL という API 体系の全体的な性質がつかめたのみならず、ステートマシンという言葉を自分の IT 用語録に新しくストックできた次第である。

Look Mom, I Got a Red Triangle!(原文ママ)

Zechner 本では、Orthographic 投影、ダイレクトバッファ(JavaVM のヒープではなく、ネイティヴメモリーを使うため)の使用、そして(シェーダー変数を通じた)頂点情報の送り込みについて present メソッド中における簡単な OpenGL コードを実行している。

Zechner 本では、GLES 1.x に基いているため、シェーダー変数は API としてハードコードされているものを使っている。自分版はそれに対して GLES 2.0 を使用するため、resume メソッド中において同等のシェーダーを定義し、コンパイル→使用し、シェーダー変数に対して頂点情報を送り込むようなプログラムを作成してみた。

Zechner 本のサンプルコードは GLES 2.0 の観点からは使い物にならないが、この本の 7 章は、コード以外の本文は非常に読み応えのある内容となっている。例えばダイレクトバッファの使い方など、ここまで丁寧な解説を目にしたのは初めてであり、あちこちで齧って使い方だけはわかっていたものの理解が曖昧だったことがやっとはっきりと理解できるようになった。

頂点ごとの色の指定

このセクションでは、各頂点が位置情報のみならず、色も属性として持つことが示されているが、ゲームプログラミング的にそこは本題ではない。stride というパラメーターの概念を説明することに焦点がある。いわば次の頂点データにポインターを進める場合のバイト数を示す「歩幅」のようなものである。次のセクションのテクスチャーを扱う場合に、必須となってくるものである。(サンプルプログラム

テクスチャマッピング

このセクションでは早くもテクスチャーに挑戦しているが、テクスチャーが x-y ではなく s-t 座標系であり、それも 0~1 の範囲に限定されていることが解説されている。次に GPU 側にテクスチャー用の領域を確保し、ビットマップデータをアップロードする方法の解説。さらに拡大縮小時用のフィルタリング方法の設定と、盛り沢山の内容であり、ページ数も膨らんでいる。(サンプルプログラム

本のサンプルプログラムはこの段階までのもので、三角形の領域にテクスチャーを貼ったものを表示するだけだが、解説ではさらに、テクスチャーの操作に関する処理を集めて、独立した Texture クラスとして整理(リファクタリング)することを示している。自分版の Texture クラスは、Mechner 版とかなり構成が違っている。bitmap をクラス側内部で自動的に recycle しないようにしたり、フィルタリングはピクセラレーションしか使わないつもりなので、独立したメソッドにはしなかったり、等々である。単に、テクスチャー関係の処理を Screen から追い出して隠蔽するという趣旨のみ、受け継いでいる感じである。また、この Texture クラスを利用したリファクタリング済のサンプルプログラムもついでに作成してみた。

その他、本では、テクスチャーの一辺(正方形でなくともよい)は 2ⁿ のサイズであるべきことも説明されている。

本では次の 3 つのセクションでさらに、頂点データの扱いに関して Vertices クラスを独立させたりと、より複雑な方向へと進む。自分版ではこれまで、本にはない、GLES 2.0 用のシェーダーを自前で用意して対応してきたのだが、次のセクション以降の複雑化に先立って、シェーダー関係の処理もそろそろ分離独立させておいた方がいい。ボイラープレート化してきているし、コード量のバランス的にも邪魔になってきている。上のサンプルプログラムからさらに、シェーダー関係をShader クラスとして分離独立し、それを利用する形にさらにサンプルコードを更新した。ただし、Shader クラスの構成の仕方は完全に僕の独自のもので、趣味的なものであり、世間一般的に、「こうするのが適切」という模範を示すのが目的ではない点に留意。

インデックス〜アルファブレンド〜三角ストリップ

この 3 つのセクションでは、いよいよテクスチャーを本格的に利用するために、三角形を発展させて四角形(矩形)を扱っている。矩形は三角形を二つ使うことによって実現されるが、頂点の利用を効率的に行うために、インデックスを使用する(サンプルプログラム)。

インデックスにより、頂点データのうち使用する順番(筆順)を指定し、glDrawElements を使うのが特徴である。

それに対して、自分は三角ストリップを使うのに慣れていたので、当初、三角ストリップを使ったやり方で以後のプログラミングを作成して進めていた。本章(7 章)はそれで全く問題はなかった。しかし後に、次章(8 章)の最後から 2 番目のステップで、三角ストリップの限界が発覚。このステップまで戻って、通常の三角形とインデックスを使った矩形の描画で全てやり直さざるをえなくなった(次章では、複数の三角形の描画を一塊の頂点データ群として一気に処理させる「バッチ」と呼ぶ手法で高速化を図る。それが、三角ストリップだと、どんどんつながって全体で一つの帯状の図形として扱われるので、バッチ化できないのである)。

ブレンドについては、ブレンドを有効化するコマンドを追加すれば、元のビットマップがアルファ値付きであれば、透過すべき部分は透過する。

本のこの 3 つのセクションではさらに、頂点データの扱いに関して Vertices クラスを独立させ、頂点の色属性とテクスチャー属性を自由に組み合わせられるようなサンプルコードへと進んでいる。

本のサンプルプログラムとはかなりかけ離れてきてしまっているが、色タイル、通常のテクスチャー、テクスチャー+彩色の 3 種類のモードをデモンストレーションするものにしてみた。

モデル行列

次なるセクションではモデル行列の利用について解説されている。同じキャラクターを複数描画する場合、キャラクターの数の分だけ頂点データを用意して処理させるのではなく、頂点データは1セットのみ保持し、モデル行列を操作することで、様々な位置に描画できるのである。このモデル行列の使い方にコツがあり、行列を適用する順番が重要な点が説明されている。行列が右から掛けるのと左から掛けるのでは全く効果が違ってくるという、数学の行列分野の常識を知っている人であれば、わかるだろう。

この段階のサンプルプログラムでは、モデルを分離独立させている。本では Bob というキャラクターなので Bob クラスだが、自分版では Xanadu 風戦士のキャラクターなので Xanadu クラスと命名した。

本では 100 体の Bob を描画しているが、時代的に処理能力に格段の開きがあるので、1000 体の Xanadu 風戦士を描画させてみた。まるで大量のテントウムシが蠢いているかのようである。

パフォーマンスチューニング

7 章最後となるこのセクションでは、FpsCounter という LogCat に fps を報告するヘルパークラスを用意してパフォーマンスを計測しつつ、チューニングを行っている。自分版のサンプルプログラムでは既にある程度のチューニングを行っていたので、本に書かれていたチューニングすべき項目のうち残っていたものは 1 つだけだった。

チューニング前では 800 体を描画した場合、26fps だった。

まず、Screen#present からできるだけ無駄な OpenGL の状態変更を(Screen#resume 等に)追い出すのが肝であるが、自分版ではこれは既に達成済で、改善の余地はない。

本で最後に行っている改善項目のが自分版で残っていた唯一のものである。これが劇的な効果があった。800 体をループさせている処理の内側に潜んでいるもので、Screen#present から呼び出している先の、Vertices クラス側の処理(Vertices#draw)に OpenGL の状態変更があったのだ。この頂点データ等のバインド/アンバインドの繰り返しを回避し、ループの外側に移動することで、パフォーマンスは 1.5 倍近くになり、37fps を達成した。

さらにもう一つオマケに本とは関係のない最適化として、シェーダーの改良も行ってみた。元々は、毎フレームごとにループ内で 800 体の Xanadu 戦士毎に繰り返される translate 行列を作成し、それを scaleRotate 行列と掛け合わせて、Model 行列とし、さらにその Model 行列を VP 行列と掛け合わせて MVP 行列にし、それを GPU 側のシェーダー変数 uMvpMatrix にセットするという方式だった。本当は、毎フレーム毎に変化するのは translate 行列だけなのに、その都度、scaleRotate 行列との掛け合わせ、VP 行列との掛け合わせが 800 体分発生することになる。

これを、シェーダーの設計として、translate 行列を単独でセット可能なように改良してみた。この場合、計算を Java 側ではなくて、GPU 側で行うことになる。ちなみに VP 行列や、scaleRotate 行列は、固定値なので、初期化過程で一度だけセットすればいい。

シェーダーの修正に伴い、シェーダーをコンストラクター引数として受け取る BindableVertices の方も修正が必要になる。

結果、パフォーマンスは 26fps の 2.3 倍(!)になり、60fps を達成した。

同じ行列の計算をさせるにせよ、GPU の方が処理能力が 1.6 倍良いという結果となった。


以上、Mario Zechner 氏の著書“Beginning Android Games”(邦題は『Android ゲームプログラミング A to Z』)の 7 章で解説されている内容を、ほぼ同様のステップを辿るような形で、OpenGL ES 2.0 かつ Kotlin での自分版サンプルプログラムを独自に作成して追体験してみた。各ステップでの解説内容はあくまでも実際に本を読んでもらうなりすることにして、ここで解説するようなことはしない。また同様に、OpenGL ES 2.0 についての解説も別の機会に譲ることにする。

続く 8 章の内容は、OpenGL ES そのものの解説というよりも(OpenGL ES の学習は 7 章で一通り終っているという建前になっている)、ゲームグラフィックスとしてさらに発展した解説となっており、物理シミュレーション的な話などが主である。その各解説項目のサンプルプログラムを、Screen に OpenGL ES で実装していく内容となっている。8 章もできれば OpenGL ES 2.0 と Kotlin で追体験したいと思っているが、その場合は記事を改めて行う予定である。

追記:8 章の内容が記事にできました。→ その 2

コメント

このブログの人気の投稿

清水俊史『上座部仏教における聖典論の研究』

シークエンスパパとも 本物の霊能力

シークエンスパパともの先見の明