信号 signal
在 EnTT 框架中,entt::sigh 和 entt::sink 是信号-槽机制(Signal-Slot Pattern)的核心组件,它们提供了一种事件驱动的通信方式,用于解耦不同系统或组件之间的依赖关系,一种 信号 代表一个 事件,当 事件 发生时,触发响应的 信号,这显然是一种 观察者模式 Observer Pattern
基本思想是,不同的 listener 通过监听同一个 signal 信号的出现来执行自身的逻辑,这些 listener 是 C++ 可调用对象,在底层通过 entt::delegate 统一包装管理 (而不是 std::function)
信号通过监听器的 返回值类型 和 可接收参数 (而不是参数签名) 来定义
entt::sigh<void(int, char)> signal;信号包含一些可查询的必要基本信息,例如包含多少个正在监听本信号的 监听器 等信息
void foo(int, char) { /* ... */ }
struct listener {
void bar(const int &, char) { /* ... */ }
};
// ...
entt::sink sink{signal};
listener instance;
sink.connect<&foo>();
sink.connect<&listener::bar>(instance);
// ...
// disconnects a free function
sink.disconnect<&foo>();
// disconnect a member function of an instance
sink.disconnect<&listener::bar>(instance);
// disconnect all member functions of an instance, if any
sink.disconnect(&instance);
// discards all listeners at once
sink.disconnect();如上所示,监听器 不需要严格遵循信号所定义的输入参数签名类型,只要可以使用给定参数在 信号 发布时调用 监听器 执行并给出对应的返回值类型即可
信号 通过 publish 方法进行发布
signal.publish(42, 'c');为了获得 监听器 执行后的返回值,可以使用 收集器 collector
int f() { return 0; }
int g() { return 1; }
// ...
entt::sigh<int()> signal;
entt::sink sink{signal};
sink.connect<&f>();
sink.connect<&g>();
std::vector<int> vec{};
signal.collect([&vec](int value) { vec.push_back(value); });
assert(vec[0] == 0);
assert(vec[1] == 1);收集器 必须有公开的 函数运算符 ,并且需要接受一个类型作为输入参数,并且需要能够将 监听器 的返回值转换为该输入参数的类型,然后就可以在该 收集器 内部操作输入参数来实现收集返回值的逻辑,并且收集器本身可以选择性的返回一个 布尔值,如果该值为 false 则继续执行收集,为 true 则停止收集
struct my_collector {
std::vector<int> vec{};
bool operator()(int v) {
vec.push_back(v);
return true;
}
};
// ...
my_collector collector;
signal.collect(std::ref(collector));也可以使用 函子 而不是 lambda 来充当 收集器,在这种情况下应该使用 std::ref 以避免复制
事件调度器 dispatcher
通过 信号 机制,我们可以实现发生相应 事件 以后执行对应的逻辑,但是在实际的系统中,我们存在大量的 事件 需要处理和调度,例如我们可能希望一些 事件 立即被 触发 trigger 以执行对应逻辑,而另一些 事件 则被延迟到稍后再执行
// define a general purpose dispatcher
entt::dispatcher dispatcher{};
struct an_event { int value; };
struct another_event {};
struct listener {
void receive(const an_event &) { /* ... */ }
void method(const another_event &) { /* ... */ }
};
// ...
listener listener;
dispatcher.sink<an_event>().connect<&listener::receive>(listener);
dispatcher.sink<another_event>().connect<&listener::method>(listener);可以通过 dispatcher 的成员函数 trigger 立即向注册监听某事件的 监听器 推送 事件 的发生
dispatcher.trigger(an_event{42});
dispatcher.trigger<another_event>();与事件相关的 监听器 会立刻被调用,但是执行的顺序无法保证,这种触发方式可以被用于推送 紧急消息
而 enqueue 方法则将 事件 放入队列,等待后续触发调用
dispatcher.enqueue(an_event{42});
dispatcher.enqueue<another_event>();事件将被存储在队列中,直到 update 方法被调用,事件才被逐一 触发 emit
// emits all the events of the given type at once
dispatcher.update<an_event>();
// emits all the events queued so far at once
dispatcher.update();如此一来,调度器可以在主循环中执行,每个 tick 依次触发 事件 ,将 事件 分发到相应的 系统 (监听器) 去执行响应逻辑
注意,监听器的执行顺序并不保证是注册顺序
命名队列 Named Queue
在默认情况下,调度器中所有的 事件 都位于同一个默认队列中,Entt 也允许使用唯一标识符创建特殊的 命名队列 以实现更细粒度的事件管理和处理
dispatcher.sink<an_event>("custom"_hs).connect<&listener::receive>(listener);要将事件加入特定的 命名队列,需要使用以下方式
dispatcher.enqueue_hint<an_event>("custom"_hs, 42);事件发射器 emitter
当我们需要管理多个 事件 的发生时,我们会发现 信号 只能实现对单一 事件 通知多个 监听者 执行,如果我们想要管理多个 事件,无疑需要定义多个 信号,而 任务分发器 虽然足以胜任这个任务,但是我们又并不需要它的可延迟处理或者队列功能,此时我们可以使用 事件发射器 emitter 来实现多事件管理和即时触发
事件发射器 类型的声明和定义,只需要继承基础发射器类
struct my_emitter: emitter<my_emitter> {
// ...
}创建 事件发射器 的实例无需任何参数
my_emitter emitter{};事件的 监听器 是可移动和调用的对象 (函数,labmda,函子,std::funtion 等等),并且需要满足以下签名
void(Type &, my_emitter &)其中 Type 是 监听器 希望监听并收到调用的 事件 ,并且 监听器 可以收到 事件发射器 本身
要将 监听器 附加到 事件发射器 中特定的 事件 上,需要调用 事件发射器 的 on 方法
emitter.on<my_event>([](const my_event &event, my_emitter &emitter) {
// ...
});类似的,也有移除 事件发射器 中单个和所有监听器的方法
// resets the listener for my_event
emitter.erase<my_event>();
// resets all listeners
emitter.clear()可以看到,事件发射器 不用预先注册所有可能发射的 事件,只需要直接连接相应 事件 的 监听器,然后直接发射对应的事件
struct my_event { int i; };
// ...
emitter.publish(my_event{42});最后,empty 方法可以测试 发射器 是否没有绑定任何 监听器,contains 方法用于检测特定的 事件 是否存在有效的 监听器 (因为 发射器 只注册 监听器 而不注册 事件)
if(emitter.contains<my_event>()) {
// ...
}事件发射器 对于异步执行非常有用