From Demos to Games
Throughout the last eight articles, we''ve taken a tour of different game elements, each an integral part of game development. Of course, what we''ve written thus far could hardly be considered games. But let''s face it -- you can''t write games unless you can display/animate images, control timing, understand resolutions and colors, and harness DirectX to make all of these work together. Now that we have some skill in these departments, what''s missing?
Continuity. That''s what we''re missing. Games don''t just start with players running around and shooting. There''s intro screens, menus, multiple levels, intros/outros and cutscenes, etc. In other words, a game consists of many different components, and all we''ve got so far is our single entry point,
Game_Main(). Obviously if we want to write anything robust, we''re going to need a little help.
Life in
Game_Main()
We know that
Game_Main() is executed continually, and the approach we''re taking is to render a single frame of animation each time. We also know that the only way to remind ourselves in
Game_Main() of what we''re supposed to be doing each frame is by keeping track of everything with static/global variables and other data structures. All that we need to do is add an additional level of organization so that our game not only knows ''how'' to do things, but ''when'' to do things.
As an example, say our game consists of an intro screen (something that says, "Press SPACE to begin"), and the actual game where our sprites are moving around, etc. We''d start by thinking of these two components as separate, and assign a new global variable to keep track of where the user is:
enum GameState {GS_INTRO, GS_GAME};
// Our G structure in GLOBALS.H
struct
{
...
GameState gameState;
...
} G;
Somewhere in
Game_Initialize() we would initialize this variable like so:
G.gameState = GS_INTRO;
...and in
Game_Main(), we could branch-off depending on the game state:
void Game_Main()
{
if (G.gameState == GS_INTRO)
{
// Show the intro screen
}
else
{
// The game -- move sprites
}
}
This is about the simplist mechanism for game states that I could think of. Of course, for this particular example it works fine, so there would be no need to go further. In reality though, we''ll have more than two game states, and this if/else structure could quickly become large and unweildy. How else could we implement this type of ''filter''? How about this:
void Game_Main()
{
switch (G.gameState)
{
case GS_INTRO: GS_Intro(); break;
case GS_GAME: GS_Game(); break;
}
}
This approach definitely seems a lot smarter, as it calls a corresponding game function for each possible game state. And, let me tell you, you will not want to be writing entire games in
Game_Main() unless they''re very small...
Sometimes you might want to use a combination of the two -- a main game state that determines which function gets called, and a secondary, or sub-state variable to act as a filter once in the proper game function. Here''s an example of what I mean:
enum MainGS {MGS_INTRO, MGS_GAME};
enum SubGS {SGS_MOVING, SGS_SHOOTING, SGS_DYING};
// Our G structure in GLOBALS.H
struct
{
...
MainGS gameState;
SubGS gameSubState;
...
} G;
Here, we could use gameState to call the appropriate game function (e.g. using the switch construct), and we could use gameSubState from within the chosen game function as a helper. What you get is two levels of indirection that can help you better organize what code needs to be executed when in order for your game to work properly.
Another technique involves using function pointers, which is a powerful programming technique that''s covered in Tricks and Techniques. Sufficive to say, we don''t need anything of these calibre for our immediate purposes.
Tracing Your Footsteps
The use of enumerated game states is definitely going to help us organize a simple game, and there''s one more game state that we should be keeping track of to make life a little easier on us. It turns out that it''s often important to know not only what state we''re currently in, but what state we''re coming from. To validate this need, take a look at the following:
...
if (G.gameState == GS_DEAD)
{
// Code to animate player''s death
}
...
Let''s assume that the code for this block is responsible for an animation spanning multiple sprite frames controlled by a timer. You see, this block of code is going to be executed repeatedly because the game state GS_DEAD will be the current game state for the whole time the player is seen crouching over and dying, and this means that there will be variables that need to be initialized. One way to accomplish this is to add a little logic like so:
...
if (G.gameState == GS_DEAD)
{
if (G.prevGameState != GS_DEAD)
{
// Initialize local variables
// ...
G.prevGameState = GS_DEAD;
}
// Code to animate player''s death
}
...
The first time this code is executed, it knows that it''s the first time and therefore can initialize any local variables it needs to render the player''s death properly. Once this code has completed the sequence, it will change the current game state to something other than GS_DEAD. The new game state can then in turn utilize this initialization technique as well.
Of course, there are other ways to do this:
...
if (G.gameState == GS_DEAD)
{
static bool bInit = FALSE;
if (bInit == FALSE)
{
// Initialize local variables
// ...
bInit = TRUE;
}
// Code to animate player''s death
// Are we done with this game state?
if (bDeathSequenceComplete == TRUE)
{
bInit = FALSE;
G.gameState = GS_SOMETHINGELSE;
}
}
...
Here we''re using a static variable, just like we did with
Game_Main() in earlier articles. When this game state is complete, it''s important to reset the bInit flag so that later on in the game if GS_DEAD starts again, it will once again be initialized properly.
Variable Scope
When talking about game states, I alluded to the fact that large games need more than one main function. By the same token, large games should use more than one source code file for their game code as well. Luckily for us, our global G structure is designed so that any source file (module) can automatically ''see'' these global variables. All that you have to do is create a new source file and make sure this is the top:
#include "Globals.h"
You can then start writing functions inside of this new source file that work with your global variables, thereby contributing to the game.
When working with multiple modules, it sometimes becomes important to further organize variables, as games can end up with a lot of them. Consider this example source file:
// EXAMPLE.CPP
#include "Globals.h"
static struct
{
int xpos, ypos;
int spriteState;
} L;
void Function1()
{
int local1;
// ...
}
void Function2()
{
bool bLocal;
// ...
}
Bacause of the
#include at the top of the file, this file can freely make use of our G structure. At the top of this file is another structure called L, which stands for LOCAL, and holds some variables that only functions in this file can use. There are two reasons for grouping variables in this way:
- Using L gives us the look and feel of C++ classes, where all variables that belong to the module exist. If we combine all of the functions with a common purpose together with the variables they share, we have a clean and intuitive source code arrangement to work with.
- When reading the source code, one can easily distinguish which variables are local to the function, and which are also used by other functions in the module (i.e. there''s an ''L.'' before them).
Putting everything together, we get a template like this:
G
{
FILE1
{
L
{
Function1
{
local (stack) variables
}
Function2
{
local (stack) variables
}
...
}
}
FILE2
{
L
{
Function10
{
local (stack) variables
}
...
}
}
...
}
This illustration shows that G is visible by everything, and L is visible to the file in which it resides. In order to realize the benefits of this system of data organization, you have to be sure to properly organize your variables:
- If a variable is only needed by a single function, create it within the function
- If a variable is needed by more than one function,
- if the functions seem like they belong to a group, group them to a file and place the variable in an L structure (i.e. file scope)
- if the functions are in different files and shouldn''t be moved, place the variable in G
Hopefully this already makes logical sense to you, and I should point out that we haven''t been using this system in our template code thus far, but then again it hasn''t been necessary to either as our template is small and only has one run-time game module (GAMEMAIN.CPP). When it''s time to start working with L structures, you''ll know it -- your code will be too large and unmanageable without it.
The End of the Introduction
Well folks, that about wraps up this series! I was supposed to introduce everyone to a simple base code template, and that''s what I did. Furthermore, I threw in introductory material on some other important elements, so I think that if you''ve come this far, you''ve done well indeed.
I wish that I had in front of me a ''master list'' of all of the skills we''ll be covering, but I don''t. Instead, we''ll have to continue picking away at things in increasing complexity, and the best way to do that is to practice by writing actual games. After all, that''s what we''re all here for.
Therefore, we''ll be starting on a real game in the next article. That should make a lot of you happy
I hope that everyone can appreciate the need to start small; there are people out there who tackle 3D engines as their first true game development endeavour, and I can''t help but feel sad for some of them because they''re missing the skills that makes a game developer great -- grass-roots evolutionary skill development. I thoroughly believe that if you want to really learn to develop games, you need to follow the same hard road that those before us have built with their determination, trials and tribulations. As games evolved, so too did the techniques that made them. It would seem logical that the earliest games are the simplest to analyse, and by tracing their evolution and taking on the skills that were developed along the way, we too will inheret the experiences and talent of their creators, and hopefully their fame.
A special note to those who were here the first time the forum existed -- you''re going to recognize the first game we attempt, but I''ve rewritten the code and all of the articles, so it wouldn''t hurt to go through them again. Eventually, we''ll all be breaking new ground, so bear with me.
Teej
Questions? Comments? Please reply to this topic.