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