GLoid is OpenGL Arkanoid




  1. The game
  2. Install
  3. Play
  4. The Engine
  5. Credits


The Game

The purpose of this project is to migrate the gameplay and feeling of the classic arcade game "Arkanoid" into three dimensions, using OpenGL.

Coloring, animation and graphic design has been implemented as closely as possible to the original. All levels were designed in a way that the concepts of the equivalent original levels of Arkanoid remain more or less the same.

Although the size of levels looks a lot smaller, it is not. (each level <= 7x7x7 bricks instead of 13x13 on the original).

This is the second release of GLoid, which has been implemented entirely with free software.
Available for Linux, MacOS X and Windows.


Install


Windows

Download the self-extracting .exe installer from www.sourceforge.net/projects/gloid


You have to have installed the manufacturers' VGA drivers, and enabled hardware acceleration.


Linux

Download the tar.gz file from www.sourceforge.net/projects/gloid


Unzip the source files in a directory and run in superuser mode

./configure;make;make install 

You have to have installed OpenGL and SDL on your machine, and preferably your VGA card's proprietary drivers (although it has been reported to work with MESA framebuffer hardware accelaration, as well)


Mac OS X

Download the .pkg package from www.sourceforge.net/projects/gloid


Play

If you have played Arkanoid, you should feel at home.
The objective is simple: break all the bricks, go to next level, repeat, gather score points, enter the high score hall of fame, rejoice.
The ball bounces on the spaceship, the bricks and the walls according to physics, and randomly when it hits an alien.
Color bricks take one hit to break, silver bricks take 2 and golden don't break at all.
If you lose the ball, you die.
Every brick gives 70pts., every alien you kill gives 150 pts., every powerup pill you take gives 1000pts.
You gain a new life every 100000 points.

Controls:

Bonus pills:
When the ball is on the spaceship, you can see a moving crosshair to help you aim.
When it is coming at your direction, you can see a moving target on the spot it will bounce.
If you press fire when you catch the ball, it bounces retrospectively to the distance from the center of the spaceship to the center of the target.
Otherwise it bounces just like it does on the wall.


The Engine

So i decided to implement an entire, although small and basic, 3d game engine from scratch, based on NeHe's tutorial engine, and using SDL. This site works both like a development log, and a basic, absolute beginner's guide on SDL vs. 3d graphics coding (hence the trivial stuff).

It is by no means a complete tutorial: It is just bare bone stuff that i gathered on other docs or researched myself.

So, what does a game engine do?
A game engine in its simplest form, consists of a hierarchic structure of visible objects, and a main loop that polls input events, keeps track of time, swaps audio and video buffers, and calls several functions that

  1. Apply physics and update the positioning of the visible objects, and the camera point of view
  2. Draw 3d objects and on screen 2d displays, play sounds
  3. Apply the game logic, (the "rules" of the game) to predict what will happpen on the next loop iteration
  4. Handle the rest of the stuff that every application handles:exceptions, messages, logging etc.
This tutorial examines how these basic concepts are handled with SDL.


SDL

The decision to use SDL and OpenGL is clear from the beginning: This is a free software project, and I want it to compile both on Windows and Linux. (And Mac - thanks Tom).

For Windows development, I used DevCpp, so i had to download the SDL win32 c libraries and some addons (sdl_audio and sdl_ttf) (which i did in DevCpp package form here)

Link my DevCpp project to them

-mwindows -lmingw32 -lopengl32 -lglu32 -lglaux -lSDLmain -lSDL -lSDL_ttf

And include the include files in my header file

#include <SDL.h>								
#include <SDL_ttf.h>
#include <SDL_audio.h>

Linux development was done in Debian, so the libraries were included in the distribution in the debian packages libsdl-dev, libsdl-sound-dev, and libsdl-ttf-dev

I installed them, and edited the following lines to my autotools' Makefile.in:

INCS  = -I/usr/include/SDL
LIBS  = -L/usr/lib64/GL -L/usr/lib64/SDL -lSDL -lSDL_ttf -lGL -lGLU

and configure.ac:

AC_CHECK_LIB([GL], [glEnable])
AC_CHECK_LIB([GLU], [gluLookAt])
AC_CHECK_LIB([SDL], [SDL_Init])
AC_CHECK_LIB([SDL_ttf], [TTF_Init])

Note that SDL is a library that works on top of OpenGL, therefore they both have to be installed separately.

When you have your library all set up, you can start calling its functions from your c code.
First of all, when your application starts running, you have to initialise the library:

	if(SDL_Init(SDL_INIT_VIDEO| SDL_INIT_TIMER | SDL_INIT_AUDIO)<0)	// Init The SDL Library, The VIDEO, AUDIO, TIMER Subsystem
	{
		Log("Unable to open SDL: %s\n", SDL_GetError() );	// If SDL Can't Be Initialized
		exit(1);						// Get Out Of Here. Sorry.
	}
	atexit(SDL_Quit)// SDL's Been init, Now We're Making Sure Thet SDL_Quit Will Be Called In Case of exit()
    

Explaining:
First, There's the function SDL_init which tells SDL which subsystems we will be using. We will reserve the video, audio and timer subsystems for this game.
Then, there's the all useful function SDL_GetError that returns the last error generated from SDL in string form.
Finally, there's SDL_Quit that cleans up your memory from SDL before quitting.

Of course if we want to be typical, when our app quits normally, we must notify SDL with an event ( a little more on events later).

void TerminateApplication()							// Terminate The Application
{
	static SDL_Event Q;							// We're Sending A SDL_QUIT Event

	Q.type = SDL_QUIT;							// To The SDL Event Queue

	if(SDL_PushEvent(&Q) == -1)						// Try Send The Event
	{
		Log("SDL_QUIT event can't be pushed: %s\n", SDL_GetError() );	// And Eventually Report Errors
		exit(1);							// And Exit
	}

}

Setting the video mode

Moving on, SDL has a basic struct that represents a drawable surface, namely the almighty SDL_Surface.

SDL_Surface         *Screen; //SDL screen structure

So, we just set SDL's OpenGL attributes and call the function SDL_SetVideoMode to initialise our OpenGL window.

BOOL CreateWindowGL(int W, int H, int B, Uint32 F)				// This Code Creates Our OpenGL Window
{
	SDL_GL_SetAttribute( SDL_GL_RED_SIZE, 5 );				// In order to use SDL_OPENGLBLIT we have to
	SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, 5 );				// set GL attributes first
	SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, 5 );
	SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 16 );
	SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );				// colors and doublebuffering
	if(!(Screen = SDL_SetVideoMode(W, H, B, F)))				// We're Using SDL_SetVideoMode To Create The Window
		return false;							// If It Fails, We're Returning False
	SDL_FillRect(Screen, NULL, SDL_MapRGBA(Screen->format,0,0,0,0));	// A key event!  We have to open the Screen Alpha Channel!
...
Vflags = SDL_HWSURFACE|SDL_OPENGLBLIT| SDL_FULLSCREEN;		// We Want A Hardware Surface And Special OpenGLBlit Mode
...
CreateWindowGL(SCREEN_W, SCREEN_H, SCREEN_BPP, Vflags);
...

One interesting thing is that under Windows, OpenGL has to be initialised again after SDL_init.
There is no other known declination.

Another issue is how to detect the correct settings for the video mode that match our desktop, in order to make fullscreen mode work correctly.
As of version 1.3.10, SDL supports a nice, handy function called SDL_GetVideoInfo that detects the desktop's resolution.
Unfortunately, this function does not detect the pixel depth, so we have to resort to trial and error to find the correct value:

Desktop = SDL_GetVideoInfo();
   Screen_W = Desktop->current_w;
   Screen_H = Desktop->current_h;
   for(bpp = 32; bpp > 8; bpp -= 8)
      {
         Screen = SDL_SetVideoMode(Screen_W , Screen_H , bpp, Vflags);

         if(Screen != NULL)
         {
            Screen_BPP = bpp;
            break;
         }
      }

And in case the end user's SDL is older than 1.3.10, we have to go hunting for the resolution as well:

int modes[][2] = {
      {2048,1152},
      {1920, 1200},
      {1600, 1200},
      {1280, 1024},
      {1280, 768},
      {1280, 720},
      {1024, 768},
      {1024, 600},
      {800, 600},
      {720, 576},
      {720, 480},
      {640, 480}
   };
...
#if ((SDL_MAJOR_VERSION > 1) || (SDL_MAJOR_VERSION == 1 && SDL_MINOR_VERSION > 2) || (SDL_MAJOR_VERSION == 1 && SDL_MINOR_VERSION == 2 && SDL_PATCHLEVEL > 9))
//SDL version > 1.3.10, get video info and hunt for pixel depth
   ...
	
#else 
	Log("libSDL v.%d.%d.%d doesn't support SDL_GetVideoInfo(), testing video modes... \n",SDL_MAJOR_VERSION, SDL_MINOR_VERSION,SDL_PATCHLEVEL);
 // Hunt for video modes
   	while((Screen == NULL) && (n < 12))
   	{
      	for(bpp = 32; bpp > 8; bpp -= 8)
      	{
         Screen = SDL_SetVideoMode(modes[n][0], modes[n][1], bpp, Vflags);

         if(Screen != NULL)
         {
            Screen_W = modes[n][0];
            Screen_H = modes[n][1];
            Screen_BPP = bpp;
            break;
         }
      }
      n++;
   }

#endif

Two more things of note are SDL_GL_DOUBLEBUFFER, which enables us to doublebuffer our graphics, meaning that everything is drawn in one buffer while another buffer is displayed; this results in a nice smooth motion effect. This is done by calling SDL_GL_SwapBuffers in our main loop.
And also, the fact that an Alpha channel *has* to be initiated in order to see anything on the screen.

These will do for a brief introduction; time to move on to specifics.
Note that these basic concepts of SDL that i have used so far, are covered in greater length in NeHe's SDL tutorial application.


Mouse and Keyboard input

SDL handles events pretty straightforward. First of all, there's the SDL_Event struct that stores the queue of events caught by the operating system or sent by the application.

	SDL_Event	E;	

Then, there's the function SDL_PollEvent that polls the event queue for events and stores the last one on a SDL_event struct.
Now, let's define a struct to store the state of the mouse and an array of integers that contain the values of the keys that are pressed:


typedef struct 
{//mouse struct
        int x;
        int y;
        BOOL leftclick;
} mousecntl;// We Call It mousecntl
Uint8 *keys;// A Pointer To An Array That Will Contain The Keyboard Snapshot

Types of events of interest right now, are SDL_KEYDOWN (key pressed), SDL_MOUSEMOTION (mouse moved), SDL_MOUSEBUTTONDOWN (mouse left button pressed) and SDL_MOUSEBUTTONUP(mouse left button released). These values are stored in E.type, so all we do is call SDL_PollEvent and switch on E.type.
Mouse motion is stored in integers E.motion.x and E.motion.y, and then there's the function SDL_GetKeyState that stores every key that is pressed in an integer array.


	while(looping == TRUE)// And While It's looping
	{
		if(SDL_PollEvent(&E))// We're Fetching The First Event Of The Queue
		{
			switch(E.type)// And Processing It
			{	
			.
			.
			.
			case SDL_KEYDOWN:// Someone Has Pressed A Key?
				keys = SDL_GetKeyState(NULL);//Take A SnapShot Of The Keyboard 
				break;// And Break;
			case SDL_MOUSEMOTION:
				mouse.x = E.motion.x;                                       
				mouse.y = E.motion.y;
				break;
			case SDL_MOUSEBUTTONDOWN:
				mouse.leftclick = TRUE;//we are firing
				break;
			case SDL_MOUSEBUTTONUP://we stopped firing
				mouse.leftclick = FALSE;
				break;
			default:break;
			}
		}
		else// No Events To Poll? (SDL_PollEvent()==0?)
		{ 
		.
		.
		.
		//the real game core
			.
			.
			.
Easy? let's move on.


Loading and displaying 2d bitmaps

Let's see how to load some .bmp files from disk. For convenience, we store the filenames in a char * array and enumerate its contents:

enum 
{//all bitmaps enumerated
	BMP_BG_1,
	BMP_BG_2,
	BMP_BG_3,
	BMP_BG_4,
	...
	N_BMP
};
const char * Bmp_files[N_BMP] = 
{
   "arka1",
   "arka2",
   "arka3",
   "arka4",
...
};

These bitmaps are stored in SDL_surface structs. Obviously we need an array of those, (although in a larger application this would be a dynamic structure like a linked list).

 SDL_Surface Texture[N_BMP];	

Now, to load the bitmaps with the convenient function SDL_LoadBMP.

for(i = 0; i < N_BMP;i++)
{
	sprintf(filename, "./textures/%s.bmp",  Bmp_files[i]);
	Texture[i] = *SDL_LoadBMP(filename);

And finally we set the color key to be whatever corresponds to black in each bitmap's format:

SDL_SetColorKey(&(Texture[i]), SDL_SRCCOLORKEY|SDL_RLEACCEL,SDL_MapRGB(Texture[i].format, 0, 0, 0) );

In case we want to display bitmaps in 2-d, we have to create an openGL window, bind it on a base surface, and on top of that blit all other surfaces, just like we would if we were programming 2d graphics in Win32, so we need to set SDL_OPENGLBLIT mode.

For example, let's just draw the game logo on the screen.
First of all, we need a struct named SDL_rect that represents the area on which our bitmap will be drawn.
Then, we need to initialise all four channels of it with the color black.
Finally, we call SDL_BlitSurface to blit it on our main window surface, and SDL_UpdateRects to actually display it on the screen.

void ready_display(SDL_Surface * window)	
{
	static SDL_Rect logo_rect={0,0,0,0};
	glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);				
	SDL_FillRect(window, &logo_rect, SDL_MapRGBA(window->format,0,0,0,0));
    logo_rect.w = (Sint16)Texture[BMP_LOGO].w;
    logo_rect.h = (Sint16)Texture[BMP_LOGO].h;
    logo_rect.y = 100;
    logo_rect.x = 200;
    SDL_BlitSurface(&(Texture[BMP_LOGO]), NULL, window, &logo_rect);// And finally blit and update
    SDL_UpdateRects(window, 1, &logo_rect);  
}

The blit function is obsolete, and more importantly, it is not compatible with OpenGL's alpha channel, so it can't handle transparency with OpenGL objects.
In fact the correct way to use a bitmap is to bind its texture on a OpenGL surface.
Before we are able to do that though, he have to extract three informations from our SDL_surface.
First, if our textures' dimensions are powers of two. If that is not true, then we can't bind it to a 3d surface.

		        if ( (Texture[i].w & (Texture[i].w - 1)) != 0 ) 
		          Log("warning: image.bmp's width is not a power of 2\n");
	            if ( (Texture[i].h & (Texture[i].h - 1)) != 0 ) 
		          Log("warning: image.bmp's height is not a power of 2\n");

Second, if our texture has an Alpha channel. This can be checked from the field SDL_surface->format->BytesPerPixel
Third, if our texture is formatted in RGB, or in BGR. We need to check the field SDL_surface->format->Rmask for that.

		bpp = Texture[i].format->BytesPerPixel;
        if (bpp == 4)     // contains an alpha channel
        {
                if (Texture[i].format->Rmask == 0x000000ff)
                        texture_format = GL_RGBA;
                else
                        texture_format = GL_BGRA;
        } else if (bpp == 3)     // no alpha channel
        {
                if (Texture[i].format->Rmask == 0x000000ff)
                        texture_format = GL_RGB;
                else
                        texture_format = GL_BGR;
        } else {
                Log("warning: the image is not truecolor..  this will probably break\n");
                // this error should not go unhandled
        }

Once he have all three sorted out, we can call the function glTexImage2D to do the actual binding.

glTexImage2D(GL_TEXTURE_2D, 0, bpp, Texture[i].w, Texture[i].h, 0, texture_format, GL_UNSIGNED_BYTE, Texture[i].pixels); 

The tricky part here is that, unlike in SDL blitting, our bitmaps' dimensions absoloutely have to be exact powers of two, for OpenGL to render it correctly, otherwise all we get is random noise on the screen.

So if they aren't, or even worse, they are dynamic and cannot be known aforehand, as is the case with bitmap creating functions like TTF_RenderText (see below), we can use a convenience function like this:

// Compute the next power of two: 2^i < x <= 2^(i+1) = y
int nextpoweroftwo(int x)
{
   double y;

   y = pow(2, ceil(log(x) / log(2)));
   return (int)y;
}


Another thing that's done differently when drawing bitmaps with OpenGL rendering instead of SDL blitting is handling the bitmaps' coordinate system.
When blitting with SDL, we can use SDL_rect to map our source and destinations' coordinate systems.
In OpenGL, we can rotate our projection matrix instead.
For example, in the pill drawing function we need to rotate the pill's texture before rendering it on a cylinder:


void pills::display()
{
   extern unsigned int Font_Size;

   if(active)
   {
      GLUquadricObj * base = gluNewQuadric();

      glPushMatrix();
       glEnable(GL_TEXTURE_2D);
       glColor3f(col.x, col.y, col.z);
       glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
       glTranslatef(place.x,place.y, place.z);
       gluQuadricTexture(base, GL_TRUE);	
       glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
       glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, Font_Size,
                    Font_Size, 0, GL_RGBA, GL_UNSIGNED_BYTE,
                    surf->pixels);//An SDL_Surface where the pill texture gets rendered 
				//from TTF_RenderText
       glPushMatrix();
        glPushMatrix();
         glRotatef(rotx, -1.0f, 0.0f, 0.0f);
         glRotatef(90.0f, 0.0f, 1.0f, 0.0f);
         glMatrixMode(GL_TEXTURE);
         glPushMatrix();
          glTranslatef(len/2, 0.0f, 0.0f);
          glRotatef(90.0f, 0.0f, 0.0f, 1.0f);
          glRotatef(180.0f, 1.0f, 0.0f, 0.0f);   
          gluCylinder(base, rad, rad, len, 12, 12);
         glPopMatrix();
         glMatrixMode(GL_MODELVIEW);	
        glPopMatrix();
        glColor3f(col.x, col.y, col.z);
        glDisable(GL_TEXTURE_2D);
        glPushMatrix();
         glTranslatef(0, 0.0f, 0.0f);
         gluSphere(base, rad, 12, 12); 
        glPopMatrix();
        glPushMatrix();
         glTranslatef(len, 0.0f, 0.0f);
         gluSphere(base, rad, 12, 12); 
        glPopMatrix();
       glPopMatrix();
      glPopMatrix();
   }
}


TrueType fonts with SDL_ttf

Now that we know how to draw bitmaps, it is time to examine how to print messages on the screen.
First, we pick a nice copyright-free TrueType font. DejaVuSans.ttf as included in any Linux distribution will do.
TrueType font loading requires an add-on called SDL-ttf that was mentioned earlier.

After we have set up the SDL-ttf library, we must initiate it:

if (TTF_Init() == -1)//init TTF
{
    Log(TTF_GetError());
    Log(":Unable to initialize SDL_ttf\n");
    return FALSE;
}

SDL-ttf has its own error returning function.

There is a struct named TTF_Font that stores the font when we load it from our filesystem with a function called TTF_OpenFont:

TTF_Font *DejaVuSans;
DejaVuSans = TTF_OpenFont("DejaVuSans.ttf", size);

Yes, everytime we want to change our font size we have to load the .ttf file that contains our font.
But we don't have to do that everytime we want to change the color of our font.
We can use the struct SDL_Color instead.

SDL_Color white = {255,255,255,128};

On screen text is basically the same thing as any on screen 2d display, so any displayed text will have a SDL_Surface which will contain the drawable data, and a SDL_Rect with its location and size.
For our own convenience, let's define our own struct to store these, along with the text itself:

typedef struct 
{
    SDL_Surface *T;
    SDL_Rect src; 
    char msg[MAXLINE];
}text2d;

Once we have the TTF_Font that stores our font, the SDL_Color that holds the desired color, and the message we want to display, we will need to use a function from the TTF_RenderText family to render our text in solid, shaded or blended mode.
After the rendering, the size of the resulting bitmap is known so we can copy it to our SDL_Rect.
Then we can blit it on the screen just like in the previous section.
And that's all we need to make a function that prints a c-formatted string onto an SDL_surface starting at (X,Y):

void printText(SDL_Surface *S, text2d * text, int x, int y, char * buf, ...)
{//print a c formatted string @ x, y
	extern TTF_Font *DejaVuSans;
	extern SDL_Color white;// = {255,255,255,128};
    
	va_list Arg;// We're Using The Same As The printf() Family, A va_list
		// To Substitute The Tokens Like %s With Their Value In The Output
	va_start(Arg, buf);	// We Start The List
	vsprintf(text->msg,buf, Arg);
	va_end(Arg);		// We End The List

	text->T = TTF_RenderText_Solid(DejaVuSans,text->msg,white);
	text->src.w = text->T->w;
	text->src.h = text->T->h;
	text->src.x = x;
	text->src.y = y;
	
}

Again it must be noted that in new applications, the proper (non-deprecated) way is to replace BlitSuface with an OpenGL texture binding:

// Draw SDL_ttf rendered text to an OpenGL texture
void DrawOpenGLText(text2d* text)
{
   SDL_Surface *s, *p;
   int h, w;
   float xx, yy;

   p = text->T;
   w = nextpoweroftwo(p->w);
   h = nextpoweroftwo(p->h);

   xx = SCENE_MIN + 2 * SCENE_MAX * text->src.x / Screen_W;
   yy = -SCENE_MIN + 2 * SCENE_MAX * text->src.y / Screen_H;

   s = SDL_CreateRGBSurface(p->flags, w, h, Screen_BPP,
                            0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000);

   SDL_SetColorKey(s, SDL_SRCCOLORKEY|SDL_RLEACCEL,
                   SDL_MapRGBA(s->format, 0, 0, 0, 0));

   SDL_BlitSurface(p, 0, s, 0);

   glEnable(GL_TEXTURE_2D);
   glBindTexture(GL_TEXTURE_2D, TextureID[N_BMP]);//use warp ID + 1
   glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
   glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

   glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA,
                GL_UNSIGNED_BYTE, s->pixels);

   glColor3f(1.0f, 1.0f, 1.0f);        

   glBegin(GL_QUADS);
   glTexCoord2f(1.0f, 1.0f);
   glVertex3f(xx + Font_Size * w / Screen_W,
              yy - Font_Size * h / Screen_H, -SCENE_AIR);
   glTexCoord2f(1.0f, 0.0f);
   glVertex3f(xx + Font_Size * w / Screen_W, yy, -SCENE_AIR);
   glTexCoord2f(0.0f, 0.0f);
   glVertex3f(xx, yy, -SCENE_AIR);
   glTexCoord2f(0.0f, 1.0f);
   glVertex3f(xx, yy - Font_Size * h / Screen_H, -SCENE_AIR);
   glEnd();

   glDisable(GL_TEXTURE_2D);
   SDL_FreeSurface(s);
}   


Playing sounds

A great thing about SDL is that it integrates sound and video, so you don't have to mix and match different libraries.
Now we will take a look at how to play sounds from SDL.
Initially i have to note that this is the simplest, most barebone sound playing function you can have, using SDL's core library:
You can load only .wav files, you can only mix 2 channels and you have to write your own mixer function.
For the users with more advanced needs in sound, the library SDL_mixer is highly recommended.

So, we've shown initially how to link, include and declare that we will be using the SDL_audio subsystem, let's initialise it.
First of all we need to fill up an SDL_AudioSpec struct, so that SDL will know what is the audio format we will be playing:

SDL_AudioSpec Audio;    //Our very own audio format specification
...
/* Set 16-bit stereo audio at 22Khz */
    Audio.freq = 22050;
    Audio.format = AUDIO_S16;
    Audio.channels = 2;
    Audio.samples = 512;
    Audio.callback = mixer;
    Audio.userdata = NULL;

The Audio.callback points to a user defined function that actually sends data to the audio hardware.
It gets called immediately as soon as we call SDL_OpenAudio to initialise the audio subsystem, so we must call SDL_PauseAudio to stop playing garbage and wait until we have something to play.
If our application quits normally, we have to call SDL_CloseAudio to shut down the sound system.

if ( SDL_OpenAudio(&Audio, NULL) &t; 0 ) {
        Log( "Unable to open audio: %s\n", SDL_GetError());
        exit(1);
    }
    SDL_PauseAudio(0);//start playing whatever gets in the buffers

Now, the data that the mixer fuction sends to our sound card needs to be stored somewhere, and we will also need its size, and a pointer to know how much of it has been already played, so we define a struct to store these things.


struct sample {
    Uint8 *data;
    Uint32 dpos;
    Uint32 dlen;
} Soundbuffer[NUM_BUFFERS]; 
/*sound buffers: Standard SDL only works with double buffering, therefore NUM_BUFFERS = 2*/

Because in games sound effects appear asynchronously, and we don't want to interrupt one sound that's already being played to play the next sound we might want to play, we need multiple buffering.
This means that we need to be able to mix a sound that is currently being written in the hardware stream with the new sound, and to do that we need more than one buffers to store our sounds.
Unfortunately, core SDL does not support mixing more than 2 channels, so we will use only 2 buffers to load data to one while the other one is being copied to the stream, and in the coincidence that both are full at the same time, any new sound will be skipped.
Lets pause on that for now until the mixer function section, and see how we can load .wav files in our buffers.
Just like we did with the .bmp files, we enumerate our filenames

enum
{//all sounds
      WAV_ALIEN,
      WAV_BOUNCE0,
      WAV_BOUNCE1,
      WAV_ENLARGE,
      WAV_GO,
      WAV_INTRO,
	  ...
      N_WAV
};
const char * Wav_files[N_WAV] = 
{
      "alien",
      "bounce0",
      "bounce1",
      "enlarge",
      "go",
      "intro",
	  ...

so we can easily load them randomly.

First we have to read our.wav files in Uint8 format using SDL_LoadWAV, and then we have to convert them from their format to our own format with SDL_ConvertAudio.
To do that, we use a structure named SDL_AudioCVT which is initialised with the function SDL_BuildAudioCVT.
Summing up, a wav loading function will look like this:

int loadSounds()
{
    int i;
    char filename[MAXLINE];
    Uint8 *data;
    Uint32 dlen;
    extern SDL_AudioSpec Audio;
    extern SDL_AudioCVT Wave[N_WAV];
    for(i = 0; i < N_WAV; i++)
    {// Load the sound file and convert it to 16-bit stereo at 22kHz 
        sprintf(filename, "./sounds/%s.wav", Wav_files[i]);
        if ( SDL_LoadWAV(filename, &Audio, &data, &dlen) == NULL ) 
        {
           Log( "Couldn't load %s: %s\n", filename, SDL_GetError());
           return FALSE;
        }
        SDL_BuildAudioCVT(&(Wave[i]), Audio.format, Audio.channels, Audio.freq,
                            AUDIO_S16,   2,             22050);
        Wave[i].buf = (Uint8*)malloc(dlen*Wave[i].len_mult);
        memcpy(Wave[i].buf, data, dlen);
        Wave[i].len = dlen;
        SDL_ConvertAudio(&Wave[i]);
        SDL_FreeWAV(data);
    }
    return TRUE; 	
}

Notice how Uint32 pointers allocated with SDL_LoadWAV have to be freed with SDL_FreeWAV.

At this point, we should have an array of sounds converted to our own output format.
Now we need a function to copy any one of these sounds to our sound buffers if there is one available,

BOOL PlaySDLSound(int wavidx)
{
    int index;
    Uint8 *data;
    Uint32 dlen;
    // Look for an empty (or finished) sound slot 
    for ( index=0; index < NUM_BUFFERS; ++index ) 
    {
        if ( Soundbuffer[index].dpos == Soundbuffer[index].dlen ) 
            break; //found
    }
    
    if ( index == NUM_BUFFERS )
        return FALSE; //failed, buffer is full
    //our available waves are already converted since loading time
    // Put the sound data in the slot (it starts playing immediately) 
    Soundbuffer[index].data = Wave[wavidx].buf;
    Soundbuffer[index].dlen = Wave[wavidx].len_cvt;
    Soundbuffer[index].dpos = 0;
    return TRUE;
}
and a mixer function to send the buffer contents to our hardware, keep track of what point of the buffer has currently been played and, if necessary, mix them.
stream is a file stream provided by our sound card driver, and we can use SDL_MixAudio to mix a portion of a converted sound with whatever is currently written on it.

void mixer(void *udata, Uint8 *stream, int len)
{//mixes maximum len bytes from the sound channels onto the stream
         int i;
        Uint32 amount;//how much is left from a sound to play?
        for ( i=0; i < NUM_BUFFERS; ++i ) 
        {
            amount = (Soundbuffer[i].dlen-Soundbuffer[i].dpos);
            if ( amount > len ) 
                amount = len; //amount cannot be more than len
            SDL_MixAudio(stream, &Soundbuffer[i].data[Soundbuffer[i].dpos], amount, SDL_MIX_MAXVOLUME);
            Soundbuffer[i].dpos += amount;
    }
}

That's it; we can call our PlaySDLSound at any point in our code, and the correspondant sound will be played, provided that our buffers are not already filled with previous sounds.


Motion and framerate

First of all, SDL has a nice function for keeping track of time with 10 millisecond precision, namely SDL_GetTicks.
We can use it in our main loop to count how many milliseconds have passed between loops i.e. frames.
That way, we know our FPS at any given time.
The next problem that we have to solve is that a loop that's always busy is extremely CPU-intensive, and may generate glitches.
We can cool the CPU intensiveness by delaying our loop by calling SDL_Delay with a value of at least 10ms.
A great way of defining how many milliseconds SDL_Delay will have to wait, is by defining the maximum Frame Per Second rate for our application, and then make sure that if the loop takes less time, SDL_Delay will delay it so as to keep the FPS steadily below the maximum.
Suppose for example that we define our maximm FPS at 25.
That means we have to keep our loops slower than 40 milliseconds.

double minmspf = 40; // = 1000/fps
...
while(looping == TRUE)							// And While It's looping
{
	...
	toc = SDL_GetTicks();	// Get Present Ticks
	Update(toc-tic, keys, &mouse);		// And Update The Motions And Data
    //toc - tic is milliseconds per frame
     //from this we calculate the motion smoothing factor ms10 (moving average ms in last 10 frames)
     ms10 = moving_average(toc-tic,msperframe,10);
    delay = minmspf - toc+tic;
    if(delay > 0)
    {
        SDL_Delay(delay);
    }
    tic = toc;
	...
}

Then, since we know the minimum milliseconds a frame takes, we can calculate how fast we want our moving objects to move, and apply that speed to their positions at every loop iteration.

animate()
{
...
	//animate
	x += speedx * ms10*minmspf/1000;// max FPS
	...

And than we can further smoothen their motion by multiplying that speed with a moving average of the actual ms per frame across the last 10 frames.

double moving_average(double x, double * a, int size)
{//moving average of 10 (size) samples, a must be allocated with size values
    int i;
    double sum = x;
    for(i = size-1;i>0;i--)
    {
        a[i] = a[i-1];
        sum += a[i];
    }
    a[0] = x;
    return sum/size;
}

Credits

Please send feedback,suggestions, questions, requests, help(?) to:
Antonis K.(kalamara AT users.sourceforge.net)

Thanks to Thomas Kircher (tkircher AT gnu.org) for the original Linux and MacOS X packages, and the rest of his help

Also thanks to everyone at gamedev.net, SDL.org and OpenGL.org
SourceForge.net Logo SDL Logo OpenGL Logo