 | Level: Intermediate Sam Lantinga, Lead programmer, Loki Entertainment Software Lauren MacDonell, Technical writer, Loki Entertainment Software
01 Mar 2000 Over the last month Sam Lantinga and Lauren MacDonell began the initial coding and graphic design of "Pirates Ho!". In this installment of their diary of the creation of their swashbuckling, role-playing game, the authors demonstrate the first steps in coding the game, using C++ and a variety of open source tools. Sam also addresses object caching, error handling, function logging, and more.
In this installment of the series on the new game "Pirates Ho!", we'll get down to the business of the initial coding of the game, working in C++ and introducing several GNU tools to help create some of the files we'll need. We'll then move on to look at error handling, object caching, logging functions, and finish up with a few notes on where to go from there. Why C++
At Loki, the majority of games use C or C++ with a smattering of assembly for speed-critical routines, but games can and have been written in any language. I decided to use C++ because it is natural to think of the game logic in an object-oriented manner during the design process, and because I am comfortable with C++ (see Sidebar)  |
Static C++ objects
It's tempting to use static C++ objects for globals that are used throughout the lifetime of a game, but there are some pitfalls to using them. Static C++ objects have their constructors run before main, and the order in which the constructors and destructors are run is not specified. This means that if they rely on any external conditions, they may not behave as expected.
Using static C++ objects within dynamically loaded shared objects is even more perilous since the current implementation of gcc (2.95.2) does not call the destructors when you unload the shared object. Instead, the destructor is called at exit, causing a crash because the code for the destructor is no longer available.
|
|
C++ is also widely supported on different platforms, although you need to be careful of what C++ language features you use, because the different compilers may or may not implement or enforce all of the ANSI C++ specification. In the course of developing Pirates Ho!, we will be compiling with g++ on Linux and Visual C++ on Windows, and using the free MPW tools on MacOS.
Automake and autoconf, the Linux way
Automake and autoconf are a set of GNU tools that allow automatic configuration of source code for different compilation environments. SDL supports applications that are autoconf'd by providing an m4 macro. This macro allows the configuration script to detect whether the appropriate version of SDL is available on the system. To create a new application that uses automake and autoconf, we followed these 6 steps:
-
Create a README. This file is used by autoconf later to verify that the source distribution is complete. It's also helpful for your users.
-
Create configure.in. The GNU autoconf tool reads a configuration file called configure.in, which tells it what features it needs to look for on the system, and what output files it needs to generate using this information. Here is the configure.in file for our game:
configure.in:
# This first line initializes autoconf and gives it a file that it can
# look for to make sure the source distribution is complete.
AC_INIT(README)
# The AM_INIT_AUTOMAKE macro tells automake the name and version number
# of the software package so it can generate rules for building a source
# archive.
AM_INIT_AUTOMAKE(pirates, 0.0.1)
# We now have a list of macros which tell autoconf what tools we need to
# build our software, in this case "make", a C++ compiler, and "install".
# If we were creating a C program, we would use AC_PROC_CC instead of CXX.
AC_PROG_MAKE_SET
AC_PROG_CXX
AC_PROG_INSTALL
# This is a trick I learned at Loki - the current compiler for the alpha
# architecture doesn't produce code that works on all versions of the
# alpha processor. This bit detects the current compile architecture
# and sets the compiler flags to produce portable binary code.
AC_CANONICAL_HOST
AC_CANONICAL_TARGET
case "$target" in
alpha*-*-linux*)
CXXFLAGS="$CXXFLAGS -mcpu=ev4 -Wa,-mall"
;;
esac
# Use the macro SDL provides to check the installed version of the SDL
# development environment. Abort the configuration process if the
# minimum version we require isn't available.
SDL_VERSION=1.0.8
AM_PATH_SDL($SDL_VERSION,
:,
AC_MSG_ERROR([*** SDL version $SDL_VERSION not found!])
)
# Add the SDL preprocessor flags and libraries to the build process
CXXFLAGS="$CXXFLAGS $SDL_CFLAGS"
LIBS="$LIBS $SDL_LIBS"
# Finally create all the generated files
# The configure script takes "file.in" and substitutes variables to produce
# "file". In this case we are just generating the Makefiles, but this could
# be used to generate any number of automatically generated files.
AC_OUTPUT([
Makefile
src/Makefile
]) |
-
Create acinclude.m4. The GNU aclocal tool reads a list of macros that it uses from the file acinclude.m4, and merges these into the file aclocal.m4, which is used by autoconf to generate the "configure" script.
In our case, we just wanted to add support for SDL, so we copied the file "sdl.m4" that comes with the SDL distribution into acinclude.m4. The file "sdl.m4" is usually found in the /usr/share/aclocal directory, if you have the SDL-devel rpm installed.
-
Create Makefile.am. The GNU automake tool uses the Makefile.am file as a description of the source and libraries used to create a program. It uses this description to generate the Makefile.in templates that are turned into makefiles when the user runs the configure script.
In our case, we have a top-level directory containing the README, some documentation and scripts, and a subdirectory "src" that contains the actual source code for the game.
The top level Makefile.am file is very simple. It just tells automake that there is a subdirectory that has a Makefile.am file that needs to be read, and gives a list of extra files that need to be added to the distribution when we build a source archive:
Makefile.am
SUBDIRS = src
EXTRA_DIST = NOTES autogen.sh |
The source Makefile.am contains the real meat:
src/Makefile.am
bin_PROGRAMS = pirates
pirates_SOURCES = \
cacheable.h game.cpp game.h image.cpp image.h logging.cpp logging.h \
main.cpp manager.h music.cpp music.h nautical_coord.h paths.cpp \
paths.h screen.h screen.cpp ship.h splash.cpp splash.h sprite.cpp \
sprite.h status.cpp status.h text_string.h textfile.h widget.h \
widget.cpp wind.h |
Here we tell automake that the program we are building is called "pirates", and it consists of a large number of source files. Automake has built-in rules to build C++ source files, and will put them into the generated makefile so we don't have to worry about it and can concentrate on writing good code.
-
Bootstrap the configuration process: automake needs to copy a few auxiliary files that it uses to detect the current architecture and so forth. To bootstrap automake, re-run "automake -a -v -c --foreign" until it stops telling you that it is copying files.
-
Create autogen.sh: This step is optional, but provides a way for you to regenerate all of the configure-related files in one easy step, and then configure the software for your current build environment. We use the following script:
autogen.sh
#!/bin/sh
#
aclocal
automake --foreign
autoconf
./configure $* |
 |
Error handling
The first thing I wrote when I started was a base class for handling the status of an object: status.h
/* Basic status reporting class */
#ifndef _status_h
#define _status_h
#include "textstring.h"
typedef enum {
STATUS_ERROR = -1,
STATUS_OK = 0
} status_code;
class Status
{
public:
Status();
Status(status_code code, const char *message = 0);
virtual ~Status() { }
void set_status(status_code code, const char *fmt, ...);
void set_status(status_code code) {
m_code = code;
m_message = 0;
}
void set_status_from(Status object) {
m_code = object.status();
m_message = object.status_message();
}
void set_status_from(Status *object) {
set_status_from(*object);
}
status_code status(void) {
return(m_code);
}
const char *status_message(void) {
return(m_message);
}
protected:
status_code m_code;
text_string m_message;
};
#endif /* _status_h */ |
This class provides a way of storing an object's status and propagating that status up to the level where messages are printed to the screen. For example, when loading a sprite object definition from a file, the load function may call an image constructor with the name of an image file. If the image object cannot load the file, then it sets the status to STATUS_ERROR, and sets the error message, which is propagated up to the top level:
bool Image::Load(const char *image)
{
const char *path;
/* Load the image from disk */
path = get_path(PATH_DATA, image);
m_image = IMG_Load(path);
free_path(path);
if ( ! m_image ) {
set_status(STATUS_ERROR, IMG_GetError());
}
return(status() == STATUS_OK);
}
bool Sprite::Load(const char *descfile)
{
...
m_frames = new Image *[m_numframes+1];
for ( int i=0; i<m_numframes; ++i ) {
m_frames[i] = new Image(imagefiles[i]);
// This function is in the Status base class, and copies
// the status any error message from the image object
if ( m_frames[i]->status() != STATUS_OK ) {
set_status_from(m_frames[i]);
}
}
return(status() == STATUS_OK);
} |
if ( ! sprite->Load(spritefile) ) {
printf("Couldn't load sprite: %s\n", sprite->status_message());
} |
This style of error handling code is used heavily throughout our game.
Object caching
Early on I realized that I would need some sort of caching algorithm for images that may be shared, sound samples that may play simultaneously, etc. I decided to write a generic resource manager that would cache accesses to all objects in the game that could be used more than once simultaneously. The first step was to create a general base class for all cacheable objects so they could be manipulated inside the cache without knowing exactly what type of objects they are: cacheable.h
/* This object can be cached in the resource manager */
#ifndef _cacheable_h
#define _cacheable_h
class Cacheable
{
public:
Cacheable() {
ref_cnt = 1;
}
virtual ~Cacheable() {}
void AddRef(void) {
++ref_cnt;
}
void DelRef(void) {
/* Free this object when it has a count of 0 */
if ( --ref_cnt == 0 ) {
delete this;
}
}
int RefCnt(void) {
return(ref_cnt);
}
private:
int ref_cnt;
};
#endif /* _cacheable_h */ |
All cacheable objects have a reference count associated with them so that each time they are used, the count is incremented, and each time they are released, the count is decremented. In this implementation, the object frees itself when the reference count reaches zero. This allows the object to be created, passed to a function that may keep a reference to the object, and then released by the creator, which will only free the object if it is no longer in use. This type of implementation is prone to bugs where an object is accidentally freed multiple times. I will be adding code to catch this case later in the project. Essentially I will keep a separate pool of cacheable objects and will record the stack trace information for the last object release and raise a trap signal when I detect the object being released again. This can be done on Linux using a set of functions included in glibc 2.0 and newer that allow you to record and print stack trace information from within your application. See /usr/include/execinfo.h for more information. Note that the destructor for the cacheable object is virtual. This is necessary so that when the base class is freed, the proper derived class destructors are called. Otherwise, just the base class destructor will be called and objects left around in the derived classes will be leaked. Once the cacheable objects are created, I need a cache to hold them:
/* This is a data cache template that can load and unload data at will.
Items cached in this template must be derived from the Status class
and have a constructor that takes a filename as a parameter.
*/
template<class T> class ResourceCache : public Status
{
public:
ResourceCache() {
m_cache.next = 0;
}
~ResourceCache() {
Flush();
}
T *Request(const char *name) {
T *data;
data = 0;
if ( name ) {
data = Find(name);
if ( ! data ) {
data = Load(name);
}
if ( data ) {
data->AddRef();
}
}
return(data);
}
void Release(T *data) {
if ( data ) {
if ( data->RefCnt() == 1 ) {
log_warning("Tried to release cached object");
} else {
data->DelRef();
}
}
}
/* Clear all objects from the cache */
void Flush(void) {
while ( m_cache.next ) {
log_debug("Unloading object %s from cache",
m_cache.next->name);
Unload(m_cache.next->data);
}
}
/* Clear all unused objects from the cache
This could be faster if the link pointer wasn't trashed by
the unload operation...
*/
void GarbageCollect(void) {
struct cache_link *link;
int n_collected;
do {
for ( link=m_cache.next; link; link=link->next ) {
if ( link->data->RefCnt() == 1 ) {
Unload(link->data);
break;
}
}
} while ( link );
log_debug("Cache: %d objects garbage collected", n_collected);
}
protected:
struct cache_link {
char *name;
T *data;
struct cache_link *next;
} m_cache;
T *Find(const char *name) {
T *data;
struct cache_link *link;
data = 0;
for ( link=m_cache.next; link; link=link->next ) {
if ( strcmp(name, link->name) == 0 ) {
data = link->data;
break;
}
}
return(data);
}
T *Load(const char *file) {
struct cache_link *link;
T *data;
data = new T(file);
if ( data->status() == STATUS_OK ) {
link = new struct cache_link;
link->next = m_cache.next;
link->name = strdup(file);
link->data = data;
m_cache.next = link;
} else {
set_status_from(data);
delete data;
data = 0;
}
return(data);
}
void Unload(T *data) {
struct cache_link *prev, *link;
prev = &m_cache;
for ( link=m_cache.next; link; link=link->next ) {
if ( data == link->data ) {
/* Free the object, if it's not in use */
if ( data->RefCnt() != 1 ) {
log_warning("Unloading cached object in use");
}
data->DelRef();
/* Remove the link */
prev->next = link->next;
delete link;
/* We found it, stop looking */
break;
}
}
if ( ! link ) {
log_warning("Couldn't find object in cache");
}
}
}; |
This implementation is relatively straightforward. I use a template because this cache object will be used with several different types of data: images, music, sounds, etc. Cacheable data is kept in a singly linked list, and if a requested object is not in the cache, it will be loaded dynamically. When data is no longer needed, it is not immediately freed, but is released to the general pool in case it will be needed in the near future.  |
Using printf() for debugging
One of the most powerful debugging tools at your fingertips is printf(). I like to use printf() instead of cout in my code because it is part of the standard I/O library and doesn't introduce dependencies on iostream, which has different implementations on different platforms.When tracking down a subtle problem, I often find that the code goes through many complicated steps before I find the source of the problem. I tend to sprinkle printf() statements throughout the suspect areas of code to get a feeling for what is being executed and when. Often this is the only way to debug a problem when cross-compiling to Win32.
|
|
The cache has a garbage collection function that frees all unused objects. I will use this during development to detect object leaks in the code. When I perform garbage collection, I can traverse the list of remaining objects to make sure that nothing is left that should have been freed.
Logging functions
It is useful to have various logging functions to print debug messages, show warnings to the user, etc. I whipped up a set of functions that we use extensively in the game. At a later point, we will probably need better control over what is being printed and when. For example, we may want to print object cache information, but not widget constructor/destructor logs, etc. Here is what we use now:
/* Several logging routines */
#include <stdio.h>
#include <stdarg.h>
void log_warning(const char *fmt, ...)
{
va_list ap;
fprintf(stderr, "WARNING: ");
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, "\n");
}
void log_debug(const char *fmt, ...)
{
#ifdef DEBUG
va_list ap;
fprintf(stderr, "DEBUG: ");
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, "\n");
#endif
} |
Note the use of varargs: va_start(), vsprintf(), va_end(). This allows us to have printf-like functionality in our logging, allowing statements such as: log_warning("Only %d objects left in cache", numleft); These functions could be extended to pop up dialog boxes, or log to a file as well as print to standard error. We'll see what we'll need for our game in the future.
Reinventing the wheel
In many cases, if you need to do something in your code, chances are good that it has been done before. Whenever you embark on a coding project, the first thing to do is search the net for other projects that are similar. You may find that you don't need to do any work at all -- someone may have already written the code you need. A good place to look on the net is Freshmeat (see Resources). Many open source projects have their projects listed on Freshmeat. In our case, we needed code to load various image formats, and code to load and play sounds and music. Since this article focuses on designing a game with SDL, we will look for code already designed to work with our library of choice. To load images, we will use the SDL_image library; to load and play audio clips and music, we will use the SDL_mixer library (see Resources). We also needed a simple singly linked list implementation, and for that we turned to the Standard Template Library (or STL for short) released by SGI (see Resources). The Standard Template Library has varying implementations on different platforms, so we may end up using something different in the final game, but for now it provides a nice set of basic utility objects.
Putting it all together
We now have all the basic pieces to begin putting something on the screen. We have code to load images and music, code to display to the screen, and glue to put it all together. We added a sample splash screen, and Nicholas Vining contributed sample title music. This is the result: Sample splash screen

You will need the libraries mentioned above already installed to run the binary sample available in Resources.
Conclusion
We have begun the initial coding for our game, and the results are promising. So far we can play the initial splash sequence and we have some of the basic infrastructure for the game completed. Next month we hope to have the first pass of the ship combat ready for testing, and lots of pictures of ships and landscapes.
Resources
About the authors  | |  | Sam Lantinga is the author of the Simple DirectMedia Layer library, and is currently employed as lead programmer at Loki Entertainment Software, a company dedicated to bringing best-selling games to Linux. His involvement with Linux and games began in 1995 with various DOOM! tool ports and the port of the Macintosh game Maelstrom to Linux. |
 | |  | Lauren MacDonell is a technical writer and amateur artist. She and Sam are co-developing a pirate role-playing game "Pirates Ho!". When she isn't working, writing, or belly dancing, she takes care of her tropical fish. |
Rate this page
|  |