Skip to content

Tilemaps

Tilemaps allow you to build levels efficiently by reusing small tile sprites. This guide covers creating tilemaps, rendering them, and using them with scrolling cameras.

What are Tilemaps?

A tilemap is a 2D grid where each cell references a tile sprite. Instead of placing individual sprites, you define which tile appears at each grid position.

Advantages: - Memory efficient: Reuse tile sprites many times - Easy level design: Edit level data, not code - Fast rendering: Optimized tilemap drawing - Large levels: Create levels bigger than screen

Creating a Tilemap

1. Define Tiles

First, create the tile sprites you'll reuse:

#include <graphics/Renderer.h>

// Empty tile (transparent)
static const uint16_t TILE_EMPTY_BITS[] = {
    0x0000, 0x0000, 0x0000, 0x0000,
    0x0000, 0x0000, 0x0000, 0x0000
};

// Ground tile (solid)
static const uint16_t TILE_GROUND_BITS[] = {
    0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
    0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
};

// Wall tile
static const uint16_t TILE_WALL_BITS[] = {
    0xFF00, 0xFF00, 0xFF00, 0xFF00,
    0xFF00, 0xFF00, 0xFF00, 0xFF00
};

// Create tile sprites (8x8 tiles)
static const pixelroot32::graphics::Sprite TILES[] = {
    { TILE_EMPTY_BITS, 8, 8 },  // Index 0: Empty
    { TILE_GROUND_BITS, 8, 8 }, // Index 1: Ground
    { TILE_WALL_BITS, 8, 8 }    // Index 2: Wall
};

2. Create Tile Index Array

Define which tile appears at each position:

// Tilemap dimensions (30 tiles wide, 20 tiles tall)
static const int TILEMAP_WIDTH = 30;
static const int TILEMAP_HEIGHT = 20;

// Array of tile indices (each byte is a tile index)
static uint8_t TILEMAP_INDICES[TILEMAP_WIDTH * TILEMAP_HEIGHT];

// Initialize to empty
void initTilemap() {
    for (int i = 0; i < TILEMAP_WIDTH * TILEMAP_HEIGHT; i++) {
        TILEMAP_INDICES[i] = 0; // Empty
    }

    // Draw ground at bottom
    int groundRow = TILEMAP_HEIGHT - 1;
    for (int x = 0; x < TILEMAP_WIDTH; x++) {
        TILEMAP_INDICES[groundRow * TILEMAP_WIDTH + x] = 1; // Ground tile
    }

    // Add some walls
    TILEMAP_INDICES[5 * TILEMAP_WIDTH + 10] = 2; // Wall at (10, 5)
    TILEMAP_INDICES[5 * TILEMAP_WIDTH + 11] = 2;
    TILEMAP_INDICES[5 * TILEMAP_WIDTH + 12] = 2;
}

3. Create TileMap Structure

#include <graphics/Renderer.h>

static pixelroot32::graphics::TileMap myTileMap = {
    TILEMAP_INDICES,                    // indices array
    TILEMAP_WIDTH,                      // width (in tiles)
    TILEMAP_HEIGHT,                     // height (in tiles)
    TILES,                              // tiles array
    8,                                  // tile width (pixels)
    8,                                  // tile height (pixels)
    sizeof(TILES) / sizeof(pixelroot32::graphics::Sprite) // tile count
};

Rendering Tilemaps

Basic Rendering

void draw(pixelroot32::graphics::Renderer& renderer) override {
    // Draw tilemap at position (0, 0)
    renderer.drawTileMap(
        myTileMap,
        0,                              // x position
        0,                              // y position
        pixelroot32::graphics::Color::White
    );
}

With Camera/Scrolling

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

    // Draw tilemap (camera offset is automatically applied)
    renderer.drawTileMap(
        myTileMap,
        0,                              // World position (0, 0)
        0,
        pixelroot32::graphics::Color::White
    );

    // Draw game objects
    Scene::draw(renderer);
}

Complete Example: Platformer Level

#include <core/Scene.h>
#include <graphics/Renderer.h>
#include <graphics/Camera2D.h>

class PlatformerLevel : public pixelroot32::core::Scene {
private:
    static const int TILE_SIZE = 8;
    static const int TILEMAP_WIDTH = 100;  // 800 pixels wide
    static const int TILEMAP_HEIGHT = 30;   // 240 pixels tall

    // Tile definitions
    static const uint16_t TILE_EMPTY_BITS[] = {
        0x0000, 0x0000, 0x0000, 0x0000,
        0x0000, 0x0000, 0x0000, 0x0000
    };

    static const uint16_t TILE_GROUND_BITS[] = {
        0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
        0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
    };

    static const uint16_t TILE_GRASS_BITS[] = {
        0x0000, 0x0000, 0x0000, 0x0000,
        0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
    };

    static const pixelroot32::graphics::Sprite TILES[] = {
        { TILE_EMPTY_BITS, TILE_SIZE, TILE_SIZE },  // 0: Empty
        { TILE_GROUND_BITS, TILE_SIZE, TILE_SIZE }, // 1: Ground
        { TILE_GRASS_BITS, TILE_SIZE, TILE_SIZE }   // 2: Grass top
    };

    static uint8_t LEVEL_INDICES[TILEMAP_WIDTH * TILEMAP_HEIGHT];

    pixelroot32::graphics::TileMap levelTileMap;
    pixelroot32::graphics::Camera2D camera;

public:
    PlatformerLevel() 
        : camera(240, 240) {
        // Initialize tilemap structure
        levelTileMap = {
            LEVEL_INDICES,
            TILEMAP_WIDTH,
            TILEMAP_HEIGHT,
            TILES,
            TILE_SIZE,
            TILE_SIZE,
            sizeof(TILES) / sizeof(pixelroot32::graphics::Sprite)
        };
    }

    void init() override {
        // Initialize all tiles to empty
        for (int i = 0; i < TILEMAP_WIDTH * TILEMAP_HEIGHT; i++) {
            LEVEL_INDICES[i] = 0;
        }

        // Create ground level
        int groundY = TILEMAP_HEIGHT - 1;
        for (int x = 0; x < TILEMAP_WIDTH; x++) {
            LEVEL_INDICES[groundY * TILEMAP_WIDTH + x] = 1; // Ground
        }

        // Add grass on top of ground
        int grassY = groundY - 1;
        for (int x = 0; x < TILEMAP_WIDTH; x++) {
            LEVEL_INDICES[grassY * TILEMAP_WIDTH + x] = 2; // Grass
        }

        // Add platforms
        // Platform 1: x=10 to x=15, y=20
        for (int x = 10; x < 16; x++) {
            LEVEL_INDICES[20 * TILEMAP_WIDTH + x] = 1; // Ground tile
        }

        // Platform 2: x=30 to x=35, y=15
        for (int x = 30; x < 36; x++) {
            LEVEL_INDICES[15 * TILEMAP_WIDTH + x] = 1;
        }

        // Set camera boundaries
        camera.setBounds(0, TILEMAP_WIDTH * TILE_SIZE - 240);
        camera.setVerticalBounds(0, TILEMAP_HEIGHT * TILE_SIZE - 240);
    }

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

        // Follow player (example)
        // camera.followTarget(player->x, player->y);
    }

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

        // Draw tilemap
        renderer.drawTileMap(
            levelTileMap,
            0, 0,
            pixelroot32::graphics::Color::White
        );

        // Draw game objects
        Scene::draw(renderer);
    }
};

Tilemap with Scroll

For scrolling levels, combine tilemaps with cameras:

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

    // Draw tilemap (automatically scrolled by camera)
    renderer.drawTileMap(
        levelTileMap,
        0, 0,
        pixelroot32::graphics::Color::White
    );

    // Draw entities (also scrolled)
    Scene::draw(renderer);
}

Optimizing Tilemap Rendering

Viewport Culling

Only draw visible tiles:

void drawTileMapOptimized(
    pixelroot32::graphics::Renderer& renderer,
    const pixelroot32::graphics::TileMap& tileMap,
    int offsetX, int offsetY,
    pixelroot32::graphics::Color color
) {
    int screenWidth = renderer.getWidth();
    int screenHeight = renderer.getHeight();

    // Calculate which tiles are visible
    int startTileX = (offsetX < 0) ? (-offsetX / tileMap.tileWidth) : 0;
    int startTileY = (offsetY < 0) ? (-offsetY / tileMap.tileHeight) : 0;
    int endTileX = startTileX + (screenWidth / tileMap.tileWidth) + 1;
    int endTileY = startTileY + (screenHeight / tileMap.tileHeight) + 1;

    // Clamp to tilemap bounds
    if (startTileX < 0) startTileX = 0;
    if (startTileY < 0) startTileY = 0;
    if (endTileX > tileMap.width) endTileX = tileMap.width;
    if (endTileY > tileMap.height) endTileY = tileMap.height;

    // Draw only visible tiles
    for (int ty = startTileY; ty < endTileY; ty++) {
        for (int tx = startTileX; tx < endTileX; tx++) {
            uint8_t tileIndex = tileMap.indices[ty * tileMap.width + tx];
            if (tileIndex < tileMap.tileCount) {
                int x = tx * tileMap.tileWidth + offsetX;
                int y = ty * tileMap.tileHeight + offsetY;
                renderer.drawSprite(
                    tileMap.tiles[tileIndex],
                    x, y,
                    color,
                    false
                );
            }
        }
    }
}

Note: The built-in drawTileMap() already performs viewport culling, so you typically don't need to implement this yourself.

Best Practices

Tile Design

  • Keep tiles small: 8x8 or 16x16 pixels work best
  • Reuse tiles: Design tiles that can be used in multiple ways
  • Consistent style: All tiles should match visually
  • Limit tile count: Too many unique tiles uses more memory

Level Design

  • Use indices efficiently: 0 = empty, 1+ = different tiles
  • Plan layout: Design level on paper/grid first
  • Test on hardware: Large tilemaps may impact performance
  • Optimize data: Use compact level data format

Performance

  • Limit tilemap size: Very large tilemaps can be slow
  • Use appropriate tile size: Smaller tiles = more tiles to draw
  • Combine with culling: Only draw visible area
  • Test scrolling: Ensure smooth scrolling performance

Common Patterns

Level Data in Code

// Define level as 2D array (easier to read)
static const uint8_t LEVEL_DATA[][TILEMAP_WIDTH] = {
    {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,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,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,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}, // Ground
};

// Copy to tilemap indices
void loadLevel() {
    for (int y = 0; y < TILEMAP_HEIGHT; y++) {
        for (int x = 0; x < TILEMAP_WIDTH; x++) {
            TILEMAP_INDICES[y * TILEMAP_WIDTH + x] = LEVEL_DATA[y][x];
        }
    }
}

Collision Detection with Tilemaps

bool isTileSolid(int tileX, int tileY) {
    if (tileX < 0 || tileX >= TILEMAP_WIDTH ||
        tileY < 0 || tileY >= TILEMAP_HEIGHT) {
        return true; // Out of bounds = solid
    }

    uint8_t tileIndex = TILEMAP_INDICES[tileY * TILEMAP_WIDTH + tileX];
    return tileIndex != 0; // 0 = empty, others = solid
}

bool checkCollision(float x, float y, int width, int height) {
    // Convert world position to tile coordinates
    int tileX1 = static_cast<int>(x) / TILE_SIZE;
    int tileY1 = static_cast<int>(y) / TILE_SIZE;
    int tileX2 = static_cast<int>(x + width) / TILE_SIZE;
    int tileY2 = static_cast<int>(y + height) / TILE_SIZE;

    // Check all tiles actor overlaps
    for (int ty = tileY1; ty <= tileY2; ty++) {
        for (int tx = tileX1; tx <= tileX2; tx++) {
            if (isTileSolid(tx, ty)) {
                return true; // Collision!
            }
        }
    }

    return false; // No collision
}

Troubleshooting

Tiles Not Appearing

  • Verify tile indices are correct (0 = first tile, 1 = second, etc.)
  • Check tilemap dimensions match indices array size
  • Ensure tiles array has enough entries
  • Verify tile size matches sprite size

Performance Issues

  • Reduce tilemap size
  • Use smaller tiles
  • Limit number of unique tiles
  • Test viewport culling

Scrolling Problems

  • Ensure camera is applied before drawing tilemap
  • Check tilemap position matches camera offset
  • Verify tilemap boundaries are correct
  • Test with simple tilemap first

Next Steps

Now that you understand tilemaps, learn about: - Particles and Effects - Add visual effects - Cameras and Scrolling - Combine with scrolling - Performance Optimization - Optimize rendering


See also: - API Reference - TileMap - API Reference - Renderer - Manual - Cameras and Scrolling