オブジェクト指向はゲームづくりに向かない のその2です。
その1では、クラスの継承構造が、後から変更しにくいという問題点を、例を用いて紹介しました。
今回は「データの依存関係が見えにくい」です。
前回同様、見下ろし視点のアクションゲームで考えてみます。
敵の攻撃がプレイヤーにヒットする部分の実装は、オブジェクト指向で設計すると、例えばこんな感じになると思います。
class Enemy
{
//敵の更新処理
public void Update()
{
//攻撃状態なら
if (IsAttackState())
{
var player = GetPlayer();
//攻撃があたっていたら
if (IsAttackHit(player))
{
//ダメージ計算
var damage = CalcDamage(this, player);
//ダメージをプレイヤーに与える
player.ApplyDamage(damage);
}
}
}
つづいて、プレイヤーが毒によるダメージを受ける部分です。
class Player
{
//プレイヤーの更新処理
public void Update()
{
//毒状態だったら
if (IsPoisonState())
{
var damage = CalcPoisonDamage();
this.ApplyDamage(damage);
}
}
}
では次に、ダメージ床がプレイヤーに攻撃する処理です。
class DamageFloor
{
//ダメージ床の更新処理
public void Update()
{
var player = GetPlayer();
if (IsAttackHit(player))
{
//ダメージ計算
var damage = CalcDamage(this, player);
//ダメージをプレイヤーに与える
player.ApplyDamage(damage);
}
}
}
では次に、回復アイテムを取得したらHPが回復する処理です。
class RecoverItem
{
//回復アイテムの更新処理
public void Update()
{
var player = GetPlayer();
//プレイヤーが触ったら
if (IsTouch(player))
{
//プレイヤーを回復
player.Recover(GetRecoverHp());
}
}
}
ダメージを受ける処理能力 Player.ApplyDamage()と、回復処理 Player.Recover()を見てみましょう。
class Player
{
public void ApplyDamage(int damage)
{
this.hp -= damage;
if (this.hp <= 0) {
Dead();
}
}
public void Recover(int recover)
{
this.hp += recover;
}
}
オブジェクト指向の一般的な考え方で実装すると、大体上記のようなコードになると思います。
さて、あるフレーム(ゲームループ1回分の処理)で、プレイヤーのHPが変化するシチュエーションは、何パターンあるでしょうか?
- Enemy.Update() から Player.ApplyDamage()が呼ばれる
- Player.Update() から Player.ApplyDamage()が呼ばれる
- DamageFloor.Update() から Player.ApplyDamage()が呼ばれる
- RecoverItem.Update() から Player.Recover()が呼ばれる
これらが1フレーム内で1つだけ起こる、もしくは同時に2つ、3つ起こる事もあります。
また、呼ばれる順番も、その時々で様々です。
(オブジェクトの管理方法によりますが、生成された順番に依存することが多い)
そう考えると、プレイヤーのHPが何に依存して変化するかは、プログラマーが把握しきれない程、多くのパターンが存在することになります。
しかもその複雑さは、開発が進んで機能が追加されればされる程、増していきます。
そして、どこかのタイミングで、「○○と✕✕が同時に起こる」などのレアなシチュエーションでしか起こらないバグが発生します。
例えば、上記のコードでは、HP0になる攻撃を受けた後、同じフレームで回復されると、HPが1以上にも関わらず、Dead()が呼ばれてプレイヤーが死んでいる状態になりそうですね。
HPなどは簡単な方ですが、状態異常やスキルなどは複雑化する傾向にあり、プログラマーは常にいろいろな可能性を考慮しながらコードを書かなければなりません。
このようなバグが発生した場合、調査は困難ですし、治すと言っても根本対処しようとするとシステムの土台から修正することになり、エンバグのリスクが高くなります。
仕方なく、無理やり不具合を塞ぐ様な実装をすることになり、ツギハギだらけのプログラムになっていってしまいます。
一般に、オブジェクト指向言語では、ある機能は外から見れば公開(public)か非公開(private)しかありません。
そして公開されている機能は、どこからでも呼べてしまいます。よって依存関係も際限なく増えてしまうんですね。
このような複雑さを回避する方法はないでしょうか?
それについては、次回、解説してみたいと思います。