Skip to content

TileMap

Structure for tile-based background rendering.

Description

TileMap is a compact structure for rendering tile-based backgrounds efficiently. It uses 1bpp sprites as tiles and stores level data as a compact array of tile indices.

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 {
    struct TileMap {
        // ...
    };
}

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 Sprite* tiles

Array of tile sprites.

Type: const Sprite*

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:

static const Sprite TILE_SPRITES[] = {
    EMPTY_TILE,   // Index 0
    WALL_TILE,    // Index 1
    FLOOR_TILE,   // Index 2
    // ... 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