![]() |
|
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.
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.
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)
Download the .pkg package from www.sourceforge.net/projects/gloid
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:
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
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 |
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 } } |
Moving on, SDL has a basic struct that represents a drawable surface, namely the almighty SDL_Surface.
SDL_Surface *Screen; //SDL screen structure |
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; } } |
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.
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 . . . |
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"); |
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 } |
glTexImage2D(GL_TEXTURE_2D, 0, bpp, Texture[i].w, Texture[i].h, 0, texture_format, GL_UNSIGNED_BYTE, Texture[i].pixels); |
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; } |
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(); } } |
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); |
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); } |
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; } |
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; } |
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.
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; ... } |
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; } |
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