Introduction

bevy_ecs_tiled is a Bevy plugin for working with 2D tilemaps created with the Tiled map editor.

It relies upon:

Each tile or object is represented by a Bevy entity:

  • layers are children of the tilemap entity
  • tiles and objects are children of layers

Visibility and Transform are inherited: map -> layer -> tile / object


Disclaimer: both this book and the whole crate have been heavilly inspired by the bevy_ecs_ldtk crate, which is basically the equivalent of bevy_ecs_tiled but for the LDTK map editor. Thanks for the inspiration! :)


Purpose of this book

This book aims to give you a better understanding of how bevy_ecs_tiled works, what you can achieve with it and how you can do it.

It will focus on concepts, design concerns and basic tutorials. If you need an API reference, please have a look at the bevy_ecs_tiled API reference.

Finally, this book assume you already have some sort of knowledge about Bevy and Tiled map editor. There are already some good documentations available on these topics and some resources are referenced in the dedicated section.

Architecture of this book

This book is divided in several categories:

  • Design and explanation: how does the plugin work and why some technical choices have been made;
  • How-To's: in-depth tutorials about a specific aspect of the plugin;
  • Migration guides: migration guides for specific versions;
  • Miscellaneous: other topics that did not fit in other categories.

Good reading ! :)

FAQ

What is the current status of the crate ?

While the crate is already definitely usable, it is still under active development.

Expect bugs and missing features !

I plan to follow semver and try to only break API upon new Bevy release.

What kind of maps are supported ?

Currently, we support :

  • orthogonal maps
  • (mostly) isometric "diamond" maps
  • hexagonal "flat-top" maps
  • hexagonal "pointy-top" maps

Isometric "diamond" maps currently have an issue with colliders not having the proper shape (see GH issue #32).

Isometric "staggered" maps are not supported at all (see GH issue #31).

Is it possible to automatically add physics colliders ?

Yes, see the dedicated guide.

We currently support both Avian and Rapier physics backend.

Is it possible to use Tiled "custom properties" ?

Yes, see the dedicated guide.

I'm using an isometric map and it seems all messed up!

Make sure you are actually using a "diamond" map and not a "staggered" one (which are not supported).

Also, for isometric maps, you may want to tweak the TilemapRenderSettings Component from bevy_ecs_tilemap. More information in the isometric maps example

How to enable map hot-reload ?

You need to enable Bevy file_watcher feature. bevy_ecs_tiled will then be able to automatically reload a map that was updated with Tiled.

I found a bug! What should I do ?

Please have a look to already openned issues and if it does not already exists, please fill a new one !

I want to add a new feature that's not yet in the crate!

Great news! Please have a look to the dedicated section

Why using Tiled ?

Tiled may feel a bit outdated in terms of "look and feel", especially when compared with more modern map editor tools like LDTK. However it has a lot of features which make it very interesting.

If we compare with LDTK, they both have a set of powerful features like:

  • auto-tiling
  • adding gameplay informations to map tiles and objects
  • working with worlds

But Tiled also have a set of unique features:

  • support for both isometric and hexagonal maps
  • native support for animated tiled

Since I specifically wanted to work with hexagonal maps the choice was obvious for me!

However, if it's not your case and you just want to use orthogonal map, you could give a shot at using LDTK instead, especially using the bevy_ecs_ldtk crate. Or stay with Tiled, it also works :)

Entities tree and marker components

When a map is loaded, it spawns a lot of entities: for the map, for layers, for tiles, for objects, for colliders, ... These entites are organized in a parent / child hierarchy.

It notably brings the capability to inherit some of the component down the tree. For instance, if you change the Visibility of an entity, it will automatically apply to all entities below in the hierarchy.

It also helps to keep things nice and clean.

Tree hierarchy

Map

At the top of the tree, there is the map. It notably holds the TiledMapHandle pointing to your .TMX file and all the settings that apply to it. It can be easily identified using a dedicated marker component: TiledMapMarker.

Layers

Below the map, we have the layers. They can be of different kinds, which each have their own marker component:

All of them are also identified by the same generic marker: TiledMapLayer.

Objects & Tiles

Objects are directly below their TiledMapObjectLayer. They are identified by a TiledMapObject marker.

For tiles, it's a little more complicated. Below the TiledMapTileLayer, we first have one TiledMapTileLayerForTileset per tileset in the map. Finally, below these, we find the actual TiledMapTile which correspond to every tiles in the layer, for a given tileset.

Physics colliders

At the end of the hierarchy, we find physics colliders. They are spawned below they "source", ie. either a tile or an object and can be identified using their marker component: TiledColliderMarker.

Layers Z-ordering

When designing your map under Tiled, you expect that a layer will hide another one which is below in the layer hierarchy. This is very useful when using isometric tiles for instance.

To reproduce this behaviour under Bevy, we add an arbitrary offset on the Z transform to each layers of the hierarchy.

If we call this offset OFFSET:

  • the top-level layer will have a Z transform of 0
  • the second one will have a Z transform of - OFFSET
  • the next one of - 2*OFFSET
  • etc...

By default this offset has a value of +100. It can be changed by tweaking the TiledMapSettings component. Since bevy_ecs_tilemap also play with the Z-transform to adjust how tiles from a given layers are rendered, you probably don't want to have a "too low" value.

Map loading events

After loading a map, the plugin will send several events:

These events are a way to access directly raw Tiled data and easily extend the plugin capabilities.

For instance, you can access a tiled::Object from the corresponding event:


#![allow(unused)]
fn main() {
use bevy::prelude::*;
use bevy_ecs_tiled::prelude::*;

fn object_created(
    trigger: Trigger<TiledObjectCreated>,
    map_asset: Res<Assets<TiledMap>>,
) {
    // Access raw Tiled data
    let _map = trigger.event().map(&map_asset);
    let _layer = trigger.event().layer(&map_asset);
    let object = trigger.event().object(&map_asset);
    info!("Loaded object: {:?}", object);
}
}

A dedicated example is available to demonstrate how to use these.

Minimal working example

Add dependencies to your Cargo.toml file:

[dependencies]
bevy = "0.14"
bevy_ecs_tiled = "0.4"
bevy_ecs_tilemap = "0.14"

Then add the plugin to your app and spawn a map:

use bevy::prelude::*;
use bevy_ecs_tiled::prelude::*;
use bevy_ecs_tilemap::prelude::*;

fn main() {
    App::new()
        // Add Bevy default plugins
        .add_plugins(DefaultPlugins)
        // Add bevy_ecs_tilemap plugin
        .add_plugins(TilemapPlugin)
        // Add bevy_ecs_tiled plugin
        .add_plugins(TiledMapPlugin::default())
        // Add our startup function to the schedule and run the app
        .add_systems(Startup, startup)
        .run();
}

fn startup(mut commands: Commands, asset_server: Res<AssetServer>) {
    // Spawn a 2D camera
    commands.spawn(Camera2dBundle::default());

    // Load the map: ensure any tile / tileset paths are relative to assets/ folder
    let map_handle: Handle<TiledMap> = asset_server.load("map.tmx");

    // Spawn the map with default options
    commands.spawn(TiledMapHandle(map_handle));
}

Please note that you should have the map.tmx file in your local assets/ folder, as well as required dependencies (for instance, associated tilesets).

You can customize various settings about how to load the map by inserting the TiledMapSettings component on the map entity.

Also, you can browse the examples for more advanced use cases.

Spawn / Despawn / Reload a map

These aspects are also covered in the dedicated example.

Spawn a map

Spawning a map is done in two steps:

  • first, load a map asset / a map file using the Bevy AssetServer
  • then, spawn a TiledMapHandle containing a reference to this map asset

#![allow(unused)]
fn main() {
fn spawn_map(
    mut commands: Commands,
    asset_server: Res<AssetServer>
) {
    // First, load the map
    let map_handle: Handle<TiledMap> = asset_server.load("map.tmx");

    // Then, spawn it, using default settings
    commands.spawn(TiledMapHandle(map_handle));
}
}

Note that you can perform the initial map loading beforehand (for instance, during your game startup) and that there is no restriction on the number of maps loaded or spawned at the same time.

Despawn a map

If you want to despawn a map, the easiest is to actually remove its top-level entity:


#![allow(unused)]
fn main() {
pub fn despawn_map(
    mut commands: Commands,
    maps_query: Query<Entity, With<TiledMapMarker>>,
) {
    // Iterate over entities with a TiledMapMarker component
    for map in q_maps.iter() {
        // Despawn these entities, as well as their child entities
        commands.entity(map).despawn_recursive();
    }
}
}

All child entities, like layers and tiles, will automatically be despawned.

Reload a map

If you want to reload a map, you can of course despawn it then spawn it again. It's tedious, but it works.

However, there is an easier way. Instead, you can insert the RespawnTiledMap component:


#![allow(unused)]
fn main() {
fn respawn_map(
    mut commands: Commands,
    maps_query: Query<Entity, With<TiledMapMarker>>,
) {
    if let Ok(entity) =  maps_query.get_single() {
        commands.entity(entity).insert(RespawnTiledMap);
    }
}
}

This will reload the exact same map but using new entities. It means that if you updated some components (for instance, a tile color or an object position) they will be back as they were when you first loaded the map. It's useful to implement a level respawn for instance.

Another use case is to load a new map over an existing one. An easy way to do that is to just spawn a new TiledMapHandle over an existing map.


#![allow(unused)]
fn main() {
fn handle_reload(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    maps_query: Query<Entity, With<TiledMapMarker>>,
) {
    if let Ok(entity) = maps_query.get_single() {
        commands.entity(entity)
            .insert(
                TiledMapHandle(asset_server.load("other_map.tmx"))
            );
    }
}
}

Add physics colliders

Tiled allows you to add objects to your map: either directly on an object layer or attached to a tile. bevy_ecs_tiled can use these objects to automatically spawn physics colliders when loading the map.

We provide two working physics backend: one using avian and another one using bevy_rapier. They are the same in terms of features regarding bevy_ecs_tiled so feel free to choose the one you prefer.

The API also allows you to provide a custom physics backend or to customize existing ones.

Create objects in Tiled

First step is to have on your map something that we can actually use to spawn a collider: objects.

In Tiled, there are two kinds of objects:

  • objects attached to an object layer: these are the regular objects you can place on the map when working on an object layer.
  • objects attached to a tile: for these, you will need to actually edit your tileset.

These two kinds of objects, eventhough they are handled a bit differently, will actually produce the same result: using the object shape we will be able to spawn a collider. So, as you can imagine, a "Point" object will not produce anything physics-wise :)

One important aspect is to remember that you change the name of your objects.

Automatically spawn colliders

In order to automatically spawn colliders from Tiled objects, you need two things:

  • having the proper feature enabled: you will either need avian or rapier, depending on your backend choice (you can eventually only enable physics, but you it means you will provide the backend yourself).
  • instanciate the TiledPhysicsPlugin with an associated TiledPhysicsBackend.
use bevy::prelude::*;
use bevy_ecs_tiled::prelude::*;

// This is a minimal example: in a real world scenario, you would probably
// need to load additionnal plugins (TiledMapPlugin and TilemapPlugin for instance)

fn main() {
    App::new()
        // bevy_ecs_tiled physics plugin: this is where we select
        // which physics backend to use (in this case, Avian)
        .add_plugins(TiledPhysicsPlugin::<TiledPhysicsAvianBackend>::default())
        // Add our systems and run the app!
        .add_systems(Startup, startup)
        .run();
}

// Just load the map as usual
fn startup(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.spawn(Camera2dBundle::default());
    commands.spawn(TiledMapHandle(asset_server.load("finite.tmx")));
}

By default, we will spawn physics colliders for all objects encountered in the map. Eventhough it's probably the most common behaviour, you can also fine tune for which objects you want to spawn colliders for by updating the TiledPhysicsSettings component.


#![allow(unused)]
fn main() {
use bevy::prelude::*;
use bevy_ecs_tiled::prelude::*;

// Load the map with custom physics settings (and an Avian backend)
fn startup(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.spawn(Camera2dBundle::default());
    commands.spawn((
        TiledMapHandle(asset_server.load("finite.tmx")),
        // With this configuration, we will restrict the spawn of collider
        // for objects named 'collision' attached to a tile.
        // No collider for objects attached to objects layer.
        TiledPhysicsSettings::<TiledPhysicsAvianBackend> {
            objects_layer_filter: ObjectNames::None,
            tiles_objects_filter: ObjectNames::Names(vec!["collision".to_string()]),
            ..default()
        },
    ));
}
}

Custom physics backend and colliders event

If you need to, the API will let you to add your own physics behaviour.

You can eventually define your own physics backend. All you need to do is to create a struct that implements the TiledPhysicsBackend trait, ie. provide an implementation for the spawn_collider function.

You can even use one the provided backend, but if their implementation have something missing for you, you can wrap your own implementation around and existing backend (see this example for Avian or this one for Rapier).

Finally, whatever the backend you are using, a dedicated TiledColliderCreated event will be fired after a collider is spawned. Note that you will have one event per spawned collider. These events can be used for instance to add a missing component to the collider (or anything you want).

Use Tiled custom properties

In Tiled we can add "custom properties" on various items like layers, tiles, objects or maps.

These custom properties can be either:

  • a "standard type", like a string, an integer, a float, a color, etc...
  • a "custom type", which is basically a structure with sub-properties that can either be a "standard type" or another "custom type"

In bevy_ecs_tiled we support mapping a Tiled "custom type" to a Bevy Component, Bundle or even Resource. Basically, it means that we can define some game logic directly in the Tiled map to use it in your game with Bevy.

Using this mechanism, we could for instance:

  • associate a "movement cost" to a given tile type
  • create an object that represent our player or an ennemy
  • add a generic "trigger zone" that could either be a "damaging zone" or a "victory zone"
  • ... whatever you need for your game!

In addition to this guide, there is also a dedicated example.

Declare types to be used as custom properties

Your Tiled map, layer, tile or object will be represented by a Bevy Entity. So, it makes sense that if you want to add custom properties to them, these properties should either be a Component or a Bundle.

Also, Tiled custom properties use Bevy Reflect mechanism. So, in order to be usable in Tiled, your custom types must be "Reflectable". To do, these types must derive the Reflect trait and get registered with Bevy.

use bevy::prelude::*;

// Declare a component that is "reflectable"
#[derive(Component, Reflect, Default)]
#[reflect(Component, Default)]
struct SpawnInfos {
    has_spawned: bool,
    ty: SpawnType,
}

// Any 'sub-type' which is part of our component must also be "reflectable"
#[derive(Default, Reflect)]
#[reflect(Default)]
enum SpawnType {
    #[default]
    Unknown,
    Player,
    Enemy,
}

// Register our type with Bevy
fn main() {
    App::new()
        .register_type::<SpawnInfos>();
}

And that's all !

Note that in the above example, our custom type also derive the Default trait. It is particulary useful to do so: if you don't, you would have to fill all the fields of your custom type when you use it in Tiled.

Finally, note that you can also add Resource to your map. They won't be attached to a particular entity and as such are only allowed on Tiled maps.

Add custom properties to your map

Before you can add custom properties to your map, you will need to export them from Bevy then import them in Tiled.

When running with the user_properties feature, your app will automatically produce an export of all types registered with Bevy. By default, this file will be produced in your workspace with the name tiled_types_export.json. You can change this file name or even disable its production by tweaking the TiledMapPlugin configuration (see TiledMapPluginConfig).

You can then import this file to Tiled. To do so, in Tiled, navigate to View -> Custom Types Editor:

view-custom-types

Click on the Import button and load your file:

import-custom-types

Once it is done, you will be able to see all the custom types that you have imported from your application. Note that it concerns all the types that derive the Reflect trait: there can be quite a lot !

view-custom-types

You can now add them to different elements of your map, like tiles objects, layers or the map itself. For more information on how to do add custom properties, see the official TIled documentation. You should only add properties imported from Bevy: adding ones that you created only in Tiled will not be loaded.

Debug your project

bevy-inspector-egui

This may be obvious but this plugin is a must have for debugging.

Just add the required dependency in Cargo.toml:

[dependencies]
bevy-inspector-egui = "0.25.2"

Then add the WorldInspectorPlugin to your application:

use bevy::prelude::*;
use bevy_inspector_egui::quick::WorldInspectorPlugin;

fn main() {
    App::new()
        .add_plugins(WorldInspectorPlugin::new())
        .run();
}

Now, you can browse components from all entities spawned in your game.

More informations on the project github page.

TiledMapDebugPlugin

bevy_ecs_tiled provides a debug plugin that displays a gizmos where Tiled object are spawned.

To use it, you just have to enable the debug feature and add the plugin to your application:

use bevy::prelude::*;
use bevy_ecs_tiled::prelude::*;

fn main() {
    App::new()
        .add_plugins(TiledMapDebugPlugin::default())
        .run();
}

More informations in the API reference.

Physics

Both Avian and Rapier provide their own way of debugging. It can be very useful, especially when working with colliders. Note that physics debugging is enabled by default in all bevy_ecs_tiled examples using physics.

To enable physics debugging in Avian, you need to simply add the corresponding plugin:

use bevy::prelude::*;
use avian2d::prelude::*;

fn main() {
    App::new()
        // Add Avian regular plugin
        .add_plugins(PhysicsPlugins::default().with_length_unit(100.0))
        // Add Avian debug plugin
        .add_plugins(PhysicsDebugPlugin::default())
        .run();
}

For Rapier, you need to enable a debug plugin:

use bevy::prelude::*;
use bevy_rapier2d::prelude::*;

fn main() {
    App::new()
        // Add Rapier regular plugin
        .add_plugins(RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(100.0))
        // Add Rapier debug plugin
        .add_plugins(RapierDebugRenderPlugin::default())
        .run();
}

And you also need to enable either the debug-render-2d feature on bevy_rapier2d crate or the rapier_debug feature on bevy_ecs_tiled

Logging

Bevy uses the tracing crate for logging, which is very powerful in debugging and profiling, you can find more information in the official documentation.

We recommend to enable the trace level in your application to get more informations about what's happening, just set the RUST_LOG environment variable to trace:

RUST_LOG=trace cargo run

But this will be very verbose, so you can also filter the logs to only display the informations you need:

RUST_LOG=bevy_ecs_tiled=trace cargo run

This will only display logs from the bevy_ecs_tiled crate in trace level.

From v0.3.X to v0.4.X

Overview

Version 0.4 was initially motivated by an update to the way we handle user properties but ended as a major overhaul of the plugin to provide a better API and to give more control to the user.

Plugin instanciation

The plugin now has an associated configuration, which you need to provide when you add it to your application.

The easiest way is to use the default configuration value :

use bevy::prelude::*;
use bevy_ecs_tiled::prelude::*;

fn main() {
    App::new()
        // You still need the bevy_ecs_tilemap plugin
        .add_plugins(TilemapPlugin)
        // And now, you have to provide a configuration for bevy_ecs_tiled plugin
        .add_plugins(TiledMapPlugin::default())
        .run();
}

The plugin configuration is described in the API reference

Tiled map spawn and configuration

The plugin entry point, ie. the TiledMapBundle bundle is gone. It was cumbersome and did not allow for a proper separation of concerns (for instance, for physics).

Also, the Handle<TiledMap> type is not a Bevy component anymore. It was done in order to anticipate expected changes in Bevy where Handle<T> won't be able to derive the Component trait anymore.

Anyway, the new way to spawn a map is now easier: you just have to spawn a TiledMapHandle referencing your .TMX file asset:


#![allow(unused)]
fn main() {
use bevy::prelude::*;
use bevy_ecs_tiled::prelude::*;

fn startup(mut commands: Commands, asset_server: Res<AssetServer>) {
    // Load the map: ensure any tile / tileset paths are relative to assets/ folder
    let map_handle: Handle<TiledMap> = asset_server.load("map.tmx");

    // Spawn the map with default options
    commands.spawn(TiledMapHandle(map_handle));
}
}

You can customize various settings about how to load the map by inserting the TiledMapSettings component on the map entity.

Tiled user properties

Before this change, you had to define your custom types both in Tiled and in your rust code. It was not user-friendly and error-prone.

Now, we take advantage of bevy_reflect to generate a file containing all the types known to Bevy. This file can be imported in Tiled so you can use these types directly in the editor.

Migrating from the old implementation should be straight-forward.

First, you need need to update your custom types so they actually implement the Reflect trait :

  • remove #[derive(TiledObject)], #[derive(TiledCustomTile)], #[derive(TiledClass)] and #[derive(TiledEnum)] derived traits. Make sure to also remove associated attributes.
  • add #[derive(Reflect)] derive trait on the types you want to use in Tiled.
  • make sure your components have the #[reflect(Component)] attribute

Then, in your main / in your plugin setup, you then need to register your types with Bevy :

  • replace calls to register_tiled_object::<T>() with calls to register_type::<T>().
  • replace calls to register_tiled_custom_tile::<T>() with calls to register_type::<T>().

The final step is to actually generate the types import file (run your game once) and import the types to Tiled. Note that you may have to update your map / your tilesets to use the new types you just imported.

A dedicated guide about how to setup user properties is available in this book.

Tiled physics

Eventhough functionnalities around physics did not change much, the internals have been completely reworked and the API was updated a bit.

Notably, now you need to instanciate another plugin and specify which physics backend you want to use. The physics section of the book should get you through.

Map events

This is a new feature of this version which gives more control to the user over what he wants to do with a Tiled map. More information in the dedicated section

Misc changes

enum MapPositioning

Both enum name and fields name have been updated to better reflect what they actually do. You should now use the new LayerPositioning enum.

fn from_isometric_coords_to_bevy()

Parameters tiled_position: Vec2 and iso_coords: IsoCoordSystem have been swapped for better consistency with other utility functions.

Useful links

Here is an unordered list of useful resources that may help when working on a 2D tile-base game.

Feel free to suggest any other link!

Notable crates

Notable resources

Contributing

If you would like to contribute but you're unsure where to start, here is a short wishlist and some notes on how to get started.

First, you can have a look at GH issues. More specifically, the ones that are:

If you feel like you can tackle on of these, please, feel free to submit a PR!

Helping other users, respond to issues, help review PRs or just openning new issues is also very helpful !

Also, another way of helping is to contribute on crates we rely on, namely rs-tiled and bevy_ecs_tilemap, or anything else in the Bevy ecosystem.

Contribution guidelines

If you submit a PR, please make sure that:

  • the CI is green: you can locally run the ./ci_check.sh script
  • you add proper in-code documentation
  • you update the CHANGELOG.md file with a description of your fix
  • if you add a new example, update the examples/README.md file with a description of your example and the Cargo.toml file with your example name (and any feature it may need)
  • if you add a new map, update the assets/README.md file with your map characteristics
  • if you add a new asset, update the "Assets credits" section of the README.md file and make sure that you actually have the right to use it!

Thanks in advance! :)