『ゲームプログラミングパターンズ』を読んだので感想
先日CEDECに行った時に10%引きセールをやってたので買った。効率のいいクラス設計や応用の効くクラス設計を調べるのは今後役に立ちそうなのと、純粋にそういうのを調べるのが好きなのでとても面白かった。サンプルコードはC++で書かれているが、デザインパターンが主題なので言語はあまり関係ない。有名なGoFのデザインパターンについても触れられているが、特にゲームプログラミングの分野においてはどのように応用され得るのかについてじっくり詳しく書かれている。また、GoFのパターン以外でもゲームの分野で使う機会が多いパターンについても詳細に書かれており、以下では目から鱗だったパターンについて備忘録もかねて記しておこうと思う。
〜目次〜 1. コマンドパターン 2. ステートパターン 3. 型オブジェクト 4. イベントキュー 5. サービスロケーター
1. コマンドパターン
GoFのパターンのひとつ。ゲームプログラミングではコントローラー入力の実装で使うことができる。
/*コマンドインターフェイス*/ class Command{ public: virtual void execute(GameActor& actor) = 0 }
/*ジャンプコマンド実装*/ class JumpCommand : public Command{ public: virtual void execute(GameActor& actor){ actor.jump(); } }
/*キーインプットクラス的なところでハンドラー関数を定義する*/ Command* InputHandler::handleInput(){ if(isPressed(BUTTON_X) return buttonX_; if(isPressed(BUTTON_Y) return buttonY_; return NULL; }
/*コマンドを受け付けて実行する*/ Command* command = inputHandler.handleInput(); if(command) { command->execute(actor); }
このパターンを使えば、キー設定の変更も簡単に実装できる。また、AIの行動もキーインプットと切り離してCommandの操作だけで実装できる。
2. ステートパターン
GoFのデザインパターンのひとつ。このパターンは明らかにゲームに向いてるパターンだと思し、以前個人でゲームを作っていた時に触れたことがあるので、詳しい設計は省略する。しかし、今回新しくこのパターンの応用で気付いたのはスタックを用いた状態管理である。例えば、しゃがんでいる状態から銃を撃って、打ち終わったら、元のしゃがんでいる状態に戻って欲しい、また、立っている状態から銃を撃っても元の立っている状態に戻って欲しい、などあるアクションをした後にその前の状態に戻れるようにするには、スタック型の状態管理システムが便利だという気づきがあった。また、あるステートに入った時に一度だけ実行して欲しい処理を今までステートクラスのコンストラクターで記述していたが、enter関数を作ってそこで初めの処理を行う、という設計パターンもあるようだ。これについてはコンストラクター内に各記述とは何が違うのかはわからない。
3. サブクラスサンドボックスパターン
ある基底クラスSuperPowerから継承して様々な能力を実装したい場合、派生クラスとベースコードの結合を少なくするパターン。これによって、ベースコードの変更による影響が基底クラスに集約されるためメンテナンスが楽になる。GoFのテンプレートメソッドパターンと似ているが、ユーティリティメソッドを実装するクラスが基底クラス(サブクラスサンドボックス)なのか派生クラス(テンプレートメソッド)なのかの違いがある。
class Superpower{ public: protected: virtual void activate() = 0; /* 以下は派生クラスで使うユーティリティメソッド */ void move(double x,double y,double z){ //実際のコードが入る。 } void playSound(SoundId sound){ //実際のコードが入る。 } void spawnParticles(ParticleType type, int count){ //実際のコードが入る。 } }
class SkyLaunch : public Superpower{ protected: virtual void activate(){ move(0,0,20); playSound(Sound_SPROING); spawnParticles(PARTICLE_DUST,10); } }
4. 型オブジェクトパターン
モンスターの種類をたくさん増やそうとする時に、基底クラスを作って、それからモンスターの種類ごとに派生クラスを作ると、似たようなコードの派生クラスがたくさん現れることになる(体力や攻撃力などのメンバ変数を変えただけなど)。それを防ぐためにモンスターの属性を表すクラスBleedに情報を格納し、それによってモンスターの挙動を変えるようにする。これによって、デザイナーやプランナーも変数の調整に参加しやすくなる、などのメリットがある。
5. イベントキューパターン
ユーザーによるキーやタッチなどによる入力とそれに応じた出力コールバックを時間的に分離するパターン。リングバッファ(ダブルバッファの多要素版みたいなもの)で有限要素の配列のキューを効率的に使う。
//音声情報をキューに登録 void Audio::playSound(SoundId id,int volume){ pending_[tail_].id = id; pending_[tail_].volume = volume; tail_=(tail_+1)%MAX_PENDING; //リングキューの先頭を更新 }
//キューのheadから音声を読み取って、成功したらstartSoundする void Audio::update(){ if(head_== tail_) return; ResourceId resource = loadSound(pending_[head_].id); int channel = findOpenChannel(); if(channel == -1) return; startSound(resource, channel, pending_[head_].volume); head_ = (head_+1)%MAX_PENDING; }
自分がよくやっているシャドウバースでも、ユーザーがカードを出す手順をキューに蓄えておき、適切なタイミングで実際に場にカードが出るようにキューから取り出していると思うので、このパターンを使っていると思った。オブザーバーパターンの非同期版とも言えるパターンである。
6. サービスロケータパターン
GoFのシングルトンパターンに似ている。
//サービスロケータークラス。これを使ってユーザーは音声サービスを呼び出す。 class Locator{ public: static Audio* getAudio(){return service_;} static void provide(Audio* service){ service_ = service; } private: static Audio* service_; }
Audio *audio = Locator::getAudio(); audio->playSound(VERY_LOUD_BANG);
このパターンではAudioクラスを仮にシングルトンにしていなくても後から、サービスロケーターを適用できる。 このパターンではあらかじめLocatorにAudio(プロバイダ)を登録する必要がある。
ConsoleAudio *audio = new ConsoleAudio();
Locator::provide(audio);
GoFのデコレーターパターンと組み合わせてログ出力できるようにすることも可能。分からなかったのは、最後の補足に, 「Unityは、サービスロケーターをコンポーネントパターンを組み合わせて、GetComponent()の中で使っている。」の部分。