事件系统。继承与类型转换或联合。

问题描述 投票:0回答:2

我目前正在为我的游戏引擎开发一个事件系统。我想了两种实现方式。

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 枚举,以知道发生了哪种事件。

当你使用继承的时候,你必须投掷事件来获得它的值(例如投掷 EventKeyPressedEvent 来获取密钥代码)。) 所以你必须将原始指针投向需要的类型,这通常是非常容易出错的。

当你使用union时,你可以简单地检索已经发生的事件。但这也意味着,内存中union的大小总是和它所包含的最大可能的类一样大。这意味着可能会浪费大量的内存。

现在来谈谈这个问题。

是否有任何争论要用一种方式而不是另一种方式?我应该浪费内存还是冒着投错的风险?

还是有什么更好的解决方法是我没有想到的?

最后,我只想把例如 a KeyEvent 作为一个 Event. 然后我想检查 Event::Type 使用枚举类。如果类型等于说 Event::Type::Key 我想获得关键事件的数据。

c++ events event-handling game-engine unions
2个回答
3
投票

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);
}

1
投票

不同类型的事件通常包含非常不同的数据,并且有非常不同的用法。我想说这是不适合子类型化(继承)的。

首先我们要明确一点:你不需要用 type 字段。在继承的情况下,你可以通过铸造来确定当前对象的子类。当使用在指针上时。dynamic_cast 返回一个正确子类的对象,或者一个空指针。有一个很好的例子 关于cppreference. 在标记联合的情况下,你应该真正使用一个代表标记联合的类,如 std::variant,而不是有一个 type 旁边的联合体作为你的类的成员。

如果你要使用继承,典型的方法是将你的事件放在一个数组(向量)中,其中包括 Event 指针,并在由这些指针提供的界面上做一些事情。Event 类。然而,由于这些都是事件,你可能不会根据事件类型做同样的事情,所以你最终会将当前的 Event 对象中的一个子类(KeyEvent),然后才从子类提供的接口中对其进行处理(使用 keyCode 以某种方式)。)

然而,如果你要使用一个标记的联合,或变体,你可以直接让你的 Event 对象的数组中,而不必处理指针和降频。当然,你可能会浪费一些内存,但你的事件类到底有多大?另外,你可能会因为 Event 对象在内存中的距离很近,此外还不用去引用指针。

同样,我想说变体比子类型化更适合这种情况,因为不同类型的事件通常以非常不同的方式使用。例如,一个 KeyEvent 在几百个键中有一个相关的键("键代码"),没有位置,而一个 MouseEvent 在三个(可能是五个,但不是更多)按钮中,有一个相关联的按钮,还有一个位置,我想不出两者之间有什么共同的行为,除了像时间戳或哪个窗口接收事件这样的一般事情。我想不出两者之间有什么共同的行为,除了很一般的东西,比如时间戳或哪个窗口接收事件。


1
投票

你可以使用多态,我相信这是一个解决方案,如果你的派生类共享大量的方法,你可以有一个抽象的事件类,从它派生并实现这些方法,然后你可以让它们在同一个容器中,如果做得正确,你将不需要铸造。

类似于这样的做法。

现场演示

#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

这是一个简化的版本,你还是要处理好... 复制构造函数、赋值运算符等.

© www.soinside.com 2019 - 2024. All rights reserved.