2026-03-26 16:45:24 +08:00
|
|
|
|
# Event
|
|
|
|
|
|
|
|
|
|
|
|
**命名空间**: `XCEngine::Core`
|
|
|
|
|
|
|
2026-03-27 19:18:53 +08:00
|
|
|
|
**类型**: `class template`
|
2026-03-26 16:45:24 +08:00
|
|
|
|
|
|
|
|
|
|
**头文件**: `XCEngine/Core/Event.h`
|
|
|
|
|
|
|
2026-03-27 19:18:53 +08:00
|
|
|
|
**描述**: 一个轻量模板事件广播器,支持订阅、延迟取消订阅和参数化回调调用。
|
2026-03-26 16:45:24 +08:00
|
|
|
|
|
2026-03-27 19:18:53 +08:00
|
|
|
|
## 角色概述
|
2026-03-26 16:45:24 +08:00
|
|
|
|
|
2026-03-27 19:18:53 +08:00
|
|
|
|
`Event<Args...>` 是当前代码库里最常用的基础广播机制之一。它的定位很明确:
|
2026-03-26 16:45:24 +08:00
|
|
|
|
|
2026-03-27 19:18:53 +08:00
|
|
|
|
- 用模板参数表达回调签名
|
|
|
|
|
|
- 用 `Subscribe()` 返回监听器 ID
|
|
|
|
|
|
- 用 `Invoke()` 把参数广播给所有当前监听器
|
2026-03-26 16:45:24 +08:00
|
|
|
|
|
2026-03-27 19:18:53 +08:00
|
|
|
|
和一些商业引擎里的事件系统相比,它更偏轻量 C++ 容器封装,而不是完整的消息总线、typed dispatcher 或 editor event graph。
|
2026-03-26 16:45:24 +08:00
|
|
|
|
|
2026-03-27 19:18:53 +08:00
|
|
|
|
## 当前真实使用
|
2026-03-26 16:45:24 +08:00
|
|
|
|
|
2026-03-27 19:18:53 +08:00
|
|
|
|
当前已能在多个核心模块里看到它的直接使用,例如:
|
|
|
|
|
|
|
|
|
|
|
|
- `Scene::OnGameObjectCreated()`
|
|
|
|
|
|
- `Scene::OnGameObjectDestroyed()`
|
|
|
|
|
|
- `SceneManager` 的场景切换事件
|
|
|
|
|
|
- `InputManager` 的键鼠输入事件
|
|
|
|
|
|
|
|
|
|
|
|
这说明它不是“理论上的基础设施”,而是已经进入主代码路径的常用类型。
|
|
|
|
|
|
|
|
|
|
|
|
## 当前实现行为
|
|
|
|
|
|
|
|
|
|
|
|
### 1. `Subscribe()` 会分配递增 ID
|
|
|
|
|
|
|
|
|
|
|
|
- 监听器以 `(uint64_t id, Callback)` 形式保存在 `m_listeners` 中
|
|
|
|
|
|
- ID 由 `m_nextId` 递增生成
|
|
|
|
|
|
- `tests/core/test_core.cpp` 已验证不同订阅返回的 ID 不同
|
|
|
|
|
|
|
|
|
|
|
|
### 2. `Unsubscribe()` 是延迟取消订阅
|
|
|
|
|
|
|
|
|
|
|
|
`Unsubscribe(id)` 当前不会立刻从 `m_listeners` 删除,而是先把 ID 放进 `m_pendingUnsubscribes`。
|
|
|
|
|
|
|
|
|
|
|
|
真正删除发生在两种时机:
|
|
|
|
|
|
|
|
|
|
|
|
- 显式调用 [ProcessUnsubscribes](ProcessUnsubscribes.md)
|
|
|
|
|
|
- 下一次 [Invoke](Invoke.md) 开始前
|
|
|
|
|
|
|
|
|
|
|
|
这意味着 API 语义更接近“标记取消订阅”,而不是“立刻从容器抹掉”。
|
|
|
|
|
|
|
|
|
|
|
|
### 3. `Invoke()` 会在锁外执行回调
|
|
|
|
|
|
|
|
|
|
|
|
`Invoke()` 的当前流程是:
|
|
|
|
|
|
|
|
|
|
|
|
1. 加锁
|
|
|
|
|
|
2. 先处理 `m_pendingUnsubscribes`
|
|
|
|
|
|
3. 复制 `m_listeners` 到本地 `listenersCopy`
|
|
|
|
|
|
4. 释放锁
|
|
|
|
|
|
5. 逐个执行回调
|
|
|
|
|
|
|
|
|
|
|
|
这套做法的关键收益是:
|
|
|
|
|
|
|
|
|
|
|
|
- 回调执行期间不会长期占着互斥锁
|
|
|
|
|
|
- 回调内部即使再次订阅/取消订阅,也不直接破坏当前这次遍历
|
|
|
|
|
|
|
|
|
|
|
|
代价是会有一次监听器数组复制,且它不是为极端高频无分配场景优化的实现。
|
|
|
|
|
|
|
|
|
|
|
|
## 线程语义
|
|
|
|
|
|
|
|
|
|
|
|
### 已有保护
|
|
|
|
|
|
|
|
|
|
|
|
- `Subscribe()`、`Unsubscribe()`、`ProcessUnsubscribes()`、`Invoke()`、`Clear()` 都会访问 `m_mutex`
|
|
|
|
|
|
|
|
|
|
|
|
### 不能误解的地方
|
|
|
|
|
|
|
|
|
|
|
|
- [begin](begin.md) / [end](end.md) 直接返回内部 `std::vector` 迭代器,没有加锁包装
|
|
|
|
|
|
- 如果你跨线程持有这些迭代器,同时别的线程订阅/取消订阅,就没有安全保证
|
|
|
|
|
|
|
|
|
|
|
|
所以可以说:它对常规订阅和调用路径做了基础互斥保护,但并不是“所有访问方式都线程安全”的容器。
|
|
|
|
|
|
|
|
|
|
|
|
## 当前实现限制
|
|
|
|
|
|
|
|
|
|
|
|
- `begin()` / `end()` 暴露的是内部容器迭代器,不适合并发或长时间持有。
|
|
|
|
|
|
- 回调列表在 `Invoke()` 时会拷贝一份,适合易用性,但不适合把它当成极致性能事件系统。
|
|
|
|
|
|
- 没有 listener priority、事件消费中止或过滤器机制。
|
|
|
|
|
|
- 没有 scoped subscription / RAII subscription token。
|
|
|
|
|
|
|
|
|
|
|
|
## 测试覆盖
|
|
|
|
|
|
|
|
|
|
|
|
`tests/core/test_core.cpp` 已覆盖:
|
|
|
|
|
|
|
|
|
|
|
|
- 订阅与回调调用
|
|
|
|
|
|
- 带参数事件
|
|
|
|
|
|
- 取消订阅
|
|
|
|
|
|
- 清空监听器
|
|
|
|
|
|
- 多监听器广播
|
|
|
|
|
|
- 返回 ID 唯一性
|
|
|
|
|
|
|
|
|
|
|
|
## 相关方法
|
|
|
|
|
|
|
|
|
|
|
|
- [Subscribe](Subscribe.md)
|
|
|
|
|
|
- [Unsubscribe](Unsubscribe.md)
|
|
|
|
|
|
- [ProcessUnsubscribes](ProcessUnsubscribes.md)
|
|
|
|
|
|
- [Invoke](Invoke.md)
|
|
|
|
|
|
- [Clear](Clear.md)
|
|
|
|
|
|
- [begin](begin.md)
|
|
|
|
|
|
- [end](end.md)
|
|
|
|
|
|
|
|
|
|
|
|
## 相关指南
|
|
|
|
|
|
|
|
|
|
|
|
- [Core Foundations: Ownership, Events, And Layers](../../../_guides/Core/Core-Foundations-Ownership-Events-And-Layers.md)
|
2026-03-26 16:45:24 +08:00
|
|
|
|
|
|
|
|
|
|
## 相关文档
|
|
|
|
|
|
|
2026-03-27 19:18:53 +08:00
|
|
|
|
- [当前模块](../Core.md)
|
|
|
|
|
|
- [Scene](../../Scene/Scene/Scene.md)
|
|
|
|
|
|
- [InputManager](../../Input/InputManager/InputManager.md)
|
|
|
|
|
|
- [API 总索引](../../../main.md)
|