Managing application events
Event management is one of the cornerstones that provide us with fluid control experience. Any key presses, window changes, or even custom events created by the GUI system we'll be covering later are going to be processed and handled by this system. In order to effectively unify event information coming from different sources, we first must unify their types by enumerating them correctly:
enum class EventType { KeyDown = sf::Event::KeyPressed, KeyUp = sf::Event::KeyReleased, MButtonDown = sf::Event::MouseButtonPressed, MButtonUp = sf::Event::MouseButtonReleased, MouseWheel = sf::Event::MouseWheelMoved, WindowResized = sf::Event::Resized, GainedFocus = sf::Event::GainedFocus, LostFocus = sf::Event::LostFocus, MouseEntered = sf::Event::MouseEntered, MouseLeft = sf::Event::MouseLeft, Closed = sf::Event::Closed, TextEntered = sf::Event::TextEntered, Keyboard = sf::Event::Count + 1, Mouse, Joystick, GUI_Click, GUI_Release, GUI_Hover, GUI_Leave }; enum class EventInfoType { Normal, GUI };
SFML events come first, since they are the only ones following a strict enumeration scheme. They are then followed by the live SFML input types and four GUI events. We also enumerate event information types, which are going to be used inside this structure:
struct EventInfo {
EventInfo() : m_type(EventInfoType::Normal), m_code(0) {}
EventInfo(int l_event) : m_type(EventInfoType::Normal),
m_code(l_event) {}
EventInfo(const GUI_Event& l_guiEvent):
m_type(EventInfoType::GUI), m_gui(l_guiEvent) {}
EventInfo(const EventInfoType& l_type) {
if (m_type == EventInfoType::GUI) { DestroyGUIStrings(); }
m_type = l_type;
if (m_type == EventInfoType::GUI){ CreateGUIStrings("", ""); }
}
EventInfo(const EventInfo& l_rhs) { Move(l_rhs); }
EventInfo& operator=(const EventInfo& l_rhs) {
if (&l_rhs != this) { Move(l_rhs); }
return *this;
}
~EventInfo() {
if (m_type == EventInfoType::GUI) { DestroyGUIStrings(); }
}
union {
int m_code;
GUI_Event m_gui;
};
EventInfoType m_type;
private:
void Move(const EventInfo& l_rhs) {
if (m_type == EventInfoType::GUI) { DestroyGUIStrings(); }
m_type = l_rhs.m_type;
if (m_type == EventInfoType::Normal){ m_code = l_rhs.m_code; }
else {
CreateGUIStrings(l_rhs.m_gui.m_interface,
l_rhs.m_gui.m_element);
m_gui = l_rhs.m_gui;
}
}
void DestroyGUIStrings() {
m_gui.m_interface.~basic_string();
m_gui.m_element.~basic_string();
}
void CreateGUIStrings(const std::string& l_interface,
const std::string& l_element)
{
new (&m_gui.m_interface) std::string(l_interface);
new (&m_gui.m_element) std::string(l_element);
}
};
Because we care about more than just the event type that took place, there needs to be a good way of storing additional data that comes with it. C++11's unrestricted union is a perfect candidate for that. The only downside is that now we have to worry about manually managing the data inside the union, which comes complete with data allocations and direct invocation of destructors.
As event call-backs are being invoked, it's a good idea to provide them with the actual event information. Because it's possible to construct more complex requirements for specific call-backs, we can't get away with unions this time. Any possible information that may be relevant needs to be stored, and that's precisely what is done here:
struct EventDetails { EventDetails(const std::string& l_bindName): m_name(l_bindName){ Clear(); } std::string m_name; sf::Vector2i m_size; sf::Uint32 m_textEntered; sf::Vector2i m_mouse; int m_mouseWheelDelta; int m_keyCode; // Single key code. std::string m_guiInterface; std::string m_guiElement; GUI_EventType m_guiEvent; void Clear() { ... } };
This structure is filled with every single bit of information that is available as the events are processed, and then passed as an argument to the call-back that gets invoked. It also provides a Clear()
method, because instead of being created only for the time during the call-back, it lives inside the binding structure:
using Events = std::vector<std::pair<EventType, EventInfo>>; struct Binding { Binding(const std::string& l_name) : m_name(l_name), m_details(l_name), c(0) {} void BindEvent(EventType l_type, EventInfo l_info = EventInfo()) { ... } Events m_events; std::string m_name; int c; // Count of events that are "happening". EventDetails m_details; };
A binding is what actually allows events to be grouped together in order to form more complex requirements. Think of it in terms of multiple keys needing to be pressed at once in order to perform an action, such as Ctrl + C for copying a piece of text. A binding for that type of situation would have two events it's waiting for: the Ctrl key and the C key.
Event manager interface
With all of the key pieces being covered, all that's left is actually managing everything properly. Let's start with some type definitions:
using Bindings = std::unordered_map<std::string, std::unique_ptr<Binding>>; using CallbackContainer = std::unordered_map<std::string, std::function<void(EventDetails*)>>; enum class StateType; using Callbacks = std::unordered_map<StateType, CallbackContainer>;
All bindings are attached to specific names that get loaded from a keys.cfg
file when the application is started. It follows a basic format like this:
Window_close 0:0 Fullscreen_toggle 5:89 Intro_Continue 5:57 Mouse_Left 9:0
Of course these are very basic examples. More complex bindings would have multiple events separated by white spaces.
Call-backs are also stored in an unordered map, as well as tied to the name of a binding that they're watching. The actual call-back containers are then grouped by state, in order to avoid multiple functions/methods getting called when similar keys are pressed. As you can imagine, the event manager is going to be inheriting from a StateDependent
class for this very reason:
class EventManager : public StateDependent{ public: ... bool AddBinding(std::unique_ptr<Binding> l_binding); bool RemoveBinding(std::string l_name); void ChangeState(const StateType& l_state); void RemoveState(const StateType& l_state); void SetFocus(bool l_focus); template<class T> bool AddCallback(const StateType& l_state, const std::string& l_name, void(T::*l_func)(EventDetails*), T* l_instance) { ... } template<class T> bool AddCallback(const std::string& l_name, void(T::*l_func)(EventDetails*), T* l_instance) { ... } bool RemoveCallback(const StateType& l_state, const std::string& l_name){ ... } void HandleEvent(sf::Event& l_event); void HandleEvent(GUI_Event& l_event); void Update(); sf::Vector2i GetMousePos(sf::RenderWindow* l_wind = nullptr) const { ... } private: ... Bindings m_bindings; Callbacks m_callbacks; };
Once again, this is quite simple. Since this is a state-dependent class, it needs to implement the ChangeState()
and RemoveState()
methods. It also keeps track of when the window focus is obtained/lost, in order to avoid polling events of minimized/unfocused windows. Two versions of AddCallback
are provided: one for a specified state, and one for the current state. Separate HandleEvent()
methods are also available for every event type supported. So far, we only have two: SFML events, and GUI events. The latter is going to be used in the upcoming section.