01 - 事件模块
想往游戏引擎中添加事件系统,刚好 EnTT 库里有相关内容,康康能不能应用上。
EnTT 事件模块
委托 Delegate
委托可以作为一个通用的没有内存开销的调用者,用于触发一般函数,Lambda 函数,实例提供的成员函数等。
创建一个空委托的代码如下:
entt::delegate<int(int)> delegate{};
需要提供让委托触发的函数签名。如果直接让空委托触发函数,可能会造成如崩溃等的未定义行为。因此有必要检查要使用的委托是否为空:
if(delegate) { // ... }
为了初始化一个委托,需要使用它的.connect()
成员函数:
int f(int i) { return i; } // 向委托绑定一个一般函数 delegate.connect<&f>(); struct my_struct { int f(const int &i) const { return i; } }; // 向委托绑定一个成员函数 my_struct instance; delegate.connect<&my_struct::f>(instance);
如果想要同时创建并初始化一个委托,需要使用 entt::connect_arg
:
entt::delegate<int(int)> func{entt::connect_arg<&f>};
接下来看看如何调用委托绑定的函数。对于一般函数,像平常调用函数那样即可。对于成员函数,如果它与类实例有关联,就需要显式提供一个实例作为第一参数:
entt::delegate<void(my_struct &, int)> delegate; delegate.connect<&my_struct::f>(); my_struct instance; delegate(instance, 42);
需要注意的是,没有形如.disconnect()
的成员函数,因此不能注销单个绑定的函数。但可以通过.reset()
来清除委托的所有绑定。
此外,对于形如 void(T&, args...)
的一般函数,第一个参数 T&
是该函数的 “载荷”,函数在每次被触发时都会引用该参数:
void g(const char &c, int i) { /* ... */ } const char c = 'c'; delegate.connect<&g>(c); delegate(42);
其中,函数 g
随着引用 c
和参数 42
被触发。但委托的函数签名实际上是 void(int)
。并且,在委托触发其绑定的函数时会忽略掉过多的参数。
Lambda 支持
entt::delegate
只支持没有捕获的 Lambda 函数:
my_struct instance; // use the constructor ... entt::delegate delegate{+[](const void *ptr, int value) { return static_cast<const my_struct *>(ptr)->f(value); }, &instance}; // ... or the connect member function delegate.connect([](const void *ptr, int value) { return static_cast<const my_struct *>(ptr)->f(value); }, &instance);
委托接收的函数签名仍是 int(int)
,这里的第一参数 const void*
只是用于传递用户定义的数据结构。
原始数据访问
尽管不推荐该操作,但 entt::delegate
支持对委托存储可调用对象的直接访问操作。这使得对特定实例强制调用一些委托函数成为可能:
my_struct other; delegate.target(&other, 42);
在不知道可调用对象是什么类别(普通函数,成员函数,Lambda)时,谨慎使用该操作。 常规操作则是利用此特性来辨别特定的委托,通过 traits
。
信号 Signals
信号一般和类引用,函数指针,成员指针配合使用。监听器可以是任何类型,并且用户可以对一个信号在不同生命周期进行连接和断连操作。信号在内部使用委托,因此它可以提供和委托类似的功能。
一个信号句柄(Signal handler)可被用作一个私有数据成员,并且不会向类的客户端暴露任何功能。信号句柄的 API 很直白。如果该信号句柄支持收集功能,那么就能在接收事件的时候获取相关参数,并在之后使用;如果该信号句柄不支持收集功能,那么可用于周期发射事件。
要想新建一个信号句柄实例,需要为它指定接收的函数类型:
entt::sigh<void(int, char)> signal;
常用到的成员函数如下:
.size()
:获取当前信号句柄内监听器的个数;.empty()
:当前信号句柄内监听器是否为 0;.swap()
:交换两个信号句柄;
信号的相关操作(如连接和断连)需要 sink
类帮忙:
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();
如上所述,监听器的函数签名并不和信号的严格匹配。只需要监听器可以被所给参数正确触发,并返回一个可被转化为目标返回类型的值即可。
.connect()
成员函数会默认返回一个 connection
对象,该对象可被用作断连操作的替代方法,需要使用它的.release()
成员函数。此外,也能通过 connection
对象创建一个 scoped_connection
对象,当脱离它的作用域后,会自动断连。
发布与收集
当信号被附加监听器(或根本没有)时,就可以通过它的.publish()
成员函数来发布事件和数据:
signal.publish(42, 'c');
要想收集数据,需要使用.collect()
成员函数:
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);
收集器必须支持 operator()
,并且要接收一个能被监听器返回值转换的类型参数。此外,还能通过返回一个布尔值标识是否该停止收集数据,这可以避免在非必要条件下调用所有监听器。
收集器不仅可以是 Lambda,还能是仿函数:
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));
事件分发器
事件分发器允许用户触发立即事件,或将其放到队列中延迟一起发布。创建事件分发器的代码如下:
// define a general purpose dispatcher entt::dispatcher dispatcher{};
与事件分发器注册的监听器,它的类型可以提供一或多个成员函数,接收 Event&
类型的参数,不管返回值。
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);
可以通过.disconnect()
来解绑事件对应的监听器(一个或所有):
dispatcher.sink<an_event>().disconnect<&listener::receive>(listener); dispatcher.sink<another_event>().disconnect(&listener);
可以通过.trigger()
来向所有注册的监听器发送立即事件:
dispatcher.trigger(an_event{42}); dispatcher.trigger<another_event>();
此时相关监听器被马上调用,不保证执行顺序。
可以通过.enqueue()
让延迟发送的事件进入消息队列:
dispatcher.enqueue<an_event>(42); dispatcher.enqueue(another_event{});
在合适的事件通过.update()
发送队列中的所有事件:
// 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();
命名队列
默认是按事件类型进行队列区分,也可以通过不同的名字来创建队列(可以造成同类型事件但多个队列):
dispatcher.sink<an_event>("custom"_hs).connect<&listener::receive>(listener); dispatcher.enqueue_hint<an_event>("custom"_hs, 42);
事件发射器
这个通用事件发射器被用于异步环境下。一般来说,当派生类封装异步操作时,它是一个方便的工具,但并不局限于这种用途。
创建的事件发射器类型必须基于 enitter
类:
struct my_emitter: emitter<my_emitter> { // ... }
创建一个新的发射器实例不需要参数:
my_emitter emitter{};
监听器是可移动且可调用的对象(一般函数,Lambda 函数,仿函数,std::function
),并且它们的签名为:
void(Type &, my_emitter &)
其中 Type
就是它们要接收的事件类型。
可通过.on()
将监听器绑定到发射器上:
emitter.on<my_event>([](const my_event &event, my_emitter &emitter) { // ... });
可以通过.erase()
移除特定事件的监听器,也能通过.clear()
移除所有监听器:
// resets the listener for my_event emitter.erase<my_event>(); // resets all listeners emitter.clear()
可通过 publish()
发送事件:
struct my_event { int i; }; // ... emitter.publish(my_event{42});
可通过.empty()
查看注册的监听器是否为空;可通过.contains<T>()
查看是否有相应事件的监听器。
实践
详见我