Event Handling Tutorial for DarkGDK By Zotoaster
Welcome to my newest tutorial. This is actually my first one for DarkGDK, and infact it's not exactly specific to the GDK, but more for C++. I hope to cover some of the basics of event handling, what it is, how to use it, and how it can be applied to games. This is a very useful technique to make your game easier to produce and maintain.
Note: You will need some basic experience of C++ to understand this tutorial
1) What is an "event handler"?
An event handler does what it says on the tin - it handles events. When you think of a 3D object, you think of something solid. You can pick it up, move it around, spin in, texture it, scale it, etc. But an event is a little more abstract - it's something that happens. An event may infact consist of a series of different (or similar) events, all happening at the same time.
3D objects are very easy to handle, but events aren't. When something important happens in your game you may think you'd need to hard code it. For example, if you walk into a building you might want everything to be silent. When you get to the end you press a button, and as you run out you want explosions to occur around you as you pass by the relevant points. You dont want the building to explode as you walk in. That wouldn't make sense. It might seem to you that the only way to achieve this is by coding individual levels, one by one.
But you don't have to code an object everytime you use it. Everytime you rotate it a little, you don't have to program the vertices to move, and then render it to the screen. How can we apply this useful thinking to something as abstract as events? This is where event handlers come in.
Imagine being able to take an event. Screw around with it. Change it, play with it, etc, as if it were an object. Well, this is what this tutorial is all about.
2) The components of an event handler
The way to handle events is a lot simpler than you might think. Let's begin by looking at real life events. Do they ever just happen? By that I mean do they ever happen without a cause? If they didn't, random events would happen all the time for no reason. So we know that we must have certain triggers to set off these events, and thus, we must find a way to formalise these triggers that link events. Events and triggers are basically the only two things you have to worry about when handling events. The two are almost completely the same in terms of structure and the way you make them. The only main difference is that a) Triggers are checking mechanisms, whereas events are being told to happen, rather than being checked to, 2) Triggers link events, but events don't (unless the event itself is a trigger).
Both triggers and events are built up from two basic ingredients: types of event/trigger, and parameters. This is exactly the same when it comes to objects (atleast in GDK). You have certain object types, such as cube, sphere, mesh, etc., and certain parameters that follow, such as position, angle, etc. If you wanted two cubes, one the right way up and the other tilted to the side, you wouldn't make 8 vertices to represent one cube and position them accordingly, then make another 8 for the next and position them differently. No, you would simply make two cubes with the same default angles, and then rotate one of them. The same goes for events and triggers. Let's say you wanted this to happen: you stand infront of a door and it opens, but at the same time you want all your nearby allies to turn against you and want to kill you.
We'll go through it step by step:
- You make a trigger. This trigger has a type, and it has parameters. In this instance, the type is a box in 3D space that checks if you are standing within it. It would only have two parameters: One for the position of the trigger, and one for the size.
- You make an event. The event type is to open the door. The parameter is the door that you wish to open (this could be a number representing the object number that the door is, or you could pass an object (an OOP object of course) that represents the door that contains all the data about it. It doesn't matter which you do).
- You make another event. The type would be to turn your allies against you. The parameter would be an OO (object oriented) object representing you. You would have to specify yourself, otherwise they would all start killing each other since they are no longer in the same team).
- Finally, you get the trigger's list of events, and add these two to it.
When the trigger detects you walking in the box, the door will open and your friends will betray you.
3) The basic setup of an event handler
Lets look at triggers and events in a little more detail before we figure out how to code them. To start off with, we'll look at events.
--The event--
The first thing you have to do is know how you are going to have your different event types. I would go with an enumerator (enum in C++). These are perfect for storing a list of integers that you don't have to worry about the value. The next thing you have to do is know how to store your parameters. An std::vector is perfect for that. The funny thing about parameters is that you have to have lots of different datatypes. The easiest way to do this is set up a list for each parameter type that you add, then set up the main parameter list. This list has to have two pieces of data: the parameter type (which you can set up with a different enum), and then an integer, who's basic function is to point to a position in any list. The list it points to ofcourse is given in the first piece of data. Then all you have to do is compile this all into a single class.
From a heirarchial point of view, it might look like this:
Event types -> End
-> Die
-> Explosion
-> etc.
Data types -> Integer
-> Float
-> Char*
Event -> Event type
-> List of integer parameters
-> List of float parameters
-> List of char* parameters
-> List of data types / integers
(The coding bit we will come to later).
--The trigger--
Now the trigger is basically *exactly* the same as far as we are concerned right this moment, but requires an extra piece of information. This would be the list of events that you intend to call when you set the trigger off.
Again, from a heirarchial point of view, it might look like this (take notice of the extra piece of information in the trigger):
Trigger types -> Step in box
-> Press button
-> Kill boss
-> etc.
Data types -> Integer
-> Float
-> Char*
Trigger -> Trigger type
-> List of integer parameters
-> List of float parameters
-> List of char* parameters
-> List of data types / integers
-> List of events (class was defined in previous code snippet)
4) Programming an event handler in C++
Finally, we get to the fun bit, I hear you say. Damn right, I reply.
The event handler is really not too difficult to code, so we will get straight to it.
Before we go into the individual components, we will start by enumerating our data-types. This is just a basic list. You can add whatever types you wish in the future, such as particle systems and players.
enum tDataType
{
DT_Int,
DT_Float,
DT_String
};
With that out of the way, let's concentrate on the event.
--Programming the event--
We start obviously will the event types:
enum tEventType
{
EVT_End,
EVT_Die,
EVT_Explode
};
Then, onto the class (we love you C++):
class Event
{
public:
Event( tEventType Type );
void AddParameter( int Data );
void AddParameter( float Data );
void AddParameter( char* Data );
private:
tEventType eType;
std::vector<int> iParam;
std::vector<float> fParam;
std::vector<char*> sParam;
std::vector<std::pair<tDataType, size_t>> Param;
};
A few notes: Firstly, you have to include <vector> into your project for this. Second, you can add as many lists here as you list, each for a different data-type (make sure you also add an identifier for it to the data-type enumerator). Thirdly, notice the last list. The data-type it uses is std::pair<tDataType, size_t>. std::pair<> is a very simple container class that can take two different pieces of data of different types, and can be referenced by .first and .second. size_t is a short way of making an unsigned integer (an integer that can't be negative).
Now that the class has been defined, the functions are easy to make:
// Setup event
Event( tEventType Type ) : eType( Type )
{
// Nothing needs to go in here
}
// Add integer parameter
void AddParameter( int Data )
{
// Add to integer list
iParam.push_back( Data );
// Now, add to parameter list: integer, size of integer list
Param.push_back( std::pair<tDataType, size_t>( DT_Int, iParam.size() - 1 ) );
}
// - These functions are almost identical to the previous -
// Add float parameter
void AddParameter( float Data )
{
// Add to float list
fParam.push_back( Data );
// Now, add to parameter list: float, size of float list
Param.push_back( std::pair<tDataType, size_t>( DT_Float, fParam.size() - 1 ) );
}
// Add string parameter
void AddParameter( char* Data )
{
// Add to string list
sParam.push_back( Data );
// Now, add to parameter list: string, size of string list
Param.push_back( std::pair<tDataType, size_t>( DT_String, sParam.size() - 1 ) );
}
Finally, we need to make a function that executes the event.
void Execute( void )
{
// If event type is to die
if ( eType == EVT_Die )
// Execute code that makes you die
// Might look something like:
Player[ iParam[ Param[ 0 ].second ].Die();
// If event is to end the program
if ( eType == EVT_End )
exit( 0 );
// If event type if to explode something
if ( eType == EVT_Explode )
// Explode particles
}
Let's look at this line of code in particular:
Player[ iParam[ Param[ 0 ].second ].Die();
It seems to have this type of structure: Player[index].Die(). I made this up just for the purposes of showing how parameters work. The index is what we're interested in here.
The index in this case is this: iParam[ Param[ 0 ].second ]
Let's take it apart from the outside in so we can understand it. We have iParam[index]. This will just return one of the integers that you have used as a parameter. The exact index where we have this parameter is held within the actual parameter list. We have used the first parameter: Param[ 0 ], but remember, the parameter list has two associated pieces of data. We only want the second, which points to the correct place in the integer parameter list.
You may be thinking, what's the point in the first piece of data? Well, sometimes you might want the event to have different parameters. For example, I might make the event check if it is a string we are checking for, in which case it would act differently to if it was an integer. In this case for example, I might pass the player number as a parameter, or I might pass the player's name. Either way, you get a different reaction to it.
--Programming the trigger--
This is where it gets boring. The trigger is basically the same as the event (as we already know), so to cut down my workload I'm just going to copy at paste
Trigger types:
enum tTriggerType
{
TGT_InBox,
TGT_PushButton
};
The class:
class Trigger
{
public:
Trigger( tTriggerType Type );
void AddParameter( int Data );
void AddParameter( float Data );
void AddParameter( char* Data );
// Very important
void AddEvent( Event event );
private:
tTriggerType tType;
std::vector<int> iParam;
std::vector<float> fParam;
std::vector<char*> sParam;
std::vector<std::pair<tDataType, size_t>> Param;
// This holds a list of events to be executed
std::vector<Event> Evnt;
};
Notice the extra function and the extra list. There's no point in showing you the code for the parameter functions, and there's probably not much point in showing you the event functions, but I'll do it anyway:
void AddEvent( Event event )
{
Evnt.push_back( event );
}
Now, we add an extra function
in the private section of the class
private:
bool Triggered( void )
{
// If event type is to be in a box
if ( tType == TGT_InBox )
// return true if camera is within given parameters:
if ( dbCameraPositionX() >= fParam[ Param[ 0 ].second ] )
if ( dbCameraPositionY() >= fParam[ Param[ 1 ].second ] )
if ( dbCameraPositionZ() >= fParam[ Param[ 2 ].second ] )
if ( dbCameraPositionX() <= fParam[ Param[ 3 ].second ] )
if ( dbCameraPositionY() <= fParam[ Param[ 4 ].second ] )
if ( dbCameraPositionZ() <= fParam[ Param[ 5 ].second ] )
return true;
// (excuse the mess here).
// <Do something similar for pressing an ingame button here>
// return false by default
return false;
}
This will basically check if the trigger has been set, with respect to the parameters that the trigger requires the check data with.
This is a private function because it is only used by the trigger system itself, however, you can make it a public function if you with to check if a trigger is set... but why would you wish to? This is the entire point of an event system, you don't have to check for triggers manually.
Finally, we just make a handling function. All it does is check for the trigger, and then does what it needs to do when it has been set. Very easy:
void Handle( void )
{
// If triggered
if ( Triggered() )
// loop through events
for ( size_t e = 0; e < Evnt.size(); e++ )
// call events
Evnt[e].Execute();
}
Of course, this just checks for the trigger. If it has been set, set up a simple loop to go through all the events in the list of events, and set them off.
Here is a basic example of it at work now:
// Make my event
Event MyEvent( EVT_MoveCamera ); // <- Totally made up event type
MyEvent.AddParameter( 0.0f );
MyEvent.AddParameter( 0.0f );
MyEvent.AddParameter( 0.0f ); // <- These 3 parameters represent coordinates
// Make a trigger
Trigger MyTrigger( TGT_InBox ); // <- A box that gets triggered if the camera is inside it
MyTrigger.AddParameter( 1000.0f );
MyTrigger.AddParameter( -100.0f );
MyTrigger.AddParameter( 1000.0f ); // <- Staring positions for the bounds of the box
MyTrigger.AddParameter( 5000.0f );
MyTrigger.AddParameter( 100.0f );
MyTrigger.AddParameter( 5000.0f ); // <- Ending positions for the bounds of the box
// Now, add the event to the list of events the trigger will execute
MyTrigger.AddEvent( MyEvent );
// --- Inside the main loop ---
MyTrigger.Handle();
// You don't have to worry now about whether you step inside the box. The trigger will handle this for you.
And that is how one codes an event system.
5) How can I make this useful?
The triggers and events here are very basic. In the event and trigger type enums you can easily add a different type of event. Inside the Execute() (for events) and Triggered() (for triggers) functions, you simply check if that is the given type, you do something different. Adding event and trigger should happen as you work on the game more and more, and you start to make your players, particle systems, doors, etc. It is probably impossible to have it all done early without having all your structs and classes already working, but it is important to get the foundations down early if you wish to make your levels work.
6) How can I take this further?
There is one obvious problem with the current system we have: for every single system you make you have to add another line to your loop telling it to be handled. This can be countered very easily by making an event system class, where you make all your events and triggers as usual, and then add the
triggers to a list in the event system. Then, in your main loop, simply tell it to handle itself, and it will handle everything properly for you.
These event systems are very useful for different missions in your games. You can have the same level twice but with different missions. The great thing about this type of event system is that you can literarly save these missions (the events and the triggers) to a file, that can also link to a level file. This means you can actually have stuff happen in your game without needing to hard code it, and without needing to script it. Imagine this, hypethetically:
Mission MyMission( "MyMissionFile.mis" );
// In the main loop:
MyMission.Handle();
This would automatically mean that things would happen that are essential to the plot but without having to hard code it - pretty awesome eh?
The format for these missions wouldn't be difficult either:
Event1
(
type
param1
param2
param3
etc
)
Event2
(
type
param1
param2
param3
etc
)
Trigger1
(
type
param1
param2
param3
======
Event1
Event2
)
Event1 and Event2 are simply followed by the associated data. The trigger is followed by the associated data, a line "======" to show that you are now about to read events, and then the events that it wants to use (Event1 and Event2). Load this into your game, make it handle itself, and there you go!
Hope you enjoyed my tutorial. Hope it helped, hope it gave you some new ideas, and I hope it makes your game making life easier for you.
Thanks.
Don't you just hate that Zotoaster guy?