事件系统:具有类型转换或联合的继承

我目前正在为我的游戏引擎开发事件系统。我考虑过两种实现方法:

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以获取键码)。因此,您必须将原始指针转换为neeeded类型,这通常很容易出错。

使用联合时,您可以简单地检索已发生的事件。但这确实也意味着,内存中的并集大小始终与其所包含的最大类一样大。这意味着可能会浪费大量内存。

现在要问的是:

是否有任何参数可以使用一种方法替代另一种方法?我应该浪费记忆力还是冒错铸造的风险?

或者有没有想到的更好的解决方案?

最后,我只想通过KeyEvent作为Event。然后,我想使用枚举类检查Event::Type。如果类型等于说Event::Type::Key,我想获取按键事件的数据。

iCMS 回答:事件系统:具有类型转换或联合的继承

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

您可以使用多态,如果您的派生类共享大量方法,您可以拥有一个抽象的事件类,从中派生并实现这些方法,然后将它们放在同一个容器中,这是一个解决方案,如果操作正确,则不需要强制转换。

类似的东西:

Live demo

#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

这是简化版,您仍然需要处理copy constructors,assignment operators and such

,

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

让我们先清理一件事:在任何情况下,您都不需要type字段。在继承的情况下,可以通过强制转换确定当前对象的子类。当在指针上使用时,dynamic_cast返回正确子类的对象或空指针。有一个很好的例子on cppreference。对于带标签的联合,您应该真正使用代表标签的联合的类,例如std::variant,而不是在类的联合旁边有一个type

如果要使用继承,通常的做法是将事件放在Event指针的数组(向量)中,并通过{{1}提供的接口对其进行操作}类。但是,由于这些是事件,因此您可能会根据事件类型执行不同的操作,因此最终将当前的Event对象向下转换为一个子类(Event),并且仅然后从子类提供的接口中进行处理(以某种方式使用KeyEvent)。

但是,如果要使用带标签的联合或变体,则可以将keyCode对象直接放置在数组中,而不必处理指针和向下转换。当然,您可能会浪费一些内存,但是您的事件类到底有多大?另外,除了不必取消引用指针之外,还可能因为Event对象在内存中靠得很近而获得性能。

同样,我要说一个变体比子类型化更适合这种情况,因为不同类型的事件通常以非常不同的方式使用。例如,Event在数百个中具有关联的键(“键码”),并且没有位置,而KeyEvent在三个(可能是五个但不多于三个)之中具有关联的按钮一个位置。除了时间戳或哪个窗口接收事件之外,我无法想到两者之间的共同行为。

本文链接:https://www.f2er.com/2261459.html

大家都在问