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

前回の記事 その 1 の続き。

前回は本(libGDX の創始者である Mario Zechner 氏の著書“Beginning Android Games”(邦題は『Android ゲームプログラミング A to Z』))の 7 章の内容がベースだったが、今回は 8 章の内容がベースになる。

7 章では OpenGL ES 1.x そのものの使い方の解説(インプット)に主眼があったのに対して、8 章では「OpenGL そのもの」というよりは、いよいよ「OpenGL を使って」様々なグラフィックス表現を実現する、アウトプットの段階になる。

前回の記事では、Mario Zechner 氏のサンプルプログラムと同じ結果を実現するために、最近の Android プログラミングの事情に合わせて、Kotlin と最新の Android API をベースとし、さらに OpenGL は 2.0 に準拠するため自前のシェーダーを用意して、自分流のサンプルプログラムとライブラリーを構築した。

Game クラスと Screen クラスの構造、そして OpenGL をグラフィックスエンジンとして使用するための GLSurfaceView.Renderer クラスを用意し、その他のファイル入出力やサウンド、タッチ入力などの部分は、Android の API をそのまま使えばいいだけじゃないかということで、サンプルプログラム化、ライブラリー化の対象外としていた。

しかし、8 章では、タッチ入力をサンプルプログラムで使用するので、これだけはどうにかしなければならない。もちろん、Android プログラミングのタッチ入力に熟達している人ならば問題ないが、タッチイベントは独特のノウハウがあるので、普通はかなり面倒な代物である。本の 8 章のサンプルプログラムの側では、3 〜 6 章の過程で用意したライブラリーのタッチハンドラーがあり、そのタッチハンドラーから得られるイベントによって動作するように記述されているので、タッチ入力に関しては、この 8 章の個々のサンプルプログラムで直接扱うよりは、相当するタッチハンドラー用のクラスを用意して対応するのが無難である。

  • TouchHandler(本家):本はかなり古い時代の Android API を基準としているため、API-5~7 との互換性を考慮したコードになっているが、今ではむしろ非推奨となった API を使っている点に注意(原書 3 版でも更新されていない)。自分版ではここを大幅に刷新した。
  • Pool(本家):タッチイベントで使用するオブジェクトプール

また、7 章の Vertices クラスは最終段階で最適化のために BindableVertices に改良されたが、8 章ではそれがデフォルトの Vertices クラスとして今後使われることになる点にも留意。

8.2 ベクトル

高卒学歴があってもほとんどの人が高校数学のベクトルなんてマスターしていないのだろうが(現代日本社会の形骸化恐るべし!)、ちゃんと高校数学をマスターして高校を卒業している人ならば、極めて常識的な話としての、ベクトルについての解説がこのセクション。

8.2.2 三角関数によるベクトル演算の補完

単位ベクトルを求めるのに三角関数を利用している。またさらに、位置ベクトルの回転に、加法定理を応用して導き出される公式を提示している。

(x', y') = (xcosθ - ysinθ, xsinθ + ycosθ)

8.2.3 ベクトル演算用のオリジナルクラスの実装

(原書の 3 版でもサンプルコードのみ修正されていて、本文は未修正のままだが、)本書の説明にある FloatMath.* は既に deprecated なので、通常の Math.*(Java の場合)や、kotlin.math.*(Kotlin の場合)を使うべきな点に留意する。

8.2.4 砲台(サンプルプログラム)

7 章までに用意したライブラリーをベース(上述したように、3 〜 6 章の過程で用意したライブラリーのタッチハンドラーも必要だが)に、今回実装した Vector2 クラス(本家)を使って、まずはタッチした方向にクルクルと回転する砲台を作ってみる。7 章ではただ表示しているだけだったのが、今回からは本格的にインタラクティブ・ダイナミックに「動く(!)」ものとなるわけである。7 章では空っぽのまま放置されていた Screen#update が 8 章で本格的に活躍することになる。

8.3 物理

初歩的な高校数学のベクトルだった前節に続いて、今度は初歩的な高校物理の話。高卒以上の資格を(ハリボテでなく)持つ方であれば、説明は不要だろう。Y 軸方向に重力による等加速度運動となる、斜方投射。前節に引き続き Vector2 クラスの応用例となるサンプルプログラムを作成する。

  • CannonGravityScreen(本家

8.4 衝突検出

ゲームプログラミング的に超本格的な話。8 章の目玉の 3 つのうちの一つと言って差し支えないだろう(あと 2 つの目玉は「テクスチャーアトラス」と「バッチ」)。これはもう実際には本を購入して読んでもらうしかないだろうが、以下、あらましを述べる。

まず、モデル次元で衝突判定を行っている。そのために Circle(本家)、Rectangle(本家)、OverlapTester(本家)、GameObject(本家)、DynamicGameObject(本家)などの各種モデルオブジェクトを定義するクラスを追加している。

次に、広域フェイズと狭小フェイズに分けて検出することが解説されている。毎ターン(フレーム)、全部のオブジェクトについて厳密な衝突判定を行っていると、簡単に組み合せ爆発的な事態になりかねないため、まずは広域フェイズで候補を絞って、その候補について実際の衝突判定を行うという話である。

これは実際に、スペースインベーダーでも既に行われていた手法ではあるが、例え処理能力が天文学的に向上した現代とはいえ、組み合せ爆発をさせてしまうと、簡単に処理能力を食い尽してしまうわけだから、依然として通用する重要な手法なわけである。

この広域フェイズによる候補の絞り込みを実現するために用意されたのが SpatialHashGrid(本家)クラスである。

以上のライブラリー各クラスに加え、サンプルプログラムでは砲台用の Cannon(本家)を独立させて GameObject の子クラスとして定義している。

8.5 カメラ

これはどちらかというと 7 章的な内容。orthoM の応用的な使い方として、視界範囲を水平移動することでカメラを水平に動かすだけでなく、視界範囲を狭めたり広めたりすることで、ズームイン(視界範囲狭める)・ズームアウト(視界範囲広める)を行うこともできるという話。本では、このズームイン・アウトを、弾の高さに比例させて行っているため、8 章の物理シミュレーションの一環として出してきたようだ。Camera2D(本家)というクラスを追加している。

自分版のサンプルでは、視界範囲(Projection)を直接操作することはせず、MVP (Model-View-Projection) のうちの View を操作する形で実装している。View 行列の操作には LookAtM という命令を使うだけなので、特に専用のクラスは追加していない。

8.6 テクスチャーアトラス

いわゆる「テクチャーマップ」とか「キャラクターマップ」とも呼ばれているもの。てっきり昔のメモリー資源が限られていたアーケードゲームとか、初代ファミコン時代のテクニックかと思っていたが、実は全然そうではなかった。

この本を読む前、少し OpenGL ES 2.0 を齧った段階の僕は、複数のテクスチャーを用意して、バインド・アンバインドして切り替えて使っていた。しかし、OpenGL では、このバインド・アンバインド毎に状態変更が発生するため、非常に処理コストが高くつくのである。つまり、現代のゲームグラフィックスでも、キャラクターマップは全く現役で有効なのである。単独のテクスチャーをバインドし、その中の一部を読み取って貼り付ける。大変大変、重要なノウハウである。

本では、テクスチャーアトラスの手法を使って、前節のサンプルプログラムの 3 種類の各図形(砲台、弾、標的)に貼り付けている。

  • TextureAtlasScreen(本家

本では、次節において、このテクスチャーアトラスのノウハウをカプセル化した TextureRegion(本家)クラスを追加してリファクタリングいる。

ちなみに、自分版のサンプルでは、13000 体の大量の標的を表示させたため、FPS は 5fps にまで低下し、完全にフレーム落ちが発生して、飛び飛びでしか標的が当たり判定で消去されていない。次節とのパフォーマンス比較のために同じ 13000 体にしてみた。今節ではあくまでもテクスチャーアトラスの機能の確認が目的なので、これでも問題はない。

8.7 バッチ

先述したように、今節のサンプルプログラムでは、前節のテクスチャーアトラスをカプセル化した TextureRegion(本家)クラスを使う。

8 章の目玉の最後がこのバッチである。

ネットの各所で OpenGL の情報を探すと、MVP (Model-View-Projection) 行列の話がおきまりのように登場する。さらに、モデル行列は回転・拡大縮小・平行移動であり、それらは行列演算により実現できるということは、7 章で Mario Zechner 氏も説明している。にもかかわらず、Zechner 氏は、実際のライブラリー構築やサンプルプログラムでは、あまり積極的にこれらの行列を OpenGL にインプットして処理することを実装していないのである。

さらにもう一つ、自分は、この本(の 7、8 章)を読む前に別ルートで OpenGL ES 2.0 を勉強したりしたので、GL_TRIANGLES ではなく GL_TRIANGLE_STRIP を使って矩形を描画してテクスチャーを貼っていた。このやり方を使えば、インデックスなど使う必要がないので、便利である。なぜか Zechner 氏は、GL_TRIANGLE_STRIP は融通が利かなくて使い勝手が悪いと 7 章で述べていた。

それらの秘密が、この節にあった。

まず、モデル行列を、描画するオブジェクトの位置などによって一つのオブジェクトずつ操作する場合、都度、シェーダー変数へのバインドと draw 命令の実行が必要となる。この 2 つの OpenGL 的な状態変更が、オブジェクト 1 個ずつに行われるので、オブジェクトの数が増大すると、処理速度の低下に直結する。

バッチ化は、この大量のオブジェクトの処理を、バッチとして単体の頂点データの塊にまとめてしまい、たった一度のバインドと draw 命令で済ませてしまうノウハウなのである。

このため、モデル行列の回転・拡大縮小・平行移動といった変形は、バッチ化の過程で CPU(Java)側で済ませてしまう。OpenGL のシェーダーへのモデル行列の入力は使わないのは、このためである。

本では、SpriteBatcher(本家)という専用のクラスを用意している。描画すべき複数の図形オブジェクトを単体の頂点データの塊にまとめてしまうためのクラスである。これにより、処理速度は神がかったレベルで爆速化する。13000 体の標的を使ったサンプルプログラムが、5fps から 60fps と 12 倍に大化けしたのだ(!!!!!!!!!!!!)。

  • SpriteBatcherScreen(本家

また、このバッチ処理は、GL_TRIANGLES とインデックスを使った描画に依存している。もし GL_TRIANGLE_STRIP を使ったならば、一塊の頂点データは全てつながった一本の帯として処理されてしまうため、悲惨なことになる(右の画像)。

8.8 アニメーション

最後のアニメーションは、テクスチャーアトラスの応用である。経過時間の modulo を利用して、使う TextureRegeon を切り替える。本では Animation(本家)クラスと、(AnimationScreen の内部クラスとして)DynamicGameObject の子クラスとなる Caveman(本家)クラスを追加した上で、サンプルプログラムを作成している。

また、スプライトの width/height のサイズ指定をマイナスにすると、線対称に向きが変わるという話は特筆すべき点かもしれない。


以上、Mario Zechner 氏の著書“Beginning Android Games”(邦題は『Android ゲームプログラミング A to Z』)の 8 章で解説されている内容も、前回の記事(7 章の内容)に続き、(Kotlin + OpenGL 2.0 で)一通り辿ることができた。

特にバッチ化は衝撃的だった。バッチ化のノウハウを知らないまま、ちょっと OpenGL 2.0 とシェーダーを使えるようになったからと、調子に乗って早速ゲームの製作に突っ走ったりしなくて良かったと思う(汗)。OpenGL の恩恵が台無しの状態で実際のゲーム製作に特攻するところだったわけだ。三匹の子豚の兄豚になりかねないところだった……。本当に、10 年経っても価値の褪せない良書を持っていて良かった。

コメント

このブログの人気の投稿

Mac → Mac のメールアカウントの移行

LiveData と MutableLiveData の使い分け

WireGuard の OpenWrt での運用