Tuesday, 6 July 2010

Wall Chaos (II) - Technical stuff (Run for your lives!!)

After the first post, focused on design issues, this second one will show the dirty implementation details behind the 2D version of Wall Chaos.

The language of choice was C++. Firstly, because that way I had the chance to reuse some code from the theory classes, and also because it's the language I feel most comfortable programming in excluding Python. 

If you remember, the assignment included a constraint that restricted the game to be isometric. Getting this done was probably the most challenging part, and I ended up mixing theory concepts learned in class with some ideas I got from Ernest Pazera's Isometric Game Programming with DirectX 7.0.

As you probably know, there are several subtypes of isometric maps. The most widely used ones are probably the staggered map(for example, the one used throughout the Civilization series) or the diamond map (The Age of Empires maps have this kind of layout). I was pretty sure from the beginning that I'd go with the second approach, as it seemed to fit better with the shape of the rooms.  Both types are pretty similar, but they differ in the way the map is traversed. 

This is how the x and y coordinates range in a diamond map:
This setup makes it a bit more difficult to blit a tile, since two consecutive cells aren't blitted in rows as in a typical rectangular 2d map. Besides, the cell sprites (which are rectangular) will overlap, so the traverse order matters.

This is the blitting loop (there is an outer loop for further layers, as the tiles are stackable, but for brevity's sake I will omit it)

for (int y=0;y<SCENE_HEIGHT;y++)
{
for (int x=0;x<SCENE_WIDTH;x++)
{
TTile* tile = &(gameData.scene->map[y][x][z]);
//Read the tiles' sprite sheet to get the clip rectangle
tileRect=gameData.scene->getTileRect(tile);
int tileIndex=tile->index;
if (tileIndex!=NOTILE)
g_pSprite->Draw(textures[TILESET_INDEX],&tileRect,NULL,
&D3DXVECTOR3( float((x-y-1)*(TILE_WIDTH>>1)-baseX), float((x+y)*(TILE_HEIGHT>>1)-baseY-z*TILE_HEIGHT), 0.0f),
 inkColor); 
}
}

The x,y screen coordinates are computed as follows;
x_screen = ((x_map-y_map-1)*TILE_WIDTH/2)-baseX;
y_screen = ((x_map+y_map)*TILE_HEIGHT/2 - baseY-(z_map*TILE_HEIGHT);

baseX, baseY define the offset from the screen position (0,0). This ensures that the map is rendered at the right location (at the beginning of the game, it will typically start next to the top edge and horizontally centered).

Another issue was to retrieve the cell in the map from a set of screen coordinates. One of the main uses for this is to find out which cell the player has clicked on with the mouse. As Wall Chaos was basically controlled through the keyboard -save for the GUI menus-, I used this method to quickly convert between screen and cell coordinates instead.

The algorithm consists of two parts. This time I'll just describe it:

First, get the cell's rectangle (The diamond cell and its surroundings).
To do this, from the screen coordinates we'll first get the world coordinates, taking into account the scene's offset. This means that if the mouse click resolved to position (100,200), and there is a (400,64) scene offset, the world coordinates would be, respectively, (500,264).

Then, the tentative coarse (per-cell) and fine (per-pixel) coordinates are to be retrieved. This is done in the same way as with common 2D tiled maps:

coarse = (worldX/TILE_WIDTH, worldY/TILE_HEIGHT)
fine = (worldX%TILE_WIDTH, worldY%TILE_HEIGHT)

On those maps we'd probably end the whole process here. However, there are still some more steps involved in this case. Due to the scene's offset position, there is a chance than the fine coordinates are negative and we need to adjust them, adding the tile width or height to the modulo operation result.

Then, depending on the value of the coarse coordinates, we need to adjust the pre-final map coordinates. That's achieved like this:

int mapX=0,mapY=0; //Candidate map coordinates
while(coarseY<0)//Move north
{
mapX--;
mapY--;
coarseY++;
}
while(coarseY>0)//Move south
{
mapX++;
mapY++;
coarseY--;
}
while(coarseX<0)//Move west
{
mapX--;
mapY++;
coarseX++;
}
while(coarseX>0)//...and finally, east
{
mapX++;
mapY--;
coarseX--;
}


After iterating through these loops, the first part of the procedure ends. We have a pair of coordinates corresponding to a rectangular cell (x,y). However, depending on the fine coordinates, the actual diamond cell might be one of the 4 depicted adjacent cells.



The second part's objective is, therefore, to retrieve the actual cell. Taking advantage of the fact that  the ratio width:height of the tiles was 2:1 we can use the following property:

if(fineX<(TILE_WIDTH>>1))//32
{
//Left
if(fineY<(TILE_HEIGHT>>1))//16
{
//Up
if( (TILE_HEIGHT-fineX)>>1 > fineY ) { mapX-=1;}
}
else
{
//Down
if( (TILE_HEIGHT+fineX)>>1 < fineY ) {mapY+=1; }
}
}
else
{
fineX-=(TILE_WIDTH>>1);

//Right
if(fineY<(TILE_HEIGHT>>1))
{
//Up
if( (fineX>>1) > fineY ) mapY-=1;
}
else
{
//Down
if( (TILE_WIDTH-fineX)>>1 < fineY ) mapX+=1;
}
}

After this, mapX and mapY will hold the final map coordinates.

Last, this game featured music and sound. One of the assignment requirements asked us to implement an interface to abstract and encapsulate the concrete sound library used. This was relatively simple to do. I used the old API of FMOD, which seemed good enough for my purposes, and abstracted the three kinds of supported sounds (Streams, Sounds and Samples -midis, basically) into three subclasses of a base class CSoundItem. At the resource loading time, the layer parsed the filename and, depending on the extension (*.wav, *.mid, *.mp3,...), it instanced a particular subclass.
The sound layer had an STL map indexed by an ID string containing all the loaded sounds (the load was done on a per-game-state basis), and then the application just had to keep track of the IDs to play any sound of music track. 

I could dwelve a bit more on the game's architecture (For instance, for managing the game states I defined a couple of variables to keep the current and next state IDs and then a factory to resolve and instance the particular GameState subclass to switch to next. For Once Upon a Night, the Master's final project, I opted for using a stack-based game state machine instead, which turned out to be much more flexible), but I guess I've bored you enough already. If you feel like diving a bit more into the code, you may download the sources from here.

No comments:

Post a Comment