Joshua
Joshua

Reputation: 56

C++ Event system setup

I am curious as to how I can make the following code actually work. I currently have an event system in place but it is using the observer pattern. I want my window events to work as such :

window win("Engine");

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

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

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

The problem is I don't know where to start. I also want to be able to get specific information on an event, for example the (x, y) coords of where the mouse was clicked in the window. Any input would be greatly appreciated, even if it's just a general idea of what I need to do. Thank you.

Upvotes: 0

Views: 2876

Answers (3)

Jimmy Loyola
Jimmy Loyola

Reputation: 337

This is not a complete example, it is a simple snippet to give you a generic idea on what you're trying to do.

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

Upvotes: 0

kanoisa
kanoisa

Reputation: 420

I think you are going along the right lines so far, but an event system is not a simple thing to build. There is a lot to think about in an events system. I Am assuming you are doing this as a learning exercise so lets not use any libraries. However I would reccommend looking at a few existing event systems to get some inspiration, you might find features you didn't even know you wanted until you see them. These are the steps / thought exercises i suggest for you to take.

  1. Define your events: It sounds like you have done this, but do consider if you want events to be unrelated types or part of an inheritance hierarchy, there are pros and cons to each approach.

  2. Define your publishing mechanism: You said you have observer pattern and thats a good first choice, but consider if it's everything you want. Maybe also consider these things; Should one object only deliver directly to it's indended destination or maybe there is some kind of indirection or delegation involved like a message bus or central event queue. Good news is that designing to an interface like observer pattern suggests makes this easy to change later.

  3. Define your event model in time space: do you need or want immediate, deferred or asynchronous processing of the events? do you need to store them? What about threadding? is this a single threaded system? If it is then thats an additional layer of complexity to consdier.

  4. Define receipt of messages: Kind of linked to 2, Consider object A wants to publish an event but how does it know whom to deliver it to, it might be looked up in some kind of registry it knows about, it might be the argument to a function call, it could be something else you can imagine. Traditionally i've seen this done in two ways one where you can register an event handler with some object who will receive the event then invoke your handler. This mechanism requires you to consider if handlers can be unregistered as well, and if so how. The other is the "function" argument, where a function might be a class member function, or a strategy object or similar.

I think at that stage you will have asked most of the hard questions

heres a quick example of a single threaded event system with events based in a hierarchy: (NOTE, this is NOT following best practices and its not production quality, its the absolute bare minimum to demonstrate the concept, [using c++17 standard for my convienance])

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

Unfortunately thats a fair amount of boiler plate for a simple event system, but once you have it and have tweaked it to be what you want then you can just keep re-using it.

If you run this example output should be quite simply:

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

Upvotes: 3

skaarj
skaarj

Reputation: 100

Some time ago I've had similar problem to yours. For my window I decided to inherit from entt::emitter

class Win32Window : public entt::emitter<Win32Window>

then inside 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 ...
  }
}

And the final result looks like this:

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

Ofcourse you gonna need some event types:

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

Upvotes: 0

Related Questions