C++ 事件系统设置

C++ Event system setup

我很好奇如何才能使以下代码真正起作用。我目前有一个事件系统,但它使用的是观察者模式。我希望我的 window 活动像这样工作 :

window win("Engine");

win.on_event(KEY_PRESSED, []{
    // key pressed
});

win.on_event(KEY_RELEASED, []{
    // key released
});

win.on_event(MOUSE_CLICKED, []{
    // mouse clicked
});

问题是我不知道从哪里开始。我还希望能够获得有关事件的特定信息,例如 window 中鼠标点击位置的 (x, y) 坐标。任何输入将不胜感激,即使它只是我需要做什么的一般想法。谢谢。

前段时间我遇到了和你类似的问题。 对于我的 window,我决定继承自 entt::emitter

class Win32Window : public entt::emitter<Win32Window>

然后在WndProc

里面
LRESULT Win32Window::_wndProc(HWND, UINT, WPARAM wParam, LPARAM lParam) {
  switch (uMsg) {
  case WM_MOUSEMOVE:
    if (!empty<MouseMoveEvent>()) {
      publish<MouseMoveEvent>(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
      return 0;
    }
    break;
  // other messages ...
  }
}

最后的结果是这样的:

window.on<ResizeWindowEvent>([](const auto &evt, auto &) {
  ImGui::GetIO().DisplaySize = glm::vec2{evt.width, evt.height};
});
window.on<MouseMoveEvent>([](const auto &evt, auto &) {
  ImGui::GetIO().MousePos = glm::vec2{evt.x, evt.y};
});

当然你需要一些事件类型:

struct MouseMoveEvent {
  int32_t x, y;
};
struct CloseWindowEvent {};
// and so on ...

我认为到目前为止您的方向是正确的,但是构建事件系统并不是一件简单的事情。在事件系统中有很多需要考虑的地方。我假设您将此作为学习练习,所以不要使用任何库。但是,我建议查看一些现有的事件系统以获得一些灵感,您可能会发现在看到它们之前甚至不知道自己想要的功能。这些是我建议您采取的步骤/思维练习。

  1. 定义您的活动: 听起来您已经这样做了,但是请考虑您是否希望事件是不相关的类型或继承层次结构的一部分,每种方法都有利有弊。

  2. 定义您的发布机制: 你说你有观察者模式,这是一个很好的首选,但考虑一下它是否是你想要的一切。也许还要考虑这些事情;一个对象应该只直接传送到它的预定目的地,还是可能涉及某种间接或委托,如消息总线或中央事件队列。好消息是设计一个像观察者模式这样的界面可以让以后很容易改变。

  3. 及时定义事件模型space: 您是否需要或想要立即、延迟或异步处理事件?你需要储存它们吗?线程呢?这是单线程系统吗?如果是,那就是复杂的额外一层。

  4. 定义消息接收: 有点与 2 相关联,考虑对象 A 想要发布一个事件,但它如何知道将它传递给谁,它可能会在它知道的某种注册表中查找,它可能是函数调用的参数,它可能是您可以想象的其他东西。传统上我已经看到这通过两种方式完成,一种是您可以向某个对象注册一个事件处理程序,该对象将接收事件然后调用您的处理程序。此机制要求您考虑是否也可以取消注册处理程序,如果可以,如何取消注册。另一个是“函数”参数,其中一个函数可能是一个 class 成员函数,或者一个策略对象或类似的。

我认为在那个阶段你会问出大部分难题

这是一个单线程事件系统的快速示例,事件系统基于层次结构:(注意,这不是遵循最佳实践,也不是生产质量,它绝对是演示概念的最低限度,[使用 c ++17 为我的方便标准])

#include <iostream>
#include <functional>
#include <string>
#include <string_view>
#include <vector>

// Lets define a base event type
struct event_base {
    explicit event_base(std::string const& event_type) : type{event_type} {}
    std::string type;
};

// Now a more specific event type
struct name_changed : public event_base  {
    constexpr static auto event_type = "name_changed_event";

    name_changed(std::string old_name, std::string new_name) :
        event_base(event_type),
        old_name{std::move(old_name)},
        new_name{std::move(new_name)}
    {}

    std::string old_name;
    std::string new_name;
};

// Next our observer interface
using event_handler = std::function<void(event_base const&)>;
/* could do these the traditional way with a class interface but i
 * prefer a std::function when there only one method to implement
 * and it means i dont have to bring out unique_ptrs or anything
 * for this example
 */

// And a structure to associate an observer with an event type
struct registered_handler {
    std::string event_type;
    event_handler handler;
};

// A simple example observable person 
class person_info {
public:
    person_info(int age, std::string name) :
        m_age{age},
        m_name{std::move(name)}
    {}

    void add_handler(std::string const& event_type, event_handler const& handler) {
        m_handlers.push_back({event_type, handler});
    }

    void set_name(std::string new_name) {
        // check for change
        if(new_name == m_name) {
            return;
        }
        // build event
        name_changed const event {m_name, new_name};

        // make change
        std::swap(m_name, new_name);

        // publish events
        if(auto *const handler = get_handler_for_event_type(event.type); handler != nullptr) {
            handler->handler(event);
        }
    }

    void set_age(int new_age) {
        // same thing here but using age and age change event
    }

private:
    registered_handler* get_handler_for_event_type(std::string const& event_type) {
        auto const& existing_handler = std::find_if(std::begin(m_handlers), std::end(m_handlers), [&](auto const& h){
            return h.event_type == event_type;
        });

        if(existing_handler != std::end(m_handlers)) {
            return &(*existing_handler);
        } else {
            return nullptr;
        }
    }

    int m_age;
    std::string m_name;
    std::vector<registered_handler> m_handlers;
};

// And a main to exercise it 
int main() {
    person_info my_guy{25, "my guy"};
    my_guy.add_handler(name_changed::event_type, [](event_base const& event) {
        auto const& name_change_event = reinterpret_cast<name_changed const&>(event);
        std::cout << "My guy changed his name from: " << name_change_event.old_name << " to: " << name_change_event.new_name << "\n";
    });

    my_guy.set_name("someone else");
}

不幸的是,对于一个简单的事件系统来说,这是相当多的样板文件,但是一旦您拥有它并将其调整为您想要的,您就可以继续重复使用它。

如果你运行这个例子输出应该很简单:

My guy changed his name from: my guy to: someone else

这不是一个完整的示例,它只是一个简单的片段,可以让您大致了解您正在尝试做什么。

#include <iostream>
#include <functional>
#include <thread>
#include <Windows.h>
#include <map>

enum class MOUSE_EVENT_TYPE {
    MOUSE_CLICKED,
    MOUSE_MOVE
};

struct window
{
    std::thread *th;
    bool loop{ true };
    std::map < MOUSE_EVENT_TYPE, std::function<void()>> func;

    window()
    {
        th = new std::thread([this]() {
            while (loop)
            {
                if (func[MOUSE_EVENT_TYPE::MOUSE_MOVE] && GetCursorPos())
                {
                    func[MOUSE_EVENT_TYPE::MOUSE_MOVE](p);
                }

                if (func[MOUSE_EVENT_TYPE::MOUSE_CLICKED] && GetAsyncKeyState(VK_LBUTTON))
                {
                    func[MOUSE_EVENT_TYPE::MOUSE_CLICKED]();
                }
            }
        });
    }

    void on_event(MOUSE_EVENT_TYPE event, std::function<void(POINT)> fn)
    {
        if (!func[event])
        {
            func[event] = fn;
        }
    }
};

int main()
{
    window win;
    win.on_event(MOUSE_EVENT_TYPE::MOUSE_MOVE, []() {
        std::cout << "mouse moved" << std::endl;
    });

    win.on_event(MOUSE_EVENT_TYPE::MOUSE_CLICKED, []() {
        std::cout << "mouse clicked" << std::endl;
    });

    win.th->join();
}