ホーム < ゲームつくろー! < デザインパターン習得編
実践
その1 RPGのヒットポイントもオブジェクトだ!
〜 Strategyパターン
デザインパターン実践編は、色々なパターンを使って諸問題を解決していきます。この章ではRPGだけでなくほぼすべてのゲームに存在するヒットポイントを考えてみます。
@ ヒットポイントは数字だけ持てば良いと思っていた…
ドラクエなどで同じみのヒットポイント。キャラクタの生命力であり、0になるとキャラクタは死んでしまいます。
とあるプロジェクトで、私はヒットポイントを持つキャラクタを作る事になりました。敵の攻撃を受けるとヒットポイントはその分だけ減ります。そしてヒットポイントが0になるとキャラクタは死亡フラグが立ちます。
「んじゃ、ヒットポイントを整数で持つことにするべぇ。ダメージを与えられたらヒットポイントを減らすのね。」
と次のようなクラスを作りました:
ヒットポイントを持つキャラクタクラス class Character {
int hitPoint;
public:
// ヒットポイント設定
void setHitPoint(int hitPoint);
// ヒットポイントを取得
void getHitPoint() const;
// ダメージ
void damage(int power);
};
インターフェイスの意味はコメントの通りです。このクラスを継承して個々のキャラクタクラスが作られていきます。
さて、とあるキャラクタについて企画の方からこう言われました。「このキャラクタは難易度に対して初期のヒットポイントを変えて欲しいなぁ」。な、なるほど。では、そのキャラクタは最初に与えられたヒットポイントに初期難易度を加味するようにコードを変更します:
難易度によってヒットポイントが変わるキャラクタオブジェクトを作る HogeCharacter *hoge = new HogeCharacter;
int HP = 0;
switch (difficulty) {
case Difficulty_Easy : HP = 3000; break;
case Difficulty_Normal: HP = 4000; break;
case Difficulty_Hard : HP = 5000; break;
};
hoge->setHitPoint(HP);
難易度difficultyによって場合分けをしてヒットポイントを決定しています。ヒットポイントをハードコーディングしているはさて置いて、これで企画の仕様は満たしました。
ところが、翌日「このキャラクタも難易度別にしてくれないかなぁ」という注文が入りました。「ん〜、はい、大丈夫ですよ」と、次のようにコードを拡張します:
難易度によってヒットポイントが変わるキャラクタオブジェクトを作る // HogeのHP設定
Character *hoge = new HogeCharacter;
int HP = 0;
switch (difficulty) {
case Difficulty_Easy : HP = 3000; break;
case Difficulty_Normal: HP = 4000; break;
case Difficulty_Hard : HP = 5000; break;
};
hoge->setHitPoint(HP);
// FooのHP設定
Character *foo = new FooCharacter;
int HP = 0;
switch (difficulty) {
case Difficulty_Easy : HP = 2000; break;
case Difficulty_Normal: HP = 2500; break;
case Difficulty_Hard : HP = 3000; break;
};
foo->setHitPoint(HP);
一件落着…ではありませんでした。
「このキャラクタは敵キャラに○○がいた時はHPを90%にしてほしい」
「これは今の場面が△△だったらHPを倍に」
「このキャラは前のステージのクリアタイムが長かったらHPを減らして」
「このキャラは特定部位が壊されたら途中でHPを1000減らして」
と、ヒットポイント周りの自由度たるやとんでも無かったのでした。先のような場当たり的な対処だとあっさりと限界を迎えます。setHitPointに値を渡すだけでは駄目だったんです。その数値を決める「方法」が重要だったのでした。
A Strategyパターンを使ってみよう
状況を整理します。キャラクタのヒットポイントを決める要素は非常に様々で、それはゲームの外的要因(及び企画の想像力)によって変動することがいくらでもありえます。もう一つ、ヒットポイントを決める方法は別のキャラクタにも適用されることがいくらでもあります。例えば難易度別でヒットポイントを変えるというのは他の色々なキャラクタについても大体当てはまります。
ヒットポイントの決め方が変動し、その方法は他の人も使うかもしれない。これを吸収する方法としてまず考えるのが「ヒットポイント決定関数を作る」です。これならば方法は使い回しできます。しかし、さらに「ヒットポイント自体をクラス」にすると方法と一緒にヒットポイント自体もこのクラスが管理できます。こちらの方がオブジェクト指向です。
ということで、ヒットポイントを管理するクラスを作ります:
ヒットポイントクラス class IHitPoint {
public:
// 初期化
virtual void initialize(int initHP) = 0;
// ヒットポイントを取得
virtual int getHitPoint() const = 0;
};
IHitPointインターフェイスはinitializeメソッドに与えられたベースとなるinitHPを加工して初期ヒットポイントを内部に格納します。ここでHPを決定するさまざまな方法(ストラテジー:戦略)が動きます。例えば与えられたinitHPをそのまま採用するもっともシンプルなのはこうです:
ノーマルヒットポイントクラス class NormalHitPoint : public IHitPoint {
protected:
init hitPoint;
public:
// 初期化
virtual void initialize(int initHP) {
hitPoint = initHP;
}
// ヒットポイントを取得
virtual int getHitPoint() const {
return hitPoint;
}
};
現在の難易度に合わせてヒットポイントを増減させるクラスの場合は難易度を設定してあげる必要があります。これはそういうインターフェイスを設けても良いですし、コンストラクタで難易度を渡しても良いでしょう:
難易度別ヒットポイントクラス class DifficultyHitPoint : public NormalHitPoint {
Difficulrt difficulty
public:
DifficultyHitPoint(Difficulrt difficulty) : difficulty(difficulty) {}
// 初期化
virtual void initialize(int initHP) {
switch (difficulty) {
case Difficuty_Easy: hitPoint = (int)(initHP * 0.9f); break;
case Difficuty_Normal: hitPoint = initHP; break;
case Difficuty_Hard: hitPoint = (int)(initHP * 1.1f); break;
}
}
};
Characterクラスはヒットポイントを受け取る代わりにヒットポイントオブジェクトへのポインタ(スマートポインタ)を受け取ります。
ヒットポイントを持つキャラクタクラス class Character {
sp<IHitPoint> hitPoint;
public:
// ヒットポイント設定
void setHitPoint(sp<IHitPoint> hitPoint);
// ヒットポイントを取得
void getHitPoint() const;
// ダメージ
void damage(int power);
};
後は内部では抽象化されたIHitPointのインターフェイスのみで会話します。
このように、特定の仕事(ここではヒットポイントの決定や管理)を他の人に任せる事でその仕事の違いを吸収するのは「ストラテジーパターン(Stratgy Pattern)」です。たかがヒットポイントでさえこうなのですから、ゲームのパラメータの多くは実はストラテジーパターンにしないと怖いというのが分かると思います。もちろん、限度もありますが(^-^;
B Strategyパターンがあると大抵Factoryも必要になる
さて、Strategyパターンは仕事の一部を外部のオブジェクトが担うのですから、当然そのオブジェクトを「外部が」作る事になります。その際、例えば次のような作成方法が想定されます:
キャラクタを作る人 sp<NormalHitPoint> hp( new NormalHitPoint );
hp->initialize(3000);
chara->setHitPoint( hp );
sp<DifficultyHitPoint> hp_dif( new DifficultyHitPoint(curDifficulty) );
hp_dif->initialize(3500);
chara2->setHitPoint(hp_dif);
ん〜〜ベースとなるヒットポイントを設定するのは仕方ないとしても、ここにはまずい点があります。それは生成すべき超具体的なオブジェクトが直で書かれている点です。こういう具体性はどこかで出てきてしまうのですが、それはぎりぎりまで絞って隠した方が柔軟性が上がります。生成を隠す有効な手段はFactoryクラスです。Factoryクラスを使うと上の実装は次のようになります:
キャラクタを作る人(Factoryクラス導入) HitPointFactory factory(gameState); // ゲームの状態を保持
sp<IHitPoint> hp = factory.create(HitPoint_Normal, 3000);
chara->setHitPoint( hp );
sp<IHitPoint> hp_dif = factory.create(HitPoint_Difficulty, 3500);
chara2->setHitPoint(hp_dif);
HitPointFactoryは現在のゲームの状態(gameState)を知っていて、createメソッドに渡されたフラグ(HitPoint_Difficultyとか)を元にしてヒットポイントオブジェクトを内部で作成します。こうすると、外目にはもう生成部分が見えません。引数を上のような値にしてしまうと、例えばスクリプトなどからこれらの値を与えるだけで好きなヒットポイントを持ったキャラクタが作れてしまいます。ここまで来ると、外部のファイルに書き込んだパラメータからキャラクタを作れるようになります。例えば、
キャラクタ情報ファイル HitPoint,0,3000
HitPoint,1,3500
という情報ファイルがあれば上のキャラクタオブジェクトが作れるのがわかりますよね(ファイルなのでHitPoint_Normal→0、HitPoint_Difficulty→1と数値化されています)。
という事で、Strategyパターンのような沢山の種類のオブジェクトを生成する場合では大抵Factoryクラスが必要になります。
ゲームの画面に出ているヒットポイントや攻撃力、防御力などは、単なる数値ではなくてもはや一つのオブジェクトである。その位ゲームの要素の粒度は小さいんです。そして、そうする事で、企画が発案する柔軟な発想に対応できるようになります。面倒でも考慮した方が幸せです(^-^)