ケーススタディ 12: アイテム効果¶
「ポーションはHP回復、エリクサーは全回復、爆弾は範囲ダメージ、鍵は扉を開ける」——使うと何かが起きるものの設計です。
素朴な実装¶
// C++20
void Inventory::UseItem(int itemId, Player& p) {
switch (itemId) {
case 1: p.Heal(50); break; // ポーション
case 2: p.HealFull(); p.CureAll(); break; // エリクサー
case 3: world_.ExplodeAt(p.Pos(), 3.f); break; // 爆弾
// 100種のアイテム = 100 case...
}
Remove(itemId, 1);
}
機能追加で破綻する過程¶
- アイテム100種でswitchが巨大化。効果の説明文・アイコンは別の場所にあり、効果とデータの二重管理(説明は回復60、実装は50のズレ事故)
- 「戦闘中しか使えない」「対象を選ぶ(単体回復)」→ 使用可否・対象選択の情報がswitchの外に散る
- 「装備すると攻撃+10」「持っているだけで金運UP」→ 「使う」以外の効果タイミングが現れ、この関数では表現できない
- 効果の組み合わせ(回復+解毒)のたびに新case
- 企画がアイテムを追加できない(全部プログラマ作業)
案A: 効果のデータ駆動(Type Object — 定石)¶
// アイテム定義はデータ([Type Object](../04_game_patterns/type_object_and_sandbox.md))
struct EffectDesc { std::string type; float value; float radius; }; // {"heal", 50, 0}
struct ItemDef {
int id; std::string name; std::string icon;
std::vector<EffectDesc> effects; // ★効果は小さな効果の合成(回復+解毒)
bool usableInBattle = true;
};
// 効果の実行はtype→関数の登録テーブル(効果の「種類」だけコード)
class EffectRunner {
public:
using Fn = std::function<void(const EffectDesc&, Player&, World&)>;
void Register(std::string_view type, Fn fn);
void Run(const ItemDef& item, Player& p, World& w) {
for (const auto& e : item.effects) fns_.at(e.type)(e, p, w);
}
};
- アイテム追加=データ行の追加(効果の種類が既存なら)。説明文・数値・実装が同じデータから出る(二重管理の解消)
- 効果の合成が「効果リスト」で自然に表現でき、破綻4が消える
- 新しい効果種類だけコード追加(Register 1件)——OCPがデータとコードの2層で成立
案B: Commandオブジェクト案¶
効果をIItemEffect { Execute(ctx) }のクラス階層にする(Command/Strategy)。
- 利点: 複雑な効果(「使うと3ターン後に爆発」)を型で表現できる。Undo(デバッグ)も視野
- 欠点: アイテム1種=クラス1個は量産に不向き。案Aのtype登録テーブルの「中身」がこの形なので、対立ではなく階層の違い(データで表現しきれない効果だけクラス化)
案C: タイミング分離(装備・パッシブへの拡張)¶
破綻3への回答は「効果」と「いつ発火するか」の分離:
ItemDef { effects[], trigger } trigger: OnUse / OnEquip・OnUnequip / Passive(所持中)
OnEquip系 → [ステート異常](04_status_effects.md)の効果リストに変換して装着(同じ仕組みに合流!)
装備効果と状態異常が同じ「効果リスト」基盤に乗る——システムの統合が起きるのがこの題材の面白いところです。
案D: パターンを使わない簡潔案¶
アイテム10種以下: 素朴なswitchのまま、ItemDefデータ(名前・説明・数値)だけ先に分離する(効果ロジックはswitch、数値はデータ参照)。二重管理事故だけ先に潰す形。
継承案について¶
Item → HealingItem → SuperHealingItem の継承は、効果の合成(回復+解毒)で早々に破綻します(組み合わせ爆発)。効果はリストの合成で表現するのが正解で、is-a階層に向きません。
規模別の判断¶
| 規模 | 推奨 |
|---|---|
| 小(〜10種) | 案D(switch+データ分離) |
| 中(〜50種、効果種類10前後) | 案A(データ+効果登録テーブル) |
| 大(数百種・運用追加・装備/パッシブあり) | 案A+案C(トリガー分離)+特殊効果のみ案B |
| Unity | ItemDef=ScriptableObject、効果=SO参照 or typeId。定石そのまま |
| UE | DataTable(ItemDef)+GameplayEffect(GAS)が案A+Cの完成品 |
この題材の教訓¶
- 「アイテムの種類」を数えるのではなく「効果の種類」を数える——100種のアイテムの効果が12種の組み合わせなら、書くべきコードは12個
- 効果にはタイミング(使用/装備/所持)という直交軸があり、これを見落とすと設計のやり直しになる。仕様ヒアリングで最初に聞くべき質問