// This example is designed to try and cover as many use cases
// as possible from a wide variety of LDtk source files. That's
// great for an example file, but it's probably too complex if
// you know what your game wants from LDtk.
//
// In any particular game you'll likely just load stuff up
// by referencing it directly from the LdtkFile instance instead
// of looping through and matching for all the options like I
// do here.
use bevy::prelude::*;
use bevy::render::pass::ClearColor;
use ldtk_rust::{EntityInstance, Project, TileInstance, IntGridValueInstance};
use std::collections::HashMap;
// Constants
const LDTK_FILE_PATH: &str = "assets/test_game.ldtk";
const TILE_SCALE: f32 = 2.5;
// Extend the LdtkFile object with whatever you need for your
// game engine. In a real game you might need a variety of
// fields to control how and when you use the LDtk information.
struct Map {
ldtk_file: Project,
redraw: bool,
current_level: usize,
}
// We need a place to store the assets that LDtk references
// (the spritesheets and the color materials).These could be
// added to the Map struct above, but most games likely need
// these for purposes other than tilemaps so we store them
// in a seperate struct.
//
// If you only use one spritesheet you could just store it
// directly instead of using a Hash and doing all the
// looping below.
#[derive(Clone)]
struct VisualAssets {
int_grid_materials: HashMap<i32, Vec<Handle<ColorMaterial>>>,
spritesheets: HashMap<i32, Handle<TextureAtlas>>,
entity_materials: HashMap<i32, Handle<ColorMaterial>>,
}
// storage for layer info as we loop through tiles
#[derive(Clone, Copy)]
struct LayerInfo {
grid_width: i32,
_grid_height: i32,
grid_cell_size: i32,
z_index: i32,
px_width: f32,
px_height: f32,
}
// The LDtk JSON is organized in two main sections, the "defs"
// object define things and the "levels" object includes the
// level information. Most users can ignore the "defs" object,
// but if you want something from it, here's one way to do it.
#[derive(Copy, Clone)]
struct ExtraEntDefs {
__tile_id: i32,
__width: i32,
__height: i32,
__scale: f32,
}
// implement a new() method
impl ExtraEntDefs {
fn new() -> Self {
Self {
__tile_id: 0,
__width: 0,
__height: 0,
__scale: 1.0,
}
}
}
// Bevy specific app setup. This just means we are opening a
// window for our game, running the setup() function once at
// startup and then running update() every game loop.
fn main() {
App::build()
.add_resource(WindowDescriptor {
title: "title".to_string(),
width: 1024.0,
height: 768.0,
..Default::default()
})
.add_plugins(DefaultPlugins)
.add_startup_system(setup.system())
.add_system(update.system())
.run();
}
// Our setup system will run once and will read in the LDtk file.
// It then loops through any referenced tilesets and sets those
// up as Bevy Assets. It does the same thing for color materials.
// Finally it saves the LdtkFile instance and handles to all the
// assets as Bevy Resources, which makes them "globals".
fn setup(
commands: &mut Commands,
asset_server: Res<AssetServer>,
mut materials: ResMut<Assets<ColorMaterial>>,
mut texture_atlases: ResMut<Assets<TextureAtlas>>,
) {
// Create a new Map instance and set the values. This is where we
// actually load in the LDtk file.
let map = Map {
ldtk_file: Project::new(LDTK_FILE_PATH.to_string()),
redraw: true,
current_level: 0,
};
// Create a new VisualAssets instance. This is where we will save
// handles to all our assets so we can call them in our update()
// function later.
let mut visual_assets = VisualAssets {
int_grid_materials: HashMap::new(),
spritesheets: HashMap::new(),
entity_materials: HashMap::new(),
};
// For each tileset referenced in the LDtk file, create a Texture Atlas
// and store a Handle in a Hash. The key to the Hash is the value LDtk
// assigns as the tileset's UID. If you know you only have one tileset
// asset, you could simplify this and just load it like any other asset
// using map.ldtk_file.defs.tilesets[0].rel_path
for tileset in map.ldtk_file.defs.as_ref().unwrap().tilesets.iter() {
let texture_handle = asset_server.load(&tileset.rel_path[..]);
let texture_atlas = TextureAtlas::from_grid(
texture_handle,
Vec2::new(tileset.tile_grid_size as f32, tileset.tile_grid_size as f32),
(tileset.px_wid / tileset.tile_grid_size) as usize,
(tileset.px_hei / tileset.tile_grid_size) as usize,
);
let texture_atlas_handle = texture_atlases.add(texture_atlas);
visual_assets
.spritesheets
.insert(tileset.uid as i32, texture_atlas_handle);
}
// LDtk IntGrids support setting colors for walls, sky, etc. If you are doing this
// in your game you'd probably want to loop through your IntGrid layers and create
// materials for each integer value. The Bevy snake tutorial has some good sample
// code for using materials: https://mbuffett.com/posts/bevy-snake-tutorial/
for layer in
map.ldtk_file
.defs
.as_ref().unwrap()
.layers
.iter()
.filter(|f| match f.purple_type.as_ref().unwrap() {
ldtk_rust::Type::IntGrid => true,
_ => false,
})
{
let mut colors = Vec::new();
for i in layer.int_grid_values.iter() {
let clr = match Color::hex(&i.color[1..]){
Ok(t) => t,
Err(e) => {
println!("Error: {:?}", e);
Color::BLUE
}
};
let col_mat = materials.add(ColorMaterial::from(clr));
colors.push(col_mat);
}
visual_assets
.int_grid_materials
.insert(layer.uid as i32, colors);
}
// LDtk supports placement of Entities in levels (player, chest, health potion, etc.)
// If you are using this feature you may want to do additional setup here beyond
// loading in the tilemap assets above.
for ent in map.ldtk_file.defs.as_ref().unwrap().entities.iter() {
let clr = match Color::hex(&ent.color.clone()[1..]) {
Ok(t) => t,
Err(e) => {
println!("Error: {:?}", e);
Color::BLUE
}
};
let col_mat = materials.add(ColorMaterial::from(clr));
visual_assets
.entity_materials
.insert(ent.uid as i32, col_mat);
}
// add the LDtk object and the tile assets as resources and spawn a camera
commands
.insert_resource(map)
.insert_resource(visual_assets)
.spawn(Camera2dBundle::default());
}
// Our update system runs every game loop and, if the tiles are not spawned, will spawn them.
// In practice you'll likely want to split some of this up, but it's a decent start to
// having the ability to regenerate tiles as the current level changes, etc.
fn update(commands: &mut Commands, mut map: ResMut<Map>, visual_assets: Res<VisualAssets>) {
// If we don't need to redraw the tiles, go ahead and return (do nothing)
if !map.redraw {
return;
}
// Add a background color. The "__bg_color" field should always be populated
// with either the default background color or the level's custom color.
commands.insert_resource(ClearColor(
Color::hex(&map.ldtk_file.levels[0].bg_color[1..]).unwrap(),
));
// For the current level, loop through the Layer Instances and start spawning
// tiles. These Layer Instances can be one of four different kinds of layers:
// IntGrid, Entities, Tiles or AutoLayer. Tiles and AutoLayers are easy: you
// always iterate through Tile Instances to do your work. IntGrids may or may
// not use a tileset (they can use flat colors). If they have tiles, you handle
// them just like Tiles and AutLayers (but you have to check for a tileset first).
// For entities you iterate through Entity Instances instead of Tile Instances.
//
// Your game only needs to handle the kinds of layers you want to use in LDtk.
// Here I try to organize the code to accommodate all the options, but then
// implement some simplistic code for the ones that use actual tilesets.
//
// Using .rev() allows us to handle things "bottom to top" and makes sorting
// on the z-axis easier to reason about.
for (idx, layer) in map.ldtk_file.levels[map.current_level]
.layer_instances
.as_ref()
.unwrap()
.iter()
.enumerate()
.rev()
{
// This gets us a unique ID to refer to the tileset if there is one.
// If there's no tileset, it's value is set to -1, which could be used
// as a check. Currently it is used only as a key to the hash of asset
// handles.
let tileset_uid = layer.tileset_def_uid.unwrap_or(-1) as i32;
let layer_uid = layer.layer_def_uid as i32;
// Multiply the grid size by the tile size and our scaling constant
// to calculate the total width and height of our layer. For depth
// we pick a starting point and add the loop index so we always draw
// tiles on top of previous iterations. We do all this in a struct
// instance so we can easily pass it around to functions later.
let layer_info = LayerInfo {
grid_width: layer.c_wid as i32,
_grid_height: layer.c_hei as i32,
grid_cell_size: layer.grid_size as i32,
z_index: 50 - idx as i32,
px_width: layer.c_wid as f32 * (layer.grid_size as f32 * TILE_SCALE),
px_height: layer.c_hei as f32 * (layer.grid_size as f32 * TILE_SCALE),
};
// Finally we match on the four possible kinds of Layer Instances and
// handle each accordingly.
match &layer.layer_instance_type[..] {
"Tiles" => {
println!("Generating Tile Layer: {}", layer.identifier);
for tile in layer.grid_tiles.iter() {
display_tile(
layer_info,
tile,
commands,
visual_assets.spritesheets[&tileset_uid].clone(),
);
}
}
"AutoLayer" => {
println!("Generating AutoTile Layer: {}", layer.identifier);
for tile in layer.auto_layer_tiles.iter() {
display_tile(
layer_info,
tile,
commands,
visual_assets.spritesheets[&tileset_uid].clone(),
);
}
}
"IntGrid" => {
match layer.tileset_def_uid {
Some(i) => {
// we have tiles, so handle just like Tiles and AutoLayers
println!("Generating IntGrid Layer w/ Tiles: {}", layer.identifier);
let i = i as i32;
for tile in layer.auto_layer_tiles.iter() {
display_tile(
layer_info,
tile,
commands,
visual_assets.spritesheets[&i].clone(),
);
}
}
None => {
// we do NOT have a corresponding tileset, so we need to use
// the color values to represent the level visually.
println!(
"Generating IntGrid Layer w/ Color Materials: {}",
layer.identifier
);
for tile in layer.int_grid.iter() {
display_color(
layer_info,
tile,
commands,
visual_assets.int_grid_materials[&layer_uid][tile.v as usize]
.clone(),
)
}
}
}
}
"Entities" => {
println!("Generating Entities Layer: {}", layer.identifier);
// Entities reference their tiles and colors within the instances
for entity in layer.entity_instances.iter() {
// we need some extra fields from the defs section of the
// JSON that aren't included in the entity instances.
let mut extra_ent_defs = ExtraEntDefs::new();
for ent in map.ldtk_file.defs.as_ref().unwrap().entities.iter() {
if ent.uid == entity.def_uid {
extra_ent_defs.__tile_id = 0;
extra_ent_defs.__width = ent.width as i32;
extra_ent_defs.__height = ent.height as i32;
}
match ent.render_mode.as_ref().unwrap() {
ldtk_rust::RenderMode::Tile => {
extra_ent_defs.__tile_id = ent.tile_id.unwrap() as i32;
for ts in map.ldtk_file.defs.as_ref().unwrap().tilesets.iter() {
if ts.uid == ent.tileset_id.unwrap() {
extra_ent_defs.__scale =
ent.width as f32 / ts.tile_grid_size as f32;
}
}
}
_ => (),
}
}
display_entity(
layer_info,
entity,
commands,
visual_assets.clone(),
&extra_ent_defs,
);
}
}
_ => {
println!("Not Implemented: {}", layer.identifier);
}
}
}
// Whew, we've draw everyting so update the Map instance so we don't do it every game loop.
map.redraw = false;
}
// Spawn a tile. Check to see if it needs to flip on the x and/or y axis before spawning.
fn display_tile(
layer_info: LayerInfo,
tile: &TileInstance,
commands: &mut Commands,
handle: Handle<TextureAtlas>,
) {
let mut flip_x = false;
let mut flip_y = false;
match tile.f {
1 => flip_x = true,
2 => flip_y = true,
3 => {
flip_x = true;
flip_y = true
}
_ => (),
}
commands.spawn(SpriteSheetBundle {
transform: Transform {
translation: convert_to_world(
layer_info.px_width,
layer_info.px_height,
layer_info.grid_cell_size,
TILE_SCALE,
tile.px[0] as i32,
tile.px[1] as i32,
layer_info.z_index,
),
rotation: flip(flip_x, flip_y),
scale: Vec3::splat(TILE_SCALE),
},
sprite: TextureAtlasSprite::new(tile.t as u32),
texture_atlas: handle,
..Default::default()
});
}
// spawn your entities. This is likely very game dependant, but
// here's a basic example.
fn display_entity(
layer_info: LayerInfo,
entity: &EntityInstance,
commands: &mut Commands,
visual_assets: VisualAssets,
extra_ent_defs: &ExtraEntDefs,
) {
match &entity.tile {
Some(t) => {
// process tile asset
let tileset_uid = t.tileset_uid as i32;
let handle: Handle<TextureAtlas> = visual_assets.spritesheets[&tileset_uid].clone();
commands.spawn(SpriteSheetBundle {
transform: Transform {
translation: convert_to_world(
layer_info.px_width,
layer_info.px_height,
extra_ent_defs.__height,
TILE_SCALE,
entity.grid[0] as i32 * layer_info.grid_cell_size,
entity.grid[1] as i32 * layer_info.grid_cell_size,
layer_info.z_index,
),
scale: Vec3::splat(extra_ent_defs.__scale * TILE_SCALE),
..Default::default()
},
sprite: TextureAtlasSprite::new(extra_ent_defs.__tile_id as u32),
texture_atlas: handle,
..Default::default()
});
}
None => {
// process color shape
let handle: Handle<ColorMaterial> =
visual_assets.entity_materials[&(entity.def_uid as i32)].clone();
commands.spawn(SpriteBundle {
material: handle,
sprite: Sprite::new(Vec2::new(
extra_ent_defs.__width as f32,
extra_ent_defs.__height as f32,
)),
transform: Transform {
translation: convert_to_world(
layer_info.px_width,
layer_info.px_height,
extra_ent_defs.__height,
TILE_SCALE,
entity.grid[0] as i32 * layer_info.grid_cell_size,
entity.grid[1] as i32 * layer_info.grid_cell_size,
layer_info.z_index,
),
scale: Vec3::splat(TILE_SCALE),
..Default::default()
},
..Default::default()
});
}
}
}
fn display_color(
layer_info: LayerInfo,
tile: &IntGridValueInstance,
commands: &mut Commands,
handle: Handle<ColorMaterial>,
) {
let x = tile.coord_id as i32 % layer_info.grid_width;
let y = tile.coord_id as i32 / layer_info.grid_width;
commands.spawn(SpriteBundle {
material: handle,
sprite: Sprite::new(Vec2::new(
layer_info.grid_cell_size as f32,
layer_info.grid_cell_size as f32,
)),
transform: Transform {
translation: convert_to_world(
layer_info.px_width,
layer_info.px_height,
layer_info.grid_cell_size,
TILE_SCALE,
x * layer_info.grid_cell_size,
y as i32 * layer_info.grid_cell_size,
layer_info.z_index,
),
scale: Vec3::splat(TILE_SCALE),
..Default::default()
},
..Default::default()
});
}
// LDtk provides pixel locations starting in the top left. For Bevy we need to
// flip the Y axis and offset from the center of the screen.
fn convert_to_world(
width: f32,
height: f32,
grid_size: i32,
scale: f32,
x: i32,
y: i32,
z: i32,
) -> Vec3 {
let world_x = (x as f32 * scale) + (grid_size as f32 * scale / 2.) - (width / 2.);
let world_y = -(y as f32 * scale) - (grid_size as f32 * scale / 2.) + (height / 2.);
let world_z = z as f32;
Vec3::new(world_x, world_y, world_z)
}
// Bevy doesn't have sprite flipping built in, so if tile needs to flip
// on either axis, flip it
fn flip(x: bool, y: bool) -> Quat {
let mut q1 = Quat::default();
let mut q2 = Quat::default();
if x {
q1 = Quat::from_rotation_y(std::f32::consts::PI);
}
if y {
q2 = Quat::from_rotation_x(std::f32::consts::PI);
}
q1 * q2
}