| Games and SDL SDL Installation SDL for Embedded SDL API SDL Events | SDL Graphics SDL Threads Thread Example SDL Animation SDL Sound | Raw Video Player Video Formats Video Compression | Game Trees About The Author |
An SDL Multi-threaded Example: Tic-Tac-Toe
In the game here, a user plays against the computer. The player can mark a quadrant by clicking the mouse or hitting a numeric key with a value corresponding to the desired quadrant number. Some background music is played while the game proceeds. When the game ends, the music stops and special sounds depending on the winning outcome is played.
We shall also use a tic-tac-toe game to illustrate the concept of game trees in a later chapter. At the moment we simply use a simple table lookup technique to make the move rather than using the more sophisticated technique 'minimax search' presented in the Game Tree chapter to find an optimal move.
The application consists of four short programs:
| tictactoe.cpp: | consisting of main(), creating other threads | |
| game_thread.cpp: | consisting of the thread and functions to play the game | |
| io_threads.cpp: | containing threads for handling mouse and keyboard events | |
| sound.cpp: | containing a thread and functions for handling sound |
/*
tictactoe.cpp
main entry of the tic-tac-toe game
*/
#include <SDL/SDL.h>
#include <SDL/SDL_thread.h>
#include <stdlib.h>
#include <stdio.h>
#include <deque>
#include "surface.h"
#include "io_threads.h"
#include "sound.h"
using namespace std;
SDL_mutex *key_mutex; //mutex for accessing key queues
SDL_mutex *mouse_mutex; //mutex for accessing mouse queues
extern deque<Point>mouseq; //queue to save mouse coordinates
extern bool quit; //defined in io_threads.cpp to signal all threads to quit
int main()
{
SDL_Surface *screen;
const int VWIDTH = 640;
const int VHEIGHT = 480;
Surface surf( VWIDTH, VHEIGHT, "Tic-tac-toe" );
SDL_SetEventFilter ( FilterEvents );
//create mutexes for accessing key queues and mouse queue
key_mutex = SDL_CreateMutex();
mouse_mutex = SDL_CreateMutex();
if ( key_mutex == NULL )
printf( "Failed to create key_mutex!\n");
if ( mouse_mutex == NULL )
printf( "Failed to create mouse_mutex!\n" );
SDL_Thread *kthread, *mthread, *gthread, *sthread;
kthread = SDL_CreateThread( key_thread, NULL);
mthread = SDL_CreateThread( mouse_thread, NULL);
gthread = SDL_CreateThread( game_thread, &surf );
sthread = SDL_CreateThread( sound_thread, &surf );
while( !quit ) { //keep updating screen until some thread wants to quit
SDL_PumpEvents();
if ( SDL_PeepEvents ( NULL, 0, SDL_PEEKEVENT, SDL_QUITMASK) ) {
break;
}
//Update the screen
surf.updateSurface();
SDL_Delay(20); //give up some CPU time
}
//don't exit until all threads are done
SDL_WaitThread( mthread, NULL );
SDL_WaitThread( kthread, NULL );
SDL_WaitThread( gthread, NULL );
SDL_WaitThread( sthread, NULL );
//release the resources
SDL_DestroyMutex ( mouse_mutex );
SDL_DestroyMutex ( key_mutex );
return 0;
}
|
It uses SDL_CreateThread() to create four threads, namely, kthread for handling keyboard inputs, mthread for handling mouse inputs, gthread for handling the game, and sthread for handling sound. Prior to creating the threads, it also creates two mutexes, key_mutex and mouse_mutex which will be used to enforce that only a single thread can access shared key and mouse variables. The function SDL_EventFilter() sets up a filter to process all events before they are posted to the event queue; this feature makes handling SDL events very flexible. In our case the filter function is FilterEvents(), which is defined in the program "io_threads.cpp".
Note that the Surface class discussed in Chapter SDL Graphics is used to create an SDL surface to present the graphics.
After created the threads, the program sits in a while loop to update the screen until the global variable quit is set to true by a thread. It can also break out of the while loop if the event 'SDL_QUITMASK' is detected by SDL_PeepEvents(). The details of SDL_PeepEvents() is listed below. Each thread takes care of her own business and the threads may communicate via a few shared variables.
| Synopsis |
#include "SDL.h"
int SDL_PeepEvents(SDL_Event *events, int numevents, SDL_eventaction action, Uint32 mask); |
|
|---|---|---|
| Description |
Checks the event queue for messages and optionally returns them.
Up to numevents will be added to the event queue:
action = SDL_PEEKEVENT: added at the front, matching mask, will be returned but not removed from the queue. action = SDL_GETEVENT: added at the front, matching mask, will be returned removed from the queue. You may have to call SDL_PumpEvents before calling this function. Otherwise, the events may not be ready to be filtered when you call SDL_PeepEvents(). |
|
| Returns | number of events actually store, or -1 on error |
The program game_thread.cpp listed below is responsible for playing the game. It contains the logic of making a best move. The function game_thread() is the entry point of the thread. After calling init_board() to initialize the 'board' for playing, it enters a while loop to check if the mouse queue or num queue is nonempty. If yes, it means that the player has made a move; the computer then takes appropriate action depending on the player's move. In particular, it calls the function load_image() to load a cross or a circle image on the board. When an outcome is decided, a music is played by calling the function play_sfx (). A player can start another game by hitting the key 's'. As mentioned above, a simple table-lookup technique is used to decide the computer's move.
/* game_thread.cpp -- thread functions for playing the game Four lines are drawn to form the figure #. The quadrants are labeled from 0 to 8 with the upper left corner labeled as 0, the next column as 1; next row starts at 3. */ #include "SDL/SDL.h" #include "SDL/SDL_image.h" #include "SDL/SDL_mixer.h" #include "SDL/SDL_thread.h" #include <string> #include <iostream> #include <stdlib.h> #include <vector> //STL library #include <deque> //STL Library #include "surface.h" #include "point.h" #include "sound.h" using namespace std; extern deque |
The program io_threads.cpp listed below takes care of the keyboard and mouse events. The function FilterEvents() is to customize the actions we want to take when we detect an event; we can either post the event in the SDL event queue or discard. This function is activated by SDL_setEventFilter ( FilterEvents ) in main() of tictactoe.cpp. The two threads, mouse_thread() and key_thread() run independently. The mouse_thread() detects if any mouse event has been posted to the SDL event queue; if there is, it puts the mouse coordinate into the queue mouseq. The key_thread() handles keyboard events posted to the SDL event queue. It saves any key value in the queue keyq and also in numq if the value is between 0 and 8; additionally, if the 'ESC' key is pressed, it sets the global variable quit to true to signal all the threads to quit. It is up to the game_thread() of game_thread.cpp to determine what to do with the mouse and key values. By separating the functionalities between threads cleanly, it makes programming easier and less error-prone. However, because the queues mouseq, keyq, and numq are shared between threads, we need to lock them using mutexes before operate on them and unlock afterward; this is accomplished by the functions SDL_mutexP() and SDL_mutexV() respectively. ( Practically, the program still works even if you do not use any of these locking mechanisms but then your program thus written is not robust. )
/*
io_threads.cpp -- contains the keyboard and mouse thread functions
*/
#include "SDL/SDL.h"
#include "SDL/SDL_image.h"
#include "SDL/SDL_mixer.h"
#include "SDL/SDL_thread.h"
#include <string>
#include <iostream>
#include <stdlib.h>
#include <vector>
#include <deque>
#include "point.h"
using namespace std;
deque<char>keyq(0); //a queue to save values of keys pressed
deque<Point>mouseq; //queue to save mouse coordinates
deque<char>numq; //queue to save digits
bool quit = false; //global variable
extern SDL_mutex *key_mutex; //lock to protect accessing key variables
extern SDL_mutex *mouse_mutex; //lock to protect accessing mouse variables
//Filter SDL events, set by SDL_setEventFilter ( FilterEvents ) in tictactoe.cpp
int FilterEvents(const SDL_Event *event)
{
static int reallyquit = 0;
switch (event->type) {
case SDL_ACTIVEEVENT:
// See what's happening
printf("App %s ", event->active.gain ? "gained" : "lost");
if ( event->active.state & SDL_APPACTIVE )
printf("active ");
if ( event->active.state & SDL_APPMOUSEFOCUS )
printf("mouse ");
if ( event->active.state & SDL_APPINPUTFOCUS )
printf("input ");
printf("focus\n");
// See if we are iconified or restored
if ( event->active.state & SDL_APPACTIVE ) {
printf("App has been %s\n",event->active.gain ? "restored" : "iconified");
}
return ( 0 ); //drop all these events
// Queue it if we want to quit.
case SDL_QUIT:
printf("Quit demanded\n");
return ( 1 );
// Mouse and keyboard events go to threads, queue them
case SDL_MOUSEMOTION:
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
case SDL_KEYDOWN:
case SDL_KEYUP:
return ( 1 );
// Drop all other events
default:
return ( 0 );
}
}
//thread to handle mouse events
int mouse_thread ( void *unused )
{
SDL_Event events[10];
int i, found;
Uint32 mask;
// Handle mouse events here
mask = ( SDL_MOUSEBUTTONDOWNMASK | SDL_MOUSEMOTIONMASK | SDL_QUITMASK );
while ( !quit ) { //keep checking mouse events until quit is set
//handle up to 10 events, events removed from queue
found = SDL_PeepEvents(events, 10, SDL_GETEVENT, mask);
for ( i = 0; i < found; ++i ) {
Point p;
switch ( events[i].type ) {
case SDL_MOUSEBUTTONDOWN:
p.x = events[i].button.x; p.y = events[i].button.y;
SDL_mutexP( mouse_mutex ); //lock before accessing mouse queue
mouseq.push_back ( p ); //save point in mouse queue
SDL_mutexV ( mouse_mutex ); //release lock
break;
case SDL_QUIT:
quit = true;
found = 0; //exit for loop too
SDL_mutexV ( mouse_mutex ); //wake up any sleeping thread before quit
SDL_mutexV ( key_mutex );
break;
}
}
// Give up some CPU to allow events to arrive
SDL_Delay( 100 );
}
return ( 0 );
}
//thread to handle key events
int key_thread ( void *unused )
{
SDL_Event events[10];
int i, found;
Uint32 mask;
/* Handle keyboard events here */
mask = ( SDL_KEYDOWNMASK | SDL_KEYUPMASK );
while ( quit == false ) {
found = SDL_PeepEvents(events, 10, SDL_GETEVENT, mask);
SDL_mutexP(key_mutex); //lock before accessing shared key variables
for ( i=0; i < found; ++i ) {
switch(events[i].type) {
case SDL_KEYDOWN:
char c = events[i].key.keysym.sym;
keyq.push_back ( c ); //put entered key value in queue
/* Allow hitting <ESC> to quit the app */
if ( c == SDLK_ESCAPE ) //quit
quit = true;
else if ( c >= '0' && c <= '8' )
numq.push_back ( c ); //a digit
break;
} //switch
} //for
SDL_mutexV(key_mutex); //release lock
/* Give up some CPU to allow events to arrive */
SDL_Delay( 100 );
} //while
cout << "return from keyboard routine\n";
return ( 0 );
}
|
Finally, the program sound.cpp listed below handles the music and sound effects. The thread sound_thread() plays music at the background. It first calls Mix_LoadMUS("music.ogg") to load the music file "music.ogg". It then plays the music by calling Mix_PlayMusic(music, -1) . The -1 in the argument of Mix_PlayMusic() informs the sound engine to play music again and again; if the argument value is 0, the music is played once only. We shall discuss more about sounds and music in a later chapter.
/*
sound.cpp -- thread function for producing music and sound
*/
#include <iostream>
#include <stdlib.h>
#include <string>
#include "SDL/SDL.h"
#include "SDL/SDL_image.h"
#include "SDL/SDL_mixer.h"
#include "SDL/SDL_thread.h"
#include "surface.h"
using namespace std;
extern bool quit; //global variable
extern SDL_mutex *key_mutex;
extern bool game_over;
bool musicPlaying = true;
Mix_Chunk *sfx;
void channelFinished( int channel )
{
Mix_FreeChunk(sfx);
}
//Play the audio chunk
void play_sfx( char *name )
{
int channel;
//load wav file
sfx = Mix_LoadWAV(name);
//play once
channel = Mix_PlayChannel(-1, sfx, 0);
Mix_ChannelFinished( channelFinished);
}
void musicFinished()
{
//Music is done!
musicPlaying = false;
}
int sound_thread ( void *surface )
{
Surface *surf = ( Surface *) surface;
SDL_Surface *screen = ( SDL_Surface *) surf->getSurface();
Mix_Music *music; //Pointer to our music, in memory
int audio_rate = 22050; //Frequency of audio playback
Uint16 audio_format = AUDIO_S16SYS; //Format of the audio we're playing
int audio_channels = 2; //2 channels = stereo
int audio_buffers = 4096; //Size of the audio buffers in memory
//Initialize SDL_mixer with our chosen audio settings
if(Mix_OpenAudio(audio_rate, audio_format, audio_channels, audio_buffers) != 0)
{
printf("Unable to initialize audio: %s\n", Mix_GetError());
return 1;
}
//Load our OGG file from disk
music = Mix_LoadMUS("music.ogg");
if(music == NULL) {
printf("Unable to load OGG file: %s\n", Mix_GetError());
return 1;
}
//Play that funky music!
// -1 in argument means play music again and again, if 0, means play once
if(Mix_PlayMusic(music, -1) == -1) {
printf("Unable to play OGG file: %s\n", Mix_GetError());
return 1;
}
//Make sure that the musicFinished() function is called when the music stops playing
Mix_HookMusicFinished(musicFinished);
while ( !quit ) {
if ( game_over ) {
Mix_PauseMusic(); //pause music
musicPlaying = false;
} else {
if ( !musicPlaying ) {
Mix_ResumeMusic(); //resume playing music
musicPlaying = true;
}
}
SDL_Delay ( 100 ); //give up some cpu time for others
} //while ( !quit ) //quit may be set to true in key_thread
cout << "quiting sound thread" << endl;
//Release the memory allocated to our music
Mix_FadeOutMusic( 1000 ); //fade out music in 1 sec
Mix_HaltMusic();
Mix_FreeMusic(music);
//Need to make sure that SDL_mixer and SDL have a chance to clean up
Mix_CloseAudio();
return 0;
}
|
The source code package including the music sound files and images can be obtained by clicking at tictactoe.tgz here. You can untar the package by the command,