Observerパターンは「1対多の通知」をシンプルに実現できる便利な仕組みです。
しかし、ゲームや大規模アプリケーションではイベントの種類や監視者が増え、管理が複雑になります。
そこでEventBusの仕組みを使用します。
EventBusは「イベントのハブ」として、発行者と購読者を疎結合に保ちながら柔軟なイベント処理を可能にします。
本記事ではObserverパターンを土台にしてイベントバスの仕組みを解説し、より大規模な設計へと進化させる方法を紹介します。
前提知識
-
C++の基礎文法を理解している -
Observerパターンを理解している
Observerパターンをまだ知らない方は、先に上記の記事に目を通すことを進めます。
EventBusとは
EventBusは「イベント駆動型アーキテクチャ」を支える仕組みのひとつです。
発行者(Publisher)がイベントを投げると、購読者(Subscriber)が必要に応じてそれを受け取ります。 両者は直接依存せず、イベントバスというハブを介して通信するため、疎結合で柔軟な設計が可能になります。
新しいイベントや購読者を追加しても既存コードへの影響が少なく、大規模アプリケーションでの拡張性が高いというメリットがあります。
その反面で、イベントの流れが見えにくくデバッグが難しくなるというデメリットもあります。
Observerとの比較
| 項目 | Observerパターン | EventBus |
|---|---|---|
| 通知方法 |
Subjectが直接Observerへ通知 |
バスが購読者へ一括通知 |
| 結合度 |
SubjectとObserverがインターフェースで結合 |
PublisherとSubscriberはバスのみ依存 |
| 適用範囲 | 小~中規模向け | 中~大規模向け |
| 可読性 | イベントの流れが明示的で追いやすい | イベントの流れが見えにくくデバッグが難しい |
EventBusの構造と用語
基本的な構造は以下の3つの要素から成り立っています。
-
Publisher(発行者)
- イベントを生成してバスに投げる役割
-
Subscriber(購読者)
- バスからイベントを受け取り、必要な処理を行う役割
-
EventBus(イベントバス)
-
PublisherとSubscriberの間を仲介するハブ - イベントを一元管理し、登録された
Subscriberに通知を届ける -
PublisherとSubscriberはバスに依存するだけで、互いを直接知らない
-
構造の図解
コード例
EventBusでプレイヤーのダメージ処理を管理するコード
//--------------------------------------------------------------
//! @file EventBus.h
//! @brief EventBusの定義
//! @author つきの
//--------------------------------------------------------------
#pragma once
#include <string>
#include <functional>
#include <unordered_map>
#include <vector>
//--------------------------------------------------------------
//! @class EventBusクラス
//! @note イベントの購読登録と発行を管理するクラス
//--------------------------------------------------------------
class EventBus {
public:
using Callback = std::function<void(int)>; // コールバック関数の型定義
public:
//--------------------------------------------------------------
// イベント購読登録
//! @param event [in] イベント名
//! @param callback [in] コールバック関数
//--------------------------------------------------------------
void subscribe(const std::string& event, Callback callback);
//--------------------------------------------------------------
// イベント発行
//! @param event [in] イベント名
//! @param value [in] イベント値
//--------------------------------------------------------------
void publish(const std::string& event, int value);
private:
// 購読者リスト
std::unordered_map<std::string, std::vector<Callback>> subscribers_;
};
//--------------------------------------------------------------
//! @file EventBus.cpp
//! @brief EventBusの実装
//! @author つきの
//--------------------------------------------------------------
#include "EventBus.h"
//--------------------------------------------------------------
//! @brief イベント購読登録
//--------------------------------------------------------------
void EventBus::subscribe(const std::string& event, Callback callback) {
// イベント名に対応するコールバック関数リストに追加
subscribers_[event].push_back(callback);
}
//--------------------------------------------------------------
//! @brief イベント発行
//--------------------------------------------------------------
void EventBus::publish(const std::string& event, int value) {
// イベント名に対応するコールバック関数リストを取得
auto it = subscribers_.find(event);
// コールバック関数を呼び出す
if (it != subscribers_.end()) {
// 登録されているすべてのコールバック関数を呼び出す
for (auto& cb : it->second) {
cb(value);
}
}
}
//---------------------------------------------------
//! @file Player.h
//! @brief Playerの定義
//! @author つきの
//---------------------------------------------------
#pragma once
//---------------------------------------------------
//! @brief Playerクラス
//! @details イベントバスを利用してイベントを発行するプレイヤークラス
//---------------------------------------------------
#include "EventBus.h"
class Player {
public:
//---------------------------------------------------
// コンストラクタ
//! @param hp [in] 初期体力
//! @param bus [in] イベントバスの参照
//---------------------------------------------------
Player(int hp, EventBus& bus);
//---------------------------------------------------
// ダメージを受ける
//! @param damage [in] ダメージ量
//---------------------------------------------------
void takeDamage(int damage);
private:
int hp_; //!< プレイヤーの体力
EventBus& bus_; //!< イベントバスの参照
};
//---------------------------------------------------
//! @file Player.cpp
//! @brief Playerの実装
//! @author つきの
//---------------------------------------------------
#include "Player.h"
//---------------------------------------------------
//! @brief コンストラクタ
//---------------------------------------------------
Player::Player(int hp, EventBus& bus)
: hp_(hp), bus_(bus) {
}
//---------------------------------------------------
//! @brief ダメージを受ける
//---------------------------------------------------
void Player::takeDamage(int damage) {
hp_ -= damage; // 体力を減少
// 体力が0未満にならないように補正
if (hp_ < 0) {
hp_ = 0;
}
bus_.publish("HP_CHANGED", hp_); // EventBusに通知
}
//--------------------------------------------------------------
//! @file Logger.h
//! @brief Loggerの定義
//! @author つきの
//--------------------------------------------------------------
#pragma once
#include "EventBus.h"
//--------------------------------------------------------------
//! @class Loggerクラス
//! @note イベントバスにログ出力の処理を登録する
//--------------------------------------------------------------
class Logger {
public:
//--------------------------------------------------------------
// イベントバスに処理を登録
//! @param bus [in] イベントバスの参照
//--------------------------------------------------------------
void registerTo(EventBus& bus);
};
//--------------------------------------------------------------
//! @file Logger.cpp
//! @brief Loggerの実装
//! @author つきの
//--------------------------------------------------------------
#include "Logger.h"
#include <iostream>
//--------------------------------------------------------------
//! @brief イベントバスに処理を登録
//--------------------------------------------------------------
void Logger::registerTo(EventBus& bus) {
// ログ出力用のコールバック関数を定義
auto callback = [](int value) {
std::cout << "[Log] Event=HP_CHANGED, Value=" << value << std::endl;
};
// HP_CHANGEDイベントに購読登録
bus.subscribe("HP_CHANGED", callback);
}
//--------------------------------------------------------------
//! @file SoundSystem.h
//! @brief サウンドシステムの定義
//! @author つきの
//--------------------------------------------------------------
#pragma once
#include "EventBus.h"
//--------------------------------------------------------------
//! @class SoundSystemクラス
//! @note イベントバスに音声の処理を登録する
//--------------------------------------------------------------
class SoundSystem {
public:
//--------------------------------------------------------------
// イベントバスに処理を登録
//! @param bus [in] イベントバスの参照
//--------------------------------------------------------------
void registerTo(EventBus& bus);
};
//--------------------------------------------------------------
//! @file SoundSystem.cpp
//! @brief サウンドシステムの実装
//! @author つきの
//--------------------------------------------------------------
#include "SoundSystem.h"
#include <iostream>
//--------------------------------------------------------------
//! @brief イベントバスに処理を登録
//--------------------------------------------------------------
void SoundSystem::registerTo(EventBus& bus) {
// サウンド再生用のコールバック関数を定義
auto callback = [](int value) {
std::cout << "[Sound] 再生: HP_CHANGED " << value << std::endl;
};
// HP_CHANGEDイベントに購読登録
bus.subscribe("HP_CHANGED", callback);
}
//--------------------------------------------------------------
//! @file main.cpp
//! @brief ゲーム開発を模したイベントバスの簡易的なサンプル
//! @author つきの
//--------------------------------------------------------------
#include "EventBus.h"
#include "Player.h"
#include "Logger.h"
#include "SoundSystem.h"
//エントリポイント
int main() {
// イベントバスの作成
EventBus bus;
// プレイヤーの作成
Player player(100, bus);
// ロガーとサウンドシステムの作成と処理の登録
Logger logger;
logger.registerTo(bus);
SoundSystem soundSystem;
soundSystem.registerTo(bus);
// プレイヤーがダメージを受けるシミュレーション
player.takeDamage(20);
//終了
return 0;
}
[Log] Event=HP_CHANGED, Value=80
[Sound] 再生: HP_CHANGED 80
クラス図
subscribe/購読
EventBusのsubscribe()から、ラムダ式を登録します。
登録したラムダ式はEventBusが管理を行います。
本記事ではキーをイベント名(std::string)で管理していますが、キーを自作型にすることで拡張を行うこともできます。
//--------------------------------------------------------------
//! @class EventBusクラス
//! @note イベントの購読登録と発行を管理するクラス
//--------------------------------------------------------------
class EventBus {
public:
using Callback = std::function<void(int)>; // コールバック関数の型定義
public:
//--------------------------------------------------------------
// イベント購読登録
//! @param event [in] イベント名
//! @param callback [in] コールバック関数
//--------------------------------------------------------------
void subscribe(const std::string& event, Callback callback);
// 中略
private:
// 購読者リスト
std::unordered_map<std::string, std::vector<Callback>> subscribers_;
};
//--------------------------------------------------------------
//! @brief イベント購読登録
//--------------------------------------------------------------
void EventBus::subscribe(const std::string& event, Callback callback) {
// イベント名に対応するコールバック関数リストに追加
subscribers_[event].push_back(callback);
}
publish/発行
EventBusのpublish()を呼ぶことで、管理しているコールバック関数(本記事ではsubscribers_)の中からキー(本記事ではconst std::string& event)が対応するコールバックを発火させます。
//--------------------------------------------------------------
//! @class EventBusクラス
//! @note イベントの購読登録と発行を管理するクラス
//--------------------------------------------------------------
class EventBus {
public:
// 中略
//--------------------------------------------------------------
// イベント発行
//! @param event [in] イベント名
//! @param value [in] イベント値
//--------------------------------------------------------------
void publish(const std::string& event, int value);
private:
// 購読者リスト
std::unordered_map<std::string, std::vector<Callback>> subscribers_;
};
//--------------------------------------------------------------
//! @brief イベント発行
//--------------------------------------------------------------
void EventBus::publish(const std::string& event, int value) {
// イベント名に対応するコールバック関数リストを取得
auto it = subscribers_.find(event);
// コールバック関数を呼び出す
if (it != subscribers_.end()) {
// 登録されているすべてのコールバック関数を呼び出す
for (auto& cb : it->second) {
cb(value);
}
}
}
ObserverパターンでいうSubjectに当たるクラス(イベントの通知元になるクラス、本記事ではPlayer)は、EventBusのポインタか参照を持つことで、クラス内からpublishを呼ぶことが出来ます。
class Player {
public:
//---------------------------------------------------
// コンストラクタ
//! @param hp [in] 初期体力
//! @param bus [in] イベントバスの参照
//---------------------------------------------------
Player(int hp, EventBus& bus);
//---------------------------------------------------
// ダメージを受ける
//! @param damage [in] ダメージ量
//---------------------------------------------------
void takeDamage(int damage);
private:
int hp_; //!< プレイヤーの体力
EventBus& bus_; //!< イベントバスの参照
};
//---------------------------------------------------
//! @brief コンストラクタ
//---------------------------------------------------
Player::Player(int hp, EventBus& bus)
: hp_(hp), bus_(bus) {
}
//---------------------------------------------------
//! @brief ダメージを受ける
//---------------------------------------------------
void Player::takeDamage(int damage) {
hp_ -= damage; // 体力を減少
// 体力が0未満にならないように補正
if (hp_ < 0) {
hp_ = 0;
}
bus_.publish("HP_CHANGED", hp_); // EventBusに通知
}
発展形
EventBusの発展形をリポジトリ付きで解説した記事もあるので、気になる方はぜひ目を通してみて下さい。
総括
-
EventBusを使用することで、疎結合なイベント駆動設計を実現できる - イベント駆動にすると流れがつかみにくく、デバッグが少々難しくなるデメリットが隠れている
-
subscribeでコールバックを登録し、publishで登録したコールバックを発火させる仕組みになっている