Gentlemen, Start Your 3D Gaming Engines!
In gaming, more so than any other programming regime, it is important to specify the platform correctly from the start. Will you support Windows, Linux, and OS X? Indeed, many people would cite the revolutionary Quake game written for OpenGL in 1996 as the granddaddy of game platforms. However, to reach Quake-level gameplay standards you will also need world-class audio support, network connectivity, user-input device support, and real-time management capabilities, just to name a few… The solution to both problems, cross-platform support and the extras that make a game exciting, is Simple DirectMedia Layer (SDL). This is a cross-platform multimedia library designed to provide low-level access to audio, keyboard, mouse, joystick, OpenGL, and 2D video framebuffer. SDML supports almost every platform I can think of, including Linux, Windows, all MacOs variants, WinCE, Dreamcast, and others. Because it is distributed on GNU LGPL v2, you can use SDL freely in commercial programs as long as you link with the supplied DLL.
SDML shows up in MPEG players, hardware emulators, and many popular games, including the award winning Linux port of Civilization: Call To Power. SDML is written in C, but works with C++ natively, and has bindings to several other languages, including Ada, Eiffel, Java, Lua, ML, Perl, PHP, Pike, Python, and Ruby. The sky is the limit with SDL, which is the engine for my favorite Open Source flight simulator GL-117. In fact, there are now 568 games built on top of the SDL engine (of which 450 can build Win32 executables) registered on the SDML homepage.
Figure 1: The view from GL-117
ALIENS in Your Midst!
The best way to get inside a game engine is to look at some sample code. This time, you’re going inside a 1980s Space Invaders clone game by Sam Lantinga, called simply aliens. Remarkably, the complete game source code is only 560 lines of code! I wish I could lay out the entire thing in this article, but it exceeds my boundaries just a little. Instead, I will focus with laser-like intensity on the specifics that matter the most and ignore bits which are rote or repetitious. [I’ll be using line numbers from aliens.c if you want to follow along in Visual Studio.]
Figure 2: Screenshot of Aliens game
The first thing to notice is that “aliens” leverages code from two other SDL projects to help get the job done: SDL_mixer and SDL_image. SDL_mixer is a platform-independent sound mixing library plug-in for SDL apps. It allows your apps to play multiple samples along with music without having to code a mixing algorithm. For example, you would use this to allow gunfire noises to seamlessly mix into background music. It also simplifies loading and playing samples and music from all sorts of file formats. SDL_image is a platform-independent graphics loading plug-in for SDL apps. It allows your apps to read BMP, PNM (PPM/PGM/PBM), XPM, LBM, PCX, GIF, JPEG, PNG, TGA, and TIFF format files. So, be sure to download both packages if you want to follow along!
The main() Business
The main() function, shown below, is where the action starts, of course. In Line #524, you ask SDL_Init() to initialize the audio and video subsystems. Other options include turning on joystick, timer, and CD-ROM access as well as an option to run the event manager in a separate thread. In Line #531, you start using the SDL_Mixer manager with a call to Mix_OpenAudio(). In this example, you’re using low-quality audio suitable for older/slower CPUs: 11Khz, 8-bit unsigned samples, one channel (mono), and a 512 byte buffer. Next, you open up the video with SDL_SetVideoMode() for 640×480 display using whatever bits per pixel is appropriate for the device. You ask for a system-memory buffer and request a “full screen” display, meaning that you want your window to dominate the windowing system. Other possible options include double-buffering (smoother animation), OpenGL context, resizable window, and asynchronous repaints.
521 main(int argc, char *argv[]) 522 { 523 /* Initialize the SDL library */ 524 if ( SDL_Init(SDL_INIT_AUDIO|SDL_INIT_VIDEO) < 0 ) { 525 fprintf(stderr, "Couldn't initialize SDL: %sn", SDL_GetError()); 526 exit(2); 527 } 528 atexit(SDL_Quit); 529 530 /* Open the audio device */ 531 if ( Mix_OpenAudio(11025, AUDIO_U8, 1, 512) < 0 ) { 532 fprintf(stderr, 533 "Warning: Couldn't set 11025 Hz 8-bit audion- Reason: %sn", 534 SDL_GetError()); 535 } 536 537 /* Open the display device */ 538 screen = SDL_SetVideoMode(640, 480, 0, SDL_SWSURFACE|SDL_FULLSCREEN); 539 if ( screen == NULL ) { 540 fprintf(stderr, "Couldn't set 640x480 video mode: %sn", 541 SDL_GetError()); 542 exit(2); 543 } 544 545 /* Initialize the random number generator */ 546 srand(time(NULL)); 547 548 /* Load the music and artwork */ 549 if ( LoadData() ) { 550 /* Run the game */ 551 RunGame(); 552 553 /* Free the music and artwork */ 554 FreeData(); 555 } 556 557 /* Quit */ 558 Mix_CloseAudio(); 559 exit(0); 560 }
Running the Game
The main logic loop is, of course, all inside RunGame(). This function is about 200 lines of code, so still too much to look at all at once, but you will examine some of the more important sections by way of explaining SDL features. Where I’ve deleted some code, I’ll indicate with an ellipsis ( … ) in the comments.
301 void RunGame(void) 302 { 303 int i, j; 304 SDL_Event event; 305 Uint8 *keys; 306 307 /* Paint the background */ 308 numupdates = 0; 309 for ( i=0; i<screen->w; i += background->w ) { 310 SDL_Rect dst; 311 312 dst.x = i; 313 dst.y = 0; 314 dst.w = background->w; 315 dst.h = background->h; 316 SDL_BlitSurface(background, NULL, screen, &dst); 317 } 318 SDL_UpdateRect(screen, 0, 0, 0, 0);
In the first section (Lines #301 to 318), you are simply blit-ing slices of background color onto the screen buffer sequentially with SDL_BlitSurface() and finally set up a repaint with SDL_UpdateRect().
319 320 /* Initialize the objects */ 321 // . . . 333 CreateAlien(); 334 DrawObject(&aliens[0]); 335 UpdateScreen(); 336 337 while ( player.alive ) { 338 /* Wait for the next frame */ 339 WaitFrame(); 340 341 /* Poll input queue, run keyboard loop */ 342 while ( SDL_PollEvent(&event) ) { 343 if ( event.type == SDL_QUIT ) 344 return; 345 } 346 keys = SDL_GetKeyState(NULL); 347 348 /* Erase everything from the screen */ 349 // . . . 366 /* Decrement the lifetime of the explosions */ 367 // . . . 373 /* Create new aliens */ 374 if ( (rand()%ALIEN_ODDS) == 0 ) { 375 CreateAlien(); 376 } 377 378 /* Create new shots */ 379 if ( ! reloading ) { 380 if ( keys[SDLK_SPACE] == SDL_PRESSED ) { 381 for ( i=0; i<MAX_SHOTS; ++i ) { 382 if ( ! shots[i].alive ) { 383 break; 384 } 385 } 386 if ( i != MAX_SHOTS ) { 387 shots[i].x = player.x + 388 (player.image->w-shots[i].image->w)/2; 389 shots[i].y = player.y - 390 shots[i].image->h; 391 shots[i].alive = 1; 392 Mix_PlayChannel(SHOT_WAV, 393 sounds[SHOT_WAV], 0); 394 } 395 } 396 }
Now, look at the input-management part of the RunGame() loop. The deceptively simple SDL_PollEvent() returns an SDL_Event object with notes about interesting things that might have happened. At this time, you check for an ESC key press with the special value of SDL_QUIT. Because this is a purely keyboard-driven game, you can use SDL_GetKeyState() to get all the info you need. Remember that the PC keyboard is designed to allow for multiple simultaneous key downs, so the result is returned in an array that you index by a keyboard code value such as SDLK_SPACE (in other words, spacebar).
397 reloading = (keys[SDLK_SPACE] == SDL_PRESSED); 398 399 /* Move the player */ 400 //. . . 415 /* Move the aliens and the shots */ 416 // . . . 443 444 /* Detect collisions */ 445 for ( j=0; j<MAX_SHOTS; ++j ) { 446 for ( i=0; i<MAX_ALIENS; ++i ) { 447 if ( shots[j].alive && aliens[i].alive && 448 Collide(&shots[j], &aliens[i]) ) { 449 aliens[i].alive = 0; 450 explosions[i].x = aliens[i].x; 451 explosions[i].y = aliens[i].y; 452 explosions[i].alive = EXPLODE_TIME; 453 Mix_PlayChannel(EXPLODE_WAV, 454 sounds[EXPLODE_WAV], 0); 455 shots[j].alive = 0; 456 break; 457 } 458 } 459 }
Stop here and quickly look at Mix_PlayChannel(), which adds explosion noises at appropriate conditions. You’re actually using three channels simultaneously in this game: one each for music, shots, and explosions respectively because at any given moment you may need to output up to all three at once. Otherwise, you would be continuously cutting away from one sound to play another sound and the soundtrack would be a choppy mess. By the way, you loaded the sounds earlier with a call such as:
127 sounds[MUSIC_WAV] = Mix_LoadWAV(DATAFILE("music.wav"));
Okay, back to the code:
474 // . . . 475 /* Draw the aliens, shots, player, and explosions */ 476 for ( i=0; i<MAX_ALIENS; ++i ) { 477 if ( aliens[i].alive ) { 478 DrawObject(&aliens[i]); 479 } 480 } 481 // . . . 494 UpdateScreen(); 495 496 /* Loop the music */ 498 if ( ! Mix_PlayingMusic() ) { 499 Mix_PlayMusic(music, 0); 500 } 506 507 /* Check for keyboard abort */ 508 if ( keys[SDLK_ESCAPE] == SDL_PRESSED ) { 509 player.alive = 0; 510 } 511 } 512 513 /* Wait for the player to finish exploding */ 514 while ( Mix_Playing(EXPLODE_WAV) ) { 515 WaitFrame(); 516 } 517 Mix_HaltChannel(-1); 518 return; 519 } 520
All right, I haven’t been able to show every single thing, such as UpdateScreen(), which walks through a list of all the bitmaps you are going to display and blits them to the screen. You can follow this on your own copy of the source!
Conclusion
Even putting together a conventional 2D shooter game in 500-odd lines of code with music and realtime play is a pretty good feat in my imagination. And of course the same code can be compiled and linked to run on Windows and Linux platforms and a lot more. In some respects, you’ve barely scratched the surface and if you are encouraged to give SDL a try, then I think I’ve met my goal for this article!
About the Author
Victor Volkman has been writing for C/C++ Users Journal and other programming journals since the late 1980s. He is a graduate of Michigan Tech and a faculty advisor board member for the Washtenaw Community College CIS department. Volkman is the editor of numerous books, including C/C++ Treasure Chest and is the owner of Loving Healing Press. He can help you in your quest for open source tools and libraries; just drop send an email to sysop@HAL9K.com