Sprocket Invaders
Volume Number: 13 (1997)
Issue Number: 2
Column Tag: Game Development
Shift into High Gear with Apple Game Sprockets
By Jamie Osborne, Apple Computer, Inc.
Turning your game idea into a reality, step by step
In the Beginning, There was a Cool Idea...
So, you want to write a cool game for the Macintosh. Well, you're in luck. This article will take you step-by-step through the whole process, beginning with an idea for a game, all the way to playing with your friends over a network. It will deal with loading and blitting sprites to the screen, getting input from any input device, no matter how snazzy, generating sounds that are correctly attenuated for distance and location, and doing all the work necessary to play your game over a network.
If that sounds like way too much to cover in a single MacTech article, you're right. At least, you would have been right if this article were written a year ago. Since the advent of Apple Game Sprockets, it is now possible to discuss everything you need to know (with a small amount of strategic glossing-over) in the space of a single article. I won't be able to delve deeply into each of the Sprockets, but that's OK because there's an on-line reference manual for that. I'll also gloss over some of the details about how to do things, such as how to load or store resources, or what goes into the application shell. However, the entire source for the game I will develop here is available with this issue's code disk, as well as on the Internet.
Now, what game am I going to do? A snazzy 3D blood bath like Marathon? No. A multi-player flight simulator that can have hundreds of people playing together over the Internet, a la Warbirds? No.
I am going to start off with something that has proven game play, yet is straightforward enough that I can actually get it done. I am going to do a game called "SprocketInvaders." Why? For two very good reasons. First, it's doable. For every game that you've seen on the store shelves or in the shareware section of INFO-MAC, there are a dozen other games that were started with the best intentions but never completed. Many of us have had good ideas for games. Some of us have even started games ourselves. However, very few of us have ever finished writing a computer game. Therefore, I am going to write something that I know I can do in a reasonably small period of time. The second reason is that despite (or perhaps because of) the fact that it is a very simple game, it lends itself very well to showing off the different areas I am concentrating on - rendering, sound, input, and networking.
Step One - Design
Designing SprocketInvaders was greatly aided by the fact that the "Invaders" game archetype is firmly established. Of course, you must make your game sufficiently unique so that you don't violate anyone's copyright. My design was pretty easy. I knew that I wanted aliens that moved in the invader style. I wanted a ship that would be able to move left, move right, and fire. I wanted the waves to get progressively harder by speeding up the rate at which the aliens moved and fired.
When you're starting a new game, it's easy to get bogged down in over-design. With SprocketInvaders that was not a problem, but with something more complex, perhaps a game where each level introduced new aliens or weapons, you might find yourself spending a whole bunch of time designing and not much time implementing. Of course, designing before implementing a game, or any software for that matter, is better than implementing first and then designing as an afterthought. Again, the best way is to set attainable goals, achieve those goals, and build on your success. Design your game with one level, get it all running, then use your success to work on more sophisticated features or levels.
Step Two - The Shell
The best way to begin any new game is to start with a good application shell. This is true on any platform. Over the years, thousands of people have written thousands of Macintosh applications, nearly all of which had the same design.
Initialize the Toolbox and globals.
Do the event loop.
Quit.
There is much more complexity than I've outlined here - handling events, managing the menu bar, etc. I have included a Macintosh shell application that is designed specifically for writing games that use the Apple Game Sprockets. It is a CodeWarrior project, but you should be able to use the source code in any development environment. You might notice that the shell code in SprocketInvaders differs significantly from the Game Shell I am providing. This is because the shell was done after SprocketInvaders was written. Since I really want to talk about the game itself, I will leave it to you to check out the shell.
Step Three - Artwork
Artwork is not really a stage, but more of an ongoing process. The better the artwork, the more impressive your game will look to the end user. The artwork for SprocketInvaders was done before the game was even started, since the main programmer's hobby is artwork, he enjoyed doing it up front. He is the exception. Most programmers cringe when they think about doing art for their game, and they have good reason - most people are not good artists.
The best recommendation I have about doing your art is that you find someone who has at least some talent for it. If that means you, fantastic! If not, you should call up friends and relatives looking for someone who has a knack for it. The artwork in SprocketInvaders will not likely wind up in a museum of computer art, but it's not too shabby. If you can't manage to make art as good as the art in SprocketInvaders, find someone who can. As a last resort, cajole them into helping you with promises of fame and fortune. Many amateur artists are willing to do it just to have their name in the About box. A small price to pay for a better game.
If you plan on selling your game commercially you must have your art done by someone who is a professional, or at least does professional-quality art. However, if you hope to have some company publish your game, you might be able to get away with doing your own art for the first versions and letting the publisher redo the artwork. Doing that, of course, will cost you some of the profit.
On the technical side of things, the sprites were drawn in a graphics program, then stored in SPRT' resources that include the height and width of the sprite, as well as some information about the different versions used to animate the sprites.
Listing 1: Sprite.h
typedef struct Frame
{
UInt16 height, width;
SInt16 hotX, hotY;
UInt8 *image;
} Frame, *FramePtr;
typedef struct Sprite
{
UInt16 numFrames;
FramePtr *frames;
} Sprite, *SpritePtr;
The Sprite structure is simply a pointer to an array of Frames, with a count. The Frame structure is a little more interesting. It contains the pointer to the actual pixmap (the image), as well as information about the height and width of the sprite. Keep in mind that the pixmap is bit-depth dependent. In the case of SprocketInvaders, all of the artwork is 8-bit, and the user's monitor must be in 8-bit mode when the game runs, or else evil things will happen. Luckily, DrawSprocket takes care of setting the user's monitor to the exact bit depth and resolution you specify, so your game will always be guaranteed to look right. If you want your game to be playable in several bit depths, you need to create artwork for all of the bit-depths that you want to support.
You should be able to use the sprite libraries in SprocketInvaders "right out of the box" for your game. There are some dependencies on other parts of the code, such as the blitters, so you might need to make modifications if you intend to deviate very much from SprocketInvaders' sprite/graphic routines. There are also some very good sprite libraries available on the Internet.
Step Four - Initialization
Initialization is one of those things that should be a snap, right? Call a couple of Toolbox init functions, load in some data, and you're done. Well, the answer is "yes and no." Once you have a good game shell and you're familiar with all the different services you will be using, such as the Game Sprockets, initialization is easy. However, the first time can be pretty tricky, and there are many snares you can set for yourself by incorrectly initializing your game. These snares can trap you later on in the form of bizarre crashes or strange behavior. That is why it is essential to get your initialization right. It's the price you pay up front for the convenience you get later on.
Listing 2: main.c
main Snippet
Main is where we do all our initialization.
ToolboxInit();
// Make sure that the DrawSprocket and InputSprocket libraries are present
if (DSpStartup == nil)
FatalError("This game requires DrawSprocket.");
if (ISpInit == nil)
FatalError("This game requires InputSprocket.");
// Reseed the random number generator
qd.randSeed = TickCount();
startupModifiers = EventInit();
gNetSprocketPresent = InitNetworking();
MenuInit();
GraphicsInit(startupModifiers);
SoundHandlerInit(startupModifiers);
RedbookHandlerInit(startupModifiers);
The first thing you notice is that I check to make sure that DSpStartup and ISpInit are valid symbols, to ensure that the DrawSprocket and InputSprocket shared libraries are installed in the user's system. I could also do this by strong-linking the libraries when I build the SprocketInvaders application. However, doing it this way allows me to present the user with a slightly more friendly error message than the default one, "Couldn't start the application SprocketInvaders because an unexpected error occurred."
Input
In addition to doing some MacOS event-handling stuff, I initialize InputSprocket. At first glance, the code for InputSprocket initialization may seem utterly obtuse. However, it is not as tricky as it seems.
The myNeeds structure is simply a list of all the input elements wanted in order to play the game. For SprocketInvaders, I need a movement axis and fire button for each player, as well as a key to exit the game. Look at the first element in the needs list.
Listing 3: EventHandler.c
EventInit Snippet
{
"\pGreen Movement",
200,
0,
kISpElementKind_Axis,
kISpElementLabel_XAxis,
0,
0,
0,
0
},
This is a pretty big structure to fill in, but most of the fields are used only for expert customization. For my needs, filling in a couple of the fields and leaving the rest set to 0 will do just fine. The ISpNeed structure requires:
The name of the control for display in the configuration dialog.
The icon suite resource id that InputSprocket should use in the configuration dialog. (You must create the icon and store it in your app's resource fork. You could just punt and specify 0 here, telling InputSprocket to use default icons, but that's lame.)
A reserved field that should be set to 0.
The kind of information wanted, such as an axis, button, or direction pad. In this case, I want a true analog axis.
A label telling InputSprocket how you intend to interpret the element - in this case, the X (horizontal) axis. InputSprocket uses this information to provide default mappings from an input device to the input needs. For example, a joystick's horizontal axis will be mapped to the X axis need by default.
Flags and reserved fields you don't need to worry about.
I set up the rest of the needs structure the same way, then call
theError = ISpElement_NewVirtualFromNeeds(numInputs, myNeeds,
gInputElements, 0);
This turns the list of needs into an element list that I then pass to the ISpInit function. Again, this might seem like excessive work to set up input, but the reason I have to do so much here is that InputSprocket is extremely flexible. It needs to be able to support input devices that didn't even exist when the game was written. By using the InputSprocket API, the game will work with all devices that have an InputSprocket driver, present and future. Just think - your game could be the first one played with the Direct Neural Implant Controller in the year 2001!
Networking
Going back to main, I've handled InputSprocket initialization. Next up is initializing the network code.
Listing 4: NetSprocketSupport.c
InitNetworking Snippet
if (! NetSprocketPresent())
return (false);
// Initialize NetSprocket
status = NSpInitialize(kStandardMessageSize, kBufferSize,
kQElements, kGameID, kTimeout);
FAIL_OSSTATUS(status, "NSpInitialize failed");
Unlike DrawSprocket and InputSprocket, I don't need NetSprocket to be installed in order to play the game. If the user does have NetSprocket, I enable the game to be played over the network. If the user doesn't have it, I simply disable the menu item that allows the user to play over the network. They can still play with one or two players on a single machine. When linking the game, I import NetSprocketLib as a weak' link. Doing this allows the game to run even if NetSprocket isn't present. The key is to make sure that you do not try to call any of the library's functions if not present at runtime. I do this in SprocketInvaders by checking if the NSpInitialize function is valid (non-null) and setting the gNetSprocketPresent global flag accordingly.
Once I have determined that NetSprocket is present, I initialize it by calling NSpInitialize. The values you pass to NSpInitialize help NetSprocket optimize the way it handles your messages. You give it the standard' message size, which in the case of SprocketInvaders is the size of the player input message. The next two parameters tell NetSprocket how much buffer space to pre-allocate. You can set both of these to 0 to have NetSprocket use default values. The game ID you pass should be your application's creator type. NetSprocket uses this as a unique identifier for your game, allowing it to pick a default TCP/IP port number or an AppleTalk NBP type that is unique for your game. The time-out parameter simply tells NetSprocket how long it should wait, in milliseconds, before giving up on certain network operations, such as making a connection.
Drawing
main does a few more initializations, then calls GraphicsInit:
Listing 5: Graphics.c
GraphicsInit Snippet
Initializes our color table, gets our buffers ready, and fades the screen.
DSpStartup();
// CLUT 8 is the standard 8-bit system color table and is always present
gGameCLUT = GetCTable(8);
DetachResource((Handle) gGameCLUT);
// Define and create our drawing context
GraphicsSelectContext(modifiers);
// Add our various colors to the color table
GraphicsMergeCLUT(128, kFirstIconColor, kLastIconColor);
GraphicsMergeCLUT(kPICTGasCloud, kFirstBGColor,
kLastBGColor);
GraphicsMergeCLUT(kCLUTSprites, kFirstSpriteColor,
kLastSpriteColor);
// Create our underlay and load a picture into it
GraphicsCreateUnderlay();
GraphicsLoadBackground(kPICTGasCloud);
// Swap the buffers once to get the underlay image on the screen
DSpContext_GetBackBuffer(gDisplayContext, kDSpBufferKind_Normal, &backBuffer);
DSpContext_InvalBackBufferRect(gDisplayContext,
&backBuffer->portRect);
GraphicsUpdateScreen();
// Fade up from black and then put the context in the paused state
// so that the user has access to the menu bar.
DSpContext_FadeGammaIn(nil, nil);
GraphicsPaused();
A color lookup table, or CLUT, is simply a table of colors that the video hardware uses when in 8-bit video mode. Technically, a CLUT is a MacOS resource that describes a palette. Since one cannot express a complete range of colors with only 8 bits, computers use indexed colors that simply create a palette of colors, much like a painter's palette, that the computer uses to draw everything. When using indexed colors, every pixel on the screen must be one of the colors in the palette. More sophisticated games use custom color tables designed to make the art in the game look very cool. If a game has no need to draw anything in yellow, for instance, yellow's spot in the palette can be used for another color - perhaps another shade of pinky-russet. An even more sophisticated use of CLUTs is to animate the color table, rapidly changing the contents of each entry in the table. This technique is used in Marathon and many other high-end games to produce stunning lighting effects.
However, SprocketInvaders really doesn't need to be that sophisticated. In fact, if it weren't for the neat background image used, I probably could have gotten away with using the standard system palette. I wanted everything to look just right though, so I took three different CLUTs - for the backdrop, the icons, and the sprites, and merged them all into one CLUT that DrawSprocket uses for the front buffer (the one that is actually displayed on the screen.) Once I've created the custom CLUT, I load in the backdrop PICT as an underlay, which is simply a buffer that contains the image that should be drawn under everything else.
The astute reader might note that I have glossed over the GraphicsSelectContext function. The reason is that the code is pretty self-explanatory. DrawSprocket quite handily allows you to specify exactly what kind of video environment you want your game to run in. If the user has a multi-sync monitor that uses 32-bit color in 1024x768 resolution, but your game wants to run in a 640x480 screen with 8-bit color, DrawSprocket can set the monitor up exactly as you want it, then set it back to exactly the way it was before the user ran your game. It may not sound like much work, and it's not hard for you to do using DrawSprocket, but I can tell you that using DrawSprocket to set your video mode saves you, the game developer, numerous headaches.
Once DSpFindBestContext returns a DrawSprocket context (an object from which I get the front and back buffers), I get the back buffer and invalidate it. Finally, I call DSpContext_SwapBuffers, causing DrawSprocket to move the back buffer to the front. Since the back buffer is invalid and I have set an underlay buffer, DrawSprocket copies the contents of the underlay to the front buffer, leaving a screen that contains the nice backdrop.
I should note that I also use DrawSprocket's gamma fade routines to fade out the screen before I do the mode switch (to 640x480x8), then fade back in. Gamma fades are a very nice way to hide the ugliness of switching monitor modes from the user. I highly recommend that you follow the same procedure when initializing your game.
Sound
The last service to initialize is sound. The sound initialization requires that I allocate new sound channels using the Sound Manager, as well as create new SoundSprocket objects, if SoundSprocket is installed.
Good sounds are a must for any game because they dramatically improve the enjoyability. For the amount of work done in coding, the payoff is huge. I use the Sound Manager in SprocketInvaders for all of my sounds. As with most games, I allocate a set of sound channels that I use for all the sounds played in the game. I decided to use the same number of channels as I had sounds, assigning each to a channel. If a sound is already playing in the channel when I start a new sound, the new sound simply preempts the old one.
This approach works just fine for SprocketInvaders, or for any game with a limited number of sounds. However, if your game uses lots of sounds, you should consider another approach to allocating sound channels, such as selecting an appropriate channel based on how important it is for the user to hear the sound.
Once I have initialized the Sound Manager items, I initialize SoundSprocket, if it is present at runtime.
Listing 6: SoundHandler.c
SoundHandlerInit Snippet
Initializes the Sound Manager, then creates the necessary SoundSprocket objects to do 3D sound localization.
// Create the listener
theErr = SSpListener_New(&gListener);
if (theErr)
FatalError("Could not create a sound sprocket
listener.");
// Define our unit of distance measurement
theErr = SSpListener_SetMetersPerUnit(gListener, 0.05);
if (theErr)
FatalError ("Could not set reference distance for
listener.");
// Attach the sound localization component to each of the sound
// channels that we'll be playing through.
for (i = 0; i < soundNumSounds; i++)
{
SoundComponentLinkmyLink;
// Create the source
theErr = SSpSource_New(&gSource[i]);
if (theErr)
FatalError("Could not create a sound sprocket
source.");
// Install the filter
myLink.description.componentType = kSoundEffectsType;
myLink.description.componentSubType =
kSSpLocalizationSubType;
myLink.description.componentManufacturer = 0;
myLink.description.componentFlags = 0;
myLink.description.componentFlagsMask = 0;
myLink.mixerID = nil;
myLink.linkID = nil;
theErr = SndSetInfo(gChannels[i],
siPreMixerSoundComponent, &myLink);
if (theErr)
FatalError("Could not install the sound sprocket filter
into the channel.");
}
gSoundSprocket = true;
SetListenerLocation();
All I need to do is allocate a new listener object (so that I'll know the relative locations of the sound generators), set the distance units (which are pretty arbitrary), and attach a localization component to each sound channel. The localization component is a filter placed into the Sound Manager that allows me to specify coordinates when I play the sound. The filter takes those coordinates and attenuates the sound accordingly.
Well, I said it wasn't easy. Initialization is hard work. I skipped some of the details in each area, and I didn't fully explain some of the services used - I will do that later. Remember, though, once you have initialized one game, you are well on your way to initializing many others.
Step Five - The Infinite Loop
I am referring to the Game Loop. GameLoop (in SprocketInvaders.c) is called by the application shell's main event loop (after it takes care of the business of handling MacOS events). This step is actually quite involved. It can be broken down into stages, each of which will require a good deal of explaining.
- Handle player input.
- Do collision detection.
- Handle game logic.
- Draw everything.
The event loop is a very important part of the game because it is the heartbeat that keeps the game alive. In addition to the stages listed above, there are some other sundry tasks the game loop routine takes care of - things like checking to see if the current wave of the game is over or the user wants to quit. Those pieces of code are straightforward MacOS programming. Let's look in more detail at the stages listed above.
Handle player input
In the earlier incarnations of the game I had an input implementation that used GetKeys. It took me under an hour to add InputSprocket support. Now, SprocketInvaders can be played with just about every conceivable input device available.
How to handle input depends on whether or not the game is being played over the network. When playing locally with either one or two players, I simply call GetGreenInput and GetRedInput. In a one-player game, the player is always the green player. In a two-player game, player two is the red player.
When playing over the network, I need a little bit of extra logic. The player who hosted the game (advertised it on the network for another to join) is always considered player one, the green player. The person who joined the game is player two, the red player. Therefore, when I do my input, the host player (gIAmHost is true) first reads the Green input from the local computer then sends it to the other player. The player who joined reads the Red input from the local computer and then sends it. Each then calls the GetPlayerInput function for the other player. The input for that other player will have come in via the network by the time the GetPlayerInput function returns.
Listing 7: SprocketInvaders.c
GameLoop Snippet
Get the input from both players.
if (gNetPlay)
{
if (gIAmHost)
{
GetGreenInput();
SendInputState(gGameKeys.greenLeft, gGameKeys.greenRight,
gGameKeys.greenFire);
GetRedInput();
}
else
{
GetRedInput();
SendInputState(gGameKeys.redLeft, gGameKeys.redRight,
gGameKeys.redFire);
GetGreenInput();
}
}
else
{
GetGreenInput();
if (gTwoPlayers)
GetRedInput();
}
For now, I'll gloss over how the input is delivered via the network. Just take it as during net play calling the GetPlayerInput function for the remote player will get the necessary input over the network. I do, however, want to look more closely at what happens when getting the input for the local player.
I use InputSprocket for getting the player's input because it is by far the most reliable, expressive, and compatible input API that exists for the MacOS. Reliable because it has been extensively tested and, unlike other methods such as GetKeys, you'll never miss an input event. Expressive because you can tell InputSprocket your game's wildest needs. Need three analog axes, two buttons, and a toggle switch to play your game? No problem. InputSprocket maps your game's needs to the player's input devices. Compatible because you know it's going to work with most input devices available, including the keyboard and mouse, Thrustmaster FCS, WCS, and RCS (try playing SprocketInvaders with the rudders - it's wild!), all CH products, Gravis Game Pad, Gravis Firebird, etc. The list keeps growing.
Enough extolling of virtues. Let's look at the code for getting information from InputSprocket.
Listing 8: SprocketInvaders.c
GetGreenInput Snippet
Obtains the input state for the local player from InputSprocket.
UInt32 input;
// Check the movement axis
ISpElement_GetSimpleState(gInputElements[greenMovement],
&input);
gGameKeys.greenLeft = false;
gGameKeys.greenRight = false;
if (input < 0x2FFFFFFF)
{
gGameKeys.greenLeft = true;
}
else if (input > 0xBFFFFFFF)
{
gGameKeys.greenRight = true;
}
else if (input < 0x5FFFFFFF)
{
gGreenAccum += ((float) input - (float) 0x5FFFFFFF) /
(float) 3FFFFFFF;
if (gGreenAccum < -1)
{
gGameKeys.greenLeft = true;
gGreenAccum += 1;
}
}
else if (input > 0x9FFFFFFF)
{
gGreenAccum += ((float) input - (float) 0x9FFFFFFF) /
(float)0x3FFFFFFF;
if (gGreenAccum > 1)
{
gGameKeys.greenRight = true;
gGreenAccum -= 1;
}
}
// Check the fire button
gGameKeys.greenFire = GetAutoFireButton(gInputElements[greenFire]);
The game specifies what its "needs" are at initialization time - for SprocketInvaders, a left/right axis and a fire button. During the initialization, I expressed those needs to InputSprocket. Now, as I check the input for the player, I get the left/right movement information in the form of an unsigned value between 0 and 0xFFFFFFFF. 0 means the axis is all the way left. 0xFFFFFFFF means it is all the way right. The game has no idea how InputSprocket is getting this information from the user, and frankly, it doesn't care. All it knows is that InputSprocket and the user have agreed on how the user indicates left, right, and fire.
The first line of this function requests the axis information from InputSprocket. Remember that this information could be coming from a true analog axis, such as a joystick, or it could be coming from a digital source, such as the keyboard. In either case, you will get back an analog value. Because I would like to allow people with joysticks to finesse their movements, I do a little extra work than necessary to deal with a true analog device being moved only a portion of the distance between the center and the extreme limit. For instance, if the value I get back is between 0x2FFFFFFF and 0x5FFFFFFF (towards the left, but not quite all the way), then I use a global to accumulate left movement until I reach a threshold, at which point the player input for that iteration through the game loop becomes "move left."
Listing 9: SprocketInvaders.c
GetAutoFireButton Snippet
Determines whether the user is trying to fire.
// poll
error = ISpElement_GetSimpleState(inElement, &input);
if (! error && (input == kISpButtonDown))
fire = true;
// but don't miss fast clicks or macros do
{
ISpElementEvent event;
error = ISpElement_GetNextEvent(inElement,
sizeof(ISpElementEvent), &event, &wasEvent);
if (! error && wasEvent && (event.data == kISpButtonDown))
{
fire = true;
break;
}
} while (wasEvent && !error);
// flush the queue
ISpElement_Flush(inElement);
Here I get the current state of the fire button (up or down), but I also check the InputSprocket event queue to see if there was a button down event. I check the event queue in case the user hit the fire button really quickly and then released. Checking the current state of the fire button would not tell me that this had happened, but getting the input events from InputSprocket will. This is a good example of how using InputSprocket is superior to other methods, such as using GetKeys.
Why do I check the button state and get the input events? The answer is simple. I want the user to be able to "auto-fire," so that holding down the fire button will cause the next shot to be fired as soon as the current one is done. While there are other ways to handle auto-fire, such as keeping the button state in a global variable, I felt that this implementation was the easiest method.
Do collision detection
The game maintains several lists, including players, enemies, and shots. Each time through the loop, I need to check to see if any of the enemies' shots have hit the players or any of the players' shots have hit the enemies. There are many subtle ways of doing collision detection. Nearly all of them come down to comparing one rectangle to another and determining if there is any overlap The more sophisticated methods reduce the number of comparisons done. SprocketInvaders uses the brute-force approach, which is just fine for its needs.
Listing 10: SprocketInvaders.c
CollideShotsToEnemies Snippet
Performs all collision detection for the players' shots and the invaders.
GameObjectPtr shot;
GameObjectPtr target;
if (! gEnemyList)
return;
// Iterate over the green player shots
if (gGreenPlayerShotList)
{
for (shot = gGreenPlayerShotList; shot; shot = shot->next)
{
// Iterate over the enemies
for (target = gEnemyList; target; target = target->next)
{
Rect u;
// Union of shot and enemy screen rects
SectRect(&shot->screenRect, &target->screenRect, &u);
if (! EmptyRect(&u))
{
SoundHandlerPlay(soundEnemyHit, target->screenX,
target->screenY);
shot->action = PlayerShotDestroy;
target->action = EnemyDestroy;
AddPoints(target->screenX, target->screenY);
}
}
}
}
For every shot the green player has created, I check each enemy in the enemy list to see if the shot's rectangle overlaps with the enemy's rectangle. If I determine there is an overlap, I set the shot's action proc to be PlayerShotDestroy, and then set the hapless enemy's action proc to be EnemyDestroy. Later in the game loop, I will execute these functions. I also play a sound effect and call AddPoints, which causes the little "100" sprite to appear. When there are two players, the CollideShotsToEnemies function repeats the same process for the red player's shots. After the game loop does collision detection for the players' shots, it checks the invaders' shots, using a similar process.
The part about setting the objects' action procs may sound confusing, but it really isn't. Each object in the game, as part of its data structure, includes a function pointer that, when non-null, is executed in the game logic phase of the game loop. It is an approach to dealing with the game actions that is object-oriented but does not require C++. Also, you might wonder why I have a shot list for each player, when each player can have only one shot at a time in the air. The reason is that I wrote the game to be extensible and flexible. In the design phase, I decided not to allow more than one shot. However, should I have changed my mind later on in the development process, it would have been trivial to change the game to allow more shots. By coding it in at the beginning, I remained flexible in the implementation. Flexibility is an extremely important part of game design and implementation, especially when radical changes (such as adding network support) come along.
Handle game logic
Let's face it. Invaders are dense. They move left and right in a completely predictable pattern and speed. As their numbers are reduced by the player's skill at shooting fish in a barrel, they get madder and madder, and thus move faster and shoot more often. They don't actually target the player. They don't follow the player at all. They are dumb.
Nonetheless, you need to have logic in your game, if the computer controls any entities in it. Even the simple task of making the Invaders behave in their single-minded manner took quite a bit of work. It is not as trivial as one would think.
Consider the way aliens speed up as their numbers decrease and the wave number increases. Of course, there are several potential ways of speeding up the aliens. You could assign each alien a velocity at the beginning of the wave and increase it as their numbers decrease. However, you might note that the aliens not only move faster and faster, but also shoot more often. You'd have to process all the aliens faster and faster as the game went by, perhaps requiring you to check the TickCount all the time. Clearly, this approach has a good deal of complexity.
I believe the solution I used is more elegant. To be honest, I was inspired by the way the original Space Invaders handled this problem. I set a global variable, gEnemyGas, that is increased at the beginning of each wave, but remains constant during the entire wave. This value is the total number of aliens processed each time through the game loop. For example, gEnemyGas is set to 2 for wave one. If you look closely, you will see that the aliens seem to move in pairs. That is because I process two of them each time through the game loop.
This may not seem very interesting until you consider what happens as you destroy the little buggers. At the beginning of the level, there are 5 rows of 11 aliens, totaling 55 invaders. Since I process only two of them each time through the loop, it takes 28 iterations through the game loop to move the entire swarm one step to the right (or left). During those 28 iterations, you are free to move around and shoot at your leisure. Put in more technical terms, your input is processed 28 times for each time any single invader's action proc is executed.
Now consider what happens as their numbers are reduced. If you destroy half of the invaders, you are left with 28 aliens. Since 2 aliens per iteration are processed, it now takes only 14 iterations of the game loop for a given alien to be processed twice. Relative to the number of times your input is processed, they are moving and firing more often. As the waves advance, and the value of gEnemyGas increases, the enemies will move and fire faster and faster. In fact, there comes a time in each wave when there is more gas than there are aliens. When that happens, the aliens have the upper hand, because they get processed multiple times for every time your input is handled. Eventually, the aliens will win. Resistance may be futile, but it can be alot of fun!
Let's look more closely at exactly how to "process" a single invader. During the initialization phase of each wave, I create and initialize each enemy invader. One part of that process is assigning the enemy's action proc to be EnemyAction. Until an external event occurs, such as being hit by the player's missile, this action proc is executed each time the alien is processed.
Listing 11: ObjectActions.c
EnemyAction Snippet
Defines the default behavior of an enemy.
if (theObject->refCon > gEnemyLevel)
return;
theObject->refCon++;
gNumEnemiesProcessed++;
if (theObject->refCon & 1)
theObject->frame = 1;
else
theObject->frame = 0;
// Determine if this enemy needs to shoot
if (Game_Random((2 * kNumEnemies) - gNumEnemies) == 1)
EnemyShoot(theObject);
// Adjust screen position depending on whether we're moving left or right
if (gEnemyTask == kEnemyMovingRight)
{
theObject->screenX += gEnemyVelocity;
}
else if (gEnemyTask == kEnemyMovingLeft)
{
theObject->screenX -= gEnemyVelocity;
}
else if (gEnemyTask == kEnemyDropping)
{
theObject->screenY += kEnemyVDrop;
}
// Check for bottom bound
if (theObject->screenY > theObject->bounds.bottom)
{
gNumRedPlayerLives = 0;
gNumGreenPlayerLives = 0;
return;
}
// Check left bounds
if (theObject->screenX < theObject->bounds.left &&
(gEnemyTask == EnemyMovingLeft))
{
gEnemiesChangeDirection = kEnemyMovingRight;
return;
}
// Check right bounds
if (theObject->screenX > theObject->bounds.right &&
(gEnemyTask == EnemyMovingRight))
{
gEnemiesChangeDirection = kEnemyMovingLeft;
return;
}
I set the refcon for the object so the enemy sprite is animated as it moves. If the refcon is even, one version of the sprite is drawn. If odd, another is used. (The refcon is also used to help determine where in the enemy object list I left off when processing the aliens.) I then increment the gNumEnemiesProcessed variable so that I'll know when I run out of "gas" for this iteration though the loop. Next, I randomly decide if this particular invader should fire. I move the unit left, right, or down, check to make sure it hasn't reached the bottom of the screen (in which case the game ends), and finally check to see if the aliens need to change direction. Note that once one enemy decides to change direction, all the others follow suit.
That's it. Not extremely sophisticated, yet tricky enough that it takes time and effort to do it right.
Draw everything
By this time, I have gathered all the input, performed all the logic, and done all of the collision detection. Now I want to draw everything so the user can see it.
All of the objects in SprocketInvaders are stored in lists. Look back at the code for GameLoop, you see that I call GameObjectListDraw for all lists maintained. Note that I pass backBuff as the second parameter to GameObjectListDraw. This background buffer is created during the initialization of DrawSprocket.
Anyone who has tried doing any computer animation is likely to be familiar with the bane known as "tearing" - when objects moving on the screen have their top halves drawn in one location and their bottom halves drawn in another. This effect takes place because the game is changing the contents of the video buffer while the computer is busy reading the video frame buffer and drawing it on the screen.
One of the best ways to avoid this ugly effect is to do all of your drawing into a back buffer, in main memory or in VRAM, then copy the entire back buffer into the frame buffer that is being drawn to the screen. An even more sophisticated method does this process while "syncing to VBL", which means that the copy is done only when the monitor's scan line has reached the bottom-right corner of the screen, and the monitor is resetting it to the top-left corner. These methods have been around for years and there are many articles and books that describe how to do them on MacOS computers. However, with the advent of DrawSprocket, all of the ugly details of doing double-buffered graphics are hidden from you, and you are left with an extremely easy API allowing you to get all the benefits of double-buffered, VBL-synced graphics without any of the annoying mess.
Listing 12: GameObject.c
GameObjectListDraw Snippet
Iterates through the given object list, drawing each object.
for (index = list; index; index = index->next)
{
index->screenRect = SpriteToGWorld(index->sprite,
index->frame, dest, index->screenX, index->screenY);
GraphicsSetRectDirty(&index->screenRect);
index->oldScreenRect = index->screenRect;
}
GameObjectListDraw calls SpriteToGWorld to copy the sprite image into the back buffer. It then calls GraphicsSetRectDirty to tell DrawSprocket that, when it is time to copy the back buffer to the front buffer, this particular area needs to be refreshed. This is an extremely important step to ensure optimal performance. When you specify the "dirty" (changed) areas of your back buffer to DrawSprocket, it copies the contents of only those areas to the front buffer. If you move only one or two sprites, copying only the areas that have changed is far more efficient than copying the contents of the entire back buffer to the front buffer.
You might note that I am not calling DrawSprocket directly from within this routine. The reason is that I wanted to make the code as portable as possible. This is a good example of what I mean when I say you should implement your code in a flexible manner. By abstracting out the blitting of the sprite to the back buffer and the function for setting the dirty rectangles, you allow yourself the option of easily removing DrawSprocket and putting in another drawing library. This might be necessary if I wanted to port the game to Windows, where I wouldn't have DrawSprocket available.
After I've copied all of the sprites into the back buffer, it is time to finally move the image to the screen so that the user sees it. DrawSprocket has made this task extremely easy. Here is the entire code for GraphicsUpdateScreen, the last function called in GameLoop.
Listing 13: Graphics.c
GraphicsUpdateScreen Snippet
Code that gets everything displayed on the monitor.
DSpContext_SwapBuffers(gDisplayContext, nil, nil);
This function copies the dirty areas of the back buffers into the front (display) buffer. Not only that, but it also syncs this copy to the next VBL to eliminate tearing. DrawSprocket uses on-the-fly code generation to gain optimal performance for your copies. Without DrawSprocket, you would have to do a large amount of work to get the same performance and features you get out of this single call.
Step Six - Networking
Network support was the last feature added to the game. I didn't plan on making SprocketInvaders a networked game, but NetSprocket makes it so easy to add networking that I couldn't resist. If I had wanted bare-bones NetSprocket support, I could have had it with much less code than I ended up with in NetSprocketSupport.c. I decided it would be cool to have a chat window before the game actually begins. In retrospect, this may not have been the best design decision, since SprocketInvaders is limited to two players and there really isn't much to chat about. However, you should still be able to use the chat-related code in your game.
I already discussed initializing NetSprocket in step four. What I want to cover here are three other areas - hosting, joining, and playing. There are some other parts of handling the networking, such as what to do when the game ends, that I will gloss over. Once you understand the code for the three areas listed, you should understand the rest.
With NetSprocket, hosting a game on a network is a piece of cake. To host a game, you need to do two things. First, you need to present a dialog box that allows the user to specify the game's name on the network and the network protocol(s) to use. Since NetSprocket hides the whole issue of transport protocol from you, it also presents a configuration interface to the user.
Listing 14: NetSprocketSupport.c
DoHostGame Snippet
Hosts a game on the network so that others may join.
status = NSpProtocolList_New(NULL, &theList);
// Do the host dialog
GetChooserName(playerName);
CopyPStr(kNetworkGameName, gameName);
password[0] = 0;
OKHit = NSpDoModalHostDialog(theList, gameName, playerName,
password, NULL);
if (!OKHit)
return (false);
// Now host the game
status = NSpGame_Host(&gNetGame, theList, kMaxPlayers,
gameName, password, playerName, 0, kNSpClientServer, 0);
FAIL_OSSTATUS(status, "NSpGame_Host returned an error");
NSpProtocolList_Dispose(theList);
When the NSpDoModalHostDialog returns, it tells you whether or not the user hit OK. If so, you need to call the NetSprocket function that actually creates all the necessary endpoints, etc., so that others may join your game. Information gathered from the user in the dialog box is passed along to the NSpGame_Host function using an opaque protocol list. You don't need to know anything about the protocol lists or the protocol refs, except that you need to create an empty list to pass to NSpDoModalHostDialog and then to NSpGame_Host. For more information about the parameters to the two host functions, see the NetSprocket documentation available on the Web.
After I successfully host the game on the network, the DoHostGame function calls WaitForAllPlayers. I won't get into this function here, since it really doesn't make up an integral part of the game. Feel free to take a look at the code and borrow whatever you want.
Joining a game on the network is even easier than hosting the game.
Listing 15: NetSprocketSupport.c
DoJoinGame Snippet
Joins a game being hosted on the network.
GetChooserName(name);
password[0] = 0;
gIAmHost = false;
theAddress = NSpDoModalJoinDialog(kNBPType, kJoinDialogLabel,
name, password, NULL);
if (theAddress == NULL)
// The user cancelled
return (false);
status = NSpGame_Join(&gNetGame, theAddress, name, password,
0, NULL, 0, 0);
FAIL_OSSTATUS(status, "NSpGame_Join returned an error");
Again, I call a NetSprocket HI function so it can enable the user to choose a network protocol and select a game to join. I assume that the host and the joiner will somehow agree on what networking protocol they are going to use.
Rather than a protocol list, the NSpDoModalJoinDialog returns an opaque address ref that is passed to NSpGame_Join. The reason is that you can join only one particular game at a time, while NetSprocket allows you to host a game on multiple networks at the same time. It is entirely possible for you to be playing a NetSprocket game that has some people using TCP/IP and others using AppleTalk. Of course, that is not possible in SprocketInvaders, since it limits the number of players to two.
The last thing I want to discuss about the networking is how to handle sending and receiving messages. Recall that I get the remote player's input from the network. Let's look at the code that actually does that.
Listing 16: NetSprocketSupport.c
HandleNetworking Snippet
Gets all network messages from the other player.
theMessage = NSpMessage_Get(gNetGame);
if (theMessage != NULL)
{
DoHandleMessage(theMessage);
NSpMessage_Release(gNetGame, theMessage);
}
DoHandleMessage is just a big dispatcher that handles different kinds of messages that I might get, including player input, chat message, game over, etc. Since I am interested in input messages, I will look at the code that deals with an input message.
Listing 17: NetSprocketSupport.c
DoHandlePlayerInput Snippet
Processes the player input message from the remote player.
if (gIAmHost)
{
gGameKeys.redLeft = inMessage->left;
gGameKeys.redRight = inMessage->right;
gGameKeys.redFire = inMessage->fire;
}
else
{
gGameKeys.greenLeft = inMessage->left;
gGameKeys.greenRight = inMessage->right;
gGameKeys.greenFire = inMessage->fire;
}
gReceivedInput = true;
Definitely not rocket science. I just fill in the remote player's keyboard state and hit a flag to let the game loop know that I've received the other player's input.
I have defined a NetSprocket message structure for player input messages. Like all NetSprocket messages, the player input message has the NSpMessageHeader structure as its first field. This header is what NetSprocket uses to deliver messages to the intended recipients, and it also contains the information necessary for the recipient to determine what kind of message it is. When I want to send a new message, such as a local input, I fill in the message structure.
Listing 18: NetSprocketSupport.c
SendInputState Snippet
Sends our input state to the remote player.
PlayerInputMessage theMessage;
OSStatusstatus;
NSpClearMessageHeader(&theMessage.h);
theMessage.h.to = kNSpAllPlayers;
theMessage.h.what = kInputMessage;
theMessage.h.messageLen = sizeof(PlayerInputMessage);
theMessage.left = left;
theMessage.right = right;
theMessage.fire = fire;
status = NSpMessage_Send(gNetGame, &theMessage.h,
kNSpSendFlag_Registered);
Here I create a message that is addressed to all players. Conveniently, this means "the other player" in a two player game, since messages sent to everyone are by default not delivered to the sender. I fill in what kind of message it is so the recipient will know how to interpret it, and how big it is so that NetSprocket will know how to deliver it. With the header information completed, all that remains is to fill in the actual message information, which I obtain from the globals (which were, in turn, filled in by the GetPlayerInputState). I then tell NetSprocket to send it, and I am done.
Without NetSprocket, adding network support for SprocketInvaders would be a daunting task, requiring a large amount of work and testing. I probably would have had to choose between using AppleTalk or TCP/IP, and probably would not have bothered with a chat screen. To be honest, it probably would not have been worth the effort.
Step Seven - Sound
I made the effort to use Sound Sprocket to localize the sounds in SprocketInvaders, just to show how easy it is to do. By localizing, I mean that I attenuate the sound's volume and left/right balance based on the position of the sound-generating object relative to the listener. You can write the localization code yourself, but why bother when SoundSprocket can do it for you?
Earlier in this article I talked about initializing SoundSprocket. All I really did was allocate sound channels, install filters in the channels, and set the listener location (effectively, the player's location) to be the bottom middle of the screen. As I play the sounds, I'll specify the location of the sound-generating objects (such as an invader firing a missile) in the same coordinate system as the listener, so that the filters alter the sounds as if the human player were inside the game.
Every time I want to play a sound, I call the SoundHandlerPlay function.
Listing 19: SoundHandler.c
SoundHandlerPlay Snippet
Plays a sound, doing 3D localization first if SoundSprocket is installed.
if (gSoundSprocket)
{
TQ3CameraPlacement location;
SSpLocalizationDatalocalization;
// Position the sound in space. The lower left corner of the screen is (0, 0)
location.cameraLocation.x = 640 - x;
location.cameraLocation.y = 480 - y;
location.cameraLocation.z = 0;
// Orient the sound so that it is down directed down towards the listener
location.pointOfInterest.x = 320;
location.pointOfInterest.y = 0;
location.pointOfInterest.z = 0;
location.upVector.x = 0;
location.upVector.y = 0;
location.upVector.z = -1;
theErr = SSpSource_SetCameraPlacement (gSource[theSound],
&location);
if (theErr)
FatalError ("Failed to set the source location");
theErr = SSpSource_CalcLocalization (gSource[theSound],
gListener, &localization);
if (theErr)
FatalError ("Failed to calculate the localization");
// We don't do doppler, since we only localize the sound at the instant it is played.
localization.currentLocation.sourceVelocity = 0;
localization.currentLocation.listenerVelocity = 0;
// We set the reference distance to a reasonable number that seems to get good
// results
// We also provide a minimum distance or else we run into really LOUD, really
// CLIPPED sounds
localization.referenceDistance = 20.0;
if (localization.currentLocation.distance < 5.0)
localization.currentLocation.distance = 5.0;
theErr = SndSetInfo (gChannels[theSound],
siSSpLocalization, &localization);
if (theErr)
FatalError("Failed to localize the channel");
}
// Play the selected sound immediately
theErr = SndPlay(gChannels[theSound], (SndListHandle)
gSounds[theSound], true);
if (theErr)
FatalError("Failed to start sound in SndPlay().");
If SoundSprocket is installed, use the position information of the object in the game to create a location that SoundSprocket can understand. I also need to create a point of interest, because SoundSprocket allows you to have an object project a sound in a certain direction (think of the difference in sound of a person yelling depending on whether that person is facing you or facing away from you.) SoundSprocket calculates the necessary localization information that I then pass into the sound channel, which in turn passes the information to the sound filter installed at initialization time.
That completed, I actually play the sound by calling SndPlay. If SoundSprocket is not installed, all of the steps above are skipped, and the sound is just played - always at a single loudness, always with equal stereo balance. I believe you can hear and appreciate the difference in effect if you run SprocketInvaders with SoundSprocket installed, then run it without.
Step Eight - Enjoy
Over the course of this article, I have gone through all of the major steps necessary to take an idea for a game and turn it into reality. I hit all of the major areas that once would have presented serious, possibly insurmountable, challenges for a would-be game developer. I have shown how the Apple Game Sprockets make it much easier for you to implement the most difficult aspects of your game - input, high-speed blitting, and networking - and to add cool features you might never have considered like 3D sound.
I presented SprocketInvaders in a very structured manner, but obviously things don't always happen that way in real life. Do not be discouraged if things don't happen in quite the same structured way when you sit down to write your game. Different aspects take different amounts of effort for different people. Here is an estimate of how long it took to utilize the various Game Sprockets.
- InputSprocket - under an hour.
- SoundSprocket - about an hour.
- DrawSprocket - about four hours.
- NetSprocket - two hours for the basic support, another day for the chat dialog.
The SprocketInvaders source code has some other little programming jewels that I didn't even mention. For instance, there is source to play "Red Book" audio from CDs and a set of debugging memory management functions. It can be a fantastic resource for your next game.
I know there are many cool MacOS games waiting to be written or sitting half-written in archived disks. I hope that what you've learned here will enable you to dust off those archives or sit down and start working on your game, comfortable with the knowledge that you'll be free to work on the game itself, rather than on the tedious infrastructure necessary to implement it. I hope yours is the next insanely great, totally addictive, totally cool game for the Mac.
Credit Where it's Due
This article would not exist if it weren't for the sample code SprocketInvaders, written by Chris De Salvo, with contributions from Cary Farrier, Michael Evans, Tim Carrol, and myself.
Thanks to reviewers Kathy Osborne, Chris De Salvo, Cary Farrier, and Brent Schorsch.
No llamas were injured in the creation of this article.