jueves, 19 de junio de 2014

Adding keyboard control for the player and improving map rendering.

After a comment I received I decided it was time to improve the draw() step of the application we were developing until now and make it to display faster in order to show the frame smoother. In order to achieve this we need to reduce the amount of printf() calls we are doing during our draw() step, because when printing every row of the map as independent calls, the text in the console moves up and this makes the whole display too hard to watch.

Now we will be placing our letters representing the map into an auxiliary array so we can then draw it with a single printf() call. For this we will be declaring an array and a variable for the cell position in which we're going to place our map character inside the array.

    char imageMap[4096]={}; // The assignment of empty braces initializes all elements to 0
    int nImageCursor = 0; // The position where we should place our next character


Then we will assign the characters representing the map in this array. For this we will assign the letter and increment the cursor to the next position like this: 

    imageMap[nImageCursor++] = 'P'; // assign 'P' at imageMap[nImageCursor] then increment nImageCursor

We need to set a 0 value at the end of our map text so printf() and other string operations know that the text ends there, so once we finished to place our map characters into the array, we set the value for the cursor position to 0:

    imageMap[nImageCursor] = 0; // a null char indicates the end of the character string or text

After that, we simply call printf() by sending our map image as parameter as follows:

    printf( "%s", imageMap ); // %s gets replaced by the imageMap string.

This leaves our code at the draw() function looking somewhat like this:

    // We're going to draw our map in the imageMap array.
    char imageMap[4096]={}; // the assignment of empty braces initializes all chars in the array to 0
    int nImageCursor = 0; // The position where we should position our next character
    for( int z=0; z< map.Depth; z++ ) // iterate over every row
    {
        for( int x=0; x< map.Width; x++ ) // iterate over every column for the z row
        {
            if( player.x == x && player.z == z )
                imageMap[nImageCursor++] = 'P'; // draw the player as an ascii character

            else if( enemy.x == x && enemy.z == z )
                imageMap[nImageCursor++] = 'E'; // draw the enemy

            else
            {
                // Replace the tile value with a space character if the value is 0, otherwise the null character will terminate our string and the map will display truncated. 
                imageMap[nImageCursor++] = map.FloorCells[z][x] ? map.FloorCells[z][x] : ' '; // inline conditional is (condition  ? value_if_true : value_if_false)
            }
        }
        imageMap[nImageCursor++] = '\n'; // \n is the code character for "new line" inside a text. We use it to display the next cells in the next row.
    }
    imageMap[nImageCursor] = 0; // a null char indicates the end of the character string or text.

    printf( "%s", imageMap ); // draw the map to the console



Now that we have our display improved, we can start adding some controls to our player. We are going to do this during our update() step by checking the state of the arrow keys and changing the value of the player position accordingly:

    if(GetAsyncKeyState(VK_UP))
        gameObject->Player.z = gameObject->Player.z - 1; // decrease z by 1
    if(GetAsyncKeyState(VK_DOWN))
        gameObject->Player.z += 1; // increase z by 1
    if(GetAsyncKeyState(VK_RIGHT))
        gameObject->Player.x ++; // increase x by 1
    if(GetAsyncKeyState(VK_LEFT))
        gameObject->Player.x --; // decrease x by 1


Not that difficult, was it? So now let's enable more enemies into our game so it doesn't look so empty. For this we're going to replace our single SCharacter instance we used for the enemy with a template type called vector that comes with the standard library of the compiler. We need to include a new file in order to have access to vector's definition:

#include <vector> // so we can use the std::vector<> type.

Now we can declare the player and our enemy list as part of our new SGame structure that will hold the game data:

struct SGame // holds game data
{
    SMap Map; // declare a variable of type SMap
    SCharacter Player; // Declare a variable of type SCharacter for the player
    std::vector<SCharacter> Enemy; // Here we're going to store our list of enemies
};


Also we should add a new map layer for storing our enemies as part of the map structure:

struct SMap // The struct is a block of variables to be used to store our map information
{
    int Width, Depth; // Declare Width and Depth variables which will hold the active map size
    int FloorCells[MAX_MAP_DEPTH][MAX_MAP_WIDTH]; // 2-D Array representing the tile cells
    int EnemyCells[MAX_MAP_DEPTH][MAX_MAP_WIDTH]; // Map that holds indices to the enemy list.
};


We could use a new function in order to manage the setup of enemies, like this:

void setupEnemies( SGame* gameObject )
{
#define INITIAL_ENEMY_COUNT 4
    for( int iEnemy=0; iEnemy < INITIAL_ENEMY_COUNT; iEnemy++ )
    {
        SCharacter newEnemy;
        newEnemy.MaxPoints        = { 100, 50, 1000000 }; // HP, MP and XP
        newEnemy.CurrentPoints    = { 100, 50, 0 };
        newEnemy.x = rand() % gameObject->Map.Width; // random value between 0 and width-1
        newEnemy.z = rand() % gameObject->Map.Depth;

        gameObject->Enemy.push_back( newEnemy ); // copy the new enemy as a new element at the end of our enemy list.
    }
};


At the update function, we refresh the enemy layer of the map from the enemy list. For this we first clear the enemy layer with the INVALID_ENEMY value:

    for( int z = 0; z < gameObject->Map.Depth; z++ ) // clear each row
       memset( gameObject->Map.EnemyCells[z], INVALID_ENEMY, sizeof(int)*gameObject->Map.Width );


Then we iterate through each enemy and place the index in the corresponding cell:

    for( unsigned int iEnemy=0; iEnemy < gameObject->Enemy.size(); iEnemy++ )
    {
        gameObject->Map.EnemyCells[currentEnemy->z][currentEnemy->x] = iEnemy; // assign enemy index to the cell corresponding to this enemy
    }


Now only the display of the new enemy data is missing, and we can change our previous lines at draw() with these new ones, which check for a valid enemy index in the cell being rendered:

            else if( gameObject->Map.EnemyCells[z][x] != INVALID_ENEMY )
                imageMap[nImageCursor++] = 'E'+gameObject->Map.EnemyCells[z][x]; // draw the enemy


The source code for this program can be read from http://code.google.com/p/gpftw/source/browse/trunk/Tutorial5/main.cpp

That's it for now, I hope you liked it. In the next tutorial we we'll be talking about SVN which we're going to use through the rest of the tutorials. See you there!

No hay comentarios:

Publicar un comentario