はじめに
『ゲームプログラミングC++』という本の三章では『Asteroid』というゲームを制作しますが、第一章で作る『Pong』と比べて遥かに複雑で、私自身大変苦労しました。
そこで、学んだことやプログラム全体像を確かな形で整理したいと思い、本記事を書くことにします。
私自身プログラミング初学者であるため、分かりにくいところもあるとは思いますが、温かい目で見て頂ければ幸いです。
本書のソースコード
この記事ではコードを詳細に載せませんので、以下の公式レポジトリを参考にしてください。
構造
今回のモデルは第二章で示されたように「階層構造」と「コンポーネント」を組み合わせた形です。
-
Actorクラスがそのオブジェクト(キャラクター、弾など)の状態、数値(座標、回転角など)を管理します -
Componentクラスが、動作や処理(描画、移動、入力受付など)の機能を提供します
Actorに複数のComponentを「部品」として組み込むことで、ゲームオブジェクトの振る舞いを決定し、Gameクラスが全てのActorと、描画対象となるSpriteComponentを管理し、ゲームループを回します。
実際のモデル(クラス階層)
Main
└─ Game
├─ Actor (アクター : オブジェクトの基底クラス)
│ ├─ Asteroid(小惑星)
│ ├─ Ship(プレイヤーの船)
│ └─ Laser(レーザー弾)
├─ Component(コンポーネント : 機能部品の基底クラス)
│ ├─ CircleComponent(円形の衝突判定)
│ ├─ SpriteComponent(スプライト描画)
│ └─ MoveComponent(移動処理)
│ └─ InputComponent(プレイヤー入力処理)
└─ Library(補助機能)
├─ Math(数学関連)
└─ Random(乱数生成)
Library(補助機能)
まずは土台となる補助的な機能です。(.libファイルがあるわけではないのですが、何と呼べばいいのかわからないので今回はライブラリとして扱います。)
Math
ベクトルや行列の計算、円周率の定義など、ゲームを制作する上で必要となる数学的機能を提供します。今後何度も使うと思うので、SDLと同じようにプロパティから追加するようにすれば楽かもしれないですね。
Random
疑似乱数を生成する機能です。floatやVectorなど複数の型に合わせた範囲指定の乱数を返します。
Asteroid(小惑星)の初期座標や移動方向をランダムに設定するために使用します。
Component
Actorに追加する「機能部品」です。Component基底クラスを継承して作られます。
-
Componentは自分が所属するActorへのポインタ(mOwner)を保持します。これにより、コンポーネントはGet関数などを通して、オーナーであるActorの各種数値にアクセスできます - コンストラクタで
ActorのAddComponentを呼び出すことでコンポーネントを管理します -
Actorから更新処理を呼び出してもらうため、仮想関数UpdateとProcessInputが宣言されています
CircleComponent
Actorに円形の衝突判定機能を提供します。衝突判定を円の中心点同士の距離と半径の合計の比較によって行うので、このコンポーネントにはbool型のIntersect関数を持ちます。
LaserもしくはShipとAsteroidの衝突判定に使われます。
SpriteComponent
テクスチャの保持と描画を行います。
スプライトの管理自体はGameが行っています。ですからActorのGetGame関数からAddSprite関数呼び出し、同様にデコンストラクタでRemoveSprite関数を呼び出します。
インスタンスの生成と同時に、Gameで生成したテクスチャをSet関数で受け取とります。
最後にGameクラスのGenerateOutputでSpriteComponentのDraw関数を呼び出しSDLの独自関数によって描画します。
MoveComponent
Actorの移動(座標と回転)を処理します。Update関数をオーバーライドし、現在の回転速度(mAngularSpeed)と前進速度(mForwardSpeed)に基づいて、オーナーであるActorの回転角(GetRotation)と座標(GetPosition)をデルタタイム(deltaTime)を使って更新します。
InputComponent
MoveComponentを継承して、プレイヤーによる操作を提供します。
仮想関数ProcessInputをオーバーライドし、キー入力を検知します。押されたキーに応じて最大回転速度(mMaxAngularSpeed)と最大前進速度(mMaxForwardSpeed)をMoveComponentの回転速度、前進速度に設定(SetAngularSpeed,SetForwardSpeed)します。
実際の座標更新は、基底クラスであるMoveComponentのUpdateが行います。
Actor
ゲームオブジェクトの基底クラスです。
- メンバ変数として、状態(
Active,Paused,Dead)と座標、スケール、回転角を持ちます - 自身にアタッチされたコンポーネントを保持する
std::Vectorを持ちます - それぞれ変数のGetSet関数を持ち、状態の初期値は
Activeにします - 状態が
Activeの時、Update関数(Actorが持つコンポーネントのUpdateとUpdateActorを呼ぶ)やProcessInput(Actorが持つコンポーネントのProcessInputとActorUpdateを呼ぶ)をGame`でループさせます -
UpdateActorやActorInputは仮想関数になっており、派生クラス(Shipなど)が固有の処理を実装できます
Asteroid
小惑星のアクターです。
- コンストラクタで
Randomを使い、出現座標と移動方向(回転角)をランダムに設定します -
CircleComponent、MoveComponent、SpriteComponentを追加します -
Gameで自身を管理するのでAddとRemoveを忘れずに行います
Ship
プレイヤーが操作する船のアクターです。
-
SpriteComponentとInputComponentを追加します -
ActorInputをオーバーライドし、スペースキーが押されたらLaserを生成します -
UpdateActorをオーバーライドし、レーザーのクールダウンを更新します
Laser
Shipから発射される弾のアクターです。
-
CircleComponent、MoveComponent、SpriteComponentを追加します -
UpdateActorをオーバーライドし、Laserの状態を管理します - 生存時間(
mDeathTimer)が0以下になった時、もしくはAsteroidと交差した時も、その状態をDeadにします
Game
ゲーム全体の流れを管理するクラスです。
- 初期化(
Initialize)と終了(Shutdown)、ゲームループ(RunLoop)を行います - 全ての
ActorとSpriteComponentの保持と管理のための配列と関数を持ちます
ゲームループでは、主に以下の動作を実行します
1.. ProcessInput(入力処理):
- 各
ActorのProcessInputを呼び出します - この処理と更新を行っている最中に生成された
Actorは一時的に別の配列(mPendingActors)におきます
2. UpdateGame(更新処理)
- デルタタイムの実装と管理をします
- 各
ActorのUpdateを呼び出し、その後に保留されたActorを配列に追加します -
Dead状態になったActorを配列から削除します
3. GenerateOutput(描画処理)
- 各
SpriteComponentのDrawを呼び出します
また、初期化\終了の際、LoadData`UnloadData`(オブジェクト管理)を呼びます
-
LoadDataでは各オブジェクトを必要数生成します -
UnloadDataではオブジェクトとテクスチャを解放します
終わりに
初見ではあまり全体像を掴めなかったのですが、この記事を通して自分が持つ解像度も高まったように思います。『Pong』と比べると様々な要素が一気に出てきて、躓いてしまう方もいらっしゃると思いますが、この記事が何かの一助になると幸いです。