An adaptation of a chapter from the author's upcoming book entitled Programming Linux Games.
Linux gaming is exploding, partly because of the simple fact that geeks like games, and partly because of recent developments in Linux multimedia. Over the past few years a number of excellent Linux-oriented multimedia toolkits have emerged, such as the GGI graphics interface and the ALSA sound system. The SDL library has also made a bit of a splash recently. SDL is a general-purpose, multimedia-programming library that provides fast and portable access to graphics, sound, input devices, threads and OpenGL rendering. The core SDL library is portable to several flavors of UNIX as well as BeOS, MacOS and Win32. This makes it an excellent choice for developing cross-platform games without compromising performance.
Unlike many multimedia toolkits, SDL does not actually talk to the system's hardware. Instead, it serves as a layer between an application and the underlying system. For instance, SDL's graphics system might use the frame buffer console or X11 under Linux, but DirectDraw under Windows. In either case, SDL's API is unchanged, and the application need not worry about what's going on underneath, and in some cases a carefully written SDL application can be ported to a new platform with a quick recompile.
In this article we'll take a tour of SDL's video API from the ground up. We'll also demonstrate how to collect input from the keyboard. Most of this article has been excerpted from a chapter in the author's upcoming book, on Linux game development (No Starch Press and Loki Entertainment Software, scheduled for early 2001).
SDL is free software (under the LGPL), and it's available for download at their web site (http://www.libsdl.org/). In addition to the actual SDL library, the SDL home page is full of example source code, demos, games and extensions. SDL is easy to install from source, but the SDL home page also provides binaries for several of the more common platforms.
If you've ever worked with Microsoft's DirectX toolkit, you'll notice that SDL is a tiny library in comparison. The source code to the core library weighs in at just under six megabytes, and that includes a lot of extra code that would never be linked into a Linux application. Don't be fooled, though—those six megabytes are well used, and the core SDL library provides almost everything you need to develop high-quality Linux games and media players. In addition, the web site is home to a number of add-on libraries that provide extra features such as image loading and advanced audio mixing. By keeping these features separate from the core library, SDL remains small and easy to learn.
The SDL library consists of several sub-API's, providing cross-platform support for video, audio, input handling, multithreading, OpenGL rendering contexts and other things that game programmers appreciate. Unfortunately, we don't have enough room to cover all of this, so we'll stick to video programming and input handling, the two things you really need in order to get your feet wet with SDL.
The SDL video API's sole purpose is to find a suitable video device and set it up for your application to use. After it has initialized the display (created a window or switched the video card into a particular mode) SDL gets out of your way, providing only a minimal set of functions for pushing blocks of pixels around. SDL is not a drawing toolkit; what you do with the video device after it is initialized is not SDL's business.
SDL uses structures called surfaces (of type SDL_Surface) to represent graphical data. A surface is simply a block of memory for storing a rectangular region of pixels (individual colored dots). Each surface has a width, a height and a specific pixel format (more on this later). SDL loads image files directly into surface structures, and the screen is also a surface (albeit a special one).
The most important property of surfaces is that they can be copied onto each other very quickly; that is, one surface's pixels can be transferred to an identically-sized rectangular area of another surface. This operation is called a blit (block image transfer). Blits are a fundamental part of game programming because they allow complete images to be composed out of pre-drawn graphics (often created by artists with image processing software). Since the screen is a surface like any other, entire images can be sent to the screen with a single blitting operation. SDL provides a generic function for performing fast blits between surfaces, and it can even convert between surfaces of different pixel formats on the fly.
Before we can begin blitting surfaces to the screen, we need to initialize the SDL library and switch the display into an appropriate mode. Take a look at Listing 1, the equivalent of “Hello, world!” in SDL.
Listing 1. Setting Up the Display
This program includes the SDL.h header file, which is the master header for SDL. Every SDL application should include this file. The program also includes two standard headers, for the printf and atexit functions.
We begin by calling SDL_Init to initialize SDL. This function takes an ORed list of arguments to indicate which subsystems should be initialized; we are only interested in the video subsystem, so we pass SDL_INIT_VIDEO (if we wanted audio, for instance, we would call this function with SDL_INIT_VIDEO | SDL_INIT_AUDIO). Unless a fatal error occurs, this function should return zero. We also use C's atexit facility to request that SDL_Quit be called before the program exits. This function makes sure that SDL has a chance to shut down properly (which becomes especially important if a fullscreen application crashes).
Next, we use the SDL_SetVideoMode function to inform the display of our desired resolution (in this case 640 pixels across by 480 pixels down) and color depth (16-bit packed pixel). There is a catch here: SDL will try to set up the display as requested, but it might fail. If this happens, SDL won't tell us, but it will instead emulate the requested mode internally. This is usually acceptable, since the emulation code is relatively fast, and we would usually rather not deal with multiple modes internally. SDL_SetVideoMode returns a pointer to the surface that represents the display. If something goes wrong, this function returns NULL.
Finally, we report success and exit. The C library calls SDL_Quit automatically (since we registered it with atexit), and SDL returns the video display to its original mode. (We could also call SDL_Quit explicitly if we wanted to shut the system down before exiting our application; there's no harm in calling it more than once.)
Now that we've created an SDL application, we need to compile it. SDL applications are easy to build; assuming a proper installation of SDL, they just require a few flags and libraries. The standard SDL distribution includes a program called sdl-config (similar to the gtk-config and glib-config programs that ship with the GTK+ toolkit) for supplying the appropriate commandline arguments to gcc. The command sdl-config --cflags produces a list of the options that should be passed to the compiler, and sdl-config --libs produces a list of libraries that should be linked in. We can use backtick substitution to drop this into the gcc command line. If SDL is installed on your system, you can compile this example with the following command:
$ gcc sdltest.c -o sdltest `sdl-config --cflags --libs`
Putting data into an SDL surface is simple. Each SDL_Surface structure contains a pixels member. This is a void pointer to the raw graphic image, and we can write to it directly if we know which type of pixel the surface is set up for. We must call the SDL_LockSurface function before we access this data (because some surfaces reside in special memory areas and require special handling). When we are finished with the surface, we must call SDL_UnlockSurface to release it. The width and the height of the image are given by the w and h members of the structure, and the pixel format is specified by the format member (which is of type SDL_PixelFormat). SDL often emulates nonstandard screen resolutions with higher resolutions, and the pitch member of the pixel format structure indicates the actual width of the frame buffer. You should always use pitch instead of w for calculating offsets into the pixels buffer, or else your application might not work on some display devices.
The example shown in Listing 2 will use the SDL pixel format information to draw individual pixels on the screen. We have chosen to use a 16-bit (hicolor) mode for demonstration purposes, but other modes are equally simple to program.
Listing 2. Drawing Individual Pixels on the Screen
The code's comments give the play-by-play, but a few things might not be obvious. This program employs a very general routine for constructing hicolor pixel values; this routine will work with any hicolor (16-bit) pixel format that SDL recognizes. Although we could write a separate (faster) routine for each possible hicolor data layout, this would require a lot of work and would only marginally improve performance. The hicolor 565 (5 red bits, 6 green bits, and 5 blue bits) pixel format is perhaps the most widely used and could be reasonably optimized, but 556 and 555 are not uncommon. In addition, there is no guarantee that the bit fields will be in the red-green-blue order. Our CreateHicolorPixel routine solves this problem by referring to the data in the SDL_PixelFormat structure. For instance, the routine uses the Rloss member of the structure to determine how many bits to drop from the 8-bit red component, and it then uses the Rshift member to determine where the red bits should be located within the 16-bit pixel value.
Another important issue involves the SDL_UpdateRect function. As we mentioned earlier, SDL sometimes emulates video modes if the video card is unable to provide a certain mode itself. If the video card does not support a requested 24-bit mode, for instance, SDL might select a 16-bit mode instead and return a fake frame buffer setup for 24-bit pixels. This would allow your program to continue normally, and SDL would handle the conversion from 24-bits to 16-bits on the fly (with a slight performance loss). The SDL_UpdateRect function informs SDL that a portion of the screen has been updated and that it should perform the appropriate conversions to display that area. If a program does not use this function, there is a chance that it will still work. It is better to be on the safe side and call this function whenever the frame buffer surface has been changed.
Finally, if you run the program you might notice that it runs in a window instead of taking over the entire screen. To change this, replace the zero in the SDL_SetVideoMode call with the constant SDL_FULLSCREEN. Be careful, though; fullscreen applications are harder to debug, and they tend to mess things up badly when they crash.
We've seen how to draw pixels directly to a surface, and there's no reason one couldn't create an entire game with this alone. However, there is a much better way to draw large amounts of data to the screen. Our next example will load an entire surface from a file and draw it with a single SDL surface copying function. The code can be seen in Listing 3.
Listing 3. Drawing Large Amounts of Data to the Screen
As you can see, the bitmap file is loaded into memory with the SDL_LoadBMP function. This function returns a pointer to an SDL_Surface structure containing the image, or a NULL pointer if the image cannot be loaded. Once this file has been successfully loaded, the bitmap is represented as an ordinary SDL surface, and a program can draw it onto the screen or any other surface. Bitmaps use dynamically allocated memory, and they should be freed when they are no longer needed. The SDL_FreeSurface function frees the memory allocated to a bitmap.
The SDL_BlitSurface function performs a blit of one surface onto another, converting between pixel formats as necessary. This function takes four arguments: a source surface (the image to copy from), an SDL_Rect structure defining the rectangular region of the source surface to copy, a destination surface (the image to copy to), and another SDL_Rect structure indicating the coordinates on the destination that the image should be drawn to. These two rectangles must be of the same width and height (SDL does not currently perform stretching), but the x and y starting coordinates of the regions may be different.
Games often need to simulate transparency. For instance, suppose that we have a bitmap of a game character against a solid background, and we want to draw the character in a game level. It would look silly to draw the character as is; the background would be drawn too, and the character would be surrounded by a block of solid color. It would be much better to draw only the pixels that are actually part of the character and not its solid background. We can do this with a colorkey blit. SDL provides support for this, and it even provides support for run-length colorkey acceleration (a nice trick for speeding up drawing). RLE acceleration provides an enormous performance boost for blitting colorkeyed images, but this is only practical for bitmaps that will not be modified during the course of the program (since modifying an RLE image necessitates unpacking and repacking the image).
A colorkey is a particular pixel value that a program declares to be transparent (in SDL, this is done with the SDL_SetColorKey function). Pixels that match an image's colorkey are not copied when the image is blitted. In our example of a game character, we could set the colorkey to the color of the solid background, and it would not be drawn. Colorkeys make it simple to combine rectangular images of non-rectangular objects.
In the next example we will use a colorkey blit to draw an image of Tux against another image [see Listing 4, available at ftp.linuxjournal.com.pub/lj/listings/issue81/]. Tux is stored against a solid blue background, and so we will use blue (RGB 0, 0, 255) as our colorkey. For comparison, we will also draw the same penguin image without a colorkey.
SDL assigns a “virtual keysym” to each key on the keyboard. These codes (integers) map at some level to the operating system's keyboard scan codes (which in term map to the codes produced by the keyboard's hardware), but SDL takes care of the mapping behind the scenes. SDL provides a preprocessor symbol for each virtual keysym; for instance, the Escape key corresponds to the symbol SDLK_ESCAPE. (You can find a list of valid keysyms in SDL's documentation.) We use these codes whenever we need to directly check the state (up or down) of a particular key, and SDL uses them when it reports key events. Virtual keysyms are represented by the SDLKey data type.
Since we won't be messing with the event interface for now (indeed, we haven't really even mentioned it), we'll need to ask the keyboard explicity for its current state whenever we need to know about a key. A program can obtain a snapshot of the entire keyboard in the form of an array. The SDL_GetKeyState function returns a pointer to SDL's internal keyboard state array, which is indexed with the SDLK_ keysym constants. You only need to call this function once; the pointer remains valid for the duration of the program. Each entry in the array is a simple Uint8 flag indicating whether that key is currently down. You should call SDL_PumpEvents periodically to update the data in the array.
That should be enough to get you started with SDL. We've skipped over a lot, notably animation, alpha blending and audio playback. If you want to learn more about programming with this library, the best place to start is the SDL Documentation Project at http://www.libsdl.org/. You might also want to stop by the #sdl channel on irc.openprojects.net, where you're likely to find a good number of SDL fans with varying amounts of experience. Have fun, and happy hacking!