Fase: 4 — O Cérebro Namespace:
Caffeine::EventsArquivos:src/events/EventBus.hpp,src/events/EventBus.cpp,src/events/Events.hppStatus: ✅ Implementado RF: RF4.6
O Event Bus permite comunicação desacoplada entre sistemas. Um sistema publica um evento; outros se inscrevem sem conhecer quem publica. Isso elimina dependências diretas (ex: Physics não precisa conhecer Audio).
Dois modos de entrega:
publish<T>()— imediato, chama listeners agora (main thread)publishDeferred<T>()— enfileira para o próximodispatch()(thread-safe)
namespace Caffeine::Events {
using ListenerHandle = u32;
class EventBus {
public:
// Inscreve um listener tipado. Retorna handle para cancelamento.
template<typename T>
ListenerHandle subscribe(std::function<void(const T&)> callback);
// Remove o listener. No-op se handle inválido. Zero dangling pointers.
void unsubscribe(ListenerHandle handle);
// Entrega imediata: chama todos os listeners agora (snapshot seguro).
template<typename T>
void publish(const T& event);
// Enfileira evento para o próximo dispatch(). Thread-safe.
template<typename T>
void publishDeferred(T event);
// Processa toda a fila diferida. Chamar uma vez por frame, no GameLoop.
void dispatch();
// Número de eventos pendentes na fila.
usize pendingCount() const;
// Remove todos os listeners e esvazia a fila.
void clear();
};
} // namespace Caffeine::EventsCada tipo T recebe um ID único em runtime via endereço de variável static local — zero colisão, zero registro manual:
template<typename T>
u32 eventTypeId() {
static char s_sentinel = 0;
return static_cast<u32>(reinterpret_cast<uintptr_t>(&s_sentinel));
}Todos em src/events/Events.hpp, namespace Caffeine::Events.
struct OnEntityCreated { u32 entityId; };
struct OnEntityDestroyed { u32 entityId; };struct OnCollision2D {
u32 entityA;
u32 entityB;
Vec2 contactPoint;
Vec2 normal;
f32 impulse;
};
struct OnTrigger2D {
u32 triggerEntity;
u32 otherEntity;
bool entered; // true = enter, false = exit
};struct OnHealthChanged {
u32 entityId;
f32 previousHealth;
f32 currentHealth;
f32 maxHealth;
};
struct OnDeath {
u32 entityId;
u32 killerEntityId; // 0 = environmental / no killer
};struct OnScoreChanged {
u32 playerId;
i32 previousScore;
i32 currentScore;
i32 delta;
};struct OnLevelLoaded { u32 levelId; c8 levelName[64]; };
struct OnLevelUnloaded { u32 levelId; };struct OnActionPressed { u32 actionId; u32 playerId; };
struct OnActionReleased { u32 actionId; u32 playerId; };Caffeine::Events::EventBus bus;
// Inscrever
ListenerHandle h = bus.subscribe<OnDeath>([](const OnDeath& e) {
if (e.killerEntityId == 0) {
// morte ambiental
}
});
// Publicar imediato (main thread)
bus.publish(OnDeath{ entityId, killerId });
// Cancelar
bus.unsubscribe(h);// PhysicsSystem rodando em job worker:
bus.publishDeferred(OnCollision2D{
entityA, entityB, contactPoint, normal, impulse
});
// GameLoop, início do fixed update:
bus.dispatch(); // entrega todos os eventos enfileirados// AudioSystem (não conhece PhysicsSystem)
bus.subscribe<OnCollision2D>([&audio](const OnCollision2D& e) {
audio.playSFX("impact.caf");
});
// ScoreSystem (não conhece nada de gameplay)
bus.subscribe<OnDeath>([&score](const OnDeath& e) {
score.add(100);
});
// PhysicsSystem publica, ambos recebem
bus.publishDeferred(OnCollision2D{ ... });Frame N:
├── InputManager.poll()
├── EventBus.dispatch() ← entrega diferidos do frame N-1
├── World.update(fixedDt)
│ ├── PhysicsSystem → publishDeferred(OnCollision2D)
│ ├── CombatSystem → publish(OnDeath) ← imediato
│ └── ...
└── render(alpha)
Frame N+1:
├── EventBus.dispatch() ← entrega os OnCollision2D do frame N
└── ...
| Decisão | Justificativa |
|---|---|
Type-erased void* internamente |
Permite std::unordered_map<u32, vector<Record>> sem virtual por tipo |
Snapshot antes de iterar em publish() |
Listener pode chamar unsubscribe() sem invalidar iteração |
| Mutex apenas na fila deferred | publish() e subscribe() são main-thread only; só publishDeferred() precisa de lock |
ListenerHandle = u32 |
Sem ponteiros pendentes; unsubscribe por ID, não por referência |
eventTypeId<T>() via static char |
Zero custo de registro; ID único garantido por endereço de static |
| Operação | Thread-safe? | Motivo |
|---|---|---|
subscribe() |
Main thread only | Modifica m_listeners sem lock |
unsubscribe() |
Main thread only | Modifica m_listeners sem lock |
publish() |
Main thread only | Lê m_listeners sem lock |
publishDeferred() |
✅ Qualquer thread | Protegido por m_queueMutex |
dispatch() |
Main thread only | Consome fila após swap; não precisa de lock |
| Critério | Resultado |
|---|---|
| 1K eventos/frame sem overhead perceptível | ✅ 1000× publish() = 0.085ms total |
publish() < 0.1ms (hot path) |
✅ 0.086µs/call (10K reps, Release) |
publishDeferred thread-safe |
✅ 8 threads × 125 eventos = 1000/1000 sem race |
Eventos diferidos entregues no próximo dispatch() |
✅ Zero entregues antes, todos depois |
Zero listeners mortos após unsubscribe |
✅ 0 callbacks após unsubscribe (imediato e deferred) |
tests/test_eventbus.cpp — 18 test cases, 39 assertions:
| Teste | Cobertura |
|---|---|
| subscribe + publish imediato | Fluxo base |
| Múltiplos listeners no mesmo tipo | Fan-out |
| unsubscribe remove listener | Cancelamento correto |
| unsubscribe handle inválido | No-op seguro |
| Tipos de evento isolados | Sem cross-talk |
| publish sem listeners | No-op seguro |
| publishDeferred não entrega imediatamente | Fila correta |
| dispatch entrega todos | Entrega garantida |
| dispatch esvazia fila | Estado pós-dispatch |
| Ordem de entrega deferred | FIFO preservado |
| clear remove listeners e fila | Reset completo |
| Handles únicos por subscription | IDs não colidem |
| Unsubscribe seletivo entre vários | Precisão cirúrgica |
| Deferred com tipos mistos | Multi-type queue |
OnEntityCreated predefinido |
Evento ECS |
OnDeath predefinido |
Evento combate |
OnScoreChanged predefinido + dispatch |
Evento progressão |
8 threads × 125 publishDeferred |
Thread safety |
- Upstream:
Caffeine::Core::Types,src/math/Vec2.hpp - Downstream: Physics (publica
OnCollision2D), Audio (reage a eventos), UI (reage a eventos de gameplay), GameLoop (chamadispatch())
| Tópico | Descrição |
|---|---|
| Concorrência & Runtime | Event Bus no contexto do Game Loop |
| Sistemas de Jogo | Eventos conectam sistemas de gameplay |
docs/architecture_specs.md— §6 Event Busphysics/physics-2d.md— publicaOnCollision2Dcore/game-loop.md— chamaeventBus.dispatch()- Índice de Tópicos Transversais