EnTT: Event System 事件系统

整理 EnTT 的 signal、dispatcher 和 emitter 三类事件工具及其基本使用方式。

信号 signal

在 EnTT 框架中,entt::sighentt::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>()) {
    // ...
}

事件发射器 对于异步执行非常有用