[Tutorial]Building a Simple TileMap Game, Part 7 of 9

Doors and Inventory Items


What is a game without doors, keys and other items to pick up along the way?

In this section of the tutorial, we will change the world to include a castle with a locked outer door. To escape, you need to collect the key and before attempting to unlock the door and escape, as shown in the picture below.

Download the code in the repo https://github.com/filmote/Tilemap_5 and run it. You will see a world similar to that above and will notice that there is both a key and a door in view. Attempt to go through the door without a key and you will be blocked – attempt it a second time after picking up the key and you will be able to pass through it to the green grass beyond.




I will present one method for rendering and tracking items but there are a number of other approaches – each with their own pros and cons. The method I will describe uses the same data map approach we have used in the tutorial so far but we update elements as the player interacts with them.

As you can see from the image above, our new world includes some scenic items that do not change, such as the brick and carpet, and some new items we can interact with including the door and key. These new tiles have been added to the TileType enumeration and are used when defining the initial world map.

enum TileType {
    Water = 0,
    Green = 1,
    Tree = 2,
    Grass = 3,
    Brick = 4,
    Door_Closed = 5,
    Door_Open = 6,
    Key = 7,
    Carpet = 8,
};

However, when the player touches the key we will want to pick it up and from that point onwards show the carpet (the brown / grey) tile. To do this, we need to be able to modify the world map data .. but if you recall, it is marked as const thus preventing update. Marking an array as `const` does ensure they are stored in flash - rather than RAM – which is important for large arrays.

The simplest fix would be to change this would be to remove the const declaration but in the next section I want to introduce the ability to have multiple worlds, so an alternative approach is to copy the world from flash into an array in memory. This performed by the function below.

A new array worldMap is declared as global variable with an initial size of the width multiplied by the height divided by two. The ‘divide by two’ clause has been added as the world data is actually packed into the array using ‘two bytes per tile’ hence the array is only half of the normal size. Once declared, the function simply copies the data from the flash array to the RAM array using a simple loop.

uint8_t worldMap[mapTileWidth * mapTileHeight / 2];

void initWorld() {


    // Populate the world data ..
    
    for (uint8_t i = 0; i < mapTileWidth * mapTileHeight / 2; i++) {
        
        worldMap[i] = Data::worldMap[i];

    }
  
}

Now that we have our world information in RAM we can update it as needed. Let’s look at the code that detects whether a player has touched a key or not. Firstly, I have added a simple property to the Player structure to record whether we have the key or not.

struct Player : Entity {

    bool hasKey = false;

};

To update a tile, we need to know its index in the world array of the tiles. Our world is 16 tile wide x 16 tiles high resulting in 256 tiles - the index of the top, left-hand tile is 0, the top right-had side is 15, and the bottom right is 255.

image

A simple helper function allows us to calculate the tile based on a player’s x and y position as shown below. The third parameter, width, is the width of the world measured in tiles which for our world is 16.

uint16_t getTileIndex(int32_t x, int32_t y, uint16_t width) {

    uint32_t tx = x / width;
    uint32_t ty = y / width;

    return (ty * width + tx);

}

The checkMovement() function presented earlier has been modified to calculate the tile type and index for the two tiles they could possibly touch depending on the direction they are moving. After calculating the tile types and indices, the function then tests to see if the player has touched a key. If so, the hasKey property of the player is set to true and the tile set back to TileType::Carpet thus preventing the key from being shown again.

bool checkMovement(Entity &entity, int16_t x, int16_t y, Direction direction) {

    int8_t tileId1 = 0;
    int8_t tileId2 = 0;

    uint16_t tile1Index;
    uint16_t tile2Index;

    switch (direction) {
        
        case Direction::Left:

            tileId1 = tilemap.GetTileId(x, y, 16);
            tileId2 = tilemap.GetTileId(x, y + entity.height, 16);
            
            tile1Index = getTileIndex(x, y, 16);
            tile2Index = getTileIndex(x, y + entity.height, 16);

            break;
        
        case Direction::Right:
            …
            break;

        …
            
    }


    // Handle player actions
    
    if (&entity == &player) {


        // If we have found a key, pick it up ..
        
        if (tileId1 == TileType::Key) {

            player.hasKey = true;
            updateTileType(tile1Index, TileType::Carpet);

        }

    …
        
} 

The updateTileType() function, as its name suggests, update the tile type in the world array in RAM. As mentioned earlier, the data in this array is packed ‘two tiles to a byte’ and this function test to see if the tile being update is odd or even, using the % (modulus) function, and then updating the left or right portion of the byte appropriately.

void updateTileType(uint16_t tileIndex, TileType tileType) {

 if (tileIndex % 2 == 0) {
  worldMap[tileIndex / 2] = (worldMap[tileIndex / 2] & 0x0f) | (tileType << 4);  
 }   
 else {
  worldMap[tileIndex / 2] = (worldMap[tileIndex / 2] & 0xf0) | tileType;
 }
            
}

The logic to handle the opening of the door to the `checkMovement()` function is almost as easy as the key logic with a simple check added to ensure the player actually has the key.

bool checkMovement(Entity &entity, int16_t x, int16_t y, Direction dir) {

    …

    // Handle player actions
    
    if (&entity == &player) {
        …

        // Open the door?  Only if we have a key ..

        if (player.hasKey) {
            
            if (tileId1 == TileType::Door_Closed) {
    
                player.hasKey = false;
                updateTileType(tile1Index, TileType::Door_Open);
    
            }
            …
            
        }

    }

    …
        
}

We can now pick up inventory items, open doors and escape the castle. But what is outside?




<< Prev | Next >>

Part 1 of 9: Building a Simple Tilemap Game
Part 2 of 9: Moving a Player Around the World
Part 3 of 9: Rendering the Viewport and Player
Part 4 of 9: Detecting Obstacles
Part 5 of 9: Adding Enemies
Part 6 of 9: Using FemtoIDE and Piskel
Part 7 of 9: Doors and Inventory Items
Part 8 of 9: Multiple Worlds
Part 9 of 9: Sixteen Tiles Only?

2 Likes