我目前正在为我的游戏引擎开发一个事件系统。我想了两种实现方式。
1. 用继承的方式。
class Event
{
//...
Type type; // Event type stored using an enum class
}
class KeyEvent : public Event
{
int keyCode = 0;
//...
}
2. 用联合的方式。
class Event
{
struct KeyEvent
{
int keyCode = 0;
//...
}
Type type; // Event type stored using an enum class
union {
KeyEvent keyEvent;
MouseButtonEvent mouseButtonEvent;
//...
}
}
在这两种情况下,你必须检查 Event::Type
枚举,以知道发生了哪种事件。
当你使用继承的时候,你必须投掷事件来获得它的值(例如投掷 Event
到 KeyPressedEvent
来获取密钥代码)。) 所以你必须将原始指针投向需要的类型,这通常是非常容易出错的。
当你使用union时,你可以简单地检索已经发生的事件。但这也意味着,内存中union的大小总是和它所包含的最大可能的类一样大。这意味着可能会浪费大量的内存。
现在来谈谈这个问题。
是否有任何争论要用一种方式而不是另一种方式?我应该浪费内存还是冒着投错的风险?
还是有什么更好的解决方法是我没有想到的?
最后,我只想把例如 a KeyEvent
作为一个 Event
. 然后我想检查 Event::Type
使用枚举类。如果类型等于说 Event::Type::Key
我想获得关键事件的数据。
Event
似乎更多的是以数据为中心而不是以行为为中心,所以我会选择 std::variant
:
using Event = std::variant<KeyEvent, MouseButtonEvent/*, ...*/>;
// or using Event = std::variant<std::unique_ptr<KeyEvent>, std::unique_ptr<MouseButtonEvent>/*, ...*/>;
// In you are concerned to the size... at the price of extra indirection+allocation
那么用法可能类似于
void Print(const KeyEvent& ev)
{
std::cout << "KeyEvent: " << ev.key << std::endl;
}
void Print(const MouseButtonEvent& ev)
{
std::cout << "MouseButtonEvent: " << ev.button << std::endl;
}
// ...
void Print_Dispatch(const Event& ev)
{
std::visit([](const auto& ev) { return Print(ev); }, ev);
}
不同类型的事件通常包含非常不同的数据,并且有非常不同的用法。我想说这是不适合子类型化(继承)的。
首先我们要明确一点:你不需要用 type
字段。在继承的情况下,你可以通过铸造来确定当前对象的子类。当使用在指针上时。dynamic_cast
返回一个正确子类的对象,或者一个空指针。有一个很好的例子 关于cppreference. 在标记联合的情况下,你应该真正使用一个代表标记联合的类,如 std::variant
,而不是有一个 type
旁边的联合体作为你的类的成员。
如果你要使用继承,典型的方法是将你的事件放在一个数组(向量)中,其中包括 Event
指针,并在由这些指针提供的界面上做一些事情。Event
类。然而,由于这些都是事件,你可能不会根据事件类型做同样的事情,所以你最终会将当前的 Event
对象中的一个子类(KeyEvent
),然后才从子类提供的接口中对其进行处理(使用 keyCode
以某种方式)。)
然而,如果你要使用一个标记的联合,或变体,你可以直接让你的 Event
对象的数组中,而不必处理指针和降频。当然,你可能会浪费一些内存,但你的事件类到底有多大?另外,你可能会因为 Event
对象在内存中的距离很近,此外还不用去引用指针。
同样,我想说变体比子类型化更适合这种情况,因为不同类型的事件通常以非常不同的方式使用。例如,一个 KeyEvent
在几百个键中有一个相关的键("键代码"),没有位置,而一个 MouseEvent
在三个(可能是五个,但不是更多)按钮中,有一个相关联的按钮,还有一个位置,我想不出两者之间有什么共同的行为,除了像时间戳或哪个窗口接收事件这样的一般事情。我想不出两者之间有什么共同的行为,除了很一般的东西,比如时间戳或哪个窗口接收事件。
你可以使用多态,我相信这是一个解决方案,如果你的派生类共享大量的方法,你可以有一个抽象的事件类,从它派生并实现这些方法,然后你可以让它们在同一个容器中,如果做得正确,你将不需要铸造。
类似于这样的做法。
#include <vector>
#include <iostream>
#include <memory>
class Event {//abstract class
public:
virtual void whatAmI() = 0; //pure virtual defined in derived classes
virtual int getKey() = 0;
virtual ~Event(){}
};
class KeyEvent : public Event {
int keyCode = 0;
public:
void whatAmI() override { std::cout << "I'm a KeyEvent" << std::endl; }
int getKey() override { return keyCode; }
};
class MouseEvent : public Event {
int cursorX = 0, cursorY = 0;
public:
void whatAmI() override { std::cout << "I'm a MouseEvent" << std::endl; }
int getKey() override { throw -1; }
};
int main() {
std::vector<std::unique_ptr<Event>> events;
events.push_back(std::make_unique<KeyEvent>()); //smart pointers
events.push_back(std::make_unique<MouseEvent>());
try{
for(auto& i : events) {
i->whatAmI();
std::cout << i->getKey() << " Key" << std::endl;
}
} catch(int i){
std::cout << "I have no keys";
}
}
输出。
I'm a KeyEvent
0 Key
I'm a MouseEvent
I have no keys
这是一个简化的版本,你还是要处理好... 复制构造函数、赋值运算符等.