![]() |
Multimedia »
DirectX »
Games
Intermediate
RaceX - A 2D racing game using DirectDrawBy Mauricio RitterThis is a 2D racing game that uses a DirectX wrapper library. The game has single player and multiplayer support. |
VC6, VC7Win2K, WinXP, MFC, DirectX, Dev
|
|
Advanced Search |
|
|
|
||||||||||||||||
This is the second complete game that I have created using the DirectX library (actually is the 3rd, but I lost the partial project of the 2nd one in some of mine HD reformatting sessions). The game was created using a library created by me (called cMain.lib), that works as a wrapper around the DirectX library. The library source is included with the game source code so that you can use it to create your own games. I'll start explaining how this library actually works and them I'll explain how the game works.
Before you compile the project, make sure you have the DirectX SDK 8.0 installed (the DirectX SDK, not the run-time). If you have already installed the SDK and are still having trouble compiling the project, check if the DX include and library directory is the top of the list in VC++. If this doesn't work, just post a message or send me an email that I'll help you as soon as possible.
If you open the project workspace file (.DSW) you'll notice that the workspace is composed of two projects. One project is the library project that works as a wrapper for the DirectX library and have some other functions that are needed in almost every game project. This library is composed of 14 classes, each one with its own function in the game. I will explain each one of the classes so that you understand their role in the game.
The cApplication class is basically a wrapper to a simple windows
program. Since we're working with DirectX here, this application class is also
responsible for creating the basic framework needed to use DirectDraw. In the
library we can find a global function that id responsible for the creation
of the application (WinMain). There you'll find a call to a CreateApplication()
function.
The CreateApplication() function is a virtual function that needs
to be create in the game project itself, and need to return a new instance of
an application class. This new instace will be your own application class,
derived from cApplication class.
There are three important virtual functions in the cApplication class that are
extremely important in the game creation process, they are AppInitialized,
ExitApp and DoIdle.
The AppInitialize is called when all the application startup procedures called,
so that you can start your own initialization procedures. The ExitApp is called
when we're exiting the game, and is used to destroy and deallocate anything
that was created in the AppInitialized function or during the game. The
DoIdle function is where the game actually happens. When we
don't have any
windows messages to process, the cApplication base class call this virtual
function, allowing you to process your game.
If you check the cApplication class, you'll check that it have a
cWindow class
instance. This cWindow class is responsible for creating the main window in the
game. This class is used only inside the library, and there is no need to
change its attributes during the game.
These three classes take care of the user input for our game. Since the Mouse
and Keyboard input rely on the DirectInput framework, we need a place to put
the DirectInput main objects initialization. This place is the cInputDevice
Class.
In the cInputDevice class we have a pointer to a DirectInput interface and a
reference count. The reference count is used to check how many classes are
currently using the DirectInput main interface. Notice that the reference count
and the interface pointer are static variables. This is done because the
cMouse
and the cKeyboard classes are derived from the cInputDevice class, and use the
same DirectInput main object.
The cKeyboard class take care of the keyboard entry. It have a static
variable that represents a buffer containing the state of each keyboard key.
This buffer is a static variable, allowing us to create a
cKeyboard instance anywhere in our code and use the same keyboard buffer (we read the
keyboard state once, and use it along the game iteration).
The cMouse class take care of the mouse input. It work very similar to the
cKeyboard class. Each time we call the Process() function, it change its
internal variables to reflect the current mouse position in the screen and the
state of each one of its buttons.
The surface class is a wrapper around the the DirectX Surface object. It is a structure that hold the game graphics in the video (or local) memory so that we can blit this graphics in the screen at each game iteration. If you want more information about this class and the process of surface blitting, you can read my other article here in Code Project.
The cSprite class is a wrapper to work with sprites. Basically it have an
instance of the cSurface class and some information about the sprite. The main
difference here is that you can move through the sprite steps automatically,
without worrying about the position and size you need to get from the source
surface.
These three classes are responsible for the sound handling in our game. The
cSoundInterface creates the main DirectSound objects that will be used to
create the sound buffers of the game. Its recommended that you initialize
an instance of this class in the AppInitialize virtual function of the
cApplication class, so that you initialize the Sound Interface before you start
the game.
The cSound class holds the sound buffers of the game. It have all the
compatibilities of a normal DirectSound buffer, like Frequency and Pan control,
3D sound, and looping. To load the sounds from the resource or from the file,
it uses an instance of the cWavFile class. This class is basically a loader for
wave files. All its code was taken from Microsoft DirectX Samples (I modified
then a bit, so that they work with WAV files in the resource).
This classes are wrappers around the DirectPlay interfaces, and are
used to create multiplayer games. The cMultiplayer class handles all the
DirectPlay functions like device enumeration, session enumeration, and player
connection. It also hold all the information about each one of the players in a
gaming session. It is important to notice that each player have an exclusive ID
that is store in a list in the cMultiPlayer class. This IDs can be used to
control some behaviors of the game in a multiplayer session.
To handle the multiplayer network messages, cMultiplayer class have a pointer to
a cMessageHandler class. The cMessageHandler class is a simple class with a
virtual function. This virtual function is called each time the computer
receive a DirectPlay message from a peer. Its recommended that you derive your
application class from this cMessageHandler class too, so that you can pass the
application class itself as a message handler to the multiplayer game class (as
done in RaceX).
This is a simple class used to create dynamic matrices. There's no big deal
about it, it have a Create method that allocates the necessary memory and a
Destroy() function to deallocate it. It also have a GetValue and SetValue
function used to retrieve and set the values of the matrix.
This class takes care of the hit checking in the game. It creates a GDI region and use the GDI functions the test if something has hit the region or not. This class is the same used in my other article about hit checking. If you want more information about it, just check the other article.
Now that we have a brief description of each one of the classes in the game library, let's take a look at the game project. The game project has a dependency of the game lib project, if you load the .DSW file. Each one of the classes in the game sample describes a unit of the game (the Game itself, the Track, the Car, the Competition), so I'll explain each one so that you understand how the game works.
This class is derived from cApplication and from cMessageHandler. Since we
derive it from cApplication, we have to create an implementation to the
AppInitialize, ExitApp and DoIDle Functions.
In the
AppInitialize, we do the initialization of some objects that are not
initialize in the base class. Notice that in the cRaceXApp class we have some
member variables to handle the SoundInterface, the Multiplayer and the Mouse and
Keyboard Input. All this member variables are initialized in this virtual
function implementation, calling the Initialize method of each member variable.
Notice that in the initialization of the cMultiplayer instance we are calling
the SetHandler() function, passing this as a
parameter. We can pass the "this" as a parameter because our application class
is derive from cMessageHandler. Using this implementation allow us to
handle the DirectPlay network messages from our application class itself. You
can notice that we have an implementation for the IncomingMessage function.
This function is called when we receive a message from a DirectPlay peer (as
described in the cMultiplayer class description)
The other implementation we have in this function is the DoIdle() implementation.
The DoIdle is where the entire game logic is build. The first thing we do in
this function is check the m_iState member variable, to know at
which game state we are working. When you start the game, the game state is
assigned with the GS_MAINSCREEN value, that represents the menu screen. You can
check the GS_MAINSCREEN case in the DoIdle function to see how the menu
structure is built.
Another important variable in the DoIdle implementation is the iStart
variable. This variable is used to know when we're changing from one game state
to another. When we change the game state, we set this variable to 0 so that in
the next iteration, when the base class call the DoIdle again, the new game
state can load all its surface and do the extra initialization needed. After
the game state initialize its objects, it sets the iStart to a value other
than 0 and reset it to 0 when it changes the state of the game again.
When the user select one of the game types in the menu screen (Single or Multi
Player, Single Track or Competition Mode), the game enters in the track
selection screen (or in the start competition screen). When the user enter in
the track selection screen the program initialize some other class
instances in
the cRaceXApp class, the cCompetition instance and the
cRaceTrack
instace. I'll
explain these two classes so that you understand how this screen works.
Even if the name states the this class is responsible for handling all the
competition stuff in the game, this class is used even in Single Track mode.
This class stores some basic information about each one of the players in the
game. When we start a game, we need to call the AddPlayer method of this class
do add players to our game. Adding player to this class makes them apper in the
race when we change the state of the game to GS_RACE. If you check the
GS_RACE
state, you'll notice that it uses the player list information in the
cCompetition class instance to create each one of the cars to the race. This
class also stores the points and position of each player in the case
we're
playing in competition mode, and its also responsible for telling the program
the track sequence of the competition (by using the NextRace and
GetNextRace methods).
The cRaceTrack class takes care of the track creation and handling in the game.
Most of the game logic is inside this class. The first thing we need to do when
working with this class is load a Track from a Track file. The class have a
ReadFromFile method that is used to Load the tracks from a .rxt file (Race X
Track file).
When we load the track from the file, it fills the internal member variables of
the cRaceTrack class with information about the track. The track is structured
as a bidimensional matrix and each one of the matrix cells represent a road
type. When we draw the Track in the screen, we use this road type to draw a
40x40 pixels tile in the place corresponding to the matrix position. In the
track file, an array of DWORDs describe each one of those tiles in the track.
Since a DWORD can store 4 bytes we use only the LOWORD to store the road type.
The HIWORD of this DWORD array is used to store other
information, the
CheckPoints (in the LOBYTE) and the Angle Information (in the
HYBYTE).
The CheckPoint stored in the TrackFile is used to control the sequence that run throught the Track. So if the car passed throught checkpoint 1, he needs to pass through checkpoint 2 to fulfill the track, as so on until he reach the last checkpoint. Using this checkpoint structure prevents the user to run backward from the start line and pass through the start line several times, increasing its Laps Completed counter. Since he needs to pass through all the check points, its mandatory the he runs the entire race path.
The race lap counter will only increment when we reach the checkpoint 1 again and the last checkpoint we have passed is the last check point we have avaible in this track.The Angle information is used to allow the computer to drive the car. It will
point the direction that the computer-driven car should head so that he can
finish the race. We'll now check the cRaceCar class so that we understand
what is the role of this angle information.
The cRaceCar class takes car of all the car behavior handling in the game. The
most important function of this car class is the Process function that
process the car behavior in each game iteration.
When we call the Process function the car class check how this instance of the
car is controlled. The car can be controlled by the computer, by the user or by
the network.
If the car is controlled by the user, the car class check the keyboard input to
see if the user is trying to accelerate, break or turn the car. Depending on
the information retrieved from the keyboard, the class call the Accellerate,
BreakCar, TurnCarRight or TurnCarLeft methods.
If the car is controlled by the computer, the car class checks the current position of the car in the track and get the angle information associated with this position. If the angle is different from the current angle of the car, the computer turns the car to clockwise or anticlockwise. The computer always accelerate the car when its driving, except when it checks that it'll hit in a wall he keep running at the same speed.
If the car is controlled by the "network" we need to check if we're the hoster of the game or not. If we're hosting the game we need to process the car information based on the keyboard input from the remote computer. If we're not the hoster of the game, we need to sned our keyboard infomation to the multiplayer game hoster.
When we start a new race in the game, and the game state changes to
GS_RACE, we
create instaces of the cRaceCar object based on the information found in the
cCompetiton class. Then we call the AddCar() method of the
cRaceTrack class to add each one of the cars to the race in the track. After
adding all the cars to the race track we can process the race track class, in
order to play the game.
At each iteration of the game we call the Process() method of the
cRaceTrack class. In this method, we'll loop in the car array,
available in
the cRaceTrack, class to call the Process() method of each car and move them
around the track. We'll also use the cHitChecker class to check if each one of
the cars hit the wall or hit another car. If the car hit a wall, we change its
state to CARSTATE_CRASHEDWALL. The cars will keep running around the track
until the user car finish the track (have a Lap count equal to the number of
the laps in the track).
I tried to explain the basic functionality of the game in the article, but there are lots of comments inside the game sample that will help you to understand the game much better. If you have any questions about the game implementation just post a message or send me an email.
I want to send a special thanks to all my beta testers, in special to Colin J Davies, Isaac Sasson, James T Johnson, Nishant Sivakumar, Nnamdi Onyeyiri and Smitha (Tweety), for their effort finding bugs in the game, and for all their suggestions.
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 30 Aug 2002 Editor: Chris Maunder |
Copyright 2002 by Mauricio Ritter Everything else Copyright © CodeProject, 1999-2009 Web20 | Advertise on the Code Project |