GameDev #2 ゲームオブジェクトと2Dグラフィクス [Devlog #015]
Table of Contents
ゲーム開発者の教科書:Game Programming in C++ を読んで理解したことについてを要約します(内容の転載を避け、詳しく説明しすぎないように配慮します)
ゲームプログラミング in C++
ゲームオブジェクトと2Dグラフィクス
ゲームオブジェクト
オブジェクト階層構造を利用するゲームもあれば、コンポジションを使うゲームもあり、もっと複雑な手法を用いるゲームもある
ゲームオブジェクトの種類
ゲームオブジェクトには、毎フレーム更新されて出力生成で描画されるもの、描画するが更新しない静的オブジェクト、他のオブジェクトが触れた際に何かを発生させるトリガーなどがある
ゲームオブジェクトのモデル
●クラス階層構造としてのゲームオブジェクト
オブジェクト志向の標準的なクラス階層構造でゲームオブジェクトを宣言するゲームオブジェクトモデル(is-a関係)
すべてのゲームオブジェクトが1つの基底クラスから派生されるので、モノリシックなクラス階層構造とも呼ばれる
どのゲームオブジェクトも基底オブジェクトのプロパティと関数をすべて持たなければならないという欠点がある
階層構造を拡張して、派生クラス群と基底クラスとの中間に新しいクラスを置くこともできるが、クラス階層構造が複雑化してしまう
クラス階層構造を大きくすると、後に2つの子クラスに共通機能をもたせようとした際に、2つの別の経路で継承することになり(菱形継承)、派生クラスが仮想関数の複数のバージョンを継承する問題を起こしやすい
●コンポーネントによるゲームオブジェクト
コンポーネント(構成要素)をベースとするゲームオブジェクトモデル
Unityが採用しているモデルである
1つのゲームオブジェクトクラスがあるが派生クラスを持たず、代わりに必要な機能を実装するコンポーネントオブジェクトを持つ(has-a関係)
このモデルを実装する方法の1つは、コンポーネントにクラス階層構造を使う手法
GameObjectに含まれるのは、コンポーネントを追加(Add)・削除(Remove)する関数だけである
必要とする機能だけを追加しやすいという長所がある
同じゲームオブジェクトにあるコンポーネント間の依存関係が明らかではないという欠点がある
●階層構造とコンポーネントによるゲームオブジェクト
モノリシックな階層構造とコンポーネントオブジェクトモデルのハイブリッド
UE4のゲームオブジェクトモデルから部分的にヒントを得ている
Actor.h
Actor.cpp
いくつかの仮想関数を持つ基底クラスActor
があり、各基底クラスActor
はコンポーネントを格納する動的配列(std::vector
)を持つ
列挙型のState
でクラスActor
の状態管理を行う(Update()
はEActive
状態のクラスだけを更新し、EDead
状態のクラスは削除する)
UpdateComponents()
関数は、すべてのコンポーネントをループして順番に更新する
UpdateActor()
関数は、派生クラスで独自の振る舞いを実装する
基底クラスActor
は、シングルトンで実装するアプローチ(そのクラスの複数のインスタンスに対応できない)ではなく、依存性の注入(dependency injection)で実装する(アクターがコンストラクタでGame
クラスへのポインタを受け取り、別のアクターを追加作成する)
基底クラスActor
では、アクターを追加で作成する際にGame
クラスへのアクセスが必要になる
Component.h
Component.cpp
メンバ変数mUpdateOrder
はコンポーネントの更新順を前後させる(例えば、カメラコンポーネントは移動コンポーネントがプレイヤーを動かした後に更新する)
処理順を管理するため、Actor
のAddComponent()
関数では新しいコンポーネントが追加される際にコンポーネントの配列をソートする
Component
クラスは、その所有者のアクターへのポイントを持つことで、コンポーネントからアクターの情報にアクセスできる
このハイブリッドアプローチは、モノリシックなオブジェクトモデルの"深いクラス階層構造"を避けるという点で有効である
座標データなどの重要なプロパティを個々のアクターにもたせているため、コンポーネント間のオーバーヘッドに関する問題を回避することができる
●その他のアプローチ
-
考えられるさまざまな機能をインターフェースクラスとして宣言し、個々のゲームオブジェクトで必要なインターフェースを実装する
-
コンテナ的なゲームオブジェクトを完全に排除し、代わりにIDでオブジェクトを管理するコンポーネントのデータベースを利用する
-
オブジェクトをプロパティによって定義する
ゲームオブジェクトをゲームループに統合する
Game.h
Actor
クラスへのポインタの、アクティブなアクター群を含むmActors
と待ち状態のアクター群を含むmPendingActors
(std::vector
)をGame
クラスに追加
新しいアクターは巡回処理が終わるまではmPendingActors
に追加しておく
Game.cpp
受け取ったActor
クラスへのポインタを、mUpdatingActors
フラグによってmPendingActors
かmActors
に追加するGame::AddActor()
関数を追加
Actor
クラスへのポインタを受け取って、そのアクターを削除するGame::RemoveActor()
関数を追加する
Game::UpdateGame()
デルタタイムの計算後にmActors
にあるアクターをループ処理して、それぞれのActor::Update()
を呼び出す
mPendingActors
に追加したアクターをmActors
に移動する
死んだアクターをdeadActors
に移動して削除する
Game::UnloadData()
Actor
オブジェクトはコンストラクタとデストラクタで自動的に追加・削除されるため、mActors
をループ処理してアクターを削除するには別のスタイルのループを行う
スプライト
2Dゲームの視覚的オブジェクトのスプライトは、キャラクター、背景、その他の動的なオブジェクトを表現するのに使われる
それぞれのスプライトには、1つ以上の画像ファイルが割り当てられる
iOSではPVR、PC及びXBoxではDXTなどの画像フォーマットを利用する
このコードではPNGを利用する(PNGは圧縮された画像フォーマットであり、ハードウェアはPNGファイルをネイティブで描画できないため時間が掛かるが…)
画像ファイルのロード
Game.cpp
Game::Initialize()
SDL_Init()
関数でSDL Imageを初期化する
Game::GetTexture()
Game::GetTexture()
関数はGame::LoadData()
関数の中での実行であり、SDL_Texture
と、それに対応するファイル名との連想配列を作る
IMG_Load()
関数で画像ファイルをSDL_Surface
構造体にロードする
SDL_CreateTextureFromSurface()
関数でSDL_Surface
構造体をSDL_Texture
に変換する
ゲーム開発では基本的に、あらゆる種類のアセットを一括して処理するアセット管理システムを利用するが、ここでは利用しない
Game::LoadData()
関数の役割は、ゲームワールドのすべてのアクターを作成することである
スプライトの描画
画像のアルゴリズムによって、奥から手前の順序でスプライトを描画する
コンポーネントを利用するゲームオブジェクトモデルを使うので、Component
クラスを継承したSpriteComponent
クラスを作成する
SpriteComponent.h
SpriteComponent.cpp
mDrawOrder
メンバ変数で指定された順序でスプライトコンポーネントを描画する
Game::AddSprite()
関数を通して、Game
クラスのスプライトコンポーネントの配列に追加される
Game.cpp
Game::AddSprite()
mDrawOrder
の順でスプライトコンポーネントがソートされる
Game::GenerateOutput()
関数では、単純にスプライトコンポーネントの配列をループ処理してそれぞれのSpriteComponent::Draw()
関数を呼ぶ
バックバッファをクリアして、バックバッファとフロントバッファを交換する間に処理を行う
画像のアルゴリズムは3Dを考慮するといくつかの難点があるが、2Dだと非常にうまく機能する
SpriteComponent.h
SpriteComponent.cpp
SpriteComponent::SetTexture()
mTexture
メンバ変数を設定し、SDL_QueryTexture()
関数を呼び出してテクスチャの幅と高さを得る
SpriteComponent::Draw()
テクスチャを描画する関数として、シンプルなSDL_RenderCopy()
関数、高度な挙動に対応したSDL_RenderCopyEx()
関数がある
SDL_Rect
構造体のx,y座標は、描画先の左上隅の座標に対応する
SDLは角度を度数で受け取るが、Actor
クラスはラジアンを使うため、Math::ToDegrees
を用いる
SDLでは正の角度が時計回りだが、数学で通常使われる単位円では反時計回りなので、正負逆転による調整を行う
スプライトアニメーション
AnimSpriteComponent.h
スプライトアニメーションの実現には、各フレームに対応する画像を配列に格納する
mAnimFPS
変数は、異なるフレームレートでスプライトアニメーションを再生させるために利用できる
mCurrFrame
変数は、現在のアニメーションを表示している期間を追跡管理できる
AnimSpriteComponent.cpp
AnimSpriteComponent::SetAnimTextures()
渡された配列をメンバ変数mAnimTextures
に設定し、mCurrFrame
を0にリセットする
SpriteComponent
クラスから継承したSetTexture()
関数を呼び出して、アニメーションの最初のフレームを渡す
AnimSpriteComponent::Update()
アニメーションのFPSとデルタタイムの関数としてmCurrFrame
を更新する
mCurrFrame
をint
にキャストして、mAnimTextures
から適切なテクスチャを選択してSetTexture
を呼び出す
背景のスクロール
BGSpriteComponent.h
背景用にSpriteComponent
クラスを派生する
BGTexture
構造体にそれぞれの背景テクスチャに対応するオフセットを割り当てる(このオフセットを毎フレーム更新することで、スクロール効果を生み出す)
BGSpriteComponent.cpp
BGSpriteComponent::SetBGTextures()
オフセット値はSetBGTextures()
で初期化される
追加される背景テクスチャの内部では、それぞれのテクスチャは直前の背景テクスチャの右側に配置される
BGSpriteComponent::Update()
それぞれの画像が完全に画面外に出た場合を考慮しながら、背景テクスチャのオフセットを更新する
BGSpriteComponent::Draw()
所有アクターの位置と背景のオフセット情報によって位置を調節しながら、SDL_RenderCopy()
関数を用いて背景テクスチャを描画する
背景に複数のレイヤーを使い、各レイヤーを別々の速度で動かすことで奥行きを感じさせる視差スクロールという実装がある
これを実装するには、1つのアクターに複数のBGSpriteComponents
をもたせ、それぞれに異なる描画順序を指定し、個々の背景に異なるスクロール速度を割り当てる
ゲームプロジェクト
Ship.h
Actor
クラスの派生クラスとして宇宙船を表す
左右と上下の速度を制御する2つの速度の変数を持つ
Ship.cpp
コンストラクタでは、mRightSpeed
とmDownSpeed
を0で初期化して、宇宙船に使うためのAnimSpriteComponent
を作成してテクスチャを設定する
Ship::ProcessKeyboard()
キーボード入力で船の速度を変更する
Ship::UpdateActor()
ゲームの更新時に宇宙船を動かす
動きに関しては別のコンポーネントとして実装するべきであるが、今回はShip::UpdateActor()
関数の中で実装する
参考文献
ゲームエンジンアーキテクチャ 第3版
↑ Naughty Dogは"クラッシュ・バンディクー"や"アンチャーテッド"で有名