Skip to content

Instantly share code, notes, and snippets.

@Kadajett
Created October 25, 2025 02:58
Show Gist options
  • Select an option

  • Save Kadajett/19d1e4cd1f6a0a8ec473cbec5fd15ea9 to your computer and use it in GitHub Desktop.

Select an option

Save Kadajett/19d1e4cd1f6a0a8ec473cbec5fd15ea9 to your computer and use it in GitHub Desktop.
How to implement the tiled enging in Rust

Using Tiled Map Editor with Raylib in Rust

A complete guide to integrating Tiled Map Editor with raylib-rs for 2D game development in Rust.

Table of Contents

Overview

This guide shows you how to load and render Tiled maps in raylib using Rust. The tiled crate (v0.12+) provides excellent support for loading TMX files, and with some understanding of the API, you can render beautiful tile-based games.

Why This Combination?

  • Tiled: Industry-standard, free tile map editor
  • raylib: Simple, powerful game library with minimal dependencies
  • Rust: Memory safety and performance

Prerequisites

  • Rust (2021 edition or later)
  • Basic understanding of raylib and Rust
  • Tiled Map Editor installed

Setup

1. Add Dependencies

Add to your Cargo.toml:

[dependencies]
raylib = "5.5"  # or latest version
tiled = "0.12"  # critical: 0.12+ has different API than earlier versions

2. Create a Map in Tiled

  1. Open Tiled and create a new map
  2. Set your tile size (e.g., 16x16, 32x32)
  3. Add a tileset (Image → New Tileset)
  4. Draw your map
  5. Save as .tmx format

Important: Place both your .tmx file and tileset images in your assets/ folder.

Basic Implementation

Step 1: Create the TiledMap Struct

use raylib::prelude::*;
use std::collections::HashMap;
use tiled::{Loader, Map};

pub struct TiledMap {
    map: Map,
    tilesets: HashMap<usize, Texture2D>,  // Map tileset index to texture
}

Step 2: Load the Map

impl TiledMap {
    pub fn load(
        rl: &mut RaylibHandle,
        thread: &RaylibThread,
        path: &str,
    ) -> Result<Self, tiled::Error> {
        let mut loader = Loader::new();
        let map = loader.load_tmx_map(path)?;

        // Load tileset textures indexed by position
        let mut tilesets = HashMap::new();

        for (index, tileset) in map.tilesets().iter().enumerate() {
            if let Some(image) = &tileset.image {
                let texture_path = format!("assets/{}",
                    image.source.to_string_lossy());

                match rl.load_texture(thread, &texture_path) {
                    Ok(texture) => {
                        tilesets.insert(index, texture);
                    }
                    Err(e) => {
                        eprintln!("Failed to load tileset: {}", e);
                    }
                }
            }
        }

        Ok(Self { map, tilesets })
    }
}

Step 3: Render the Map

impl TiledMap {
    pub fn draw(&self, d: &mut RaylibDrawHandle) {
        let tile_width = self.map.tile_width as f32;
        let tile_height = self.map.tile_height as f32;

        for layer in self.map.layers() {
            // Skip invisible layers
            if !layer.visible {
                continue;
            }

            // Only handle tile layers
            let tile_layer = match layer.layer_type() {
                tiled::LayerType::Tiles(tile_layer) => tile_layer,
                _ => continue,
            };

            // Handle finite tile layers
            match tile_layer {
                tiled::TileLayer::Finite(finite_layer) => {
                    let width = finite_layer.width();
                    let height = finite_layer.height();

                    for y in 0..height {
                        for x in 0..width {
                            if let Some(layer_tile) = finite_layer.get_tile(x as i32, y as i32) {
                                self.draw_tile(d, layer_tile, x, y, tile_width, tile_height);
                            }
                        }
                    }
                }
                tiled::TileLayer::Infinite(_) => {
                    // Infinite layers not covered in this guide
                    eprintln!("Infinite layers not supported");
                }
            }
        }
    }

    fn draw_tile(
        &self,
        d: &mut RaylibDrawHandle,
        layer_tile: tiled::LayerTile,
        x: u32,
        y: u32,
        tile_width: f32,
        tile_height: f32,
    ) {
        let tile_id = layer_tile.id();

        // Skip empty tiles
        if tile_id == 0 {
            return;
        }

        // Get tileset and texture
        let tileset = layer_tile.get_tileset();
        let tileset_index = layer_tile.tileset_index();

        let texture = match self.tilesets.get(&tileset_index) {
            Some(tex) => tex,
            None => return,
        };

        // Calculate source rectangle in tileset
        let columns = tileset.columns;
        let spacing = tileset.spacing as f32;
        let margin = tileset.margin as f32;

        let tsx = margin + (tile_id % columns) as f32 * (tile_width + spacing);
        let tsy = margin + (tile_id / columns) as f32 * (tile_height + spacing);

        let mut source = Rectangle::new(tsx, tsy, tile_width, tile_height);

        // Handle tile flipping
        if layer_tile.flip_h {
            source.width = -source.width;
        }
        if layer_tile.flip_v {
            source.height = -source.height;
        }

        // Draw to screen
        let dest = Rectangle::new(
            x as f32 * tile_width,
            y as f32 * tile_height,
            tile_width,
            tile_height,
        );

        d.draw_texture_pro(
            texture,
            source,
            dest,
            Vector2::zero(),
            0.0,
            Color::WHITE,
        );
    }
}

Step 4: Use in Your Game

fn main() {
    let (mut rl, thread) = raylib::init()
        .size(800, 600)
        .title("Tiled + Raylib")
        .build();

    let map = TiledMap::load(&mut rl, &thread, "assets/map.tmx")
        .expect("Failed to load map");

    while !rl.window_should_close() {
        let mut d = rl.begin_drawing(&thread);
        d.clear_background(Color::BLACK);

        map.draw(&mut d);
    }
}

Understanding the Tiled API

Key Concepts in tiled 0.12+

The tiled crate v0.12+ has a different API than older versions. Here are the critical differences:

1. No Direct Access to first_gid

Old versions (< 0.10) exposed first_gid directly. Version 0.12+ does not.

Instead, use LayerTile methods:

  • layer_tile.get_tileset() - Returns the &Tileset for this tile
  • layer_tile.tileset_index() - Returns the index in the map's tileset array
  • layer_tile.id() - Returns the local tile ID within its tileset (not global GID)

2. LayerTile vs Tile vs Tileset

  • Tileset: The tileset definition (columns, spacing, margin, image)
  • Tile: A tile definition in a tileset (with properties, animations, etc.)
  • LayerTile: An instance of a tile placed in a layer (has flip info, position)

3. Tile IDs are Local, Not Global

The layer_tile.id() returns the local ID within the tileset, not the global GID from the TMX file.

This means:

  • No need to subtract first_gid - the ID is already local
  • Use id % columns to get the column
  • Use id / columns to get the row

4. Map Structure

// Get all tilesets
let tilesets: &[Arc<Tileset>] = map.tilesets();

// Iterate layers
for layer in map.layers() {
    // layer.visible, layer.name, layer.opacity
    match layer.layer_type() {
        LayerType::Tiles(tile_layer) => { /* ... */ }
        LayerType::Objects(object_layer) => { /* ... */ }
        LayerType::Image(image_layer) => { /* ... */ }
        LayerType::Group(group_layer) => { /* ... */ }
    }
}

Complete Example

Here's a complete, working example with camera controls:

mod tiled_map;

use raylib::prelude::*;
use tiled_map::TiledMap;

fn main() {
    let (mut rl, thread) = raylib::init()
        .size(1280, 720)
        .title("Tiled Map Demo")
        .build();

    rl.set_target_fps(60);

    // Load map
    let map = TiledMap::load(&mut rl, &thread, "assets/map.tmx")
        .expect("Failed to load map");

    // Setup camera
    let mut camera = Camera2D {
        offset: Vector2::new(640.0, 360.0),
        target: Vector2::new(0.0, 0.0),
        rotation: 0.0,
        zoom: 2.0,
    };

    let camera_speed = 300.0;

    // Game loop
    while !rl.window_should_close() {
        let delta = rl.get_frame_time();

        // Camera controls
        if rl.is_key_down(KeyboardKey::KEY_W) {
            camera.target.y -= camera_speed * delta / camera.zoom;
        }
        if rl.is_key_down(KeyboardKey::KEY_S) {
            camera.target.y += camera_speed * delta / camera.zoom;
        }
        if rl.is_key_down(KeyboardKey::KEY_A) {
            camera.target.x -= camera_speed * delta / camera.zoom;
        }
        if rl.is_key_down(KeyboardKey::KEY_D) {
            camera.target.x += camera_speed * delta / camera.zoom;
        }

        // Zoom
        let wheel = rl.get_mouse_wheel_move();
        if wheel != 0.0 {
            camera.zoom += wheel * 0.5;
            camera.zoom = camera.zoom.clamp(0.5, 4.0);
        }

        // Draw
        let mut d = rl.begin_drawing(&thread);
        d.clear_background(Color::BLACK);

        {
            let mut d2d = d.begin_mode2D(camera);
            map.draw(&mut d2d);
        }

        d.draw_fps(10, 10);
        d.draw_text(
            &format!("Zoom: {:.1}x", camera.zoom),
            10,
            30,
            20,
            Color::WHITE,
        );
    }
}

Advanced Features

1. Culled Rendering (Performance Optimization)

Only draw tiles visible on screen:

pub fn draw_culled(
    &self,
    d: &mut RaylibDrawHandle,
    camera: Camera2D,
    screen_width: f32,
    screen_height: f32,
) {
    let tile_width = self.map.tile_width as f32;
    let tile_height = self.map.tile_height as f32;

    for layer in self.map.layers() {
        if !layer.visible {
            continue;
        }

        let tile_layer = match layer.layer_type() {
            tiled::LayerType::Tiles(tl) => tl,
            _ => continue,
        };

        match tile_layer {
            tiled::TileLayer::Finite(finite_layer) => {
                let width = finite_layer.width();
                let height = finite_layer.height();

                // Calculate visible tile range
                let start_x = ((camera.target.x - screen_width / (2.0 * camera.zoom))
                    / tile_width).floor().max(0.0) as u32;
                let end_x = ((camera.target.x + screen_width / (2.0 * camera.zoom))
                    / tile_width).ceil().min(width as f32) as u32;
                let start_y = ((camera.target.y - screen_height / (2.0 * camera.zoom))
                    / tile_height).floor().max(0.0) as u32;
                let end_y = ((camera.target.y + screen_height / (2.0 * camera.zoom))
                    / tile_height).ceil().min(height as f32) as u32;

                // Only draw visible tiles
                for y in start_y..end_y {
                    for x in start_x..end_x {
                        if let Some(layer_tile) = finite_layer.get_tile(x as i32, y as i32) {
                            self.draw_tile(d, layer_tile, x, y, tile_width, tile_height);
                        }
                    }
                }
            }
            tiled::TileLayer::Infinite(_) => {
                eprintln!("Infinite layers not supported in culled rendering");
            }
        }
    }
}

2. Extract Object Layers

Use object layers for collision, spawn points, etc:

pub fn get_objects_from_layer(&self, layer_name: &str) -> Vec<MapObject> {
    let mut objects = Vec::new();

    for layer in self.map.layers() {
        if layer.name != layer_name {
            continue;
        }

        if let tiled::LayerType::Objects(object_layer) = layer.layer_type() {
            for object in object_layer.objects() {
                // Get dimensions from shape
                let (width, height) = match &object.shape {
                    tiled::ObjectShape::Rect { width, height } => (*width, *height),
                    tiled::ObjectShape::Ellipse { width, height } => (*width, *height),
                    _ => (0.0, 0.0),
                };

                objects.push(MapObject {
                    position: Vector2::new(object.x, object.y),
                    size: Vector2::new(width, height),
                    object_type: object.user_type.clone(),
                    name: object.name.clone(),
                });
            }
        }
    }

    objects
}

#[derive(Debug, Clone)]
pub struct MapObject {
    pub position: Vector2,
    pub size: Vector2,
    pub object_type: String,
    pub name: String,
}

3. Layer Opacity and Tinting

// In draw_tile function, use layer opacity:
let alpha = (layer.opacity * 255.0) as u8;
let tint = Color::new(255, 255, 255, alpha);

d.draw_texture_pro(
    texture,
    source,
    dest,
    Vector2::zero(),
    0.0,
    tint,  // Use layer opacity
);

4. Tile Animations

// Get tile definition for animations
if let Some(tile) = layer_tile.get_tile() {
    if let Some(animation) = &tile.animation {
        // animation contains frames with durations
        // Implement frame switching based on elapsed time
    }
}

Performance Optimization

Tips for Large Maps

  1. Use Culled Rendering: Only draw visible tiles (see above)
  2. Batch Draw Calls: Group tiles by texture when possible
  3. Cache Static Layers: Render static layers to a RenderTexture2D once
  4. Limit Layers: Keep layer count reasonable (< 10)
  5. Optimize Tileset: Use power-of-2 textures, combine small tilesets

Render Texture Caching Example

// One-time render of static background layer
let mut static_texture = rl.load_render_texture(&thread,
    map_width as u32, map_height as u32).unwrap();

{
    let mut d = rl.begin_texture_mode(&thread, &mut static_texture);
    d.clear_background(Color::BLANK);
    map.draw_layer(&mut d, 0);  // Draw layer 0 only
}

// In game loop, just draw the cached texture
d.draw_texture(&static_texture, 0, 0, Color::WHITE);

Common Pitfalls

1. ❌ Trying to Access first_gid Directly

// DON'T DO THIS (doesn't exist in v0.12+)
let first_gid = tileset.first_gid;  // ERROR!

Fix: Use layer_tile.id() which returns the local ID.

2. ❌ Wrong Tileset Path

// Map says: <image source="tileset.png" />
// You need: assets/tileset.png

Fix: Ensure tileset images are in assets/ and paths are relative to TMX file.

3. ❌ Forgetting Spacing/Margin

If your tiles look offset or wrong, check:

  • tileset.spacing - Pixels between tiles
  • tileset.margin - Pixels around the edge
  • These are set in Tiled when creating the tileset

4. ❌ Not Handling Empty Tiles

// Always check for empty tiles (id 0)
if tile_id == 0 {
    continue;
}

5. ❌ Flipping Not Working

Make sure you're handling flip flags:

if layer_tile.flip_h {
    source.width = -source.width;
}
if layer_tile.flip_v {
    source.height = -source.height;
}

6. ❌ Layer Iteration Issues

// Layers is an iterator, not a slice
// This won't work:
let layer = self.map.layers()[0];  // ERROR

// Do this instead:
let layer = self.map.layers().nth(0).unwrap();

Resources

Documentation

Examples

Useful Tiled Features

  • Tile Layers: Main visual layers
  • Object Layers: Collision, spawns, triggers
  • Custom Properties: Add metadata to tiles, objects, layers
  • Tile Animations: Built-in animation support
  • Wang Sets: For auto-tiling

Summary

Key takeaways:

  1. Use tiled crate v0.12+ (different API from older versions)
  2. layer_tile.id() gives you the local tile ID (not global GID)
  3. Use layer_tile.get_tileset() and layer_tile.tileset_index() to get tileset info
  4. Always handle empty tiles, flipping, spacing, and margin
  5. Use culled rendering for large maps
  6. Object layers are great for game logic data

Happy mapping! If you have questions or improvements, feel free to contribute to this guide.

License: CC0 / Public Domain - Use freely!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment