C++でゲームを作っていると、「宝箱を開ける」「NPCと会話する」「ボス戦を開始する」「マップを移動する」といったイベントが増え、Event1、Event2、Event3……のようなクラスが大量に並ぶことがあります。このとき気になるのが、イベントが100個になったらゲーム側で100個の変数やインスタンスを個別に管理しなければならないのか、という問題です。
結論からいうと、Eventという基底クラスを作って各イベントを継承させる設計は有力な方法の一つです。ただし、継承しただけでは100個の生成処理そのものが消えるわけではありません。本当に改善したいのは「イベントを個別の変数として100個並べること」や「ゲーム本体が各イベントの具体的な型を全部知っていること」です。
さらに、イベントの内容が「会話する」「フラグを変更する」「アイテムを渡す」など似た処理の組み合わせなら、100種類のC++クラスを作ること自体をやめ、JSONなどからイベント内容を読み込むデータ駆動設計も検討できます。イベントの性質によって適した設計が変わるため、段階的に考えてみましょう。
- まず「100個のイベント」と「100個の変数」は別問題
- Event基底クラスを作って継承する方法
- 100個のイベントはコンテナにまとめて管理できる
- ただし100種類を手作業で生成する問題はまだ残る
- EventManagerを作るとゲーム本体を整理しやすい
- イベントへゲーム全体を渡すよりContextを用意する
- 「イベント100個=クラス100個」が本当に必要か考える
- データ駆動ならイベントをJSONなどで定義できる
- 継承とデータ駆動は二者択一ではない
- 継承を使わずstd::functionで処理を登録する方法もある
- 一瞬で終わらないイベントなら状態管理も必要になる
- イベントを最初から100個すべて生成する必要もない
- イベントを別ファイルにする基準も整理しておく
- 規模別に考えるおすすめ構成
- まとめ:Event継承は良い出発点だが「100クラス必要か」まで考える
まず「100個のイベント」と「100個の変数」は別問題
イベントが100個存在するゲームで、実行時に100個のイベントオブジェクトが存在すること自体は必ずしも問題ではありません。問題になりやすいのは、ゲーム本体へEvent1 event1; Event2 event2; Event3 event3;のようなコードを延々と追加し、それぞれを個別に扱う設計です。
例えばイベントが増えるたびにゲームクラスへメンバー変数を追加し、さらにifやswitchでEvent1、Event2……を判定する設計では、イベント追加のたびにゲーム本体まで修正する必要があります。規模が大きくなるほど管理が難しくなります。
そこで、イベントに共通するインターフェースを定義し、「ゲーム側から見れば全部Event」として扱えるようにします。これが基底クラスとポリモーフィズムを利用する基本的な考え方です。
Event基底クラスを作って継承する方法
各イベントで処理内容が大きく異なるなら、Eventを抽象基底クラスとして定義する方法が分かりやすいでしょう。例えば、イベント開始時に呼ばれるexecute()を純粋仮想関数にします。
class Event {
public:
virtual ~Event() = default;
virtual void execute() = 0;
};
class TreasureEvent : public Event {
public:
void execute() override {
// 宝箱を開ける処理
}
};
class BossEvent : public Event {
public:
void execute() override {
// ボス戦を開始する処理
}
};
こうするとゲーム本体は、実際のオブジェクトがTreasureEventなのかBossEventなのかを毎回意識する必要がありません。どちらもEventとして扱い、必要になったときにexecute()を呼べます。
この設計のポイントは「継承すればオブジェクト数が減る」ということではありません。具体的なイベント型への依存を減らし、共通の方法で管理・実行できることがメリットです。
100個のイベントはコンテナにまとめて管理できる
Eventを基底クラスにした場合、イベントを一つずつ別名の変数へ保存する必要はありません。例えばstd::vectorとstd::unique_ptrを組み合わせれば、異なる派生クラスのイベントを一つのコンテナで所有できます。
std::vector<std::unique_ptr<Event>> events;
events.push_back(std::make_unique<TreasureEvent>());
events.push_back(std::make_unique<BossEvent>());
events[0]->execute();
events[1]->execute();
std::unique_ptrを利用すれば所有関係が明確になり、コンテナが破棄されたときにイベントも自動的に破棄されます。基底クラスのデストラクタをvirtualにしておくことも重要です。
イベントIDから直接探したい場合は、連番のstd::vectorだけでなくstd::unordered_mapなども候補になります。例えば「イベントID 120を実行する」という仕組みなら、IDをキーとしてイベントを管理できます。
ただし100種類を手作業で生成する問題はまだ残る
基底クラスとコンテナを導入すると、ゲーム本体に100個の変数を書く問題は解決できます。しかし、make_unique<Event1>()からmake_unique<Event100>()まで100行書くのであれば、「生成処理が巨大になる」という別の問題は残ります。
ここで役立つのが、イベントを生成する責任をゲーム本体から分離する考え方です。例えばEventManagerやEventFactoryを作り、ゲーム側は「ID○番のイベントを実行してほしい」とだけ依頼する構造にできます。
つまり、ゲームのメイン処理が100種類のイベントについて知る必要はありません。イベントを生成する場所・保存する場所・実行する場所を分けることで、イベント数が増えてもゲーム本体を比較的シンプルに保てます。
EventManagerを作るとゲーム本体を整理しやすい
イベント管理がゲーム内の一つのシステムになっているなら、EventManagerのようなクラスを設ける方法があります。イベントの登録・検索・実行を管理クラスへ集約します。
class EventManager {
private:
std::unordered_map<int, std::unique_ptr<Event>> events;
public:
void add(int id, std::unique_ptr<Event> event) {
events[id] = std::move(event);
}
void execute(int id) {
auto it = events.find(id);
if (it != events.end()) {
it->second->execute();
}
}
};
登録側ではmanager.add(1, std::make_unique<TreasureEvent>());のように追加し、ゲーム本体ではmanager.execute(1);とするだけです。
もちろん実際のゲームでは、プレイヤーやマップなどの情報をイベントへ渡す必要があります。その場合は後述するContextを利用すると、イベント関数の引数が増え続ける問題を抑えやすくなります。
イベントへゲーム全体を渡すよりContextを用意する
イベントが増えると、「プレイヤーHPを変更したい」「アイテムを追加したい」「マップを移動したい」「フラグを変更したい」など、イベントからゲームのさまざまな機能へアクセスしたくなります。その結果、巨大なGame*を各イベントへ渡して何でも操作させる設計になりがちです。
小規模なゲームならそれでも動きますが、規模が大きくなるとイベントとGameクラスの結合が強くなります。そこでイベント実行に必要なものをまとめたEventContextを用意する方法があります。
struct EventContext {
Player& player;
World& world;
FlagManager& flags;
};
class Event {
public:
virtual ~Event() = default;
virtual void execute(EventContext& context) = 0;
};
例えば宝箱イベントならcontext.playerへアイテムを追加し、ストーリーイベントならcontext.flagsへ進行フラグを設定できます。必要な依存関係を明示できるため、イベントのテストもしやすくなります。
「イベント100個=クラス100個」が本当に必要か考える
ここが設計上かなり重要です。100個のイベントが存在するとしても、本当に100種類の異なるC++処理が必要とは限りません。例えばRPGなら、イベントの多くは「メッセージを表示する」「アイテムを与える」「フラグを変更する」「敵との戦闘を開始する」といった少数の基本処理の組み合わせかもしれません。
例えば「村人Aと話したら薬草を1個もらう」と「村人Bと話したら100ゴールドもらう」を、Event27とEvent58という完全に別のC++クラスとして実装する必要があるでしょうか。違いがデータだけなら、共通のイベント処理へパラメータを与える方が管理しやすくなります。
このような場合には、継承を増やすよりデータ駆動設計を検討する価値があります。
データ駆動ならイベントをJSONなどで定義できる
例えばイベントをJSONなどの外部データとして記述し、ゲーム側に汎用イベント実行システムを用意します。概念的には次のようなデータです。
{
"id": 25,
"trigger": "talk",
"actions": [
{ "type": "message", "text": "これを持っていきなさい" },
{ "type": "give_item", "item": "herb", "count": 1 },
{ "type": "set_flag", "flag": "received_herb", "value": true }
]
}
この方式なら、村人ごとにVillageEvent1、VillageEvent2というC++クラスを作るのではなく、イベントデータを追加するだけで済みます。イベントが100個から1000個になっても、C++のクラス数が同じ割合で増えるとは限りません。
また、シナリオ担当者がいる開発では、プログラムを再コンパイルせずイベント内容を編集できる構造へ発展させやすいメリットがあります。ただしデータの検証、エラー処理、デバッグツールなどが必要になるため、小規模ゲームで最初から大掛かりな仕組みを作る必要はありません。
継承とデータ駆動は二者択一ではない
実際には「全部を派生クラスにする」か「全部をJSONにする」かの二択にする必要はありません。両方を組み合わせる設計が実用的です。
例えば通常の会話、アイテム入手、フラグ変更、マップ移動などはデータとして定義し、特殊なボス演出や独自ミニゲームなど、汎用システムでは表現しにくいイベントだけ専用C++クラスとして実装できます。
このようにすると、単純なイベントを追加するたびに新しいクラスが増える問題を避けつつ、本当に特殊なイベントについてはC++の自由度を利用できます。
継承を使わずstd::functionで処理を登録する方法もある
イベントが小さく、状態を持つ専用クラスを作るほどでもない場合は、std::functionやラムダ式を登録する方法もあります。
using EventFunction = std::function<void(EventContext&)>;
std::unordered_map<int, EventFunction> events;
events[1] = [](EventContext& ctx) {
// イベント1
};
events[2] = [](EventContext& ctx) {
// イベント2
};
events[1](context);
この方法なら、イベントごとに派生クラスを作る必要がありません。短い処理を登録して呼び出すだけなら非常に簡潔です。
一方で、イベント自身が複雑な状態を長期間保持する、start()、update()、finish()など明確なライフサイクルを持つ場合は、Eventクラスを使った方が構造を表現しやすいことがあります。
一瞬で終わらないイベントなら状態管理も必要になる
ゲームイベントは必ずしもexecute()を一度呼んだ瞬間に終了するとは限りません。「キャラクターが歩く→会話する→カメラを移動する→戦闘開始」のように数秒から数十秒継続するイベントもあります。
この場合はEventにstart()、update()、isFinished()などを持たせ、EventManagerが現在実行中のイベントだけを更新する設計が考えられます。
class Event {
public:
virtual ~Event() = default;
virtual void start(EventContext& ctx) = 0;
virtual void update(EventContext& ctx, float deltaTime) = 0;
virtual bool isFinished() const = 0;
};
この設計なら、100個のイベントを毎フレーム全部更新する必要はありません。条件を満たして開始されたイベントだけを「active event」として管理し、終了したら次へ進められます。
イベントを最初から100個すべて生成する必要もない
イベントオブジェクトが重かったり、マップごとに大量のイベントがあったりする場合は、ゲーム開始時に全イベントを生成する必要はありません。現在のステージで必要なイベントだけ生成し、ステージ終了時に破棄する方法もあります。
例えば全世界でイベントが1000個あっても、現在いる町に20個しか存在しないなら、その20個だけロードする設計にできます。また、イベント開始時にFactoryで生成して終了後に破棄する方法もあります。
ただし、単純なイベントオブジェクト100個程度なら、メモリや生成コストを過剰に心配する必要がない場合も多いです。「100個あるから重い」と決めつけず、まず設計の分かりやすさと所有関係を整え、実際に性能問題が発生したら計測するのが基本です。
イベントを別ファイルにする基準も整理しておく
「イベントごとにファイルを分ける」こと自体が間違いというわけではありません。独自ロジックが数百行ある特殊イベントなら、専用のヘッダー・ソースファイルへ分離した方が読みやすくなります。
しかし、数行しか違わないイベントを100個の.h・.cppへ分割すると、ファイル管理の方が大変になります。処理の違いが値だけならデータ化し、アルゴリズムそのものが違う場合にクラス化する、といった基準を決めると整理しやすくなります。
つまり「イベント1個につきクラス1個」というルールを先に決めるのではなく、同じ振る舞いでデータだけ違うのか、振る舞い自体が違うのかを見て設計を選ぶことが重要です。
規模別に考えるおすすめ構成
小規模なゲームでイベント数も少ないうちは、無理に巨大なイベントシステムを作らなくても構いません。数十個程度で独自処理が多いなら、Event基底クラス+std::unique_ptr+EventManagerという構成は理解しやすく、拡張もしやすいでしょう。
イベント数が増え、会話・フラグ・アイテム・移動など同じ処理の組み合わせが多くなってきたら、JSONなどを使ったデータ駆動へ徐々に移行する方法があります。さらに複雑なRPGやアドベンチャーゲームなら、イベントコマンド列、ステートマシン、スクリプト言語などへ発展させることもできます。
設計を選ぶ目安としては、「Event1~Event100の中身を見比べたとき、ほとんど同じコードで数値や文章だけが違う」ならデータ化を優先し、「それぞれ実行するロジックが大きく違う」ならポリモーフィズムが有力と考えると分かりやすいでしょう。
まとめ:Event継承は良い出発点だが「100クラス必要か」まで考える
C++のゲームでイベントが増えたとき、Eventを基底クラスとしてEvent1、Event2などを継承させる考え方は妥当です。純粋仮想関数を使って共通インターフェースを定義し、std::vector<std::unique_ptr<Event>>やstd::unordered_mapなどへまとめれば、ゲーム本体で100個の異なる変数を個別管理する必要はありません。
さらにEventManagerやFactoryへ生成・管理の責任を移せば、ゲーム本体が具体的なイベント型を大量に知る問題も軽減できます。長時間継続するイベントなら、開始・更新・終了というライフサイクルをEventへ持たせる方法もあります。
ただし、100個のイベントが似た処理の組み合わせなら、Event1~Event100という100種類の派生クラスを作ること自体が過剰かもしれません。その場合は会話、アイテム付与、フラグ変更などを汎用アクションとして実装し、イベント内容をJSONなどのデータで表現する方が拡張しやすくなります。
実用上は、「特殊な処理はEvent派生クラス」「定型的なイベントはデータ」「小さな処理ならstd::functionやラムダ」のように使い分けるのがおすすめです。重要なのはインスタンスを100個作ること自体を恐れるのではなく、イベント追加のたびにゲーム本体へ100個分の知識と分岐が増えていく構造を避けることです。

コメント