Sunday, 2 October 2011

Intermediate Game Tutorial #4 - Tile Based Map Collision Detection

Intermediate Tutorials
Intermediate Game Tutorial #4 - Tile Based Map Collision Detection
Collision Detection
 Introduction

This tutorial deals with collision detection.
Compile and run tutorial14. The program will read the map data file and display the map on the screen. Use the arrow keys (not the ones on the numeric pad) to move the character around the screen. Pressing space will make the character jump. Should the character fall out of the map screen it will reappear after a couple of seconds.

 An indepth look

We reintroduce the Entity structure and add a few extra variables to it:
typedef struct Entity
{
    int w, h, onGround;
    int thinkTime;
    float x, y, dirX, dirY;
} Entity;
The onGround variable is used to determine if the Entity is on the ground. We use this to determine whether or not the Entity is allowed to jump or perform any other ground based action. dirX and dirY are used to apply directional force to the x and y variables. This is important when working with gravity or friction. We will see how this is used later. We will only look at three files in this tutorial since the other files and functions have been covered numerous times in previous tutorials.
map.c contains an extra function to center an Entity on the screen:
void centerEntityOnMap(Entity *e)
{
    map.startX = e->x - (SCREEN_WIDTH / 2);
    
    if (map.startX < 0)
    {
        map.startX = 0;
    }
    
    else if (map.startX + SCREEN_WIDTH >= map.maxX)
    {
        map.startX = map.maxX - SCREEN_WIDTH;
    }
    
    map.startY = e->y - (SCREEN_HEIGHT / 2);
    
    if (map.startY < 0)
    {
        map.startY = 0;
    }
    
    else if (map.startY + SCREEN_HEIGHT >= map.maxY)
    {
        map.startY = map.maxY - SCREEN_HEIGHT;
    }
}
We take the Entity's current horizontal position, subtract half the screen's width from it and assign this to startX. We then perform the usual bounds check to make sure that the startX value to make sure we don't attempt to draw non existant parts of the map. We do the same with the vertical position. We will now look at the code to handle the player, which is in player.c:
void doPlayer()
{
    if (player.thinkTime == 0)
    {
        player.dirX = 0;
    
        /* Gravity always pulls the player down */
    
        player.dirY += GRAVITY_SPEED;
        
        if (player.dirY >= MAX_FALL_SPEED)
        {
            player.dirY = MAX_FALL_SPEED;
        }
        
        if (input.left == 1)
        {
            player.dirX -= PLAYER_SPEED;
        }
        
        else if (input.right == 1)
        {
            player.dirX += PLAYER_SPEED;
        }
        
        if (input.jump == 1)
        {
            if (player.onGround == 1)
            {
                player.dirY = -11;
            }
            
            input.jump = 0;
        }
        
        checkToMap(&player);
        
        centerEntityOnMap(&player);
    }
    
    if (player.thinkTime > 0)
    {
        player.thinkTime--;
        
        if (player.thinkTime == 0)
        {
            initPlayer();
        }
    }
}
The first check we perform is that the player's thinkTime is greater than 0 and if it is then we can perform actions on it. We first set the dirX to 0. This means that the player will stop moving instantly when we release the key. If we wanted to make the player slowly stop then we could do something like the following:
player.dirX *= 0.98;
This would mean that when we released the arrow key the player would take a few frames to come to a complete halt. This could be used to give the illusion that the player is on ice where there is a low coefficient of friction. Next we apply gravity to the player's vertical movement. Not that we do not simply set the dirY to the amount of gravitational pull, but we increment it instead. This will mean that anything currently moving up will start to be pulled down after a while. We also limit the maximum speed at which the player will fall. The left and right movements should be self explanitory. When jump is detected we need to first check if the player is on the ground, otherwise they could jump any time they wanted, which is undesirable. Provided they can jump, we set the dirY to -11, which in this game is a reasonable amount to jump by. Note that this value will be decremented by gravity in the following frames. Finally, we call checkToMap and centerEntityOnMap. We will look at checkToMap shortly. If the player's thinkTime is greater than 0 then the player cannot perform any actions and we simply decrease the value. Once it hits 0 we call initPlayer to reset the player on the map. We will now look at the map collision detection. This function is stored in collisions.c:
void checkToMap(Entity *e)
{
    int i, x1, x2, y1, y2;
    
    /* Remove the user from the ground */
    
    e->onGround = 0;
    
    /* Test the horizontal movement first */
    
    i = e->h > TILE_SIZE ? TILE_SIZE : e->h;
    
    for (;;)
    {
        x1 = (e->x + e->dirX) / TILE_SIZE;
        x2 = (e->x + e->dirX + e->w - 1) / TILE_SIZE;
    
        y1 = (e->y) / TILE_SIZE;
        y2 = (e->y + i - 1) / TILE_SIZE;
        
        if (x1 >= 0 && x2 < MAX_MAP_X && y1 >= 0 && y2 < MAX_MAP_Y)
        {
            if (e->dirX > 0)
            {
                /* Trying to move right */
        
                if ((map.tile[y1][x2] != BLANK_TILE) || (map.tile[y2][x2]!=BLANK_TILE))
                {
                    /* Place the player as close to the solid tile as possible */
        
                    e->x = x2 * TILE_SIZE;
                    
                    e->x -= e->w + 1;
        
                    e->dirX = 0;
                }
            }
        
            else if (e->dirX < 0)
            {
                /* Trying to move left */
        
                if ((map.tile[y1][x1] != BLANK_TILE) || (map.tile[y2][x1]!=BLANK_TILE))
                {
                    /* Place the player as close to the solid tile as possible */
                    
                    e->x = (x1 + 1) * TILE_SIZE;
        
                    e->dirX = 0;
                }
            }
        }
        
        if (i == e->h)
        {
            break;
        }
        
        i += TILE_SIZE;
        
        if (i > e->h)
        {
            i = e->h;
        }
    }

    /* Now test the vertical movement */
    
    i = e->w > TILE_SIZE ? TILE_SIZE : e->w;
    
    for (;;)
    {
        x1 = (e->x) / TILE_SIZE;
        x2 = (e->x + i) / TILE_SIZE;
    
        y1 = (e->y + e->dirY) / TILE_SIZE;
        y2 = (e->y + e->dirY + e->h) / TILE_SIZE;
        
        if (x1 >= 0 && x2 < MAX_MAP_X && y1 >= 0 && y2 < MAX_MAP_Y)
        {
            if (e->dirY > 0)
            {
                /* Trying to move down */
                
                if ((map.tile[y2][x1] != BLANK_TILE) || (map.tile[y2][x2]!=BLANK_TILE))
                {
                    /* Place the player as close to the solid tile as possible */
                    
                    e->y = y2 * TILE_SIZE;
                    e->y -= e->h;
        
                    e->dirY = 0;
                    
                    e->onGround = 1;
                }
            }
        
            else if (e->dirY < 0)
            {
                /* Trying to move up */
        
                if ((map.tile[y1][x1] != BLANK_TILE) || (map.tile[y1][x2]!=BLANK_TILE))
                {
                    /* Place the player as close to the solid tile as possible */
        
                    e->y = (y1 + 1) * TILE_SIZE;
        
                    e->dirY = 0;
                }
            }
        }
        
        if (i == e->w)
        {
            break;
        }
        
        i += TILE_SIZE;
        
        if (i > e->w)
        {
            i = e->w;
        }
    }

    /* Now apply the movement */

    e->x += e->dirX;
    e->y += e->dirY;
    
    if (e->x < 0)
    {
        e->x = 0;
    }
    
    else if (e->x + e->w >= map.maxX)
    {
        e->x = map.maxX - e->w - 1;
    }
    
    if (e->y > map.maxY)
    {
        e->thinkTime = 60;
    }
}
This function is fairly complex, so we will look at it in sections. Firstly, we set the onGround variable to 0. During our map tests we may set this variable back to 1. The easiest way to test map collisions is to check the horizontal and vertical movements separately. We will start with the horizontal movement. Determining whether or not an Entity has collided with a map block is a case of checking the value of the map tile that each of the 4 corners of the Entity will be in after the movement has taken place. This approach will only work though if the Entity's horizontal and vertical sizes are less than or equal to the TILE_SIZE, otherwise a very large Entity will be able to move through the map tiles since its corners may not necessarily collide with a tile even though its midsection does. To get around this problem we will break the Entity's height into portions:
i = e->h > TILE_SIZE ? TILE_SIZE : e->h;
In our example, the player sprite is 55 pixels tall, i will initially be TILE_SIZE. We next enter a loop and calculate the corners that the player will be in after it has moved:
x1 = (e->x + e->dirX) / TILE_SIZE;
x2 = (e->x + e->dirX + e->w - 1) / TILE_SIZE;

y1 = (e->y) / TILE_SIZE;
y2 = (e->y + i - 1) / TILE_SIZE;
x1 is the left side, x2 is the right side, y1 is the top side and y2 is the bottom. We will combine these values together to calculate the coordinates of the corners. Next we check that the values the 4 sides are within the bounds of the tile array. If they are not then we skip over the calculation since the player has gone outside the bounds of the screen, either because they have jumped at the top of the screen or they may have fallen out of the map, and we don't want to attempt to index an array outside of its bounds. Provided the values are legal we then check if the player is moving right:
if (e->dirX > 0)
{
    /* Trying to move right */

    if ((map.tile[y1][x2] != BLANK_TILE) || (map.tile[y2][x2] != BLANK_TILE))
    {
        /* Place the player as close to the solid tile as possible */

        e->x = x2 * TILE_SIZE;
        
        e->x -= e->w + 1;

        e->dirX = 0;
    }
}
We want to check the top right and bottom right corners of the Entity so we use y1 and y2 for the top and bottom values and x2 since it is the right side of the Entity. We then check the tile type at these two corners and if either of the tiles is not empty, then we have hit a map tile. We then move the Entity as close to the tile as possible and set the dirX to 0. If we are moving left then the code is fairly similar:
else if (e->dirX < 0)
{
    /* Trying to move left */

    if ((map.tile[y1][x1] != BLANK_TILE) || (map.tile[y2][x1] != BLANK_TILE))
    {
        /* Place the player as close to the solid tile as possible */
        
        e->x = (x1 + 1) * TILE_SIZE;

        e->dirX = 0;
    }
}
We still use y1 and y2 but we use x1 since this is the left side of the Entity. Next we check if we have to test the next block of the body.
/* Exit this loop if we have tested all of the body */

if (i == e->h)
{
    break;
}

/* Test the next block */

i += TILE_SIZE;

if (i > e->h)
{
    i = e->h;
}
If the current value of i is equal to the Entity's height then we have completed testing the horizontal movement and can exit the loop. Otherwise we increment i by TILE_SIZE to test the next block. If i is greater than the Entity's height then we set it to the Entity's height since we don't want to test outside of this. We then test the vertical movement, which is similar to the horizontal checking, except that during the vertical checking, if we are moving down and we encounter a solid tile, we set the onGround variable to 1. Finally, we apply the dirX and dirY to allow the Entity to move:
/* Now apply the movement */

e->x += e->dirX;
e->y += e->dirY;

if (e->x < 0)
{
    e->x = 0;
}

else if (e->x + e->w >= map.maxX)
{
    e->x = map.maxX - e->w - 1;
}

if (e->y > map.maxY)
{
    e->thinkTime = 60;
}
Note that dirX and dirY may have been set to 0 in which case no movement will take place. We also prevent the player from being able to move off the left and right hand edges of the map. If the player's y variable is greater than the maxY of the map then the Entity has fallen out of the map, in which case we set the Entity's thinkTime to 60, which is about 1 second. As seen earlier, this will make the player reappear at the start of the map.

 Conclusion

The code for checking the map may seem very long, but in reality we are simply performing the same check 4 times, one for each corner. You can use the map editor in the previous tutorial to modify the map included in this tutorial. In the following tutorials we will look at more animation and additions to the map.

 Downloads

Source Code - tutorial14.tar.gz

Intermediate Game Tutorial #3 - A Tile Based Map Editor

Intermediate Tutorials
Intermediate Game Tutorial #3 - A Tile Based Map Editor
The Map Editor
 Introduction

In this tutorial we will create a basic map editor
Compile and run tutorial13. The program will read the map data file and display the map on the screen. Use the arrow keys (not the ones on the numeric pad) to scroll the map around. Moving the mouse cursor will move the tile block around. Clicking the left mouse button or pressing space will place a tile at the current mouse position. Clicking the right mouse button will blank the tile at the current map position. Pressing the comma or minus key (not the one on the numeric pad) will select the previous map tile. Pressing the period or plus key (not the one on the numeric pad) will select the next map tile. Pressing S will save the map data and pressing L will load the map data.

 An indepth look

We have increased the maximum number of tiles to 400 x 300 and also updated the Map structure:
typedef struct Map
{
    char *filename;
    int startX, startY;
    int maxX, maxY;
    int tile[MAX_MAP_Y][MAX_MAP_X];
    SDL_Surface *background;
} Map;
The filename variable simply stores the path to the map data so that we can load and save it. The background variable is the background image. Currently this is hardcoded but later tutorials will see the filename stored as part of the map data. We also create a structure to handle the tile cursor as follows:
typedef struct Cursor
{
    int x, y, tileID;
} Cursor;
The x and y variables are screen coordinates and the tileID is the current ID of the selected tile. We also have a message structure
typedef struct Message
{
    char text[MAX_MESSAGE_LENGTH];
    int counter;
} Message;
This structure is used to display messages on the screen. The message will be displayed on the screen as long as the counter is greater than 0. The map loading code in map.c has changed to allow dynamic setting of the maximum horizontal and vertical scrolling:
void loadMap(char *name)
{
    int x, y;
    FILE *fp;

    fp = fopen(name, "rb");

    /* If we can't open the map then exit */

    if (fp == NULL)
    {
        printf("Failed to open map %s\n", name);

        exit(1);
    }

    /* Read the data from the file into the map */
    
    map.maxX = map.maxY = 0;

    for (y=0;y<MAX_MAP_Y;y++)
    {
        for (x=0;x<MAX_MAP_X;x++)
        {
            fscanf(fp, "%d", &map.tile[y][x]);
            
            if (map.tile[y][x] != BLANK_TILE)
            {
                if (x > map.maxX)
                {
                    map.maxX = x;
                }
                
                if (y > map.maxY)
                {
                    map.maxY = y;
                }
            }
        }
    }
    
    map.maxX++;
    map.maxY++;
    
    /* Set the start coordinates */
    
    map.startX = map.startY = 0;
    
    /* Set the maximum scroll position of the map */
    
    map.maxX = MAX_MAP_X * TILE_SIZE;
    map.maxY = MAX_MAP_Y * TILE_SIZE;
    
    /* Set the filename */
    
    map.filename = name;

    /* Close the file afterwards */

    fclose(fp);
}
Before reading the map data, we set the maxX and maxY values to 0 and then, while reading the map data, we check if the current map value is not the blank tile. If this is true then we check if the current x value is greater than the current maxX value and if it is, then we set it to x. We apply the same logic to the maxY variable. Since this is a map editor however, limiting the scrolling to the bounds of the size of the map would not allow us to expand the map so, we set the size of the map to the maximum possible size. In the next tutorial we will not perform this step. Finally, we set the filename of the map. The save map code is similar in places:
void saveMap()
{
    int x, y;
    FILE *fp;

    fp = fopen(map.filename, "wb");

    /* If we can't open the map then exit */

    if (fp == NULL)
    {
        printf("Failed to open map %s\n", map.filename);

        exit(1);
    }

    /* Write the data from the file into the map */

    for (y=0;y<MAX_MAP_Y;y++)
    {
        for (x=0;x<MAX_MAP_X;x++)
        {
            fprintf(fp, "%d ", map.tile[y][x]);
        }
        
        fprintf(fp, "\n");
    }

    /* Close the file afterwards */

    fclose(fp);
}
First we open the file that the map was loaded from, then we loop through the map from processesing each row and column and writing each map value to the file. We also terminate each row with a carriage return which will improve readability if we need to open the map in a text editor. Also in this file is the loadMapTiles function:
void loadMapTiles()
{
    int i;
    char filename[40];
    FILE *fp;
    
    for (i=0;i<MAX_TILES;i++)
    {
        sprintf(filename, "gfx/map/%d.png", i);
        
        fp = fopen(filename, "rb");
        
        if (fp == NULL)
        {
            continue;
        }
        
        fclose(fp);
        
        mapImages[i] = loadImage(filename);
        
        if (mapImages[i] == NULL)
        {
            exit(1);
        }
    }
}
Since we have multiple map tiles now, the easiest way to load them all is to give each file an id number, starting with 0 for the blank tile and incrementing the number for each subsequent file. We check that the file exists for the id number we are trying to load and if it does then we load it. The freeMapTiles function is a standard loop that frees our images so we will skip over it. In input.c, we add the ability to read the mouse buttons as follows:
case SDL_MOUSEBUTTONDOWN:
    switch(event.button.button)
    {
        case SDL_BUTTON_LEFT:
            input.add = 1;
        break;
        
        case SDL_BUTTON_RIGHT:
            input.remove = 1;
        break;
        
        default:
        break;
    }
break;

case SDL_MOUSEBUTTONUP:
    switch(event.button.button)
    {
        case SDL_BUTTON_LEFT:
            input.add = 0;
        break;
        
        case SDL_BUTTON_RIGHT:
            input.remove = 0;
        break;
        
        default:
        break;
    }
break;
The code should be self explainitory as it is very similar to reading key presses. We also read in the position of the mouse cursor:
/* Get the mouse coordinates */

SDL_GetMouseState(&input.mouseX, &input.mouseY);

input.mouseX /= TILE_SIZE;
input.mouseY /= TILE_SIZE;

input.mouseX *= TILE_SIZE;
input.mouseY *= TILE_SIZE;
SDL_GetMouseState sets the x and y mouse coordinates to the passed in variables. The x and y coordinates are relative to the SDL window and also does not include the window decoration. We also want to snap the coordinates to a grid. This simply requires dividing the mouseX by the TILE_SIZE and multiplying it back up again. Since mouseX is an integer, we will not get any decimal part when we divide the value. cursor.c contains the functions for manipulating the on screen cursor:
void doCursor()
{
    cursor.x = input.mouseX;
    cursor.y = input.mouseY;
    
    if (cursor.y >= SCREEN_HEIGHT - TILE_SIZE)
    {
        cursor.y = SCREEN_HEIGHT - TILE_SIZE * 2;
    }

    if (input.left == 1)
    {
        map.startX -= TILE_SIZE;

        if (map.startX < 0)
        {
            map.startX = 0;
        }
    }
    
    else if (input.right == 1)
    {
        map.startX += TILE_SIZE;

        if (map.startX + SCREEN_WIDTH >= map.maxX)
        {
            map.startX = map.maxX - SCREEN_WIDTH;
        }
    }
    
    if (input.up == 1)
    {
        map.startY -= TILE_SIZE;

        if (map.startY < 0)
        {
            map.startY = 0;
        }
    }
    
    else if (input.down == 1)
    {
        map.startY += TILE_SIZE;
        
        if (map.startY + SCREEN_HEIGHT >= map.maxY)
        {
            map.startY = map.maxY - SCREEN_HEIGHT;
        }
    }
    
    if (input.add == 1)
    {
        map.tile[(map.startY + cursor.y) / TILE_SIZE]
        [(map.startX + cursor.x) / TILE_SIZE] = cursor.tileID;
    }
    
    else if (input.remove == 1)
    {
        map.tile[(map.startY + cursor.y) / TILE_SIZE]
        [(map.startX + cursor.x) / TILE_SIZE] = BLANK_TILE;
    }
    
    if (input.previous == 1)
    {
        do
        {
            cursor.tileID--;
            
            if (cursor.tileID < 0)
            {
                cursor.tileID = MAX_TILES - 1;
            }
        }
        
        while (mapImages[cursor.tileID] == NULL);
        
        input.previous = 0;
    }
    
    if (input.next == 1)
    {
        do
        {
            cursor.tileID++;
            
            if (cursor.tileID >= MAX_TILES)
            {
                cursor.tileID = 0;
            }
        }
        
        while (mapImages[cursor.tileID] == NULL);
        
        input.next = 0;
    }
    
    if (input.save == 1)
    {
        saveMap();
        
        setStatusMessage("Saved OK");
        
        input.save = 0;
    }
    
    if (input.load == 1)
    {
        loadMap(map.filename);
        
        setStatusMessage("Loaded OK");
        
        input.load = 0;
    }
    
    if (input.left == 1 || input.right == 1 || input.up == 1 || input.down == 1)
    {
        SDL_Delay(30);
    }
}
First, we prevent the cursor from being able to move into the bottom row of the screen, since we will use that to display messages. We then handle the left, right, up and down arrow key presses as per the previous tutorial. When the add input is 1, we place a tile at the current screen and mouse cursor position. We do this by taking the startX value and adding the cursor.x value and then dividing this value by TILE_SIZE to give us the nearest tile. We do the same for the vertical coordinate. We then set this tile value to the tileID of the cursor. The remove input does the same thing, except that it always sets the tile's value to BLANK_TILE. When the next input is set, we increment the cursor's tileID. If the image referenced by the tileID is NULL, we move to the next tile and continue to do so until we encounter a tile that is not NULL. We also wrap the tileID value around if it is greater than or equal to MAX_TILES. The previous input behaves in the same way, except that we decrement the tileID instead. If the save input is 1 then we call saveMap followed by setStatusMessage. We will look at this function later. We preform a similar process when load is set to 1, except we call loadMap. Finally, if any of our navigation inputs are true, then we call SDL_Delay to prevent the map from scrolling too fast. Also in this file is the function to draw the cursor. This draws the image referenced by the tileID. The status panel code in status.c contains 3 functions:
void doStatusPanel()
{
    message.counter--;
    
    if (message.counter <= 0)
    {
        message.counter = 0;
    }
}
doStatusPanel decrements the message's counter. This is very similar to the thinkTime variable seen in earlier tutorials.
void drawStatusPanel()
{
    SDL_Rect dest;

    dest.x = 0;
    dest.y = SCREEN_HEIGHT - TILE_SIZE;
    dest.w = SCREEN_WIDTH;
    dest.h = TILE_SIZE;
    
    SDL_FillRect(screen, &dest, 0);
    
    if (message.counter > 0)
    {
        drawString(message.text, 0, SCREEN_HEIGHT - TILE_SIZE, font, 1, 0);
    }
}
The drawStatusPanel function uses a lot of function calls seen in previous tutorials. Firstly we fill the bottom row of the map in black and then, if the message counter is greater than 0, we print the text stored in the message structure. Finally,
void setStatusMessage(char *text)
{
    strncpy(message.text, text, MAX_MESSAGE_LENGTH);
    
    message.counter = 120;
}
setStatusMessage sets a new message and resets the counter to 120, which is approximately 2 seconds. The remaining files and functions have been covered in the basic tutorials so we will not look at them.

 Conclusion
The editor is incredibly basic, but in future tutorials we will improve it to meet the needs of the game. In the next tutorial we will implement collision detection to allow an entity to move around the map.

 Downloads

Source Code - tutorial13.tar.gz

Intermediate Game Tutorial #2 - Scrolling a Tile Based Map

Intermediate Tutorials
Intermediate Game Tutorial #2 - Scrolling a Tile Based Map
Scrolling a tile based map
 Introduction

This tutorial demonstrates how to scroll around a tile based map.
Compile and run tutorial12. The program will read the map data file and display the map on the screen. Use the arrow keys (not the ones on the numeric pad) to scroll the map around. Closing the window or pressing Escape will exit the program.

 An indepth look

In the previous tutorial, the map size was restricted to 20 x 15 tiles. We have updated the Map structure to allow scrolling and also allow dynamic map sizes. Below is the change we have made to structs.h:
typedef struct Map
{
    int startX, startY;
    int maxX, maxY;
    int tile[MAX_MAP_Y][MAX_MAP_X];
} Map;
There are 4 new variables in the structure now. The startX and startY variables define the starting horizontal and vertical positions when drawing our map. The mapX and mapY variables are used to limit the amount of scrolling that the map can do. This stops unused areas of the map from being displayed on the screen. The map loading code in map.c has changed to make use of these new variables:
void loadMap(char *name)
{
    int x, y;
    FILE *fp;

    fp = fopen(name, "rb");

    /* If we can't open the map then exit */

    if (fp == NULL)
    {
        printf("Failed to open map %s\n", name);

        exit(1);
    }

    /* Read the data from the file into the map */

    for (y=0;y<MAX_MAP_Y;y++)
    {
        for (x=0;x<MAX_MAP_X;x++)
        {
            fscanf(fp, "%d", &map.tile[y][x]);
        }
    }
    
    /* Set the start coordinates */
    
    map.startX = map.startY = 0;
    
    /* Set the maximum scroll position of the map */
    
    map.maxX = MAX_MAP_X * TILE_SIZE;
    map.maxY = MAX_MAP_Y * TILE_SIZE;

    /* Close the file afterwards */

    fclose(fp);
}
We set the startX and startY values of the map to 0. This will start drawing the map from the top left corner. We also set the maxX and maxY values to the maximum allowed size of the map. Later tutorials will look at setting this value more dynamically. We have also added another function to this file:
void doMap()
{
    if (input.left == 1)
    {
        map.startX -= SCROLL_SPEED;
        
        if (map.startX < 0)
        {
            map.startX = 0;
        }
    }
    
    else if (input.right == 1)
    {
        map.startX += SCROLL_SPEED;
        
        if (map.startX + SCREEN_WIDTH >= map.maxX)
        {
            map.startX = map.maxX - SCREEN_WIDTH;
        }
    }
    
    if (input.up == 1)
    {
        map.startY -= SCROLL_SPEED;
        
        if (map.startY < 0)
        {
            map.startY = 0;
        }
    }
    
    else if (input.down == 1)
    {
        map.startY += SCROLL_SPEED;
        
        if (map.startY + SCREEN_HEIGHT >= map.maxY)
        {
            map.startY = map.maxY - SCREEN_HEIGHT;
        }
    }
}
doMap processes the input passed to it, much in the same way that we processed player movement in previous tutorials. As always, we do not allow the startX and startY values to be less than 0. The maximum values are slightly more complicated though. We must not allow the screen to scroll past the maxX value, but we must also take into account the screen's width when checking this. So, if the startX plus the screen's width is greater than or equal to the maxX, we set the startX to maxX and subtract the screen's width from it. We do the same for the vertical movement too. The final function is a revised version of the map drawing function:
void drawMap()
{
    int x, y, mapX, x1, x2, mapY, y1, y2;

    mapX = map.startX / TILE_SIZE;
    x1 = (map.startX % TILE_SIZE) * -1;
    x2 = x1 + SCREEN_WIDTH + (x1 == 0 ? 0 : TILE_SIZE);
    
    mapY = map.startY / TILE_SIZE;
    y1 = (map.startY % TILE_SIZE) * -1;
    y2 = y1 + SCREEN_HEIGHT + (y1 == 0 ? 0 : TILE_SIZE);
    
    /* Draw the background */
    
    drawImage(backgroundImage, 0, 0);

    /* Draw the map starting at the startX and startY */
    
    for (y=y1;y<y2;y+=TILE_SIZE)
    {
        mapX = map.startX / TILE_SIZE;
        
        for (x=x1;x<x2;x+=TILE_SIZE)
        {
            if (map.tile[mapY][mapX] != 0)
            {
                drawImage(brickImage, x, y);
            }
            
            mapX++;
        }
        
        mapY++;
    }
}
When drawing a scrolled map, we must be mindful to draw tiles that are only partially on the screen. If we don't do this then tiles will pop in and out of existance when scrolling the map. We first get the starting horizontal index by dividing the startX by the TILE_SIZE. This will give us the tile to start with, including any tiles that are to be partially drawn. Next, we set the starting x coordinate, x1, to the remainder of the startX divided by the TILE_SIZE. This will require slightly more explaination:
mapX = map.startX / TILE_SIZE;
Suppose the startX is 24. This will make the mapX 0 (since it is an int and therefore we get no decimal part). This means that we start at index 0 for the horizontal drawing. However, we also need to know where on the screen to start drawing the tiles.
x1 = (map.startX % TILE_SIZE) * -1;
The remainder will be 24, but we do not want to start drawing at 24 pixels. Since we have moved 24 pixels to the right, tile 0 must be 24 pixels off the left hand side of the screen, so we will set the value to negative to achieve this. Finally, we need to know where to stop drawing:
x2 = x1 + SCREEN_WIDTH + (x1 == 0 ? 0 : TILE_SIZE);
We take the starting coordinate and add on the screen width. We also have to bear in mind though that we may have started drawing off screen. If we did then we need to draw an extra tile otherwise we will have a few blank pixels at the end of the screen. The easiest way to check this is to test if our starting value is 0 and, if it was not, then we add on an extra tile at the end. We apply the same logic to the vertical drawing. Finally we, draw the blocks to the screen:
for (y=y1;y<y2;y+=TILE_SIZE)
{
    mapX = map.startX / TILE_SIZE;
    
    for (x=x1;x<x2;x+=TILE_SIZE)
    {
        if (map.tile[mapY][mapX] != 0)
        {
            drawImage(brickImage, x, y);
        }
        
        mapX++;
    }
    
    mapY++;
}
The loop is simple enough, we start at y1 and loop through to y2. mapY and mapX are our tile indexes. At the start of the outer loop, we reset mapX because we will increment it in our inner loop. In our inner loop, we test the value of the tile at the current index and, if it is not 0, we draw the tile to the coordinates in our loop. Notice that we only want to draw what's going to be displayed on the screen. Drawing the entire map every single frame, particularly if the map is very big will waste a lot of CPU and slow down a game. The remaining files and functions have been covered in the basic tutorials so we will not look at them.

 Conclusion

Implementing scrolling does require a small amount of care to ensure that it is done correctly. The map is in the same format as before so you can again edit the file to change the map layout. In the next tutorial we will create a map editor to assist with the creation of maps.

 Downloads

Source Code - tutorial12.tar.gz

Intermediate Game Tutorial #1 - Displaying a Tile Based Map

Intermediate Tutorials
Intermediate Game Tutorial #1 - Displaying a Tile Based Map

A tile based map
 Introduction

We will now look at platform games and the use of tiles to display maps.
Compile and run tutorial11. The program will read the map data file and display the map on the screen. Closing the window or pressing Escape will exit the program.

 An indepth look

When creating a tile based game, we break the screen up into a grid. In this tutorial we create a tile size of 32x32 pixels giving us 20 tiles horizontally and 15 tiles vertically. We will look at structs.h to begin with:
typedef struct Map
{
    int tile[MAX_MAP_Y][MAX_MAP_X];
} Map;
We define our Map structure which contains an multidimensional array of ints. The maximum size of this array is defined by MAX_MAP_Y and MAX_MAP_X which are defined in defs.h. We have made a minor change to init.c. The following command
SDL_ShowCursor(SDL_DISABLE);
will hide the mouse cursor when it is moved inside the SDL window. In main.c, we load up the brick image, a background image and read the map data as follows:
/* Load the brick image */

brickImage = loadImage("gfx/brick.png");

/* If we get back a NULL image, just exit */

if (brickImage == NULL)
{
    exit(1);
}

/* Load the background image */

backgroundImage = loadImage("gfx/background.png");

/* If we get back a NULL image, just exit */

if (backgroundImage == NULL)
{
    exit(1);
}

loadMap("data/maps/map01.dat");
The loadImage function is our standard image loading function. The loadMap function takes the filename of the map file that we wish to load. We will look at this shortly. Once we have successfully loaded all of our resources we enter the standard main loop and wait for user input. We will now at the map loading functions. map.c contains two functions to deal with loading the map and rendering it to the screen.
void loadMap(char *name)
{
    int x, y;
    FILE *fp;

    fp = fopen(name, "rb");

    /* If we can't open the map then exit */

    if (fp == NULL)
    {
        printf("Failed to open map %s\n", name);

        exit(1);
    }

    /* Read the data from the file into the map */

    for (y=0;y<MAX_MAP_Y;y++)
    {
        for (x=0;x<MAX_MAP_X;x++)
        {
            fscanf(fp, "%d", &map.tile[y][x]);
        }
    }

    /* Close the file afterwards */

    fclose(fp);
}
loadMap takes the filename of the map we wish to load. The data file is a very simple text file containing 0s and 1s to describe the map layout. We will take a brief look at this file:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 0 1 1 0 0 0 0 0 0 0
1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1
A 0 denotes a blank tile and 1 denotes a brick tile. Note that we must include either a space or carriage return between the digits so that scanf treats them as individual numbers and not one large number. Once we have successfully opened the file, we read in the map data one row at a time, one column at time. Once we are done we close the file. This gives us a multidimensional array containing our map data. The second function in the file deals with drawing the map:
void drawMap()
{
    int x, y;
    
    /* Draw the background */
    
    drawImage(backgroundImage, 0, 0);

    /* Draw the map */

    for (y=0;y<MAX_MAP_Y;y++)
    {
        for (x=0;x<MAX_MAP_X;x++)
        {
            if (map.tile[y][x] != 0)
            {
                drawImage(brickImage, x * TILE_SIZE, y * TILE_SIZE);
            }
        }
    }
}
First, we draw the background, just so the screen has some more colour to it. We then loop through the map from top to bottom, from left to right and if the tile at the array index is not 0, then we will draw a brick. Note that we multiply the x and y values up by TILE_SIZE. This is because we only have 20 tiles horizontally and 15 vertically as defined by MAX_MAP_X and MAX_MAP_Y, so we need to scale up the position of the tile according to TILE_SIZE. The remaining files and functions have been covered in the basic tutorials so we will not look at them.

 Conclusion

Displaying a tile based map on the screen does not require a lot of code. Since the map data file is just a text file, it would be worth editing the file and changing some of the 0s and 1s to produce a different map. Ensure though that you have 20 digits on each row and 15 rows in the file to ensure that the file loads up correctly.

 Downloads

Source Code - tutorial11.tar.gz