Black Art of 3D Game Programming, Chapter 2: Elements of the Game
From Dpfileswiki
We've discussed the principles underlying 3D games and the relationship between Cyberspace and our own dimension. Now it's time to cover teh basic architecture of 3D games and how to implement them. We need to know this so we can conjure up the subspace field that links us to the world of Cyberspace. Although the most important part of 3D games is the graphics taht are transmitted from Cyberspace to the PC, we must abide by a set of universal rules when designing the overall structure of the game. These rules, along with the details such as setup and installation, are what we'll cover in this chapter.
Contents |
The Sequence of a Game
A 3D video game, like any other program, has a sequence of steps that must be executed in some specific order. The most complex components of a game are graphics and animation, which are executed in the main loop of the game. However, there are other aspects of the game, such as setup and installation, taht are just as important and must be performed.
The Installation and Setup Phase
All video games written on the PC have some type of installation and setup phase that allows the user to install the game on the hard drive and start the game properly. As a rule, you should have a very simple installation program, preferably named INSTALL.EXE. When the user turns this program, the game will load itself off the floppy disk(s) to the hard drive and create whatever directories are necessary for the file structure of the game. Also, this installation process should query the user as to the destination drive preference.
Information about the current drive(s) installed on the PC can be determined using the DOS and BIOS calls. With this information you can create an attractive menu that states the avilable drives and the amount of space on each drive. For example, you wouldn't want to start loading the game on drive C:\> if there wan't enough space on the drive to fit the entire game!
Once the user has installed the software, the installation program should either allow the user to configure the game at that point, or it should emit a message telling the user to run a setup program named, preferably, SETUP.EXE.
Following the setup program, the user can configure all the programmable aspects of the game, such as the input device he wishes to use, the sound and music card(s), and the availibility of extra memory. Figure 2-1 shows a sample setup interface. Don't assume the user knows what he's doing during the setup process. he may misunderstand something and make an incorrect entry. To protect your game against incorrect setups, the configuration data should be tested as it's entered, and the use should be asked at the end of the sequence whether the settings are acceptable. This gives him a chance to make corrections before playing the game and possibly crashing the computer.
After the setup process is complete the setup information should be written as a file out to the disk. The game will use this file during run-time to set the proper hardware and software configurations. Finally, when writing the installation and setup programs, make the interfaces as simple and intuitive as possible. Granted, it's impossible to make interfaces too simple because of the complexity of the setup and installation process, but try to minimize the amount of information shown on the screen at once. As a rule, don't overload the user with too many decisions; use menus or some similar technique to make the process incremental.
Introducing the Player to the Game
The player will be anxious to play your game, but before letting him play, always include an introductory sequence with a storytelling phase. Introducitons to 3D games are usually rendered using a high-end modeling system such as Autodesk 3D Studio. Your purpose is to set the scene and introduce some of the elements of the game in a passive manner. The player will only be watching at this point and not interacting.
Along with the introductory sequence and animaton, provide some form of online instructions that the user can view. The instructions don't have to be complete (complete instructions can be on disk or hard copy), but the minimum information needed to play the game should be displayed in some form, whether it's visual, auditory, or both.
Last of all, the introductory phase should put the game into a demo loop or wait state that allows the player to begin at his leisure. This can be accomplished with more prerecorded animation or by actually running the game in a special mode that uses previously digitized input to control the character in the game. Figure 2-2 shows a bit of software logic to implement something like this. When the player is ready he can press the start button (whatever it may be), and the game should begin.
Run Time
The run-time portion of the game consists mostly of the active game logic and animation in contrast to the passive introductory scenes. The single most important factor of the run-time portion of any game, especially that of a 3D game, is speed. The game must execute at a rate that can sustain a constant frame rate of at least 12 FPS (frames per second). If the framerate drops below this, the video image will begin to look jerky, and the player will start to get upset! In general, a game loop will have the follwoing minimum of steps:
- Obtain the user's input.
- Perform the logic on the objects in the game and the environment.
- Transform the objects and the game world.
- Render the current video image.
- Syncronize to some time base.
- Play music and sound effects.
- Repeat the process.
We'll cover each of these steps in detail throughout the book, but for now we simply want to get an overall picutre of what goes on in a typical 3D game.
The Concluding Sequence
At some point in the game the player will either exhaust his life force or simply want to quit. In each case you should end the game with a concluding scene or animation that shows what has happened, or what would have happened (you get what I mean). After this sequence, a high score screen should display, allowing the player to input his name (if applicable), and the game should return to the demo mode or setup screen.
Make the concluding sequence very intense, much like that of the introduction. If possible, it should have a lot of sound effects, graphics, and personally directed comments to the player about his performance. This contributes to the illusion that the player is actually part of the game. This important factor is missing from many games on the market today; the games aren't personalized enough. For example, wouldn't it be cool if the player could supply a PCX or BMP file that contained an image of himself, and this image would be used during the game to help personalize it?
Input Channel
The player will be communicating with your game via a communications channel. This communications channel will normally consist of one or more input devices, such as a joystick, mouse, keyboard, bat (a flying mouse), flight sticks, and--soon--mind control. In any case, as a Cybersorcerer, you must condition the input device data into a form that controls the aspects of the player's character or ship in the game. This means that you might have to write special logic to make the input of a specific device more palatable to your game logic in additon to simply obtaining the input.
It's not that hard to obtain input from a keyboard, mouse, and joystick, but controlling the character in the game the way the player actually meant rather than what the player input is a bit of a problem. For example, if a game was orignally designed for a mouse and the player uses a keyboard or joystick, you'll need special software to help the keyboard and joystick act more like the mouse.
Another important aspect of the input channel is time lag. Time lag is the amount of time from the player's input command to when the input acts on the game logic. Figure 2-3 shows this graphically. Ideally, time lag should be zero. However, in practice this is impossible to achieve because of physical limitations of the input device and its data rate. For example, the keyboard as currently set up by DOS will produce a maximum key repeat rate of 18 characters per second. It is possible that a player could input faster than that (improbably as it may be), and these inputs would be lost. For another example, imagine that a particular game's frame rate is only 10 FPS and the player fires a missle. In the worst case, the player will have to wait one frame before the missle is fired. Therefore, it's possible for the player to fire the missle by pressing the fire button and release the button before the game has time to detect the input. This is called input loss and is shown in Figure 2-4.
This type of input loss can affect the score because the player was proably in a moment of decision, made the decision, but the game didn't register it. Hence, the player might be penalized even though he made the correct decision. If the game misses inputs due to slow frame rate, it might be wise to buffer inputs in some way so they aren't lost when a player drives an input for too short a period. you have two ways to do this. You could write an interrupt routine that continually queries and buffers the inputs faster than any human could change them. Or you could perform input testing as more than one place in the main loop, average what the player did, and use that as the final input for the game cycle. This is shown in Figure 2-5.
Although the issues just discussed are important and do arise, as long as the frame rate stays at a reasonable rate (12 FPS), the input devices can be polled and nothing will be lost.
Event Driven Programming
We've been talking about inputs, event loops, and the general architecture of a game, but we really need to understand how video games differe from normal programs and why this is so. Video games are real-time programs that are event driven by time instead of user input. Even driven programming, in general, is a method of waiting for an even to occur and then acting upon the event. For example, when you are typing at the command line ins DOS, DOS is waiting for you to enter another character (the event).
A video game, on the other hadn, doesn't wait for the user (player) to input an event. The game continues its processing with or without the input of the player. This is the real-time aspect of a video game that must be implemented by the software. Let's take a more detailed look at standard input event driven programming and that of real-time video games.
Input Driven Event Loops
Most commercial and business applications written today are input driven event systems. A word processor is a perfect example. The word processor can print, search, type on the screen, load files, and so forth; however, once launched, the word precessor will just site there waiting for the user to do something. A typical main loop for an input driven event system looks something like this:
while(!done)
{
Wait_For_User_Input();
Switch(user_input)
{
case ACTION_1:
.
.
.
case ACTION_N:
} // end switch
Update_Data_Structures();
} // end main event loop while
The key to the input driven nature of this fragment is the call to the function Wait_For_User_Input(). This call forces the system to wait for the user to do something before proceeding. Hence, it's possible that the switch statement and data structure update sections won't be executed for a long time.
Listing 2-1 shows an example of a real program that uses an input driven event loop for a simple game. The game queries the player to guess a number between 1 and 100. You can type this program in or fin it on the CD-ROM. The name of the program is GUESS.C and the executable is GUESS.EXE.
Listing 2-1 A number guessing game that uses an input driven event loop
// GUESS.C - An example of input driven event loops
// I N C L U D E S ////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <graph.h>
// M A I N ////////////////////////////////////////////////////////////////////
void main(void)
{
int done=0, // exit flag
number, // the random nunber
num_tries=0, // number of tries
guess; // the players guess
unsigned int far *clock = (unsigned int far *)0x0000046CL; // pointer to clock
// SECTION 1 //////////////////////////////////////////////////////////////////
// print out introductory instructions
printf("\nI'm thinking of a number from 1-100.");
printf("\nTry and guess it!\n");
// seed the random number generator with the time
srand(*clock);
// choose a random number from 1-100
number = 1 + rand() % 100;
// SECTION 2 //////////////////////////////////////////////////////////////////
// main event loop
while(!done)
{
// SECTION 3 //////////////////////////////////////////////////////////////////
// query user for input (the event)
printf("\nWhat's your guess?");
scanf("%d",&guess);
// SECTION 4 //////////////////////////////////////////////////////////////////
// increment number of tries
num_tries++;
// process the event
if (guess > number)
printf("\nToo big!\n");
else
if (guess < number)
printf("\nToo small!\n");
else
{
// the user must have guessed the number
printf("\nYou guessed the number in %d tries!!!\n",num_tries);
// set the exit flag
done=1;
} // end else
} // end while
} // end main
Let's take a look at what each section of the code does.
Section 1: This is the initialization section of the program. It seeds the random number generator with the current time, selects a random number, and then prints out a message to the player.
Section 2: This is the entrance to the main event loop.
Section 3: Here's where the input driven event system waits for an event. In this case, the event is a character string that represents an integer. Note that the program will not proceed until the integer is entered.
Section 4: Once th input has been entered, this section is executed. The number that the player enters is tested against the computer's selection, and messages are displayed telling the player if the guess is too high or too low.
As you can see, this type of main loop is totally unacceptable for a 3D video game. We need something that will execute the remainder of the code elements in the main loop regardless of the unser's input status!
Real-Time Event Loops
A real-time event loop is the best way to implement a video game's main loop. The computer science definition of real-time is: a program that responds to the inputs as they occur. For example, military fighter jets have real-time computers onboard that will respond to inputs and act upon them within microseconds in most cases.
This definition of real-time is not really appropriate in the context of 3D video games, since we have already concluded taht our input channel may be sluggish, and it may take an entire video frame to recognize specific inputs. Real-time in our context means this: The image of the game is continually changing, the objects in the game will function on their own, and finally, the game won't wait for user input. This definition is a bit broad, but it satisfies our needs.
The difference between an input driven event loop and a real-time event loop is very simple: The real-time loop won't wait for input. Therefore, the main loop will execute very quickly and process all the function and logic within it repeatedly. Here is the structure of a real-time event loop:
while(!done)
{
if (User_Input_Ready())
{
Get_User_Input();
Switch(user_input)
{
case ACTION_1:
.
.
.
case ACTION_N:
} // end switch
} // end if user had an input
Update_Data_Structures();
} // end main event loop while
Let's analyze the new main loop by beginning with the if statement that leads the code fragment. Instead of waiting for user input, the system is polled to see if there is an input waiting. If there is an input waiting, it is retrieved and acted upon by the switch statement. Note that the switch statement is withing the body of the if statement. This is because there is no reason to act upon the user's input if there hasn't been any. Finally, at the end of the loop, there is the call to the data structure update function, which could be anything and simply represents the game logic.
We have covered the difference between input driven event programming and real-time event programming. Now, let's see what the main loop of a video game looks like.
Typical 3D Game Loops
As we discussed in the section on run-time, a video game is mostly composed of a small set of actions that are performed each game cycle or frame. When writing games, think of each iterations through the main loop as a frame, and use this as the standard by which to gauge your game's performance. The question is, what should be in the game loop? The answer to this depends on many factors that you will learn as we go, but as a first attempt at a game loop, here is the typical functionality of one:
while(!game_over)
{
// erase all the imagery
Erase_Objects();
Erase_Player();
// is the player trying to do something?
Get_User_Input();
// perfrom game logic on player and AI on objects
Do_Players_Logic();
Do_Objects_Logic();
// test for all possible collisions
Do_Collision_Detection();
// lock onto the video display and update the screen with the next frame
Synchronize_To_Video_Display();
Display_New_Frame();
// play music and sound FX
Do_Sounds_And_Music();
// update any miscellaneous data structures and do housekeeping
Do_Misc();
} // end main game loop
The main event loop executes over and over, continually updating the screen image regardless of the player's input (or lack of input). The function names in the loop should help you understand what's going on; however, the main concept of a video game loop is very simple: All the objects are erased in the world, the user's inputs are retrieved, the game logic is performed, and finally, the next fram of animation is drawn. That's really all there is to it. Of course, this example is a bit synthetic, and many of the functions would be performed in different orders or even be combined.
To give another example, Listing 2-2 is a small demo that uses a real-time event loop. It depicts a small light cycle (from the movie TRON) moving on a game grid. The Light Cycle is turned by using the keys "A" for left and "S" for right. If the player turns the light cycle, then it will turn; otherwise it will continue moving in the direction it is pointing. This is the real-time aspect of the demo. The name of the program is LIGHT.C and the executable is named LIGHT.EXE. If you wish to build your own executable, compile it using the medium memory model and link it to the Microsoft graphics library, or to Borland's, depending on the compiler you are using.
The program uses mode 13h (320x200 with 256 colors), which we will learn about in great detail in the next chapter. The graphics are implemented with Microsoft's library since we don't yet know how to change video modes, plot pixels, and so forth. Anyway, don't worry too much about how the graphics are done. Focus on the real-time aspects of the event loop. Let's look at the code.
Listing 2-2 A light cycle demo of real-time event loops
// LIGHT.C - An example of real-time event loops
// I N C L U D E S ////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <math.h>
#include <graph.h>
// D E F I N E S /////////////////////////////////////////////////////////////
// directions
#define NORTH 0
#define EAST 1
#define SOUTH 2
#define WEST 3
// F U N C T I O N S /////////////////////////////////////////////////////////
void Delay(int clicks)
{
// this function uses the internal timer to delay a number of clock ticks
unsigned long far *clock = (unsigned long far *)0x0000046CL;
unsigned long now;
// get current time
now = *clock;
// wait for number of click to pass
while(abs(*clock - now) < clicks){}
} // end Delay
//////////////////////////////////////////////////////////////////////////////
void Draw_Game_Grid(void)
{
// this function draws a game grid out of horizontal and vertical lines
short x,y;
// the the line color to white
_setcolor(15);
// draw the verical lines
for (x=0; x<320; x+=20)
{
// position the pen and draw the line
_moveto(x,0);
_lineto(x,199);
} // end for x
// draw the horizontal lines
for (y=0; y<200; y+=20)
{
// position the pen and draw the line
_moveto(0,y);
_lineto(319,y);
} // end for y
} // end Draw_Game_Grid
// M A I N ////////////////////////////////////////////////////////////////////
void main(void)
{
int done=0, // main event loop exit flag
player_x = 160, // starting position and direction of player
player_y = 150,
player_direction = NORTH;
// SECTION 1 //////////////////////////////////////////////////////////////////
// set the graphics mode to mode 13h 320x200x256
_setvideomode(_MRES256COLOR);
// draw the game grid
Draw_Game_Grid();
// SECTION 2 //////////////////////////////////////////////////////////////////
// begin real time event loop
while(!done)
{
// SECTION 3 //////////////////////////////////////////////////////////////////
// is the player steering his light cycle or trying to exit?
if (kbhit())
{
// test for <A>,<S> or <Q>
switch(getch())
{
case 'a': // turn left
{
// turn 90 to the left
if (--player_direction<NORTH)
player_direction=WEST;
} break;
case 's': // turn right
{
// turn 90 to the right
if (++player_direction>WEST)
player_direction=NORTH;
} break;
case 'q': // quit game
{
// set exit flag to true
done=1;
} break;
default:break; // do nothing
} // end switch
} // end if kbhit
// SECTION 4 //////////////////////////////////////////////////////////////////
// at this point we need to move the light cycle in the direction its
// currently pointing
switch(player_direction)
{
case NORTH:
{
if (--player_y<0)
player_y = 199;
} break;
case SOUTH:
{
if (++player_y>199)
player_y = 0;
} break;
case EAST:
{
if (++player_x>319)
player_x = 0;
} break;
case WEST:
{
if (--player_x<0)
player_y = 319;
} break;
default:break;
} // end switch direction
// SECTION 5 //////////////////////////////////////////////////////////////////
// render the lightcyle
_setcolor((short)9);
_setpixel((short)player_x,(short)player_y);
// wait a moment and lock the game to 18 fps
Delay(1);
} // end while
// SECTION 6 //////////////////////////////////////////////////////////////////
// restore the video mode back to text
_setvideomode(_DEFAULTMODE);
} // end main
The light cycle demo has a lot of interesting lessons withing it. The program has animation, collision detection, real-time input, and temporal synchronization (which we'll learn about in detail in a few moments). Let's discuss the program section by section and see what we can learn from it.
Section 1: This is the initialization section. In this case, it sets the graphics mode to 320x200 with 256 colors (mode 13h) and draws the game grid by calling the function Draw_Game_Grid().
Section 2: This is the entrance to the main event loop. Notice the usage of the variable done as the exit flag for the whole loop.
Section 3: Here's where the real-time aspect of the loop is apparent. A test using th library function kbhit() is performed to see if the player has pressed a key. If the function returns true, then the keypress is processed and the direction of the player is changed. If the function returns false, then the code jumps to the next section and continues processing. Hence, if there is input, it will be processed, but if there isn't, then the program won't wait, the logic will continue onward.
Section 4: Based on the current direction of the player's light cycle, the position of the light cycle is moved forward one pixel in that direction. Moreover, collision detection i performed to test whether the light cycle has moved past one of the four screen boundaries. If so, then the light cycle is "warped" to the other side of the screen. This logic is shown on Figure 2-6.
Section 5: At this point the light cycle has been moved and the collision detection has been performed, so it's time to draw the actual light cycle. This is done by plotting a single pixel at the current location of the light cycle, which is at (player_x, player_y). After the light cycle is drawn, a call to the time delay function is made to slow the game down; otherwise, the light cycle would move at light speed!
Section 6: This is the end of teh game, and the computer is placed back into text mode.
That Concludes the analysis of the program. If you're feeling ambitious, add another light cycle that is controlled by the computer. Place the logic for this right after the logic for the player's light cycle, but before the rendering section.
I hope you're starting to get a feel for the complexity of a video game. You just saw how much code it took to make a little dot move around on the screen--but we have to start somewhere! To become succesful Cybersorcerers, we will master many new programming techniques that are more acclimated to real-time environments. Let's briefly touch on some of them.
Autonomous Functions and Auto-Logic
If you've ever written a program, you know taht you start off with a big problem, then break it into smaller pieces, and write functions for all of the smaller pieces (or something close to this). Video game programming is similar, but the functions that control major aspects fo the game must be powerful and have memory. The functions must be autonomous. We'll cover this topic in more detail in Chapter 8, The Art of Possession, when we talk about interrupts and multitaksing, but for now let's take a few moments to get acquainted with the subject.
A video game has many creatures and objects that are doing their thing, along with otehr events, such as user input, that are occurring while the game is running. If the link to Cyberspace is to be sustained, we need a method of programming that simplifies teh creation and operation of the creatures, objects, and events within the game. For example, say you make an underwater submarine game with moving subs taht the player tries to shoot. Each submarine in the game needs its own set of functions and conrrol logic that perform the necessary operations to keep the sub doing what it's supposed to do. This means that the functions you write for the submarine control must be very smart. They need to know when they are called for teh first time, so they can create and set up any data structures, and they need to know when they are being called to implement the normal functions for which they are designed.
This autonomy is implemented by using static variable. Since the static variables are withing the scope of the autonomous functions, they can be used to track the state of the function from call to call, as shown in Figure 2-7. State tracking is simply a method of determining what happened last cycle--it's a system of memory. In general, we want game functions that are as self-contained as possible and perform a great deal of their own logic. We don't want to burden the main() of the game program with all kinds of work, since this will obscure and complicate the delineation between the functions of each of the object in the game and their control logic.
Temporal and Video Synchronization
Video games are basically free-running programs that run as fast as possible on the hardware they are running on. This can be a ploblem, since a game may run too fast on a 486DX4 and just right on a 486SX. To solve this problem, we must synchronize the game using some kind of time base at which to lock the frame rate. This is usually accomplised by placing a function call for a wait loop at the end of teh main game loop right before it repeats. For example, a simple technique is to make a delay function that will wait one system clock tick or roughly 1/18th of a second. This guarantees that the game will never run faster than 18 FPS no matter what machine it runs on.
The only downfall to this technique is taht it may severly impeded the performance of the machines that can barely sustain 18 FPS on their own. For example, if there was no time delay at the end of the game and it ran 18 FPS, and then we placed a time delay in for one clock tick, the total time delay would be 2/18ths of a second, making the maximum frame rate 9 FPS on a machine that can actually do 18 FPS! To solve this problem, we can record the current systems timer value at the beginning of the game loop, and then at the end of the game loop test wheter on clock tick (or more) transpired. If so, then the game is running at less than 18 FPS, and there should be no delay. On the otehr hand, if the timer value hasn't changed yet, then the same logic has executed in less that 1/18th of a second, and a while loop or other similar logic should be used to wait for a complete tick to occur before repeating the game loop.
Reading the internal timer and reprogramming it isn't difficult, and we'll learn about it in Chapter 8 (you can take a peek at the Delay() function in LIGHT.C). But for now here's a code fragment that will gaurantee that a game runs at 18 FPS maximum, and slow machines will not be penalized:
// main game event loop
while(!game_over)
{
current_time = Get_Timer();
// perform game logic
// has one tick already passed?, if not wait until it does.
while(Get_Timer() - current_time<1);
} // end while
The synchronization is accomplished by recording the current value of the internal time (we'll learn about this later) and then performing the game logic. When the game logic is complete, the saved timer value is subratcted from the current timer value. If the difference is less than 1, we can deduce that 1/18th of a second hasn't passed, so we should wait until it does. This is exactly what the while loop does.
There are several kinds of synchronizations used in 3D games other than temporal ones. Many times it's desierable to perform video updating while the video screen is in the vertical retrace phase (see Figure 2-8). During this time, the video hardware isn't accessing the VGA card's video buffer and any updates to the buffer will be much faster, since there won't be a data bus contingency (as it's called) between the system processor and the VGA card. When they both try to access the video RAM, access slows and image sheering results, as shown in Figure 2-9.
The Sound System
If 3D video games are a Black Art, then sound has got to be teh Blackest Art! Two kinds of sounds can be played on the PC equipped with a Sound Blaster or similar card. The first kind is digitazed samples. These sounds are played using a digital to analog converter within the sound card. Implementing digitized playback isn't too hard. Sounds can be played back with a couple hundred lines of code along with the drivers supplied with the card. However, playing music is another story! Although Sound Blaster and most popular cards come with drivers to play music, and documentation can be found to use them, it is complex, to say the least. Moreover, if you get one specific card to play digitized sounds and music, what about the rest?
In most cases, Cybersorcerers use other software libraries taht can play digitized sound and music on any sound card. These libraries have relatively easy-to-understand interface and take care of all the details of playing the sound FX for you. Of course, you'll still have to supply the raw ditial samples and music files, but that's life. As for operation of the sound system, it is usually done in the background of the game, and the game logic doesn't need to do anything with the sound card in the foreground. For example, Sound Blaster can play a digital sample using DMA, and the computer doesn't have to do anything except start the sound. Music, on the other hand, does need the main CPU's time. But the music drivers are usually implemented as interrupts or TSRs (terminate ans stay resident programs) that latch onto the timer interrupt and update the music output every system timer tick--again, releiving the main CPU from any chores. However, there will be a slight degradation in overall system performance, since the timer interrupt will be servicing the sound car.
More detailed discussion of sound can be found in Chapter 6, Dancing with Cyberdemons.
Summary
In this chapter we covered the overall structure of 3D games and their major components. We also discussed real-time programming and synchronization techniques used in games. Finally, we touched on one of the most important elements of a game, and that's sound! You probably have a lot of questions now--more than you began with. But that's good. A lot of topics were introduced without going into much detail, but that will change as soon as you turn the page...
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, Chapter 3: The Mysterious VGA Card
| Copyright 2006 Andre LaMothe |
|
