Event Handling

From dreamcast.wiki
Jump to navigation Jump to search

Events allow us to send automated messages from one part of our program to other parts. With a simple program like we have now, that’s not so important, but in more complex programs this becomes very important. We’ll create an event handler for Gamepad Input in this section, which will let us start writing code to control our program while it’s running.

We need a central object for parts of our program to talk with. Using global variables is a bad idea on the Sega Dreamcast, the elf executable format has a section for global variables which makes accessing and reading them slow. Instead, we’ll create one object in our Main() function, then pass it around. This object will contain all the info we need to know about the game, we’ll call it a Game State. Let’s create our Game State class. Begin by making a new blank text file named gamestate.cpp. Be sure to add gamestate.o to $(OBJS) in our Makefile.

Create a second blank text file and name it gamestate.h. This is our Header file for the gamestate, we can include it in other files which will make those files able to use it’s functions and interact with the object its class defines. Let’s create our class definition right now in gamestate.h:

#ifndef GAMESTATE_H
#define GAMESTATE_H
	//Game State Class stuff
#endif // GAMESTATE_H

Begin with this ifdef. This is a header guard, it will ensure that we don’t redefine our class definition multiple times. When we include our header in other source files, the #include directive essentially copies and pastes the file into our source. Without these guards, our class definition would be copied multiple times. These guards look for a lock symbol, called GAMESTATE_H. If it doesn’t exist, as it would when this code is encountered for the first time, it’ll run the rest of the header. It begins by immediately defining GAMESTATE_H. This way, the next time this code is run, the preprocessor will skip copying it, preventing our redefinition error. You can do the same thing with #pragma once in c++ files, but this is the old school way that also works in C. We need some header files in our gamestate.h so we can use certain calls in our class:

#include "stdint.h"
#include <kos.h>                /* KalistiOS Dreamcast SDK */

We need to access KOS, along with standard integer types. Now we can begin to define our class:

/* This structure holds all the data about our global game state
* we create this object once, and other objects check and interact
* with it. This lets them communicate with each other. */
class GameState
{
public:
   GameState();
uint32_t Done;          /* Is the game finished running? */
};

Basic stuff, we have a private variable called Done, which represents the state of our program running. It is proper form to make the variable private, and use functions to access it, but for something this simple, it’s just as valid to keep the variable public.

We need to define our constructor in gamestate.cpp:

GameState::GameState()
{
   Done = 0;
}

This function is run the first time our object is created. It will automatically set Done to 0, so our program begins by running. We will create a GameState Object in main and begin using it to control our program loop. In Main.cpp, add our gamestate.h to our includes:

/* Classes */
#include "gamestate.h"

Now, in our Main function, create the GameState Object and make Done the condition in our While...Loop.

	GameState Game;
   /* keep drawing frames until start is pressed */
   while(Game.Done == 0)
   {
       DrawFrame();
    }

If we want to end our program, all we have to do is change Game.Done to 1 and our loop will stop. We can pass our GameState object by reference using pointers to other classes to keep up with this “global” variable, without the penalty of defining it in global space!

Now that we have a GameState to keep track of information, let’s build an Event Handler Class. Make a new text file called MapleEvents.cpp, and MapleEvents.h. Be sure to add MapleEvents.o to $(OBJS), and also add MapleEvents.h to our header section of Main.cpp. Begin in MapleEvents.h:

#ifndef MapleEvents_H
#define MapleEvents_H
#include <arch/gdb.h>           /* gdb debugger */
#include <kos.h>                /* KalistiOS Dreamcast SDK */
#include <png/png.h>            /* Library to load png files */
#include <stdio.h>              /* Standard Input-Output */
#include <stdint.h>             /* Standard Integer types (uint_t, etc) */
/* Classes */
#include "gamestate.h"

#endif // MapleEvents_H

Here we use Include Guards, and include the normal variety of KOS and standard headers. We also include our GameState class, so we can use and manipulate it if we want. Let’s create our MapleEvents class:

class MapleEvents
{
public:
   MapleEvents();  
   void PollEvent(GameState* GState);
};

Aside from our constructor, we also have a function called PollEvent, and it takes a pointer of GameState type. This means we can pass our GameState to it, and it can manipulate it. Define the functions in MapleEvents.cpp:

#include "MapleEvents.h"
MapleEvents::MapleEvents()
{
}

Create a dummy constructor. Now let’s start building our PollEvent function, this is the function which will talk to MAPLE on the dreamcast and get a controller state back:

void MapleEvents::PollEvent(GameState* GState)
{
   MAPLE_FOREACH_BEGIN(MAPLE_FUNC_CONTROLLER, cont_state_t, st)
   
   MAPLE_FOREACH_END()
}

Our function includes a pair of preprocessor macros that KOS provides, called MAPLE_FOREACH_BEGIN and MAPLE_FOREACH_END. These iterate through all the controllers in MAPLE and returns their state in an object. This object is of cont_state_t type. The MAPLE_FOREACH_BEGIN macro takes paramters. The first is the Function code, which tells MAPLE what to operate on. MAPLE_FUNC_CONTROLLER has us working on Dreamcast controllers. Other options available are:

MAPLE_FUNC_KEYBOARD: Poll the Dreamcast Keyboard
MAPLE_FUNC_LIGHTGUN: Poll a lightgun controllers
MAPLE_FUNC_MOUSE: Poll a Dreamcast Mouse
MAPLE_FUNC_MEMCARD: Interact with the Dreamcast VMU
MAPLE_FUNC_PURUPURU: Interact with the Dreamcast Jump Pack
MAPLE_FUNC_LCD: Interact with the VMU screen on the Dreamcast Controller

Our MAPLE_FOREACH_BEGIN macro creates a variable cont_state_t st which holds the polled controller info. This info comes in the form of a uint32_t number, which is interpreted as a bitfield. A 0 indicates a button is not being pressed, and a 1 indicates a button is currently being pressed. These are raw states, in that moment. This information isn’t too useful. If we see a 1 for a button, does that mean the button was just pressed? Is it being held down? If we see a 0, does that mean it was just released? We need more information, which means we need the last state to compare to. Our GameState object is perfect for holding this information in. In our GameState class, add a variable to hold our Previous Controller state:

   cont_state_t PrevControllerState;

Now, when we poll our controller, we can compare the results to the last frame. This means every loop, we need to update the PrevControllerState with that frame’s information. Let’s do this in our PollEvent function. For readability reasons, let’s cache our Buttons into variables:

void MapleEvents::PollEvent(GameState* GState)
{
   MAPLE_FOREACH_BEGIN(MAPLE_FUNC_CONTROLLER, cont_state_t, st)
   uint32_t PrevButtons = GState->PrevControllerState.buttons;
   uint32_t CurrButtons = st->buttons;
   
   GState->PrevControllerState.buttons = CurrButtons;
   MAPLE_FOREACH_END()
}

We get our PrevControllerState buttons into PrevButtons, and we get st->buttons (the current polled controller state) into CurrButtons. At the end of the function, just before MAPLE_FOREACH_END(), we set our PrevControllerState.buttons variable in our GameState, which is located by the GState pointer.

We can now compare button states with two samples. This lets us tell if we have just pressed, just released, or are holding down a button. All of these actions are Events. We need to create an enum type that can keep track of these actions. When we want to raise an event, we will push these enum actions onto a vector to store them. Let’s begin by defining our action events. We will do so in a new text file called EventEnums.h. This isn’t a cpp file, so it doesn’t become compiled into a translation unit object, so no EventEnums.o in our $(OBJS). In EventEnums.h, we can start building our event enum:

#ifndef GAMECONTROLLER_H
#define GAMECONTROLLER_H
enum Event_ControllerButton {
   Up = 0,
   Down = 1,
   Left = 2,
   Right = 3,
   A = 4,
   B = 5,
   C = 6,
   X = 7,
   Y = 8,
   Z = 9,
   Start = 10
};
enum Event_ControllerState {
   None = 0,
   Pressed = 1,
   Hold = 2,
   Released = 3
};
typedef struct _Event_ControllerAction
{
   Event_ControllerButton Button;
   Event_ControllerState State;
} Event_ControllerAction;
#endif // GAMECONTROLLER_H

The idea is to have two different kinds of enums, which get paired together into a structure that represents the event. On one hand of the structure, we’ll have the Event_ControllerButton enum. It has fields for every digital button, Up,down,left,right, ABCXYZ and Start. On the other hand of the event structure, we have the Event_ControllerState enum. This describes how the button behaved during the event: Just pressed down, being held down, or was just released. With this ControllerAction event defined, we can start using it. Add our header to MapleEvents.h:

#include "EventEnums.h"

We need a place to hold our Events as we throw them. Our GameState can hold our events so they can be passed around various parts of our program. In our GameState.h class, add the following header:

#include <vector>
#include "EventEnums.h"

vectors are dynamic arrays in C++. We can create a vector container for our Event_ControllerAction in our GameState, which will let us store events as we throw them. The vector array will grow and shrink as we add or delete Events from it. Add this to the public section of our GameState class:

   std::vector<Event_ControllerAction> ControllerEvents;

Now, let’s code the function analyze our controller state and throw events. We’ll create a helper function called QueueEvent to accomplish this:

void MapleEvents::QueueEvent(GameState* GState, uint32_t CONT_IN, uint32_t CurrButtons, uint32_t PrevButtons, Event_ControllerButton Button)
{
}

This function takes a GameState pointer, so we can access and read and write to the ControllerEvents vector. It takes a uint32_t mask called CONT_IN. We can compare this mask to our buttons state to check individual bits. KOS provides macros for the correct bitfield locations for all the dreamcast controller buttons. This function also takes the current button state in CurrButtons, and the previous button state in PrevButtons. Finally, we set a Event_ControllerButton variable, this is what we will set in our Event structure if we send an event. Inside our function, we begin by creating a temporary Event_ControllerAction called T:

Event_ControllerAction T;
   T.State = None;

We will use T as a blank Event to configure before adding it to our event vector. Next, we’ll check the status of the Current button. The button being checked is determined by CONT_IN. The state in T begins as “None,” which we use as a flag to see if no event related to the CONT_IN button needs to be thrown. Let’s first begin by assuming the button is pressed down:

/* The current button is down */
   if((CurrButtons & CONT_IN))
   {
       /* Was it down a frame before? */
       if(PrevButtons & CONT_IN)
       {
           /* It was down last frame, so it's being held */
           T.Button = Button;
           T.State = Hold;
       } else
       {
           /* It wasn't down a frame before, so it's just been pressed */
           T.Button = Button;
           T.State = Pressed;
       }
   }

The logic is that if the button is pressed down, we will check what it was doing right before. If the button was being pressed down the frame before, then the button is currently being held. We set the State in T to “hold” and record Button we sent in the parameters in T.Button. Otherwise, the button was not being pressed down the frame prior, which means this button has just been pressed. We set out state in T to “Pressed” and record the Button in T as well.

That covers both events that can happen when the button is currently being pressed, but an event can arise if the button is not being pressed. This would happen if the button had been pressed down the frame before. If this discrepancy occurs, then the button has just been released. We can continue with our If statement by appending else to account for the button not being pressed:

else if(!(CurrButtons & CONT_IN) && (PrevButtons & CONT_IN))
   {
       T.Button = Button;
       T.State = Released;
   }

In this case, we set the state in T to “released” and record the button. Now that we have found an event to throw, we can add it from our vector:

   if(T.State != None)
       GState->ControllerEvents.push_back(T);
}

This comes at the end of our function. It first checks if our T.State is anything other than “None.” It will be something else if we threw an event. If so, add it to our ControllerEvents vector in our GameState. Push_Back() is a function that adds an element to the vector array, at the tail end. With our QueueEvent helper function defined, we can start using it in our PollEvent function to check for individual button presses. Add the following to our function:

void MapleEvents::PollEvent(GameState* GState)
{
   MAPLE_FOREACH_BEGIN(MAPLE_FUNC_CONTROLLER, cont_state_t, st)
   uint32_t PrevButtons = GState->PrevControllerState.buttons;
   uint32_t CurrButtons = st->buttons;
   QueueEvent(GState, CONT_START, CurrButtons, PrevButtons, Start);
   QueueEvent(GState, CONT_A, CurrButtons, PrevButtons, A);
   QueueEvent(GState, CONT_B, CurrButtons, PrevButtons, B);
   QueueEvent(GState, CONT_C, CurrButtons, PrevButtons, C);
   QueueEvent(GState, CONT_X, CurrButtons, PrevButtons, X);
   QueueEvent(GState, CONT_Y, CurrButtons, PrevButtons, Y);
   QueueEvent(GState, CONT_Z, CurrButtons, PrevButtons, Z);
   QueueEvent(GState, CONT_DPAD_UP, CurrButtons, PrevButtons, Up);
   QueueEvent(GState, CONT_DPAD_DOWN, CurrButtons, PrevButtons, Down);
   QueueEvent(GState, CONT_DPAD_LEFT, CurrButtons, PrevButtons, Left);
   QueueEvent(GState, CONT_DPAD_RIGHT, CurrButtons, PrevButtons, Right);
   GState->PrevControllerState.buttons = CurrButtons;
   MAPLE_FOREACH_END()
}

We use QueueEvent to check for every button on our Controller. It throws the appropriate events if so. If we run this PollEvent function once per frame, it means all the subsequent functions in the frame can read controller events in our GameState. When some other part of our program uses an Event, it deletes it from the Queue. We call this “consuming” the event. Various parts of our program can have MapleEventss which go through the ControllerEvent vector and check if they have matching mappings for various controller events. By consuming the events, it means the topmost running Program functions take the controller input first. This style of event handler lets events naturally flow to parts of the program which can consumer them as needed. At the very end of our frame, we need to make sure to clear the ControllerEvent vector just in case no part of our program consumed the event.

Let’s demonstrate this by adding an MapleEvents to Main.cpp. Create the following function:

/* This is the function which handles input */
void HandleControllerEvents(GameState* GState)
{   
	static uint8_t Erased = 0;
   	for( std::vector<Event_ControllerAction>::iterator Event_It = ControllerEvents.begin(); 
		Event_It != ControllerEvents.end(); 
		/* not advancing iterator in for-loop */)
   	{
   	uint32_t Erased = 0;  /* flag we use to track if we erased an element or not */
				       
		/*Erasing an element advances the iterator so if we didn't erase, advance it manually */
       	if(!Erased)
	        {
            		++Event_It;
	        }    
    	} /* End ControllerEvents Iterator */
}

This function takes a GameState object via pointer. We create an iterator of type vector<Event_ControllerAction> called Event_It. This sounds scary and complex, but it’s actually very simple. The type inside the <> of the Vector in the declaration tells the Iterator how to set itself up. This allows the iterator to correctly navigate the vector array. We can use this iterator like an index into the vector which moves along a path. Our Iterator begins at the beginning of the vector, then moves to the back of the vector.

Inside this loop as the iterator moves, we use the erase() command to delete the event from the vector. We need to call erase in a way that advances the iterator correctly, or else we’ll delete the element the iterator is pointing to and it’ll get lost. We use a flag called Erased to manage this. When an element is erased, this flag is set to 1. If we get to the end of the loop and we haven’t erased an element (and thus advanced the iterator), then we’ll manually advance it. This keeps things in a way so that the next loop will point to the correct spot.

Now, in between the start of this iterator loop, and the iterator deletion, we can add the checks for button events. Lets make it so when the start button is released, our program quits. We can do that like so:

Event_ControllerButton ButtonInput = It->Button;
       Event_ControllerState StateInput = It->State;
       switch(ButtonInput)
       {
           case Start:
           {
               switch(StateInput)
               {
                   break;
                   case Released:
                   {
                       GState->Done = 1;
                   }
                   break;
               }
           }
           break;
       }; /* End Switch(Button) */

We begin by using some convenience objects for readability. ButtonInput is the Button the event is for. StateInput is the condition of the button event. We begin with a switch statement fort he ButtonInput. This lets us use cases to select by button. Inside the Case for the start button, we can use a switch statement for StateInput to check for the button condition. Under case Released: we set Gstate->Done = 1. This will cause the program to quit when this loop is finished. We need to include a break; command to get out of our switch statements.

We can continue adding cases for other buttons with their own cases for their conditions to further map events to this part of our program. By mapping Case Start: case Released, this particular MapleEvents consumes the Start button if pressed. Now when we press start while our program is running, it’ll properly quit and reboot our dreamcast to dc-load-ip.

Main in Main.cpp should now look like this:

int main(int argc, char **argv) {
   /* init systems  */
   gdb_init();
   pvr_init_defaults();
   GameState Game;
   MapleEvents Maple;
   /* Game Loop */
   while(!Game.Done)
   {    
       Maple.PollControllers(&Game);
       Game.HandleEvents();
       Render();
   }
   return 0;
}

We have a function in our gamestate to let it handle the input events that are thrown by Maple when it calls PollControllers. We will call that function HandleEvents(). Let’s fill that out:

/* We are going to be possibly changing the vector as we iterate through it.
    * This means we will call the vector.erase() function, which automatically
    * returns the address of the next element in the vector, so our iterator
    * is not lost. This creates a conflict if we advance the iterator in the
    * for-loop declaration, so to solve this, we must manually advance the iterator
    * in the loop itself, and only advance it if we haven't erased an element. */
   
   for( std::vector<Event_ControllerAction>::iterator Event_It = ControllerEvents.begin(); Event_It != ControllerEvents.end(); /* not advancing iterator in for-loop */)
   {
	uint32_t Erased = 0;  /* flag we use to track if we erased an element or not */
				       
/* 	Erasing an element advances the iterator so if we didn't erase, advance it manually */
       if(!Erased)
       {
            ++Event_It;
       }

	}

We state by creating an iterator called Event_It. This will walk through our ControllerEvents vector. After creating the iterator, we create a for loop that walks through the vector. It is possible we will be changing this vector as we iterate through it, and this causes a problem. If we advance the iterator through the for-loop declaration, then when we remove an element using vector.erase(), we’ll accidentally advance the iterator twice. This is because vector.erase() automatically advances the iterator when called. What we need to do is track if we call Vector.erase() during an iteration of a loop, and if we did not, then we’ll manually advance the iterator.

To do that, we created a flag called Erased, and set it to 0 (false). At the bottom of our for-loop iteration, we set a condition: If Erased has been set to false (not 1), then advance the iterator with Event_It++. This accounts for if we remove an element while iterating.

Next, inside our for-loop, we can create a switch statement to examine the Button input of our Event. This will let us isolate each case by button, like so:

switch(Event_It->Button)
{
     case Start:
     {
		//Logic for Start Button in here.	

} beak;

}

This first case is for how we should handle events from the start button. Inside this event case, we can create another switch statement for the StateInput from our event. The StateInput tells us what action the Button Event is throwing. We can do so like this:

switch(Event_It->State)
{
    case Released:
    {
        //Released Code
    } break;
}

When the Start button is Released, the following action is performed. In this case, we are setting Done to 1 (true). This will cause our Loop to end in the main() function, which will progress the program to the end, where it will quit. This maps Start to ending the program. Once we perform an action, it is customary for our Event handler to consume the event from the queue. We do that by deleting the iterator. Luckily, C++ provides a way to do this. Container types that impliment iterator, like vector does, have a function that can delete an iterator instance. It returns the address of the next element, which we can use to advance our iterator, so it isn’t lost when our object is deleted. We can do that like so:

case Released:
{
   Done = 1;     /* Set program to end */
   Erased = 1;   /* Set flag so we don't advance iterator twice */
   Event_It = ControllerEvents.erase(Event_It);   /* delete element 
									and advance iterator once */
}

If we have no other controller functions we want to map, we can ignore the rest. However, it’s proper form in a switch statement to cover all possible states. We can fill out the remaining states in our switch functions gracefully. In our switch(Event_It→State) condition:

  case Pressed:
  case Hold:
  case None:
break;

and in our switch(Event_It->Button) condition:

  case Up:
  case Down:
  case Left:
  case Right:
  case A:
  case B:
  case C:
  case X:
  case Y:
  case Z:
break;

The purpose of this system isn’t obvious yet because our program is a monolith, but when we have multiple components that have their own event handlers, it’ll be useful. It lets events trickle down, so you have have context-sensitive inputs. We’ll show how this works later on. For now, at the very end of our loop, we need to empty our ControllerEvents vector, so the new frame begins fresh. We do that with our ClearEvents() function:

void GameState::ClearEvents()
{
   ControllerEvents.erase(ControllerEvents.begin(), ControllerEvents.end());
}

We call this function at the end of our loop in Main.cpp:

/* Game Loop */
   while(!Game.Done)
   {
       /* Input */
       Maple.PollControllers(&Game);
       Game.HandleControllerEvents();
       /* Rendering */
       Game.Render();
       /* Clean up */
       Game.ClearEvents();
   }