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

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 つのセクションでは、いよいよテクスチャーを本格的に利用するために、三角形を発展させて四角形(矩形)を扱っている。矩形は三角形を二つ使うことによって実現されるが、頂点の利用を効率的に行うために、本書の方はインデックスを使用している。

それに対して、自分は三角ストリップを使うのに慣れているので、本書のようなインデックスは採用しないで、三角ストリップを使ったサンプルプログラム(Shader や Vertices は未だ分離させていないもの)を作成した。

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

本のこの 3 つのセクションではさらに、頂点データの扱いに関して Vertices クラスを独立させ、頂点の色属性とテクスチャー属性を自由に組み合わせられるようなサンプルコードへと進んでいる。自分版ではこれまで、本にはない、GLES 2.0 用のシェーダーを自前で用意して対応してきたのだが、Vertices クラスを分離させるとなると、シェーダー関係の処理もそろそろ分離独立させないと、バランスが悪くなってくる。そこで、一度に 2 つなので少々過激かもしれないが、シェーダー関係を Shader クラスとして分離独立、その Shader を利用する Vertices クラスも続けて分離独立させた。Shader の構成の仕方は完全に僕の独自のもので、趣味的なものであり、世間一般的に、こうするのが正しいというわけではない点に留意。

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

モデル行列

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

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

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

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

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

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

まず 1 つ目として、Screen#present からできるだけ無駄な OpenGL の状態変更を(Screen#resume 等に)追い出すのが肝であるが、自分版ではテクスチャーのバインド/アンバインドがそれであり、少なくともサンプルプログラムの現状ではテクスチャーは 1 種類しか使わないので、描画の都度バインド/アンバインドを律儀に繰り返す必要はなく、Screen#resume でテクスチャーを読み込んだ際にそのままバインドしっぱなしでも問題はない。

残念ながら、fps の数値的には、ほとんど変化が見られなかった。何しろ、900 体をループで繰り返し描画しているのに対して、そのループの外側で 1 フレームに 1 回ずつテクスチャーをバインド/アンバインドしているだけなので、プラスの影響はあっても 1/900 でしかない。

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


以上、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 で追体験したいと思っているが、その場合は記事を改めて行う予定である。

コメント

このブログの人気の投稿

EP-805A 廃インク吸収パッド交換

WireGuard の OpenWrt での運用

e-Tax: このアプリは作成コーナーの画面内でご利用いただくものです。直接クリックして起動することはできません