Events - dprandle/nsengine GitHub Wiki
The event system is used for global events - anything that multiple systems could possibly respond to. Events are distinguished by their types. When registering to receive events, an event handler registers to receive events of a certain type. Any event that is created of that type will then be routed to the handler.
Events are not handled immediately, but instead added to a queue. In the engine, all systems have their own event queue which is processed immediately before the system is updated.
Events are a good tool for situations where many things need to happen when some event occurs, and these things might be contigent upon game states or other things. For example - if a key is pressed many things might need to happen based on the current state. Perhaps the character needs to jump, or a weapon needs to fire. If all of this was handled directly where OS messages are received - it would get really messy. All of the gameplay code would essentially be wherever the OS mouse and key messages are being received. This would be fine for very small/simple games, but would quickly get unmanageable for larger projects.
Another example - what if there is a collision between two objects. Multiple things might need to happen depending on what the game state is. Maybe nothing should happen - maybe some damage needs to be applied to one of the objects - maybe a force should be applied. Rather than having to go back and edit code each time different collision requirements are added, a collision event can be sent out and different systems can register to receive the event and respond however they need to.
Why can systems register to receive events? Because....
All systems are “event handlers” - that’s because nssystem inherits from nsevent_handler. This means a function can be registered in the system to deal with all events of a given type. The given event type is determined by the function’s only parameter. A function that does not have a single parameter of a pointer type deriving from “nsevent” cannot be registered as a handler - it will result in a compile time error. As an example, take a look at raw input events.
There are four raw input event types:
- nskey_event
- nsmouse_button_event
- nsmouse_move_event
- nsmouse_scroll_event
Each of these events have some members with key information - the key event for example has a bool for whether the key was pressed or released, and a nsinput_map::key_val - which is a member with an enum type saying which key on the keyboard was pressed (ie nsinput_map::key_d).
If a system should receive key events, register a function within the system to handle key events. This is a member function with any name, but it must have a single parameter of type “nskey_event *”. A good place for this code is in the system init function, which is called once on system creation by the engine:
class my_system : public nssystem
{
public:
// other system code
// init function called on system creation
void init();
// event handling function
void handle_key_event(nskey_event * kevent);
private:
// other system vars
};
// in the cpp file
void my_system::init()
{
register_handler(&my_system::handle_key_event);
}
void my_system::handle_key_event(nskey_event * kevent)
{
if (kevent->key == nsinput_map::key_d)
{
if (kevent->pressed) // only if pressed, not released
{
do_jump();
}
}
}
In this sense, it’s possible to quickly build a simple game which responds directly to key and mouse input - or other events. However, the engine comes with a system - the “nsinput_system” - which translates raw input in to action/state events based on a key mapping file. See the Input section for more information on the input system and action/state events.
Any object can be an event handler by inheriting from nsevent_handler. This will allow that object to register functions with the macro “register_handler” - provide the function for handling events as an argument to the macro. The function provided to the “register_handler” macro must be a member function of the event handler (often will be a system since all systems inherit from event_handler), and the function must contain a single parameter with a pointer type derived from nsevent - as explained earlier.
Again, if any of the above conditions are not met, compilation will fail. You cannot, for example, register a function that has a single parameter of a type not derived from nsevent, or register a function with more than one parameter. The compiler uses the function parameter to determine what type of event to listen for.
Another thing to note - the handler function’s parameter type determines EXACTLY which events are routed to the function. You cannot use abstraction to receive all events by making a handler function for a base type. IE…
my_class::my_handler_func(nsevent * evnt);
will not receive all events. This function would only receive events if they are explicitly created as nsevents - for example an event created in the following fashion would be routed to the above function:
nse.event_dispatch()->push<nsevent>();
but an event such as:
nse.event_dispatch()->push<nskey_event>()
would not be routed as it’s explicit type is nskey_event, not nsevent despite deriving from nsevent.
After registering a function with “register_func”, all events with the same type as the function’s parameter will be routed to that function. This processing of events happens automatically for all systems in the engine immediately before the system is updated. All events are processed in FIFO (first in first out) order, which is normal for a queue.
For any other types of event handlers (if a custom handler is made by inherting from nsevent_handler), this processing needs to be manually arranged. The function to process all events for an event handler is:
nsevent_dispatch::process(nsevent_handler * handler);
It is also possible to get and/or remove the next event in the queue or the last event in the queue (most recently added). This functionality can be used to turn the event “queue” (FIFO) in to an event “stack” (LIFO). It is also possible to push events to the back of the queue (using push_back instead of push), which would cause the default “process” function to process those events first.
Though there are existing event types ready to use, custom events are easy to create. These custom events can then be pushed to the event dispatcher at any time in the engine, and any system which registers an event handler function will receive a copy of this event.
To create a new event type, create a struct which inherits from nsevent. This is the only thing required - you can then add any members to the struct that will be needed to respond to the event. A nice thing to do also is to make a constructor for the event where you can pass in all of the member values to intialize the event. For example - the “my_custom_event” is shown below.
struct my_custom_event : public nsevent
{
my_custom_event(uint32 useful_number_=0):
nsevent(),
useful_number(useful_number_)
{}
uint32 useful_number;
};
This makes it so that when you push this event, you can pass the parameters in to the event dispatcher’s push function directly. For example:
// in some function somewhere
nse.event_dispatch()->push<my_custom_event>(23);
This would set the “useful_number” to 23.
If there was no constructor in the my_custom_event, the following code would accomplish the same thing:
my_custom_event * my_event = event_dispatch()->push<my_custom_event>();
my_event->useful_number = 23;
To handle this event - the most straight forward thing to do is add a function to a custom system.
#include <nssystem.h>
class my_system: public nssystem
{
public:
my_system():nssystem() {}
~my_system() {}
void init()
{
register_handler(&my_system::custom_event_handler);
}
void release() {}
void update()
{
// Do important stuff
}
int32 update_priority()
{
return 2500; // select a number to decide when to update (see Systems)
}
private:
bool custom_event_handler(my_custom_event * evt)
{
if (evt->useful_number == 23)
{
// do some stuff
}
return true;
}
};
If Systems are new to you, check out the Systems section. To register the system type, call the following function before calling nse.start():
nse.register_system<my_system>("my_system");
The system will be automatically added when nse.start() is called.
Sometimes events are great, and sometimes they overcomplicate things. This event infrastructure is provided as another tool in the toolbox which can be used for better or for worse. They were originally created to deal with input - and work great for that purpose.