Skip to content
Draft
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 @@ -984,6 +984,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 @@ -995,6 +1006,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
1 change: 1 addition & 0 deletions crates/bevy_sprite/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ 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 }
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_sprite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mod sprite;
#[cfg(feature = "bevy_text")]
mod text2d;
mod texture_slice;
mod tilemap;

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

use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
Expand Down
150 changes: 150 additions & 0 deletions crates/bevy_sprite/src/tilemap/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use std::marker::PhantomData;

use crate::tilemap::{TileData, TileStorage, Tilemap};
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: TileData>(
&mut self,
tilemap_id: Entity,
tile_position: IVec2,
maybe_tile: Option<T>,
);

fn remove_tile<T: TileData>(&mut self, tilemap_id: Entity, tile_position: IVec2);
}

impl CommandsTilemapExt for Commands<'_, '_> {
fn set_tile<T: TileData>(
&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<T: TileData>(&mut self, tilemap_id: Entity, tile_position: IVec2) {
self.queue(move |world: &mut World| {RemoveTile::<T> {tilemap_id, tile_position, _t: PhantomData::default() }.apply(world);});
}
}

pub struct SetTile<T: TileData> {
pub tilemap_id: Entity,
pub tile_position: IVec2,
pub maybe_tile: Option<T>,
}

pub struct SetTileResult<T: TileData> {
pub replaced_tile: Option<T>,
pub chunk_id: Option<Entity>,
}

impl<T: TileData> Default for SetTileResult<T> {
fn default() -> Self {
Self { replaced_tile: Default::default(), chunk_id: Default::default() }
}
}

impl<T: TileData> 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);

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 {
tracing::warn!(
"Could not find TileStorage on Entity {:?}",
tile_storage_id
);
return None;
};

tile_storage.set(tile_relative_position, self.maybe_tile)
});
SetTileResult { chunk_id: Some(tile_storage_id), replaced_tile }
} else {
let chunk_size = tilemap.chunk_size;
let tile_size = tilemap.tile_display_size;
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<T: TileData> {
pub tilemap_id: Entity,
pub tile_position: IVec2,
pub _t: PhantomData<T>
}

impl<T: TileData> Command<Option<T>> for RemoveTile<T> {
fn apply(self, world: &mut World) -> Option<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);

if let Some(tile_storage_id) = tilemap.chunks.get(&chunk_position).cloned() {
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 {
tracing::warn!(
"Could not find TileStorage on Entity {:?}",
tile_storage_id
);
return None;
};

tile_storage.remove(tile_relative_position)
})
}
else {
None
}
}
}
112 changes: 112 additions & 0 deletions crates/bevy_sprite/src/tilemap/entity_tiles.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use std::marker::PhantomData;

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, TileData};

/// 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
pub struct EntityTile(pub Entity);

impl TileData for EntityTile {

}

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

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

#[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| {
let Some(removed) = RemoveTile::<EntityTile> {
tilemap_id: in_map.0,
tile_position: tile_position.0,
_t: PhantomData,
}.apply(world) else {
warn!("Tile {} could not be removed from map or was already removed.", entity);
return;
};

let mut removed = world.entity_mut(removed.0);
if removed.contains::<DespawnOnRemove>() {
removed.despawn();
} else {
removed.remove::<InMap>();
}
});
}
Loading
Loading