Skip to content

TileMap

Generic structure for tile-based background rendering.

Description

TileMapGeneric<T> is a template structure for rendering tile-based backgrounds efficiently. It supports multiple bit-depths (1bpp, 2bpp, 4bpp) by using the appropriate sprite type for tiles.

Tilemaps are ideal for large backgrounds, levels, and static environments. They support viewport culling (only visible tiles are drawn) for optimal performance.

Namespace

namespace pixelroot32::graphics {
    template<typename T>
    struct TileMapGeneric {
        // ...
    };

    using TileMap = TileMapGeneric<Sprite>;
    using TileMap2bpp = TileMapGeneric<Sprite2bpp>;
    using TileMap4bpp = TileMapGeneric<Sprite4bpp>;
}

Template Parameters

T

The sprite type used for tiles.

Supported types: - Sprite (1bpp) - Sprite2bpp (2bpp) - Sprite4bpp (4bpp)

Structure

uint8_t* indices

Array of tile indices mapping to tile sprites.

Type: uint8_t*

Access: Read-write

Notes: - Array size = width * height - Each value is an index into the tiles array - 0 = first tile, 1 = second tile, etc. - Should be stored in flash (const) for best performance

Example:

// 16x16 tilemap (256 tiles)
static const uint8_t LEVEL_INDICES[] = {
    0, 0, 0, 0, 1, 1, 1, 1, // Row 0
    0, 2, 2, 2, 2, 2, 2, 0, // Row 1
    // ... more rows
};

uint8_t width

Width of the tilemap in tiles.

Type: uint8_t

Access: Read-write

Example:

width = 16;  // 16 tiles wide

uint8_t height

Height of the tilemap in tiles.

Type: uint8_t

Access: Read-write

Example:

height = 16;  // 16 tiles tall

const T* tiles

Array of tile sprites of type T.

Type: const T*

Access: Read-only

Notes: - Array of sprite pointers, one per unique tile - Indices reference this array - All tiles should be the same size - Should be stored in flash (const) for best performance

Example (1bpp):

static const Sprite TILE_SPRITES[] = {
    EMPTY_TILE,   // Index 0
    WALL_TILE,    // Index 1
    FLOOR_TILE,   // Index 2
    // ... more tiles
};

Example (2bpp):

static const Sprite2bpp TILE_SPRITES_2BPP[] = {
    TILE_GRASS,   // Index 0
    TILE_DIRT,    // Index 1
    // ... more tiles
};

uint8_t tileWidth

Width of each tile in pixels.

Type: uint8_t

Access: Read-write

Notes: - All tiles must have the same width - Common values: 8, 16 pixels - Should match sprite width

Example:

tileWidth = 8;  // 8x8 tiles

uint8_t tileHeight

Height of each tile in pixels.

Type: uint8_t

Access: Read-write

Notes: - All tiles must have the same height - Common values: 8, 16 pixels - Should match sprite height

Example:

tileHeight = 8;  // 8x8 tiles

uint16_t tileCount

Number of unique tiles in the tiles array.

Type: uint16_t

Access: Read-write

Notes: - Must match the size of the tiles array - Indices must be < tileCount

Example:

tileCount = 16;  // 16 unique tiles

Creating Tilemaps

Step 1: Create Tile Sprites

// Empty tile (index 0)
static const uint16_t EMPTY_DATA[] = {
    0b00000000,
    0b00000000,
    0b00000000,
    0b00000000,
    0b00000000,
    0b00000000,
    0b00000000,
    0b00000000
};

// Wall tile (index 1)
static const uint16_t WALL_DATA[] = {
    0b11111111,
    0b10000001,
    0b10000001,
    0b10000001,
    0b10000001,
    0b10000001,
    0b10000001,
    0b11111111
};

// Floor tile (index 2)
static const uint16_t FLOOR_DATA[] = {
    0b00000000,
    0b01111110,
    0b01111110,
    0b01111110,
    0b01111110,
    0b01111110,
    0b01111110,
    0b00000000
};

static const Sprite TILE_SPRITES[] = {
    {EMPTY_DATA, 8, 8},  // Index 0
    {WALL_DATA, 8, 8},   // Index 1
    {FLOOR_DATA, 8, 8}   // Index 2
};

Step 2: Create Index Array

// 16x16 level (256 tiles)
// 0 = empty, 1 = wall, 2 = floor
static const uint8_t LEVEL_INDICES[] = {
    // Row 0: Top wall
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    // Row 1: Walls on sides
    1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1,
    // Row 2
    1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1,
    // ... more rows
    // Row 15: Bottom wall
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
};

Step 3: Create TileMap Structure

static const TileMap LEVEL_MAP = {
    const_cast<uint8_t*>(LEVEL_INDICES),  // indices (non-const for struct)
    16,        // width in tiles
    16,        // height in tiles
    TILE_SPRITES, // tile sprites array
    8,         // tile width
    8,         // tile height
    3          // tile count
};

Rendering Tilemaps

Use Renderer::drawTileMap():

void draw(pixelroot32::graphics::Renderer& renderer) override {
    // Draw tilemap at origin (0, 0)
    renderer.drawTileMap(LEVEL_MAP, 0, 0, Color::White);

    // With camera offset
    camera.apply(renderer);
    renderer.drawTileMap(LEVEL_MAP, 0, 0, Color::White);
}

Viewport Culling

Tilemaps automatically cull tiles outside the viewport:

  • Only visible tiles are drawn
  • Very efficient for large levels
  • Works with camera scrolling

Example:

// Large level (256x256 tiles)
// Only tiles visible on screen are drawn
camera.apply(renderer);
renderer.drawTileMap(LARGE_LEVEL_MAP, 0, 0, Color::White);

Collision Detection with Tilemaps

Check tile at world position:

bool isSolidTile(int worldX, int worldY, const TileMap& map) {
    int tileX = worldX / map.tileWidth;
    int tileY = worldY / map.tileHeight;

    if (tileX < 0 || tileX >= map.width || 
        tileY < 0 || tileY >= map.height) {
        return true;  // Outside map = solid
    }

    int index = tileY * map.width + tileX;
    uint8_t tileIndex = map.indices[index];

    // Check if tile is solid (e.g., wall = index 1)
    return (tileIndex == 1);
}

Usage Example

#include "graphics/TileMap.h"
#include "graphics/Renderer.h"

class LevelScene : public pixelroot32::core::Scene {
private:
    pixelroot32::graphics::TileMap levelMap;
    pixelroot32::graphics::Camera2D camera;

public:
    void init() override {
        // Level map is already defined (see above)
        // Create camera
        auto& renderer = engine.getRenderer();
        camera = pixelroot32::graphics::Camera2D(
            renderer.getWidth(),
            renderer.getHeight()
        );

        // Set level boundaries
        int levelWidth = levelMap.width * levelMap.tileWidth;
        int levelHeight = levelMap.height * levelMap.tileHeight;
        camera.setBounds(0.0f, levelWidth - renderer.getWidth());
        camera.setVerticalBounds(0.0f, levelHeight - renderer.getHeight());
    }

    void update(unsigned long deltaTime) override {
        Scene::update(deltaTime);

        // Camera follows player
        if (player) {
            camera.followTarget(player->x, player->y);
        }
    }

    void draw(pixelroot32::graphics::Renderer& renderer) override {
        // Apply camera
        camera.apply(renderer);

        // Draw tilemap (viewport culling automatic)
        renderer.drawTileMap(levelMap, 0, 0, Color::White);

        // Draw entities
        Scene::draw(renderer);

        // Reset for UI
        renderer.setDisplayOffset(0, 0);
    }

    bool checkTileCollision(float x, float y) {
        int tileX = static_cast<int>(x) / levelMap.tileWidth;
        int tileY = static_cast<int>(y) / levelMap.tileHeight;

        if (tileX < 0 || tileX >= levelMap.width || 
            tileY < 0 || tileY >= levelMap.height) {
            return true;  // Outside = solid
        }

        int index = tileY * levelMap.width + tileX;
        uint8_t tile = levelMap.indices[index];
        return (tile == 1);  // Wall tile
    }
};

Performance Considerations

  • Viewport culling: Only visible tiles are drawn (automatic)
  • Tile reuse: Reuse tile sprites across the map
  • Index storage: Compact uint8_t indices (1 byte per tile)
  • Memory: Store indices and tiles in flash (const) for best performance
  • Tile size: Smaller tiles = more tiles to draw, but more detail

ESP32 Considerations

  • Memory: Store tilemap data in flash, not RAM
  • Map size: Large maps use more flash memory
  • Tile count: Limit unique tiles to save memory
  • Culling: Viewport culling is essential for large levels

See Also