Type Object・Subclass Sandbox・Bytecode¶
「振る舞いや種類をコードからデータへ移す」系譜の3パターンです。データ化の度合いが軽い順に並べています。共通の動機: 種類の追加・調整をプログラマとビルドから切り離し、企画のイテレーションを速くする。
Type Object¶
一言で言うと¶
「種類」をクラス(継承)ではなくデータオブジェクトとして表現する。class Dragon : Enemy の代わりに、EnemyType{"dragon", hp:500, ...} というデータを個体が参照する。
問題のある実装¶
// C++20(問題例)— 種類=クラス
class Enemy { /* 共通 */ };
class Slime : public Enemy { /* maxHp=10, 弱点=火 */ };
class Dragon : public Enemy { /* maxHp=500, 弱点=氷 */ };
// 種類追加=クラス追加=プログラマ作業+再ビルド。データテーブルで100種、は不可能
適用後¶
// C++20
#include <string>
#include <vector>
struct EnemyType { // 「種類」はただのデータ(JSONやCSVからロードできる)
std::string name;
int maxHp = 10;
std::string weakness;
// ドロップテーブル、攻撃力曲線、AI種別のID…
};
class Enemy { // 個体はタイプへの参照+個体状態
public:
explicit Enemy(const EnemyType& type) : type_(&type), hp_(type.maxHp) {}
const EnemyType& Type() const { return *type_; }
private:
const EnemyType* type_; // 非所有: 全EnemyTypeはテーブルが所有し個体より長生き
int hp_;
};
// 種類追加 = データ1行。100種でも1000種でもクラスは2つのまま
検証済みサンプル: samples/type_object.cpp
トレードオフ¶
- 得: 種類追加がデータ作業に(ノンプログラマが作業可能・ホットリロード可能)。Flyweightのメモリ利益も同時に
- 失: 種類ごとの特殊な振る舞いが表現しにくい(データは値しか持てない)。「ドラゴンだけ3回行動」はどうする? → 振る舞いIDでコード側の分岐を引く/次のSubclass Sandboxとの併用/Bytecodeへ
- 使わない: 種類が数個で振る舞いの差が大きい(素直にクラスで)
Unity: ScriptableObjectがType Objectの公式実装。UE: DataTable / DataAsset(→ 第10部)。
Subclass Sandbox¶
一言で言うと¶
基底クラスが安全で便利な操作セット(砂場)を protected で提供し、派生クラスはその操作だけを組み合わせて振る舞いを定義する。派生が外部システムに直接触れない形にする。
問題のある実装¶
// 問題例: スキルの派生クラスがそれぞれ勝手にサウンド・パーティクル・カメラへ直接依存
class FireballSkill : public Skill {
void Activate() override {
AudioEngine::Play("fire"); // 派生100個がそれぞれ外部システムに結合
ParticleSystem::Spawn("fireball"); // 外部の変更で全派生が壊れる
}
};
適用後¶
// C++20
class Skill {
public:
virtual ~Skill() = default;
void Activate() { OnActivate(); } // Template Method的な入口
protected:
virtual void OnActivate() = 0; // 派生はこれを実装する
// ---- 砂場: 派生が使ってよい道具はここにある物だけ ----
void PlaySound(const std::string& id) { /* 内部でAudioへ */ }
void SpawnParticle(const std::string& id) { /* 内部でVFXへ */ }
void ShakeCamera(float strength) { /* 内部でCameraへ */ }
};
class FireballSkill : public Skill {
protected:
void OnActivate() override {
PlaySound("fire"); // 外部システムを知らない。砂場の道具だけで書く
SpawnParticle("fireball");
}
};
トレードオフ¶
- 得: 外部システムとの結合が基底1か所に集約(サウンドAPI変更→基底のみ修正)。派生を書くのが簡単・安全になり、数十個量産できる
- 失: 基底が肥大化する宿命(道具箱は増える一方)。基底の変更が全派生に波及する継承の弱点はそのまま
- 使わない: 派生が数個しかない/派生ごとに必要な道具がバラバラ(砂場が総合デパート化する)
これはTemplate Method+Facadeの複合形です。Unityでは「MonoBehaviourを継承した自作基底クラス(GameBehaviour等)にプロジェクト共通の道具を生やす」形で現れます。
Bytecode¶
一言で言うと¶
振る舞いそのものをデータ(命令列)として定義し、ゲーム内の小さな仮想マシン(VM)が解釈実行する。データ駆動の最終形態。
問題 → 適用¶
Interpreterの式ツリーは、ノードのポインタ辿り+仮想関数で遅く、メモリも食う。命令を平坦なバイト列にコンパイルし、スタックマシンで実行すれば、高速・コンパクト・シリアライズ自由になります。
// C++20 — 最小のスタックマシン
#include <cstdint>
#include <vector>
enum class Op : std::uint8_t { PushConst, Add, Mul, ReadStat };
int Execute(const std::vector<std::uint8_t>& code, const std::vector<int>& stats) {
std::vector<int> stack;
for (std::size_t pc = 0; pc < code.size(); ++pc) {
switch (static_cast<Op>(code[pc])) {
case Op::PushConst: stack.push_back(code[++pc]); break;
case Op::ReadStat: stack.push_back(stats[code[++pc]]); break;
case Op::Add: { int b = stack.back(); stack.pop_back(); stack.back() += b; break; }
case Op::Mul: { int b = stack.back(); stack.pop_back(); stack.back() *= b; break; }
}
}
return stack.back();
}
// 「attack * 2 + 5」= [ReadStat 0, PushConst 2, Mul, PushConst 5, Add]
検証済みサンプル: samples/bytecode_vm.cpp
トレードオフ¶
- 得: 振る舞いが完全にデータ(配信・MOD・エディタ生成可能)。Interpreterより高速・省メモリ。サンドボックス化(VMができること以上の悪さはできない=安全)
- 失: デバッガが効かない(自作VMの中はブレークポイントを張れない。デバッグ表示・逆アセンブラを自作する羽目に)。命令セットの設計・保守という言語処理系の仕事を抱え込む
- 使わない: 定義したい振る舞いが少ない・単純(Type Objectのデータ+数個の分岐で足りる)。既存スクリプト言語(Lua)で済む——自作VMはLua導入すら重い制約(実行環境・審査・超軽量要件)があるときの手段
実例: ポケモンの技エフェクト(原典の例)、MOD対応ゲームのスクリプト、シェーダ(GPUに送るバイトコード)、そしてUnityのIL2CPPが変換するIL自体もバイトコードです(→ 第9部)。
3パターンの選び分け¶
| 欲しいもの | 選択 |
|---|---|
| 種類ごとの数値・参照の差 | Type Object(まずこれ。大半はこれで足りる) |
| 種類ごとの振る舞いの差(プログラマが書く) | Subclass Sandbox(またはStrategy注入) |
| 種類ごとの振る舞いの差(非プログラマ・実行時定義) | Bytecode / 既存スクリプト言語 |
理解度チェック¶
- Type Objectで表現しにくいものは何ですか。その場合の選択肢は?
- Subclass Sandboxが集約する「結合」はどこからどこへの結合ですか。
- 自作Bytecode VMの最大の実務的欠点は?
演習¶
samples/type_object.cpp に「攻撃パターンID」を追加し、IDからコード側の攻撃関数を引くテーブル(std::unordered_map<int, std::function<...>>)を組み合わせて、「データで種類・コードで振る舞い」のハイブリッドを作ってください。
前: 最適化系パターン | カテゴリ目次 | 次: ステートマシン(FSM/HSM)