Black Art of 3D Game Programming, Chapter 18: Kill or Be Killed
From Dpfileswiki
This is it! We’ve made it through an incredible journey, and better yet, we are still in one piece—but not for long. Within this chapter, we’re going to do two things: first, we’re going to review the design and implementation of a full 3D game called Kill or Be Killed (KRK), and second, we’re going to take a brief look at the future of PC games.
Since you are almost a full-fledged Cybersorcerer, this chapter isn’t going to be highly technical; rather, we are going to converse at a high level, just as a couple game designers would. This is one of the last steps in your training—being able to forget about all the technology and math behind the games and look at them from a purely artistic standpoint. After we’re done with the design of the game and some predictions about the future, it will be time for the final ceremony that will initiate you into the alliance of Cybersorcerers.
The Story
The story behind KRK goes something like this: Thousands of years in the future, humankind and other races within our galaxy have formed a federation of worlds. This federation governs the planets and helps resolve any disputes between worlds over territory or other various problems. If two or more worlds have a mutual disagreement, instead of their entering into a war, the CURR RAH TAK is performed. Basically, each world selects a champion to represent them. These champions are then sent to the world of Centari Alpha 3, beyond the second sun of the Rylon system.
Since no two races are equal physically, it would be unfair for the champions to battle against each other creature to creature. For example, Figure 18-1 shows a comparison between a typical Dylusion and Pilut. As you can see, the tiny Pilut would have no chance against the much larger Dylusion. Hence, each champion will be placed in a special neurally linked ground fighter, so that only cunning and tactics separate them. Figure 18-2(a) and 18-2(b) depict the original sketches of the two fighter ships that each champion may choose from.
Once the preliminary ceremonies are complete and the negotiators have defined the goals of the contest and the bounty to the winner, the contestants are instantly teleported to the planet’s surface and the battle ensues! The battle must continue until there is only one champion alive. The rest must fall. Failure to terminate another opponent, or the blatant display of mercy, will result in termination. There are no exceptions!
The Design and Creation of KRK
The entire design and implementation of KRK took about two and a half weeks. My main goal was to show how to use the 3D engine that we have been working on and to create a reasonably simplistic 3D shooter that contained examples of everything we have covered, such as 2D, 3D, sound, music, color FX, and so on. Taking all this into consideration, I used Starblazer as a starting point. I wanted to follow the same kind of startup and selection interface, but with different artwork and maybe a screen or two more in the introduction. But that’s as far as the resemblance goes. If you recall, Starblazer is a modem-to-modem game; hence, to support two players, the main event loop and even the control interface are much different. However, I was able to take the fundamental elements of the main game and set up event loops and use them as a basis for KRK.
After I did the code hijacking from Starblazer, instead of messing with the introduction and control interface, I dove straight into the design of the game portion of KRK. My basic vision was a red planet with various obstacles such as rocks and crystals growing out of the ground. Then to make the game a little more interesting, I thought it would be nice to place some telepods at the corners of the game grid so that the aliens and the player could teleport. In the final version, the telepods only work for the aliens, but they look pretty cool. Next, to let the player know that the game grid has bounds, I placed some large electronic laser barriers around the perimeter of the grid. The barriers have large blinking green beacons and are about 100 meters tall in scale size. Finally, I wanted to have a main court area that contained some observation towers along with a main power station. Figure 18-3 shows the sketch of the game grid from a bird’s-eye view.
Now I had quite a few 3D objects that would be visible to the player, but still the game would look pretty dull without some kind of background, so I decided to put in a scrolling 2D mountainscape in the distance, using the layers system. And as the player turns, I thought it would be neat to scroll the background to make the game look more 3D, so I added this feature. At this point, I had most of the static portion of the game in place. I had background, objects, obstacles, and a large game area. Since the player isn’t really an object, but just a view position and view angle, moving the player around is nothing more than moving the view position and rotating the camera parallel to the Y axis. The problem I was faced with was the alien ships!
I started out thinking that I was going to create weird insectlike walkers, but soon realized that two days (the time I had to implement the aliens) wasn’t going to do. Not only would the actual 3D modeling of the walkers take days, but implementing the mechanics of walking would take too long. So I decided to use simple hovercraft-type ships based on sharp edges and lots of triangles. In the end, I was very happy with the look of the alien ships, and they served their purpose. Actually, I’m glad I used them instead of walkers. Anyway, I decided to use a maximum of four aliens at any one time, and I gave them the basic state machine-probability intelligence system we have studied.
After I got the aliens working and the mechanics of their interactions with the player to a satisfactory point, I added the sound, music, and completed all the introductory stuff, such as the mech selection and Rules dialogs. Finally, I went through the game and tuned and cleaned it up as much as time permitted.
In a real game design you should take more than two or three weeks. Actually, I can’t see anyone making even a low-end shareware game in less than a couple months, with the average being three months. And a real commercial game should take around a year to make. Anyway, that’s how I did it.
Components of the Game
KRK is constructed of various code and data modules. The main code for KRK is KRK.C, which includes its own header information. The game itself depends on many of the modules we have already written throughout the preceding chapters of the book, as well as the ASM modules from Chapter 17.
I compiled it using:
| KRK.C | BLACK9.C |
| BLACK3.C | BLACK18.C |
| BLACK4.C | QCPY.OBJ |
| BLACK5.C | QSET.OBJ |
| BLACK6.C | TRI_FP.OBJ |
| BLACK8.C | WLINE.OBJ |
As far as data modules go, the game consists of a number of files—here’s how to identify them:
- KRK*.PCX: all the image files
- KRK*.VOC: all the sound FX
- KRKMUS.XMI: the music for KRK
- Various PLG files with file names relating to what they represent in the game
I probably should have kept using the KRK prefix, but oh well! To create an executable, you simply do what you’ve been doing; that is, link in the various library modules included in the includes section of KRK and the assembly objects, and that’s it. However, make sure to use BLACK18.C and BLACK18.H for this game, since some minor changes had to be made to BLACK17.C to implement the alien explosions. Nothing big, but nevertheless don’t use BLACK17.C and BLACK17.H or there will be trouble. In any case, the new library module along with all the data files and the main KRK.C source file are all in the directory for this chapter, so you shouldn’t have any trouble. I’m sure that by now you are a compiler expert anyhow! (I’m still trying to change the darn colors of the IDE.)
The New 3D Library Module
In the last chapter we optimized the 3D engine to a reasonable level of performance— at least enough to make some games with. However, the engine is based on objects and doesn’t really work well with single polygons, points, or lines. This turns out to be a problem when trying to make something explode. My original idea was simply to rip apart the polygons of an object, but the problem is that once a 3D model is fried, it must be unfried, either by reloading it from disk or saving it before corrupting it. Both solutions are unacceptable, so I decided to do something a little less ambitious for explosions. Since both the aliens and the player fire plasma torpedoes (at least that’s what we’re going to pretend now), it seems to me that a ship hit by a torpedo should fission apart and disintegrate.
So I decided to make an alien that had been hit simply glow away. But there’s a catch. How can we tell the graphics engine to always draw the polygons of a model using the same color or color register? Well, we can’t. Granted, we could go into the model and reset all the polygon colors, but then the model would be corrupted. A better way to do this would be to force all the polygons to a specific color register during the lighting of an object. So what I did was rewrite the Remove_Backfaces_And_Shade( ) function to take one more parameter called force_color. This parameter, if non-negative, tells the shading portion of the code to use the force_color as the color register regardless of the shading settings, light source, and so on. This way, when an alien is disintegrating, the color register that is going to glow can simply be sent to the Remove_Backfaces_And_Shade( ) function and the polygons entered into the global polygon list will be colored appropriately, but the model itself won’t be corrupted.
Listing 18-1 is the new, modified Remove_Backfaces_And_Shade( ) function that you will find in BLACK18.C.
Listing 18-1 The new back-face removal and shading function
void Remove_Backfaces_And_Shade(object_ptr the_object, int force_color)
{
// this function removes all the backfaces of an object by setting the visibility
// flag. This function assumes that the object has been transformed into
// camera coordinates. Also, the function takes into consideration is the
// polygons are one or two sided and executed the minimum amount of code
// in addition to perform the shading calculations
// also, this function has been altered from the standard function found in
// black17.c by the addition of the parameter force_color, this is used
// to force all the polys to a specific color. this is needed for the
// explosion vaporization effect
int vertex_0, // vertex indices
vertex_1,
vertex_2,
curr_poly; // current polygon
float dp, // the result of the dot product
intensity; // the final intensity of the surface
vector_3d u,v, // general working vectors
normal, // the normal to the surface begin processed
sight; // line of sight vector
// for each polygon in the object determine if it is pointing away from the
// viewpoint and direction
for (curr_poly=0; curr_poly<the_object->num_polys; curr_poly++)
{
// is this polygon two sised or one sided
if (the_object->polys[curr_poly].two_sided == ONE_SIDED)
{
// compute two vectors on polygon that have the same intial points
vertex_0 = the_object->polys[curr_poly].vertex_list[0];
vertex_1 = the_object->polys[curr_poly].vertex_list[1];
vertex_2 = the_object->polys[curr_poly].vertex_list[2];
// the vector u = vo->v1
Make_Vector_3D((point_3d_ptr)&the_object->vertices_world[vertex_0],
(point_3d_ptr)&the_object->vertices_world[vertex_1],
(vector_3d_ptr)&u);
// the vector v = vo-v2
Make_Vector_3D((point_3d_ptr)&the_object->vertices_world[vertex_0],
(point_3d_ptr)&the_object->vertices_world[vertex_2],
(vector_3d_ptr)&v);
// compute the normal to polygon v x u
Cross_Product_3D((vector_3d_ptr)&v,
(vector_3d_ptr)&u,
(vector_3d_ptr)&normal);
// compute the line of sight vector, since all coordinates are world all
// object vertices are already relative to (0,0,0), thus
sight.x = view_point.x-the_object->vertices_world[vertex_0].x;
sight.y = view_point.y-the_object->vertices_world[vertex_0].y;
sight.z = view_point.z-the_object->vertices_world[vertex_0].z;
// compute the dot product between line of sight vector and normal to surface
dp = Dot_Product_3D((vector_3d_ptr)&normal,(vector_3d_ptr)&sight);
// is surface visible
if (dp>0)
{
// set visible flag
the_object->polys[curr_poly].visible = 1;
// a little cludge
// first test for a forced color
if (force_color>-1)
{
the_object->polys[curr_poly].shade = force_color;
continue;
} // end if forced color
// end a little cludge
// compute light intensity if needed
if (the_object->polys[curr_poly].shading==FLAT_SHADING)
{
// compute the dot product between the light source vector
// and normal vector to surface
dp = Dot_Product_3D((vector_3d_ptr)&normal,
(vector_3d_ptr)&light_source);
// test if light ray is reflecting off surface
if (dp>0)
{
// now cos 0 = (u.v)/|u||v| or
intensity = ambient_light + (dp*(the_object->polys[curr_poly].normal_length));
// test if intensity has overflowed
if (intensity > 15)
intensity = 15;
// intensity now varies from 0-1, 0 being black or grazing and 1 being
// totally illuminated. use the value to index into color table
the_object->polys[curr_poly].shade =
the_object->polys[curr_poly].color - (int)intensity;
} // end if light is reflecting off surface
else
the_object->polys[curr_poly].shade =
the_object->polys[curr_poly].color - (int)ambient_light;
} // end if use flat shading
else
{
// assume constant shading and simply assign color to shade
the_object->polys[curr_poly].shade = the_object->polys[curr_poly].color;
} // end else constant shading
} // end if dp>0
else
the_object->polys[curr_poly].visible = 0;
} // end if one sided
else
{
// else polygon is always visible i.e. two sided, set visibility flag
// so engine renders it
// set visibility
the_object->polys[curr_poly].visible = 1;
// a little cludge
// first test for a forced color
if (force_color>-1)
{
the_object->polys[curr_poly].shade = force_color;
continue;
} // end if forced color
// end a little cludge
// perform shading calculation
if (the_object->polys[curr_poly].shading==FLAT_SHADING)
{
// compute normal
// compute two vectors on polygon that have the same intial points
vertex_0 = the_object->polys[curr_poly].vertex_list[0];
vertex_1 = the_object->polys[curr_poly].vertex_list[1];
vertex_2 = the_object->polys[curr_poly].vertex_list[2];
// the vector u = vo->v1
Make_Vector_3D((point_3d_ptr)&the_object->vertices_world[vertex_0],
(point_3d_ptr)&the_object->vertices_world[vertex_1],
(vector_3d_ptr)&u);
// the vector v = vo-v2
Make_Vector_3D((point_3d_ptr)&the_object->vertices_world[vertex_0],
(point_3d_ptr)&the_object->vertices_world[vertex_2],
(vector_3d_ptr)&v);
// compute the normal to polygon v x u
Cross_Product_3D((vector_3d_ptr)&v,
(vector_3d_ptr)&u,
(vector_3d_ptr)&normal);
// compute the dot product between the light source vector
// and normal vector to surface
dp = Dot_Product_3D((vector_3d_ptr)&normal,
(vector_3d_ptr)&light_source);
// test if light ray is reflecting off surface
if (dp>0)
{
// now cos 0 = (u.v)/|u||v| or
intensity = ambient_light + (dp*(the_object->polys[curr_poly].normal_length));
// test if intensity has overflowed
if (intensity > 15)
intensity = 15;
// intensity now varies from 0-1, 0 being black or grazing and 1 being
// totally illuminated. use the value to index into color table
the_object->polys[curr_poly].shade =
the_object->polys[curr_poly].color - (int)intensity;
} // end if light is reflecting off surface
else
the_object->polys[curr_poly].shade =
the_object->polys[curr_poly].color - (int)ambient_light;
} // end if use flat shading
else
{
// assume constant shading and simply assign color to shade
the_object->polys[curr_poly].shade = the_object->polys[curr_poly].color;
} // end else constant shading
} // end else two sided
} // end for curr_poly
} // end Remove_Backfaces_And_Shade
I’ve highlighted the important changes. If the parameter force_color is set to -1, the function works exactly as before. Also, I’ve added a few old functions to BLACK18.C from BLACK11.C to help with one part of the introduction—the ship selection phase. I wanted to be able to draw wireframe versions of the ship, so I had to include the 2D line and clipping functions along with the old Draw_Object_Wire( ) function, but we have already seen these, so no need to check them out again.
Now let’s step through some of the main elements of the game from a designer’s view and see how everything is accomplished.
The Introductory Screens
Originally, I wanted to have a 3D Studio-modeled movie play for the introduction, and I actually had my friend (Joe Font) make one, but I didn’t get time to put it in. You can find the FLC on the CD-ROM if you would like to add it yourself. Anyway, the introductory sequence for KRK is fairly standard. It begins by showing a PCX file that does a little advertising for Waite Group Press, followed by a nice rendering of the red planet Centari Alpha 3 made by Richard Benson. Then I call a screen fade, and the standard green teletypewriter text is drawn out that describes the different systems starting up. This is followed by another image of the red planet (I drew this in about five minutes with Deluxe Animation). Some text is printed that describes the mass, temperature, and so forth, of the planet. Then it instantly switches to the main menu screen, and the player can start navigating through menu items with the arrow keys.
The introductory process is rather simple, but it isn’t bad considering that I drew the artwork, which resembles in some ways the work of a cabbage patch doll. However, with really good artwork and the simple screen transitions we’ve been using, a decent introductory sequence should be possible.
The Main Menu
The main menu has only four working items on it:
- Challenge
- Select Mech
- Rules
- Leave Planet
The Challenge selection sends the player down to the planet and the game begins. The Select Mech option allows the player to select either a Tallon or a Slider. The Rules menu item goes into a page-by-page description of the rules and controls of the game. Finally, the Leave Planet option exits the game and shows the extra credits.
The main menu is implemented as a real-time loop that does two things: it performs color animation on the borders of the currently selected menu item and tests for the [ENTER] key being pressed, which indicates a selection. Finally, the image for the main menu is loaded off disk each time it is displayed; that is, the main menu image is not kept in memory, but the few hundred milliseconds load time isn’t noticeable.
Now let’s take a closer look at how each menu item works.
Selecting a Mech
This is probably my favorite menu item that does just about nothing. When selected, the player is shown a rotating 3D model of the currently selected ship. This is totally 3D and not simulated with animation. The 3D engine is actually generating the view. However, instead of using the solid rendering engine, the old Draw_Object_Wire( ) function is used to draw the model of the ships. Since there are only two ships, there aren’t that many models, but it’s still a neat effect.
The player can select a new ship by pressing [ENTER] or exit the dialog with [ESC]. If a new ship is selected, the player will see the new ship on his multifunction display (MFD) when in hull mode; however, it would be nice if each ship had its own characteristics, such as speed, shield strength, turning radius, and so on.
The Rules Dialog
The Rules dialog was lifted right out of Starblazer. I simply changed the artwork and the actual instructions, but the core code is the same. In fact, I took some out! Remember those scrolling lights at the top of the Starblazer briefing image? They’re gone! This is because I was having palette problems, and it wasn’t worth the time to figure out for some blinking lights.
Entering the Game
When the player enters the game, all the system variables are reset, the image for the instrumentation is loaded from disk and copied into the lower region of the video RAM, and the game music starts.
Leaving the Planet
When the player selects the Leave Planet menu option, the main menu is immediately exited and the screen is blanked out with black. Then, using the font engine, the extra credits are displayed one by one, followed by a vertical scroll (just like the movies). This is accomplished using a line-by-line memory move. Here’s the code that smooth-scrolls the screen (notice that it uses the quad byte memory move for speed):
// scroll them away
for (index=0; index<135; index++)
{
// copy the video line to the line above it
fquadcpy(video_buffer,video_buffer+320,16000-80);
// test for exit
if (keys_active)
return;
} // end for index
The Player’s View
Once the game starts, the player is taken instantly to another world, a world that exists only in Cyberspace. The main elements of the world are real 3D objects; but other aspects, such as the instrumentation, the sky, ground, and mountain background, are all part of the illusion. Figure 18-4 shows a diagram of the screen layout. Let’s take a look at how each region is implemented.
The Sky
The sky is drawn in vertical sections in three distinct shades of red. This is done to simulate a bit of perspective. The vertical extent of each shaded region was arrived at experimentally; however, the color of the last section had to match that of the sky color behind the mountainscape.
The Background Mountainscape
The background mountains are simply a single layer. I admit that I didn’t try adding two or more layers because I was afraid that the speed wouldn’t be there, but now I think that two layers might be possible. Anyway, the mountainscape uses the layers system from Chapter 4 and is a single rectangular region with width 320 and height 43 pixels, so it takes quite a bit of memory to move each frame. The 3D effect is achieved by using the current Y component of the view angle scaled by a constant as the amount to scroll the mountainscape. This gives the effect of the world rotating as the player does, which it should.
The Ground
The ground is drawn in the same way as the sky. The ground consists of three brown layers that become darker toward the horizon. This helps with depth perception. A little more realism could probably be achieved by adding a few more shades.
Next, let’s talk about the player’s instrument panel.
The Instrument Panel
The instrument panel was drawn using Deluxe Animation by Electronic Arts. Originally, I envisioned something a little more 3D, but the end result isn’t bad. Figure 18-5 shows an actual screen shot of the game.
Before covering each area of the instrument panel, I want to explain an important design decision that I made for the control area. As you will notice from Figure 18-4, the control area is not double buffered. This is because, for the most part, not much of it changes as a function of time. This is always a hard decision to make— that is, what part of the video display to buffer. Ideally, we would like to buffer the whole screen, but redrawing a full 64,000 buffer or larger isn’t cheap, and many times a full 64,000-byte double buffer isn’t available. Luckily though, the only animation performed on the instrument panel is color animation and the scanner blips, so there isn’t any perceivable erase-draw cycle. So in this case, we got the best of both worlds—more memory and faster video update by selecting not to buffer the instrumentation area.
The Scanner
The scanner is displayed on the leftmost virtual monitor and is toggled with the [S] key. The scanner only displays the following items:
- Perimeter barriers
- Telepods
- The main power station
- Aliens
- The player
The scanner is nothing more than a top view of the world. During each frame, the positions of all the scanned objects are scaled down to fit in the scanner window and displayed in different colors. The player is blue, the aliens are red, and the other objects are various colors that I keep changing, so they could be anything by the time you read this. Also, the positions of the scanner blips are recorded in a static local array so that during erasure the positions need not be recomputed by the function. I was toying with the idea of adding the missiles to the scanner. Maybe you could add this? It would be cool to see torpedoes that are about to hit you, and it would help in shooting ahead of the enemies.
The Multifunction Display
The multifunction display, or MFD, is located on the right video screen of the instrument panel and is controlled by the [T] key. By pressing [T], the player selects the different modes of the MFD. I originally envisioned a communications mode that would allow you to communicate with another player over the modem, but that idea got canned due to time limitations on the writing of this game. Anyway, the two displays that were implemented are system meters and a hull damage indicator.
System Meters
The system meters display shows three different line graphs. The first is the velocity of the ship (blue for forward and red for reverse), the second shows the damage to the ship, and the third shows the current energy level of the ship. Right now, only the first two work. They are implemented by referring to a set of variables. These variables are then scaled down so that their maximum lengths are 20. Then lines are drawn two pixels high within the meter bitmaps to indicate the current value of the data being shown. This works quite well for such a simple output device.
Hull Damage System
The hull damage system is supposed to work hand in hand with the overall damage indicator in the system meters display mode; however, what I was thinking is that there would be multiple bitmaps for each ship, each being more and more distorted or maybe showing red areas to indicate damage. Anyway, all that is displayed right now is the hull of the currently selected ship with predrawn bitmaps.
The Heads-Up Display
The heads-up display, or HUD, is always a favorite of mine. It’s a nice way to add some extra information to a game that normally would clutter the screen if continually displayed. The HUD for KRK is toggled with the [H] key and simply writes to the double buffer (after all the other imagery has been rendered, so it is on top). The HUD currently displays the X, Y, Z position of the player, his angular trajectory, and finally, the number of aliens he has dispatched. The HUD is drawn with standard text using the font engine function Font_Engine_1( ).
The Objects in the Game
This is where I had the most trouble. The problem is that each model loaded into the computer takes up about 2.5K of memory. This means that after loading 20 objects, the NEAR data segment would be nearly full! But the question is, do we need to load multiple models of the same object? The answer is, of course, no. In fact, for any object we can get away with a single model, as long as we have a secondary data structure that holds the position and maybe the current angle of each replicated object.
Take a look at Figure 18-6, which depicts the data relationship between multiple models and the two necessary data structures. If we want 100 rocks, we load the PLG file for a single rock, and then we create a separate array or data structure that contains the positions of these 100 rocks. Then instead of simply processing the single rock model into the pipeline, we perform a little trick. We overwrite the world_pos of the single rock model with the position of the first replica and send the object down the pipeline; then we overwrite with the second replica’s world position, and so on. This way the same model can be sent down the pipeline multiple times with different positions (and possibly orientations). The graphics engine doesn’t know the difference though, and for all intents and purposes, we have replicated an object.
Just to make it clear how this is done, let’s take a look at the secondary data structure used for the static objects of the game. Static objects include everything but the missiles and the aliens. Here’s the data structure:
// this structure is used to replicate static similar objects based on the
// same model
typedef struct fixed_obj_typ
{
int state; // state of object
int rx,ry,rz; // rotation rate of object
float x,y,z; // position of object
} fixed_obj, *fixed_obj_ptr;
As you can see, this structure only takes a couple dozen bytes instead of 2.5K, but it serves the purpose of holding the position, orientation, and state of a replicated object. Now imagine that the following arrays have been allocated. These two arrays hold the actual models:
object static_obj[NUM_STATIONARY]; // there are six basic stationary
// object types
object dynamic_obj[NUM_DYNAMIC]; // this array holds the models
// for the dynamic game objects
These arrays hold the replication arrays:
fixed_obj obstacles_1[NUM_OBSTACLES_1]; // general obstacles, rocks etc. fixed_obj obstacles_2[NUM_OBSTACLES_2]; // general obstacles, rocks etc. fixed_obj barriers[NUM_BARRIERS]; // boundary universe boundaries fixed_obj towers[NUM_TOWERS]; // the control towers fixed_obj stations[NUM_STATIONS]; // the power stations fixed_obj telepods[NUM_TELEPODS]; // the teleporters
Now let’s imagine that we have initialized one of the elements of the static_obj[ ] array with the rock model and also initialized fixed_obj obstacles_1[NUM_OBSTACLES_ 1]. Then the following fragment of code would replicate the rock object through the graphics pipeline multiple times:
// phase 0: obstacle type one
for (index=0; index<NUM_OBSTACLES_1; index++)
{
// test if object is visible
// now before we continue to process object, we must
// move it to the proper world position
static_obj[OBSTACLES_1_TEMPLATE].world_pos.x = obstacles_1[index].x;
static_obj[OBSTACLES_1_TEMPLATE].world_pos.y = obstacles_1[index].y;
static_obj[OBSTACLES_1_TEMPLATE].world_pos.z = obstacles_1[index].z;
if (!Remove_Object(&static_obj[OBSTACLES_1_TEMPLATE],OBJECT_CULL_XYZ_MODE))
{
// convert object local coordinates to world coordinates
Local_To_World_Object(&static_obj[OBSTACLES_1_TEMPLATE]);
// remove the backfaces and shade object
Remove_Backfaces_And_Shade(&static_obj[OBSTACLES_1_TEMPLATE],-1);
// convert world coordinates to camera coordinates
World_To_Camera_Object(&static_obj[OBSTACLES_1_TEMPLATE]);
// clip the objects polygons against viewing volume
Clip_Object_3D(&static_obj[OBSTACLES_1_TEMPLATE],CLIP_Z_MODE);
// generate the final polygon list
Generate_Poly_List(&static_obj[OBSTACLES_1_TEMPLATE],ADD_TO_POLY_LIST);
} // end if object visible
} // end for index
The highlighted section shows how the secondary data structure is used to update the single object model each time through the main rendering loop.
That’s enough about how objects are replicated through the graphics pipeline, let’s talk about each object type.
Static Objects
The static objects compose all of the background scenery that does not translate. Any object that translates is called a dynamic object. However, as we will see, some of the static objects, such as the power station and the telepods, do rotate. We have seen the array that holds the original models for all the static objects and the arrays that hold the positions or the replicated objects, so let’s talk about the specific attributes of each object type.
Observation Towers
The observation towers are shown in Figure 18-7 and are basically two rectangular solids, one stacked on the other. They are positioned at the four points of a square that is centered at the center of the game universe at (0,0,0). Currently they do nothing. The PLG file that contains them is called TOWER.PLG.
Power Station
The power station is located at the exact center of the game grid, and when the player is sent down to the planet, he will actually be within its radius. Figure 18-8 shows the power station. Unlike the towers, there is only a single power station, and it rotates. The rotation is performed as usual using the Rotate_Object( ) function.
This brings us to an important point that is both a pro and a con. When a single model is used to represent multiple objects and is replicated in the graphics pipeline, any transformation performed to the original model will be reflected in each replicated model. This means that if we replicate the power station 100 times through the pipeline at different locations and then rotate the original model, all 100 replicas will rotate! This may be desired, but if it’s not, action must be taken so that before each replicated model is sent down the pipeline, the transformation(s) are done or undone to put the model into some known state.
Anyway, the purpose of the power station was going to be this (but I never got around to it): when the player was damaged and low on energy, he could drive over it and recharge. The name of the power station model is STATION.PLG.
Crystal Growths
The crystal growths were supposed to be the telepods, but when I saw them in 3D they looked too cool. Figure 18-9 shows what they look like. They almost look like ribs from some giant alien creature. Anyway, these are totally static and simply randomly placed on the game grid. There are 32 of them. The model name is PYLON.PLG.
Rocks
The rocks could look a bit “rockier,” but they do the job. One is shown in Figure 18-10. The rocks, like the crystals, are randomly positioned, and there are 32 of them in the entire universe. The file containing the model is ROCK.PLG.
Telepods
The telepods are probably the coolest of all the static objects. One is shown in Figure 18-11. As you can see, there is a shadow. How is this implemented? Well, I simply added a rectangular polygon that has a Y position of 0 to the basic model. This looks like a shadow when displayed. Also, the telepods are rotated. But as we learned, only one call is needed to rotate all four of the telepods, since the same model is replicated through the pipeline at different world coordinates.
The telepods are located near the four corners of the game grid and are almost totally passive. They are used as entrance positions for regenerating dead aliens. You should definitely add teleportation ability for the player. When he drives under a telepod, maybe he sees a flash of light or a star field or something, and then he is teleported to another pod that is randomly selected. The model containing the telepod is called TELE.PLG.
Electronic Barriers
The electronic barriers are a nice touch. Take a look at Figure 18-12. As you can see, they look like monoliths. Actually, they are about 100 meters tall in game scale. There are eight of them located equidistant around the game perimeter, which is (16,000x16,000) units. On top of each barrier unit is a blinking beacon, which is accomplished via color register animation. The name of the PLG file containing the model is BARRIER.PLG.
Alien Ships
The alien ships were probably the most complex of all the models in the game to deal with (Figures 18-13 and Figure 18-14 are screen shots of both alien types). Since there can be more than one alien of each model type (Tallon and Slider), the replication technique had to be used. Unfortunately, this came back to bite me, because when I rotated the Tallon model to place it in the correct orientation for one of the replicated aliens, all of the replicated aliens rotated. This meant that every time through the replication pipeline, the desired direction of the replicated alien would be compared to the actual direction of the single model. Then, based on the difference, the model would be rotated. Therefore, for each alien in the game, there is a rotation performed each frame. At worst, there can be four rotations per frame for the four aliens—still not bad!
The complete function that replicates the aliens and draws them is shown in Listing 18-2. Notice that it has separate sections for both the Tallon and the Slider models. These could have been merged, but this way it’s a little faster and simpler to understand.
Listing 18-2 Alien rendering function
void Draw_Aliens(void)
{
// this function simply draws the aliens
int index, // looping variable
diff_angle; // used to track anglular difference between virtual object
// and real object
// draw all the aliens (ya all four of them!)
for (index=0; index<NUM_ALIENS; index++)
{
// test if missile is alive before starting 3-D processing
if (aliens[index].state!=ALIEN_DEAD)
{
// which kind of alien are we dealing with?
if (aliens[index].type == ALIEN_TALLON)
{
dynamic_obj[TALLONS_TEMPLATE].world_pos.x = aliens[index].x;
dynamic_obj[TALLONS_TEMPLATE].world_pos.y = aliens[index].y;
dynamic_obj[TALLONS_TEMPLATE].world_pos.z = aliens[index].z;
if (!Remove_Object(&dynamic_obj[TALLONS_TEMPLATE],OBJECT_CULL_XYZ_MODE))
{
// rotate tallon model to proper direction for this copy of it
// look in state field of model to determine current angle and then
// compare it to the desired angle, compute the difference and
// use the result as the rotation angle to rotate the model
// into the proper orientation for this copy of it
diff_angle = aliens[index].angular_heading -
dynamic_obj[TALLONS_TEMPLATE].state;
// fix the sign of the angle
if (diff_angle<0)
diff_angle+=360;
// perform the rotation
Rotate_Object(&dynamic_obj[TALLONS_TEMPLATE],0,diff_angle,0);
// update object template with new heading
dynamic_obj[TALLONS_TEMPLATE].state = aliens[index].angular_heading;
// convert object local coordinates to world coordinate
Local_To_World_Object(&dynamic_obj[TALLONS_TEMPLATE]);
// remove the backfaces and shade object
if (aliens[index].state==ALIEN_DYING)
Remove_Backfaces_And_Shade(&dynamic_obj[TALLONS_TEMPLATE],aliens[index].color_reg);
else
Remove_Backfaces_And_Shade(&dynamic_obj[TALLONS_TEMPLATE],-1);
// convert world coordinates to camera coordinate
World_To_Camera_Object(&dynamic_obj[TALLONS_TEMPLATE]);
// clip the objects polygons against viewing volume
Clip_Object_3D(&dynamic_obj[TALLONS_TEMPLATE],CLIP_Z_MODE);
// generate the final polygon list
Generate_Poly_List(&dynamic_obj[TALLONS_TEMPLATE],ADD_TO_POLY_LIST);
} // end if object is outside viewing volume
} // end if tallon
else
{
dynamic_obj[SLIDERS_TEMPLATE].world_pos.x = aliens[index].x;
dynamic_obj[SLIDERS_TEMPLATE].world_pos.y = aliens[index].y;
dynamic_obj[SLIDERS_TEMPLATE].world_pos.z = aliens[index].z;
if (!Remove_Object(&dynamic_obj[SLIDERS_TEMPLATE],OBJECT_CULL_XYZ_MODE))
{
// rotate slider model to proper direction for this copy of it
// look in state field of model to determine current angle and then
// compare it to the desired angle, compute the difference and
// use the result as the rotation angle to rotate the model
// into the proper orientation for this copy of it
diff_angle = aliens[index].angular_heading -
dynamic_obj[SLIDERS_TEMPLATE].state;
// fix the sign of the angle
if (diff_angle<0)
diff_angle+=360;
// perform the rotation
Rotate_Object(&dynamic_obj[SLIDERS_TEMPLATE],0,diff_angle,0);
// update object template with new heading
dynamic_obj[SLIDERS_TEMPLATE].state = aliens[index].angular_heading;
// convert object local coordinates to world coordinate
Local_To_World_Object(&dynamic_obj[SLIDERS_TEMPLATE]);
// remove the backfaces and shade object
if (aliens[index].state==ALIEN_DYING)
Remove_Backfaces_And_Shade(&dynamic_obj[SLIDERS_TEMPLATE],aliens[index].color_reg);
else
Remove_Backfaces_And_Shade(&dynamic_obj[SLIDERS_TEMPLATE],-1);
// convert world coordinates to camera coordinate
World_To_Camera_Object(&dynamic_obj[SLIDERS_TEMPLATE]);
// clip the objects polygons against viewing volume
Clip_Object_3D(&dynamic_obj[SLIDERS_TEMPLATE],CLIP_Z_MODE);
// generate the final polygon list
Generate_Poly_List(&dynamic_obj[SLIDERS_TEMPLATE],ADD_TO_POLY_LIST);
} // end if object is outside viewing volume
} // end else slider
} // end if alien alive
} // end for index
} // end Draw_Aliens
I have highlighted the rotation aspect of the code. Basically, I used the state field of the model to hold its current angle—a bit of a kludge, but then beggars can’t be choosers. As far as the logic goes for the aliens, their states and probabilities are shown in Table 18-1.
The aliens move in a very simple way. They are given an angular heading and a speed. The heading is used to compute a trajectory vector in the X-Z plane, and the speed is used as a scaling factor. The only interesting state is the attack state and how it is performed. Take a look at Figure 18-15. In the figure we see that the player is located at point P, and an alien who wishes to attack is located at point A. The question is, which way should the alien turn to drive toward the player?
This is not an easy question, and a bit of math is needed. Basically, the trick is this: A vector is computed from the alien to the player, and then the cross product is computed between the alien’s current trajectory vector h and the desired trajectory vector t. Finally, the sign of the Y component of the result is tested, and based on this, we can deduce which way the alien needs to turn. If the alien is to the right of the player, then he will turn left, and if the alien is to the left of the player, then he will turn right. The logic that does this is rather simple, though, since we only need the sign of the result and not the whole cross product. Here it is for review:
case ALIEN_ATTACK:
{
// continue tracking player
// test if it's time to adjust heading to track player
if (++aliens[index].counter_2 > aliens[index].threshold_2)
{
// adjust heading toward player, use a heuristic approach
// that simply tries to keep turning the alien toward
// the player, later maybe we could add a bit of
// trajectory lookahead, so the alien could intercept
// the player???
// to determine which way the alien needs to turn we
// can use the following trick: based on the current
// trajectory of the alien and the vector from the
// alien to the player, we can compute a normal vector
// compute heading vector (happens to be a unit vector)
head_x = sin_look[aliens[index].angular_heading];
head_z = cos_look[aliens[index].angular_heading];
// compute target trajectory vector, players position
// minus aliens position
target_x = view_point.x - aliens[index].x;
target_z = view_point.z - aliens[index].z;
// now compute y component of normal
normal_y = (head_z*target_x - head_x*target_z);
// based on the sign of the result we can determine if
// we should turn the alien right or left, but be careful
// we are in a LEFT HANDED system!
if (normal_y>=0)
aliens[index].angular_heading+=(10+rand()%10);
else
aliens[index].angular_heading-=(10+rand()%10);
// check angle for overflow/underflow
if (aliens[index].angular_heading >=360)
aliens[index].angular_heading-=360;
else
if (aliens[index].angular_heading < 0)
aliens[index].angular_heading+=360;
// reset counter
aliens[index].counter_2 = 0;
} // end if
// test if attacking sequence is complete
if (++aliens[index].counter_1 > aliens[index].threshold_1)
{
// tell state machine to select a new state
aliens[index].state = ALIEN_NEW_STATE;
} // end if
// try and fire a missile
distance = fabs(view_point.x - aliens[index].x) +
fabs(view_point.z - aliens[index].z);
if ((rand()%15)==1 && distance<3500)
{
// create local vectors
// first position
alien_pos.x = aliens[index].x;
alien_pos.y = 45; // alien y centerline
alien_pos.z = aliens[index].z;
// now direction
alien_dir.x = sin_look[aliens[index].angular_heading];
alien_dir.y = 0;
alien_dir.z = cos_look[aliens[index].angular_heading];
// start the missile
Start_Missile(ALIEN_OWNER,
&alien_pos,
&alien_dir,
aliens[index].speed+25,
75);
} // end if fire a missile
} break;
As you can see, the ALIEN_ATTACK state is responsible for much of the action when we consider that there are timers running and weapon control is taking place. In any case, you should study it since most shooters use something like this to control the enemies.
Finally, if the player is lucky enough to hit an alien, the alien is regenerated at one of the telepods. The models used for the aliens are TALLON.PLG and SLIDER.PLG.
The Missiles
The missiles didn’t turn out exactly like I wanted, but they look pretty cool nevertheless. I was trying for more of a Star Trek-style photon torpedo, but I just couldn’t get it right. Anyway, the missiles’ logic is similar to the logic in Starblazer. About the only difference is that the missiles are in 3D. They are basically 3D equilateral pyramids mids that rotate as they move. There are a total of 12 missiles available, and the player can shoot up to 6 at a time. The missiles have a finite range and will dissipate and be recycled after this range is reached.
The model for the missile is MISSILE.PLG. If you refer to the alien logic code, you will notice that the aliens only fire missiles when they are in attack mode.
Collision Detection
Collision detection in a 3D game is always hard. KRK doesn’t perform player-toenvironment or alien-to-environment collisions; it only performs missile-to-alien and missile-to-player collisions. The collisions are implemented using a bounding sphere technique. The maximum radius of each object loaded into the engine is recorded in the object structure, and this is used along with the position of the missiles to determine whether a hit has been made.
However, the problem is, how do we compute the distance between a missile and a possible target? We surely can’t use the standard sqrt( ) function, since it is so slow. Instead, we are going to use an approximation that is based on a Taylor series for square root. If that doesn’t mean anything to you, don’t worry. Basically, in 2D we can approximate the distance between two points (x1,y1) and (x2,y2) within 10 percent with the following formula:
distance = abs(x1-x2) + abs(y1-y2) - minimum( abs(x1-x2), abs(y1-y2) )/2;
Of course, we can optimize this a bit, but the long form helps to show what’s going on. The first section of the formula, [abs(x1-x2) abs(y1-y2)], is called the Manhattan Distance, which will have quite a bit of error in some cases—that is, the distance will be too large. The second term helps to minimize this error. In any case, the final distance calculation is nothing more than a couple of subtractions, a comparison, and a shift—a definite improvement over a complex square root.
Once the distance between the missile being tested for collision and the object under test is computed, this value is compared to the radius of the object in question, and if the distance is less than or equal to the radius, then chances are there’s been a collision. This is shown in Figure 18-16. You might think that combined errors of using the maximum radius (instead of average radius) along with the 10 percent distance calculation error would make for terrible collision detection, but in fact it works great!
Now let’s move on to the effect of a collision between a missile and an object.
Explosions
If I had time, I was going to implement a full polygon explosion along with a scaled bitmap explosion overlay. This would be the best of both worlds. The player would see a collection of particles flying out from the explosion along with a blast of flames drawn or rendered and mapped onto a 2D surface, much like the objects in Doom or Dark Forces. However, I had to settle for something less. The little addition made to the Remove_Backfaces_And_Shade( ) function is now going to come into play. When a missile fired by the player hits an alien, the alien switches into the state ALIEN_DYING. In this state, the alien sends a non-negative parameter as the force_color parameter of the Remove_Backfaces_And_Shade( ) function, and hence, all the polygons of the alien model are added to the polygon list with the same color. Then the color register is cycled through shades of green, and the results are a vaporization. Figure 18-17 shows an alien being vaporized.
The player doesn’t really explode, but instead is knocked around, and the video system malfunctions when hit. The “screen-shake” effect is implemented by modulating the Y viewing position randomly for a few hundred milliseconds. Here is the actual screen-shaking code:
// test for ship hit message
if (ship_message == SHIP_HIT)
{
// do screen shake
view_point.y = 40+5*(rand()%10);
// test if shake complete
if (--ship_timer < 0)
{
// reset ships state
ship_message = SHIP_STABLE;
view_point.y = 40;
} // end if screen shake complete
} // end if
Also, to add a little more realism, the video display or window into the world is garbled randomly with shades of green, as if the plasma energy of the torpedo hitting the player is dissipating over the hull. This is accomplished with this fragment:
// test if screen should be colored to simulate fire blast
if (ship_message==SHIP_HIT)
{
// test for time intervals
if (ship_timer>5 && (rand()%3)==1)
Fill_Double_Buffer_32(SHIP_FLAME_COLOR+rand()%16);
} // end if ship hit
As you can see, these effects are easy to implement, but they look great. There is a lesson to be learned here: complex and amazing visual effects don’t necessarily have to be hard to code.
Terrain Following
Although the surface of Centari Alpha 3 is rather flat (like the salt flats), there are some anomalies in the surface. To give the player some visual feedback, a little trick is used. The trick is based on the screen shake but to a much lesser degree. Basically, the current velocity of the player is used to scale a random number that only varies a few points, and then this result is used to modulate the Y or vertical position of the cockpit. The result is a bumpy terrain. Again, the code is so short, I can’t resist showing it to you:
if (ship_speed) view_point.y = 40+rand()%(1+abs(ship_speed)/8); else view_point.y = 40;
That’s about it for the graphics and game play; let’s take a quick look at the sound.
Music and Sound FX
The music and sound FX system are taken directly from Starblazer. There are two main sequences of music: the introduction sequence and the main game sequence. The introduction sequence was taken from Starblazer, but the main game sequence is totally new, which might take a little getting used to. At first you may not like it, but after a while the music “feels” right and it grows on you. The music was written by Dean Hudson of Eclipse Productions.
The sound FX system for KRK is the same preemptive system used in Starblazer without change. Most of the sound effects were made by my vocal cords, with the exception of a couple that were digitized from old sci-fi movies, but I’ll never tell which. Now let’s get to actually loading and playing the game.
Loading the Game
You load Kill or Be Killed with the single executable KRK.EXE. However, to support music and sound FX, the command line parameters “m” and “s” must be used, as in:
- KRK.EXE m s [ENTER]
Of course, if you want the music and sound to play, you must load both MIDPAK and DIGPAK with the files,
- MIDPAK.COM
- SOUNDRV.COM
which are generated with the menu-driven programs SETM.EXE and SETD.EXE. I suggest that you make a batch file that looks something like this:
- MIDPAK.COM
- SOUNDRV.COM
- KRK.EXE m s
The only reason I didn’t do this is I’m not sure if you want music and sound all the time. Of course, a better way would be to allow the main menu in the game to turn music and sound on and off. Maybe your version of the game should have that? Whatever way you start the game, the game will go through the introductory sequence and drop you off in the main menu. Finally, KRK needs about 580K of memory to run after the sound and music drivers have been loaded.
Playing the Game
When you get to the main menu, you really don’t need to do anything; you can simply select the Challenge menu item and start playing. But check out the Select Mech and Rules menu items, since I worked really hard on them! Once you enter into the game grid, the ship is controlled with the following keys:
The goal of the game: hunt down and kill everything!
Problems with the Game
About the only trouble you might have with the game is not enough memory to run it. If you do encounter sound, music, or lockup problems, unload all TSRs and drivers to free up as much conventional memory as possible. And remember, if you want sound and music, you must load MIDPAK.COM and SOUNDRV.COM.
The Cybersorcerer Contest
It is unclear if this competition is still running, but feel free to challege yourself with it!
You’ve seen the game and had a peek at its insides; now it’s up to you to make it better. As you know, there is a contest (detailed in Appendix A). You can enter either as a Cybersorcerer or Cyberwizard. Cybersorcerers should use Starblazer as the foundation and improve upon it, and Cyberwizards should use KRK. Here are a few hints to get in good with the judges:
- Add sound FX.
- Add advanced alien logic.
- Add cloaking.
- Complete the instruments and make them all work.
- Add modem support.
- Ships should be able to be damaged instead of destroyed with a single shot.
- Allow the player to be teleported.
- Allow the player to fly for short distances.
- Add a layer of clouds.
- Add a judge that flies around on some kind of platform.
And any other cool stuff!
Looking Toward the Future
I think that we all feel a wind blowing and the wind is Windows. The question is, will Windows 95 and its successors be able to run games as well as 32-bit extended DOS? I think the answer is yes! By the time this book is published, Microsoft’s new DirectDraw, DirectSound, and Direct everything else will be available, along with a multitude of hardware accelerators and 3D graphics libraries (which we’ll talk about in a moment). I think that with direct access to video and sound hardware, game programmers will finally be happy. Now assuming that Windows 95 will be able to support high-speed 3D games, the next question is, will game programmers be willing to use Windows as a development platform? I think again the answer is yes. Most game programmers are already writing games in Windows, along with an IDE of some sort, so the transition won’t be that bad.
The only downside to all of this is that there is a big learning curve for people new to Windows; so Windows is an option, but DOS will be around for some time to come. No one is not going to play a game just because it’s written in DOS. However, as more and more games are written using the Windows 95 extensions, users are going to become more and more accustomed to plug-and-play technology without all the hassles of AUTOEXEC.BAT and CONFIG.SYS alterations. I think this is going to be the final blow to DOS games in the future.
As far as 3D libraries go, here is my opinion: Remember when PCs could do 2D graphics really well? There were a lot of 2D graphics libraries then, but many game programmers still wrote their own. The moral of the story is, although the technology is sufficiently high to write Doom-type games with standard libraries today, in order to push the limits, we’re always going to need to do it ourselves! Anyway, I’ve actually played with a few of these new DOS and Windows-based 3D libraries, and for DOS games I think that BRENDER by Argonaut is the way to go. For Windows games, you can’t beat the price of Intel’s 3DR (free) or the performance of Microsoft’s Rendermorphics, so there’s a little bit of future for you.
Finally, as for the games of the future, I think the graphics will start to reach a visual limit and the game play will start to be embellished again. I see a lot more AI, fun factor, and game play coming back to the games, which has been lost for quite some time.
The Final Spell
Well, well, well—you made it! I knew you would. You have traveled far and long to reach this point, hazarding many obstacles, traversing many dark chasms. You have seen things few mortals will ever lay eyes upon. You have knowledge and powers that can be used to create other worlds and realities. You are now ready to become a Cybersorcerer! The final step in your training follows. Recite the ancient Latin words inscribed below and your training will be complete.
De Gratia Gaudeamus Igitur Tempus Fugit Carpe Diem Paz Vobiscum Vale!
May your life be filled with magick.
Continue
- Black Art of 3D Game Programming: Writing Your Own High-Speed 3D Polygon Video Games in C Table of Contents
- Black Art of 3D Game Programming, Appendix A: Cybersorcerer and Cyberwizard Contests
| Copyright 2006 Andre LaMothe |
|

