Table of Contents Source Code Article Two Article Four
If you have ever been privileged enough to sit down and talk with a group of programmers, one of the most profound things you will discover is that most consider programming an art as much as it is a science. In fact, when you talk to the true masters of the trade you will find that they view it as an art, rather than a science. Which I think is true. There's a lot you can learn about the programmer just by the way that they code. Take me for instance, I have a weakness in that I tend to overdesign and overcomplicate things. It's part of who I am (it goes beyond programming I am embarrassed to say) and as such it's something I have to consciously watch for when I code.
There is an interesting thing I've noticed over the years. You could take an intermediate level programmer and a master level programmer and give them the same task. Both will get it working. Both may even use the same fundamental architecture. But I can guarantee you that the master level's code will be smaller, faster and more expandable than the intermediate's.
Think about that for a second... both have the same task, use the same programming language, and even use the same design, but one can be significantly smaller and faster than the other.
What tends to separate the masters from the rest of the herd is how they can use the system to their advantage: they can use architectural nuances, they trick the compiler, they bend the langage, capitalize on the way computers use binary, replace code with data - anything and everything is fair game. It's aggravating sometimes, because it seems like they can effortlessly get the computer to bend over backwards to do what they want.
Now that I've deified l33t programmers, I'm going to show you a little of what I mean. It's not a mystical talent reserved for a select few overwieght nerds in their underwear basking in the neon glow of monitor backlighting. It's really just a bag of sneaky tricks that are collected from years of experience.
The idea here is to show you that there are many ways to solve the same problem. What I'm going to show you is a couple of sneaky tricks that can be employed to simplify the source code in Article Two to allow much cleaner and smaller code.
Here's how...
There's something I like to call the PITA test. Basically, wherever encounter something that's a Pain-In-The-Ass it usually means that there's room for improvement. Take for instance the following piece of code from Article Two:
// DrawTile Function /////////////////////////////////////////////////////////////////////
//
// Draws a map tile for the map coordinates specified.
//
void DrawTile( int x, int y )
{
console.SetPosition( x, y );
switch( nMapArray[y][x] )
{
case TILE_ROCKFLOOR:
console.SetColor( 7 );
console << '.';
break;
case TILE_WALL:
console.SetColor( 7 );
console << '#';
break;
case TILE_CLOSEDDOOR:
console.SetColor( 6 );
console << '+';
break;
case TILE_OPENDOOR:
console.SetColor( 6 );
console << '/';
break;
case TILE_GRASS:
console.SetColor( 10 );
console << '.';
break;
case TILE_TREE:
console.SetColor( 10 );
console << 'T';
break;
}
}
It's long and tedious to type and seems very repetitive. All it does is determine which ASCII character in which color to put on the screen - and all those TILE_xxxxx constants being used are just numbers. Here's what the compiler actually sees:
switch( nMapArray[y][x] )
{
// TILE_ROCKFLOOR
case 0:
console.SetColor( 7 );
console << '.';
break;
// TILE_WALL
case 1:
console.SetColor( 7 );
console << '#';
break;
// TILE_CLOSEDDOOR
case 2:
console.SetColor( 6 );
console << '+';
break;
// TILE_OPENDOOR
case 3:
console.SetColor( 6 );
console << '/';
break;
// TILE_GRASS
case 4:
console.SetColor( 10 );
console << '.';
break;
// TILE_TREE
case 5:
console.SetColor( 10 );
console << 'T';
break;
}
If I were to draw this in a table, it would look like this:
TILE TYPE | CHARACTER COLORCODE
-----------+--------------------------
0 | '.' 7
1 | '#' 7
2 | '+' 6
3 | '/' 6
4 | '.' 10
5 | 'T' 10
-----------+--------------------------
But wait, if I can draw it as a table, then why don't I actually use one in the program? How would that work? Well, check this out:
struct TILE_TYPE
{
char nCharacter; // ASCII character for this tile type
short nColorCode; // Color code for this tile type
};
// Global array used to define all tile types used in the game
TILE_TYPE sTileIndex[] = {
{ '.', 7 }, // (0) TILE_ROCKFLOOR
{ '#', 7 }, // (1) TILE_WALL
{ '+', 6 }, // (2) TILE_CLOSEDDOOR
{ '/', 6 }, // (3) TILE_OPENDOOR
{ '.', 10 }, // (4) TILE_GRASS
{ 'T', 10 } // (5) TILE_TREE
};
Which is cool and all, but watch what happens next. That entire function gets reduced to this:
// DrawTile Function /////////////////////////////////////////////////////////////////////
//
// Draws a map tile for the map coordinates specified.
//
void DrawTile( int x, int y )
{
console.SetPosition( x, y );
int nType = nMapArray[y][x];
console.SetColor( sTileIndex[nType].nColorCode );
console << sTileIndex[nType].nCharacter;
}
25 lines of code can get reduced to 7. Not to mention how easy it is to add new tile types later on:
// Global array used to define all tile types used in the game
TILE_TYPE sTileIndex[] = {
{ '.', 7 }, // (0) TILE_ROCKFLOOR
{ '#', 7 }, // (1) TILE_WALL
{ '+', 6 }, // (2) TILE_CLOSEDDOOR
{ '/', 6 }, // (3) TILE_OPENDOOR
{ '.', 10 }, // (4) TILE_GRASS
{ 'T', 10 }, // (5) TILE_TREE
// OMG! 1 line to add a new tile type!!!
{ '~', 9 }, // (6) TILE_WATER
};
There's another function in the Article Two's source that could benefit from this idea.
// IsPassable Function ///////////////////////////////////////////////////////////////////
//
// This function analyzes the coordinates of the map array specified and returns
// true if the coordinate is passable (able for the player to occupy), false if not.
//
bool IsPassable( int x, int y )
{
// Before we do anything, make darn sure that the coordinates are valid
if( x < 0 || x >= MAP_WIDTH || y < 0 || y >= MAP_HEIGHT )
return false;
// Store the value of the tile specified
int nTileValue = nMapArray[y][x];
// Return true if it's passable
if( nTileValue == TILE_ROCKFLOOR || nTileValue == TILE_GRASS || nTileValue == TILE_OPENDOOR )
return true;
return false;
}
Our target is this chunk of the subroutine:
// Return true if it's passable
if( nTileValue == TILE_ROCKFLOOR || nTileValue == TILE_GRASS || nTileValue == TILE_OPENDOOR )
return true;
Now assuming that we've done the previous optimization, let's tweak the data structure a little bit by adding a boolean property that is true when you can walk on the tile and false if not.
struct TILE_TYPE
{
char nCharacter; // ASCII character for this tile type
short nColorCode; // Color code for this tile type
bool bPassable; // Set to true if you can walk on this tile
};
// Global array used to define all tile types used in the game
TILE_TYPE sTileIndex[] = {
{ '.', 7, true }, // (0) TILE_ROCKFLOOR
{ '#', 7, false}, // (1) TILE_WALL
{ '+', 6, false }, // (2) TILE_CLOSEDDOOR
{ '/', 6, true }, // (3) TILE_OPENDOOR
{ '.', 10, true }, // (4) TILE_GRASS
{ 'T', 10, false } // (5) TILE_TREE
};
Can you see where I'm going with this?
// IsPassable Function ///////////////////////////////////////////////////////////////////
//
// This function analyzes the coordinates of the map array specified and returns
// true if the coordinate is passable (able for the player to occupy), false if not.
//
bool IsPassable( int x, int y )
{
// Before we do anything, make darn sure that the coordinates are valid
if( x < 0 || x >= MAP_WIDTH || y < 0 || y >= MAP_HEIGHT )
return false;
// Store the value of the tile specified
int nTileValue = nMapArray[y][x];
// Return true if it's passable
return nTileIndex[nTileValue].bPassable;
}
Not only is this cleaner, it also saves at least seven machine instructions. As well, it allows you more flexibility in the future. Not to mention that it makes your life easier later on when you want to add more exotic tile types in the future - like frozen lakes.
A big thank you goes to Jeff Lait for this next trick.
There are times when adding extra code helps; sometimes, adding more variables simplifies things. Sound strange? Definately. Here's the point though: if it makes the code easier to understand, easier to modify and easier to expand, why not?
Sure there are times when code needs to be time or space critical, where you need every processor cycle or every byte of RAM you can get. But the thing is, this is a Roguelike. By nature, it is a complex interactive simulation with a virtual world in a (usually) turn-based action..response cycle. Processor load isn't the issue, complexity is. You are not dealing with 3D graphics and pipeline bandwith managment, so data isn't the issue. But clarity is. Anything you can do to your code to simplify it, to make it easier to understand and leave room for future ideas, go for it.
Ranting aside, let's take a look at an example. If you take a look at the giant switch..case block inside main() you can see that this fits our PITA test:
// Process the input
switch( nKey )
{
// Move up
case '8':
// Can we move to the tile above?
if( IsPassable(nPlayerX, nPlayerY-1) )
{
// Move up
nPlayerY--;
}
break;
// Move left
case '4':
// Can we move to the tile to the left of the player?
if( IsPassable(nPlayerX-1, nPlayerY) )
{
// Move left
nPlayerX--;
}
break;
// Move right
case '6':
// Can we move to the tile to the right of the player
if( IsPassable(nPlayerX+1, nPlayerY ) )
{
// Move right
nPlayerX++;
}
break;
// Move down
case '2':
// Can we move to the tile below the player?
if( IsPassable(nPlayerX, nPlayerY+1) )
{
// Move down
nPlayerY++;
}
break;
// Escape key
case 27:
// Quit the program
return;
}
That's an excessive amount of passablility tests, and a lot of repeated code. Yes, there are slight variations, but by and large, it's very repetitive. So let's see what can be done.
If we can store the direction we're wanting to travel, we can do the IsPassable(..) test only once at the end. Like most things of this nature, it's better explained by the code than by the nerd who wrote it:
int nDeltaX;
int nDeltaY;
// Process the input
switch( nKey )
{
// Move up
case '8':
nDeltaX = 0;
nDeltaY = -1;
break;
// Move left
case '4':
nDeltaX = -1;
nDeltaY = 0;
break;
// Move right
case '6':
nDeltaX = 1;
nDeltaY = 0;
break;
// Move down
case '2':
nDeltaX = 0;
nDeltaY = 1;
break;
// Escape key
case 27:
// Quit the program
return;
}
// Check and see if we're allowed to move in the direction specified
if( IsPassable(nPlayerX + nDeltaX, nPlayerY + nDeltaY) )
{
// If allowed, move in the direction specified
nPlayerX += nDeltaX;
nPlayerY += nDeltaY;
}
While this is fewer lines of code, it does use two extra integer variables. However the advantages far outweigh the costs. For one, it's easier on the eyes. Secondly, adding in diagonals is a piece of cake (just mix around the -1's and +1's). Thirdly, we'll be using this exact same algorithm for NPC's later on. Which means that the game treats the player's character and computer controller characters the exact same way.
I'm going to stop it here. I just want to give you a taste of the things that are possible. When you're just starting out, it's sometimes tempting to go out of your way to do these tricks. Sometimes it's disheartening because you feel like you need to do it the "proper" and most efficient way possible. Yet it's hard to find and make these tricks on your own.
Incorporating these tricks comes from experience, and there's only one way to get that. The more code you write, the better you'll be at it; and it will naturally sneak it's way into what you write.
However, I would recommend that you don't go looking too hard for them. Some might disagree, as I used to even a year ago, but let give you a little piece of advice I was given by a man much smarter than I: finished code is infinitely better than beautiful but incomplete code. Both ways get the job done, and that is the most important part. Optimization is a tool, but a tool can be a trap if you're not willing to let it go when you need to.
Table of Contents Source Code Article Two Article Four