Skip to content
Draft

Tilemaps #21756

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,17 @@ category = "2D Rendering"
# Loading asset folders is not supported in Wasm, but required to create the atlas.
wasm = false

[[example]]
name = "tilemap_entities"
path = "examples/2d/tilemap_entities.rs"
doc-scrape-examples = true

[package.metadata.example.tilemap_entities]
name = "Tilemap Entities"
description = "Renders a tilemap where each tile is an entity"
category = "2D Rendering"
wasm = true

[[example]]
name = "tilemap_chunk"
path = "examples/2d/tilemap_chunk.rs"
Expand All @@ -1028,6 +1039,17 @@ description = "Renders a tilemap chunk"
category = "2D Rendering"
wasm = true

[[example]]
name = "tilemap"
path = "examples/2d/tilemap.rs"
doc-scrape-examples = true

[package.metadata.example.tilemap]
name = "Tilemap"
description = "Renders a tilemap"
category = "2D Rendering"
wasm = true

[[example]]
name = "transparency_2d"
path = "examples/2d/transparency_2d.rs"
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_sprite/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ bevy_camera = { path = "../bevy_camera", version = "0.18.0-dev" }
bevy_mesh = { path = "../bevy_mesh", version = "0.18.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.18.0-dev" }
bevy_picking = { path = "../bevy_picking", version = "0.18.0-dev", optional = true }
bevy_platform = { path = "../bevy_platform", version = "0.18.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.18.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.18.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.18.0-dev", optional = true }
bevy_derive = { path = "../bevy_derive", version = "0.18.0-dev" }
bevy_text = { path = "../bevy_text", version = "0.18.0-dev", optional = true }

variadics_please = { version = "1.1", default-features = false }

# other
radsort = "0.1"
tracing = { version = "0.1", default-features = false, features = ["std"] }
Expand Down
5 changes: 4 additions & 1 deletion crates/bevy_sprite/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![forbid(unsafe_code)]
#![doc(
html_logo_url = "https://bevy.org/assets/icon.png",
html_favicon_url = "https://bevy.org/assets/icon.png"
Expand All @@ -16,6 +15,7 @@ mod sprite;
#[cfg(feature = "bevy_text")]
mod text2d;
mod texture_slice;
mod tilemap;

/// The sprite prelude.
///
Expand Down Expand Up @@ -50,6 +50,7 @@ pub use sprite::*;
#[cfg(feature = "bevy_text")]
pub use text2d::*;
pub use texture_slice::*;
pub use tilemap::*;

use bevy_app::prelude::*;
use bevy_asset::prelude::AssetChanged;
Expand Down Expand Up @@ -94,6 +95,8 @@ impl Plugin for SpritePlugin {
.after(bevy_app::AnimationSystems),
);

app.add_plugins(TilemapPlugin);

#[cfg(feature = "bevy_picking")]
app.add_plugins(SpritePickingPlugin);
}
Expand Down
180 changes: 180 additions & 0 deletions crates/bevy_sprite/src/tilemap/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
use crate::{
tilemap::{TileStorage, Tilemap},
TileStorages,
};
use bevy_ecs::{
entity::Entity,
hierarchy::ChildOf,
system::{Command, Commands},
world::World,
};
use bevy_math::{IVec2, Vec2, Vec3};
use bevy_transform::components::Transform;

pub trait CommandsTilemapExt {
fn set_tile<T: Send + Sync + 'static>(
&mut self,
tilemap_id: Entity,
tile_position: IVec2,
maybe_tile: Option<T>,
);

fn remove_tile(&mut self, tilemap_id: Entity, tile_position: IVec2);
}

impl CommandsTilemapExt for Commands<'_, '_> {
fn set_tile<T: Send + Sync + 'static>(
&mut self,
tilemap_id: Entity,
tile_position: IVec2,
maybe_tile: Option<T>,
) {
self.queue(move |world: &mut World| {
SetTile {
tilemap_id,
tile_position,
maybe_tile,
}
.apply(world);
});
}

fn remove_tile(&mut self, tilemap_id: Entity, tile_position: IVec2) {
self.queue(move |world: &mut World| {
RemoveTile {
tilemap_id,
tile_position,
}
.apply(world);
});
}
}

pub struct SetTile<T: Send + Sync + 'static> {
pub tilemap_id: Entity,
pub tile_position: IVec2,
pub maybe_tile: Option<T>,
}

pub struct SetTileResult<T: Send + Sync + 'static> {
pub replaced_tile: Option<T>,
pub chunk_id: Option<Entity>,
}

impl<T: Send + Sync + 'static> Default for SetTileResult<T> {
fn default() -> Self {
Self {
replaced_tile: Default::default(),
chunk_id: Default::default(),
}
}
}

impl<T: Send + Sync + 'static> Command<SetTileResult<T>> for SetTile<T> {
fn apply(self, world: &mut World) -> SetTileResult<T> {
let Ok(mut tilemap_entity) = world.get_entity_mut(self.tilemap_id) else {
tracing::warn!("Could not find Tilemap Entity {:?}", self.tilemap_id);
return Default::default();
};

let Some(tilemap) = tilemap_entity.get::<Tilemap>() else {
tracing::warn!("Could not find Tilemap on Entity {:?}", self.tilemap_id);
return Default::default();
};

let chunk_position = tilemap.tile_chunk_position(self.tile_position);
let tile_relative_position = tilemap.tile_relative_position(self.tile_position);

let chunk_size = tilemap.chunk_size;
let tile_size = tilemap.tile_display_size;

if let Some(tile_storage_id) = tilemap.chunks.get(&chunk_position).cloned() {
let replaced_tile = tilemap_entity.world_scope(move |w| {
let Ok(mut tilestorage_entity) = w.get_entity_mut(tile_storage_id) else {
tracing::warn!("Could not find TileStorage Entity {:?}", tile_storage_id);
return None;
};

let Some(mut tile_storage) = tilestorage_entity.get_mut::<TileStorage<T>>() else {
let mut tile_storage = TileStorage::<T>::new(chunk_size);
tile_storage.set(tile_relative_position, self.maybe_tile);
tilestorage_entity.insert(tile_storage);
return None;
};

tile_storage.set(tile_relative_position, self.maybe_tile)
});
SetTileResult {
chunk_id: Some(tile_storage_id),
replaced_tile,
}
} else {
let tile_storage_id = tilemap_entity.world_scope(move |w| {
let mut tile_storage = TileStorage::<T>::new(chunk_size);
tile_storage.set(tile_relative_position, self.maybe_tile);
let translation = Vec2::new(chunk_size.x as f32, chunk_size.y as f32)
* Vec2::new(tile_size.x as f32, tile_size.y as f32)
* Vec2::new(chunk_position.x as f32, chunk_position.y as f32);
let translation = Vec3::new(translation.x, translation.y, 0.0);
let transform = Transform::from_translation(translation);
w.spawn((ChildOf(self.tilemap_id), tile_storage, transform))
.id()
});
let Some(mut tilemap) = tilemap_entity.get_mut::<Tilemap>() else {
tracing::warn!("Could not find Tilemap on Entity {:?}", self.tilemap_id);
return Default::default();
};
tilemap.chunks.insert(chunk_position, tile_storage_id);
SetTileResult {
chunk_id: Some(tile_storage_id),
replaced_tile: None,
}
}
}
}

pub struct RemoveTile {
pub tilemap_id: Entity,
pub tile_position: IVec2,
}

impl Command for RemoveTile {
fn apply(self, world: &mut World) {
let Ok(mut tilemap_entity) = world.get_entity_mut(self.tilemap_id) else {
tracing::warn!("Could not find Tilemap Entity {:?}", self.tilemap_id);
return;
};

let Some(tilemap) = tilemap_entity.get::<Tilemap>() else {
tracing::warn!("Could not find Tilemap on Entity {:?}", self.tilemap_id);
return;
};

let chunk_position = tilemap.tile_chunk_position(self.tile_position);
let tile_relative_position = tilemap.tile_relative_position(self.tile_position);

if let Some(tile_storage_id) = tilemap.chunks.get(&chunk_position).cloned() {
tilemap_entity.world_scope(move |w| {
let Ok(mut tile_storage_entity) = w.get_entity_mut(tile_storage_id) else {
tracing::warn!("Could not find TileStorage Entity {:?}", tile_storage_id);
return;
};

let Some(tile_storages) = tile_storage_entity.get::<TileStorages>().cloned() else {
tracing::warn!(
"Could not find TileStorages on Entity {:?}",
tile_storage_id
);
return;
};

for (tile_storage, tile_removal) in tile_storages.removals {
let Ok(storage) = tile_storage_entity.get_mut_by_id(tile_storage) else {
continue;
};
tile_removal(storage, tile_relative_position);
}
});
}
}
}
132 changes: 132 additions & 0 deletions crates/bevy_sprite/src/tilemap/entity_tiles.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
use bevy_app::{App, Plugin};
use bevy_derive::Deref;
use bevy_ecs::{
component::Component,
entity::Entity,
hierarchy::ChildOf,
lifecycle::HookContext,
system::Command,
world::{DeferredWorld, World},
};
use bevy_math::IVec2;
use tracing::warn;

use crate::{RemoveTile, SetTile, SetTileResult};

/// Plugin that handles the initialization and updating of tilemap chunks.
/// Adds systems for processing newly added tilemap chunks.
pub struct EntityTilePlugin;

impl Plugin for EntityTilePlugin {
fn build(&self, app: &mut App) {
app.world_mut()
.register_component_hooks::<TileCoord>()
.on_insert(on_insert_entity_tile)
.on_remove(on_remove_entity_tile);
app.world_mut()
.register_component_hooks::<InMap>()
.on_remove(on_remove_entity_tile);
}
}

/// An Entity in the tilemap
#[derive(Component, Clone, Debug, Deref)]
pub struct EntityTile(pub Entity);

#[derive(Component, Clone, Debug, Deref)]
#[component(immutable)]
pub struct InMap(pub Entity);

#[derive(Component, Clone, Debug, Deref)]
#[component(immutable)]
pub struct TileCoord(pub IVec2);

impl TileCoord {
/// Iterate through the non-diagonal adjacent tiles to this coord
pub fn adjacent(&self) -> impl Iterator<Item = TileCoord> + use<> {
[
TileCoord(IVec2::new(self.x + 1, self.y)),
TileCoord(IVec2::new(self.x, self.y + 1)),
TileCoord(IVec2::new(self.x, self.y - 1)),
TileCoord(IVec2::new(self.x - 1, self.y)),
]
.into_iter()
}
}

#[derive(Component, Clone, Debug)]
pub struct DespawnOnRemove;

fn on_insert_entity_tile(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
let Ok(tile) = world.get_entity(entity) else {
warn!("Tile {} not found", entity);
return;
};
let Some(in_map) = tile.get::<InMap>().cloned() else {
warn!("Tile {} is not in a TileMap", entity);
return;
};
let Some(tile_position) = tile.get::<TileCoord>().cloned() else {
warn!("Tile {} has no tile coord.", entity);
return;
};

world.commands().queue(move |world: &mut World| {
let SetTileResult {
chunk_id: Some(chunk_id),
replaced_tile,
} = SetTile {
tilemap_id: in_map.0,
tile_position: tile_position.0,
maybe_tile: Some(EntityTile(entity)),
}
.apply(world)
else {
warn!("Could not create chunk to place Tile {} entity.", entity);
return;
};

world.entity_mut(entity).insert(ChildOf(chunk_id));

if let Some(replaced_tile) = replaced_tile {
let mut replaced_tile = world.entity_mut(replaced_tile.0);
if replaced_tile.contains::<DespawnOnRemove>() {
replaced_tile.despawn();
} else {
replaced_tile.remove::<(InMap, TileCoord)>();
}
}
});
}

fn on_remove_entity_tile(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
let Ok(tile) = world.get_entity(entity) else {
warn!("Tile {} not found", entity);
return;
};
let Some(in_map) = tile.get::<InMap>().cloned() else {
warn!("Tile {} is not in a TileMap", entity);
return;
};
let Some(tile_position) = tile.get::<TileCoord>().cloned() else {
warn!("Tile {} has no tile coord.", entity);
return;
};

world.commands().queue(move |world: &mut World| {
RemoveTile {
tilemap_id: in_map.0,
tile_position: tile_position.0,
}
.apply(world);

let Ok(mut removed) = world.get_entity_mut(entity) else {
return;
};
if removed.contains::<DespawnOnRemove>() {
removed.despawn();
} else {
removed.remove::<InMap>();
}
});
}
Loading