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

Detecting Obstacles


Download the code in the repo https://github.com/filmote/Tilemap_1 if you haven’t already.

If you run this code, you will notice that you can only walk on the plain green tiles. This is done by capturing the current location of the player before any move and then detecting the tile the player may have just moved on to. If it is not a green tile, the player is moved back to their original position. As this is all done prior to rendering the screen, the two movements are not visible.

This code is shown below.

    // Capture the old coordinates in case we need to move back ..
        
    int16_t oldX = x;
    int16_t oldY = y;

    … handle player movements.
        

    // Check the map tile under the player.
        
    uint8_t tileId = tilemap.GetTileId(x + 6, y + 7, 16);


    // If the tile is not green, do not move.
        
    if (tileId != TileType::Green) {
            
        x = oldX;
        y = oldY;

    }

Although this code works well, it uses the centre of the player to detect the tile underneath which allows the player to move half-way onto a tree or water before being stopped. To prevent this, we need to check the tile below the left side of the player if we are moving left, the right-side of the player if we are moving right and so on. In a later section of this tutorial I want to add enemies to the world and want to share the movement logic between the player and the enemy. Externalising the logic into a function will support both requirements.

Download the code in the repo GitHub - filmote/Tilemap_2 and run it. You will notice that the player cannot ‘half’ move onto a tree or the water.

The code below shows how to detect whether a player movement is possible before the movement is made. But before we look at the code, you may have noticed that I introduced two new constructs into the code – an enum and a structure. For the sake of clarity, I have also moved all of the constant values into a namespace, brilliantly named Constants, so that you can see in the code exactly which values these are.

Previously we used an enum to define the tile types which made our code more readable. Using this same logic, I have created an enum that defines the four direction of movements. We will use these later when testing whether a movement is ‘legal’.


  enum Direction {
      Up,
      Down,
      Left,
      Right
  }

The Entity structure is used to capture our player and viewport offset details and allows us to pass them as a whole to our checkMovement() function. In addition to the offsets, the structure also contains some constant information describing the player size and can be extended later to capture inventory or health details.


struct Entity {
    
    int16_t x;
    int16_t y;

    const uint8_t width = 12;
    const uint8_t height = 15;

};

Using the enum and struct above, we can now construct a function that checks whether a move is valid or not. This is partially repeated below. The first thing you will notice is the parameters that are passed – a reference to the player entity, the x and y position the player is moving to and the direction they are moving. I could easily have not passed the reference to the entity as it is already a global variable but in the next section you will see that I can pass an enemy reference to the same function!

Depending on the direction we nominate, the tiles that the player will move onto are calculated. As our player could be straddling two world tiles, they are both retrieved for evaluation. When moving left or right, the tile at the nominated x and y position and the tile immediately below this must be considered. If you are moving up or down, the tile immediately to the right must be considered.

Once the tiles are determined, they are checked to ensure the player can move and, if so, the function returns a true otherwise it returns a false. This logic could be extended to include other tiles - doors, portals etc – in a multi-level game.

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

    int8_t tileId1 = 0;
    int8_t tileId2 = 0;

    switch (dir) {
        
        case Direction::Left:
            tileId1 = tilemap.GetTileId(x, y, 16);
            tileId2 = tilemap.GetTileId(x, y + entity.height, 16);
            break;
        
        case Direction::Right:
            tileId1 = tilemap.GetTileId(x + entity.width, y, 16);
            tileId2 = tilemap.GetTileId(x + entity.width, y +entity.height,16);
            break;

        case Direction::Up:
            …
        
        case Direction::Down:
            …
            
    }
    
    // If either tile is not green, do not move.
    
    if (tileId1 != TileType::Green || tileId2 != TileType::Green) {
        return false;
        
    }

    return true;
        
}

Now that we have a function which tells us whether a move is valid or not, we can condition the player movement logic shown previously.

    if (PC::buttons.pressed(BTN_LEFT) || PC::buttons.repeat(BTN_LEFT, 1))    { 

        // Can we move to the left?
        
        if (player.xOffset < 0 && 
            checkMovement(player, player.x - 1, player.y, Direction::Left)) {
            
            player.x--;
    
        
        }

    }



<< 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?

3 Likes