ケーススタディ 02: 武器切り替え¶
素朴な実装¶
// C++20
class Player {
public:
void Attack() {
if (weapon_ == WeaponType::Sword) { /* 近接判定・剣アニメ */ }
else if (weapon_ == WeaponType::Bow) { /* 矢の生成・弓アニメ */ }
}
void SwitchWeapon(WeaponType t) { weapon_ = t; }
private:
WeaponType weapon_ = WeaponType::Sword;
};
武器2種で固定ならこれが正解です。
機能追加で破綻する過程¶
- 武器が5種に → Attackのifが伸びる。さらに
GetRange(),GetAttackSpeed(),PlayEquipAnim()にも同じ分岐が複製される(switch散らばり → ポリモーフィズムの出番のサイン) - 武器ごとの状態(弓の矢数、銃のリロード)を
Playerが全部抱え始める(arrowCount_,ammo_,reloadTimer_... 使われない変数だらけの神クラス化) - 「魔法剣(近接+弾も出る)」で分岐が入れ子に
- 企画「武器を運用で追加したい」——コード修正+ビルドが毎回必要
案A: 継承(インターフェース)案¶
class IWeapon {
public:
virtual ~IWeapon() = default;
virtual void Attack(Player& owner) = 0;
virtual void Update(float dt) {} // リロード進行など
virtual float Range() const = 0;
};
class Bow : public IWeapon {
public:
void Attack(Player& owner) override { if (arrows_ > 0) { /* 矢生成 */ --arrows_; } }
float Range() const override { return 20.f; }
private:
int arrows_ = 30; // 武器固有の状態が武器に閉じる
};
class Player {
public:
void Attack() { weapon_->Attack(*this); }
void SwitchTo(std::unique_ptr<IWeapon> w) { weapon_ = std::move(w); }
private:
std::unique_ptr<IWeapon> weapon_; // 所有。切り替え=差し替え
};
- 実行時に差し替える振る舞い+固有状態——これはStrategy(切り替えの主体が外部=プレイヤー入力)そのもの
- 利点: 武器追加=クラス1つ。分岐消滅。武器固有状態の隔離。単体テスト可
- 欠点: 切り替え時に状態をどうするか(矢数は保持したい→インスタンスを作り直すと消える→所持武器リストとして保持する設計が要る)。クラス数増
案B: コンポジション分解案(武器がさらに複雑なら)¶
武器自体を部品に分ける: IWeapon の中身を IAttackBehavior(攻撃方式)+AmmoComponent(弾管理)+WeaponStats(データ)で合成(継承・委譲・コンポジション)。「魔法剣」=近接+発射の2つのAttackBehaviorを持つ、で組み合わせ爆発を回避。
- 利点: 特徴の組み合わせ自由。データ駆動化への道
- 欠点: 部品間の調整コード。武器5種程度では過剰
案C: データ駆動案(Type Object)¶
struct WeaponData { // 武器の「種類」はデータ(JSON/ScriptableObject/DataAssetから)
float damage, range, attackInterval;
int maxAmmo; // 0なら弾なし
std::string projectileId; // 空なら近接
// 攻撃の「型」はenum+少数の分岐 or 振る舞いIDでコード側の関数を引く
};
- 利点: 企画がコードなしで武器を追加・調整。ホットリロード可
- 欠点: 「データで表現できる範囲」に振る舞いが制限される(特殊武器はやはりコードが要る → 振る舞いID+データのハイブリッドへ)
案D: パターンを使わない簡潔案¶
分岐は残すが、switchを1か所に集約(WeaponLogic.cppに全武器関数を並べ、関数テーブルで引く)。武器3〜4種で増える予定が薄いならこれで十分読みやすい。
規模別の判断¶
| 規模 | 推奨 |
|---|---|
| 小(武器2〜3種固定) | 素朴な実装 or 案D |
| 中(5種以上 or 固有状態あり) | 案A(Strategy)。切り替えは所持リスト+ポインタ差し替え |
| 大(数十種・運用追加・企画調整) | 案C(データ)+案A/B(振る舞い)のハイブリッド——実務の主流 |
| Unity | 案A+ScriptableObject(=案Cを兼ねる)が定石 |
| UE | DataAsset(案C)+WeaponコンポーネントorGAS(Gameplay Ability System) |
この題材の教訓¶
- 破綻のシグナルは「同じ分岐の複製」と「使われない状態変数の同居」。1つ目はOCP、2つ目はSRPの違反として現れる
- 「クラス階層 vs データ駆動」は二者択一ではなく、数値・参照はデータ、振る舞いはコードのハイブリッドに収束する(Type Object)