概要
- 社内勉強会で読んでます!
- 「独立したオブジェクトをまとめ、フレーム更新時に一斉に振る舞いを実行する」
- Unityにおけるupdateメソッドの話
前提
- 敵である骸骨(skeleton) が左右に動くコード
while (true)
{
// 右向きにパトロールする
for (double x = 0; x < 100; x++) skeleton.setX(x);
// 左向きにパトロールする
for (double x = 100; x > 0; x--) skeleton.setX(x);
]
- 骸骨の挙動にゲームループの仕組みを組み込んでいる
- このコードでは、骸骨はプログラムとは無関係無い別の無限ループの中を回る
- プレイヤーの入力の処理やゲーム画面の描画などと独立してはいけない
- 外部のゲームループの力を借りる
Entity skelton;
bool patrollingLeft = false;
double x = 0;
// ゲームの主ループ
while (true)
{
if (patrollingLeft)
{
x--;
if (x == 0) patrollingLeft = false;
}
else
{
x++;
if (x == 100) patrollingLeft = true;
}
skelton.setX(x);
// ユーザ入力の処理 / ゲーム画面の描画 ...
更新メソッド: update()
- 上記のコードを見る限りメンテナンスしにくそうな方向に進んでいることが分かる
- 各々のゲーム要素をカプセル化するアプローチでこの問題を解決
- 具体的には update() という抽象メソッドを用意する
- ゲームループは毎フレームに1回すべてのオブジェクトのupdate()を呼び出す
- すべてのオブジェクトが足並みをそろえて実行される
サンプルコード
class World
{
public:
World()
: Entities_(0) {}
void gameLoop()
{
while(true)
{
// ユーザ入力の処理
// 各要素の更新
for (int i = 0; i < numEntities_; i++)
{
entities_[i]->update();
}
// 物理シミュレーション/画面の描画... etc
}
}
private:
Entity* enitties_[MAX_ENTITIES];
int numEntities;
};
class Skeleton : public Entity
{
public:
Skeleton()
: patrollingLeft_(false) {}
virtual void update()
{
if (patrollingLeft_)
{
SetX(x() - 1);
if (x() == 0) patrollingLeft_ = false;
}
else
{
SetX(x() + 1);
if (x() == 100) patrollingLeft_ = true;
}
}
private:
bool patrollingLeft_;
}
更新メソッドが役に立つタイミング
- 同時に動くたくさんのオブジェクトやシステムがある
- 各々のオブジェクトのビヘイビアは他のオブジェクトがからほぼ独立している
- オブジェクトの動きはゲームの間中、続いている
更新メソッドが役に立たないタイミング
- チェスのようなボードゲーム
- 全ての駒を同時に動作させる必要は無い
- ただしアニメーション等の振る舞いを実装する際には役に立つ
使用上の注意
コードをフレーム単位で書くと複雑になる
- フレーム単位での処理の返却を意識する必要があるため多少コードが複雑になる
- (あくまで前者に比べてなのでそこまで意識する必要はない)
再開のために前のフレームでの最終状態を保持する
- 今どっちの向きに進行しているか、現在の座標 ... etc
- 状態のパターンを利用する
- ゲームプログラミングにオートマトン/ステートマシンが多いのは、中断した状態から再開する際に何らかのステートを保持しておく必要があるため
オブジェクトはフレーム単位で動くが、真の並列実行ではない
- 特定のオブジェクトの update() は他のオブジェクトに影響を及ぼす場合がある
- その場合、オブジェクトの更新順序が重要な役割を担う
- 更新メソッドを利用する多くの場合には並列実行は必要でなく逐次実行の方が素直
- 同時に処理されたときの状態や調停を考える必要がない
- (Unityの場合、スクリプトの読み込み順で処理されるが明示的に変更することも可能)
更新中のリストの変更に注意
- 振る舞いを持ったオブジェクトのリストが存在する
- リスト内のオブジェクトの振る舞いによって同リストの追加/削除が行われる場合は注意
- 追加した同一フレームでの処理の実行
- 削除時のインデックスのズレの発生
検討項目
update()を置くべきクラスの選定
- (ゲームエンジンを利用している場合、あまり意識する必要はない)
-
ゲーム要素のクラス内
- すでにゲームの要素を個々にクラス化しているのであればこれが一番簡単
- クラス化していない場合、要素が少ない場合は採択できるが、要素が多い場合は苦痛
-
コンポーネントのクラス内
- すでにコンポーネントのパターンを選択しているのであれば、無条件でこれを選択
- コンポーネントに関しては第14章で紹介
- 画面描画/物理シミュレーション/AI/サウンド ... etc
- 個々のコンポーネントが相互依存せず独立して存在出来る
-
デリゲート(委任)クラス内
- クラスのビヘイビアを別のオブジェクトに委任するパターン
void Entity::update()
{
// 状態を表すオブジェクトに処理を渭城する
state_->update();
}
休眠中のオブジェクトの処理
- 更新不要のオブジェクトに対しても更新を行うのはCPUの無駄遣い
- オブジェクトが有効化どうかチェックする方式はデータキャッシュも無駄に消費する
- 第17章にて詳しく解説
- 生きているオブジェクトのみを集合として管理する方法
- オブジェクトが無効になったらその集合から削除する
- 再び有効になったら処理に再度追加する
- ただしメモリを余分に消費する
- 「全ての要素を保持した集合」「有効な要素の重合」の二重管理
- この場合、要素の同期が取れているかもポイント
- 「有効な要素の集合」「無効な要素の集合」という管理なら要素は重複しない
- どちらを採択するかは、無効なオブジェクトの要素数による
- 無効なオブジェクトが多いほど、別の集合を用意してゲームループでの処理を避けると良い
ゲームエンジンでの使用例
- 更新メソッドは、ゲームループ/コンポーネントとともに「三種の神器」としてゲームエンジンの核をなす
- UnityではMonoBehaviorを含むいくつかのクラスでこのパターンを使用
- Microsoft XNAではGame/GameComponentの両クラスでこのパターンを使用
- Quintusでは、Spriteクラスでこのパターンを使用