From fa6511bff1e49b3095482c09e09b92b39018bb5b Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Fri, 29 Nov 2024 22:01:17 -0500 Subject: [PATCH] render: create TileMap with a 'RelativeToTile' component to position entities along the grid --- Cargo.lock | 1 + crates/lyra-game/src/sprite/mod.rs | 3 + crates/lyra-game/src/sprite/tilemap.rs | 215 ++++++++++++++++++++++++ examples/2d/Cargo.toml | 3 +- examples/2d/src/main.rs | 154 ++++++++++++----- examples/assets/sprout_lands/.gitignore | 3 + examples/assets/sprout_lands/source.txt | 1 + 7 files changed, 338 insertions(+), 42 deletions(-) create mode 100644 crates/lyra-game/src/sprite/tilemap.rs create mode 100644 examples/assets/sprout_lands/.gitignore create mode 100644 examples/assets/sprout_lands/source.txt diff --git a/Cargo.lock b/Cargo.lock index e456e5a..883fc6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -861,6 +861,7 @@ dependencies = [ "anyhow", "async-std", "lyra-engine", + "rand", "tracing", ] diff --git a/crates/lyra-game/src/sprite/mod.rs b/crates/lyra-game/src/sprite/mod.rs index bc30f73..2433352 100644 --- a/crates/lyra-game/src/sprite/mod.rs +++ b/crates/lyra-game/src/sprite/mod.rs @@ -9,6 +9,9 @@ pub use texture_atlas::*; mod animation_sheet; pub use animation_sheet::*; +mod tilemap; +pub use tilemap::*; + /// How the sprite is positioned and rotated relative to its [`Transform`]. /// /// Default pivot is `Pivot::Center`, this makes it easier to rotate the sprites. diff --git a/crates/lyra-game/src/sprite/tilemap.rs b/crates/lyra-game/src/sprite/tilemap.rs new file mode 100644 index 0000000..8ce9f63 --- /dev/null +++ b/crates/lyra-game/src/sprite/tilemap.rs @@ -0,0 +1,215 @@ +use std::collections::VecDeque; + +use glam::{UVec2, UVec3, Vec2, Vec3}; +use lyra_ecs::{ + query::{Entities, View}, + relation::ChildOf, + Commands, Component, Entity, World, +}; +use lyra_math::Transform; +use lyra_reflect::Reflect; +use lyra_resource::ResHandle; +use lyra_scene::WorldTransform; +use tracing::{debug, error}; + +use crate::{game::GameStages, plugin::Plugin}; + +use super::{AtlasSprite, TextureAtlas}; + +/// A position relative to a tile on a tilemap +#[derive(Clone, Copy, Component, Reflect)] +pub struct RelativeToTile { + #[reflect(skip)] // TODO: impl reflect for Entity + pub tilemap_entity: Entity, + /// The position of the tile to spawn at. + pub position: UVec2, + pub z_level: i32, +} + +#[derive(Clone, Copy, Component, Reflect)] +pub struct Tile { + /// The index in the atlas for the tile. + pub atlas_index: u32, + /// The tile position in the map. + pub position: UVec2, + pub z_level: i32, +} + +#[derive(Clone, Component, Reflect)] +struct TileInstance { + tile: Tile, + #[reflect(skip)] // TODO: impl reflect for Entity + entity: Option, +} + +#[derive(Clone, Component, Reflect)] +struct Layer { + tiles: Vec, + level: u32, +} + +#[derive(Clone, Component, Reflect)] +pub struct TileMap { + pub atlas: ResHandle, + /// The size of the map in tiles. + /// + /// The Z-axis is used to specify the amount of layers in the map. + pub size: UVec3, + /// Dimensions of each tile. + pub tile_size: UVec2, + layers: Vec, +} + +impl TileMap { + pub fn new( + atlas: ResHandle, + map_size: UVec2, + layer_num: u32, + tile_size: UVec2, + ) -> Self { + let size = map_size.extend(layer_num); + + let layer_size = map_size.element_product(); // x*y + let mut layers = Vec::with_capacity(layer_num as _); + for i in 0..layer_num { + layers.push(Layer { + tiles: Vec::with_capacity(layer_size as _), + level: i, + }); + } + + Self { + atlas, + size, + tile_size, + layers, + } + } + + pub fn insert_tile(&mut self, layer: u32, atlas_index: u32, x: u32, y: u32) { + let l = &mut self.layers[layer as usize]; + l.tiles.push(TileInstance { + tile: Tile { + atlas_index, + position: UVec2::new(x, y), + z_level: 0, + }, + entity: None, + }); + } + + /// Get the relative position of a tile + pub fn position_of(&self, x: u32, y: u32) -> Vec2 { + Vec2::new(x as _, y as _) * self.tile_size.as_vec2() + } +} + +/// A system to update the tilemap when tiles are inserted/removed. +pub fn system_tilemap_update( + mut commands: Commands, + view: View<(&mut TileMap, &Transform)>, +) -> anyhow::Result<()> { + for (mut map, pos) in view.into_iter() { + let tile_size = map.tile_size; + let atlas_handle = map.atlas.clone(); + let atlas = match atlas_handle.data_ref() { + Some(a) => a, + None => continue, + }; + + for layer in &mut map.layers { + for tile in &mut layer.tiles { + if tile.entity.is_none() { + if let Some(frame) = atlas.frames.get(tile.tile.atlas_index as usize) { + let sprite = AtlasSprite { + atlas: atlas_handle.clone(), + sprite: *frame, + pivot: super::Pivot::TopLeft, + }; + + let grid = tile.tile.position * tile_size; + let sprite_pos = *pos + + Transform::from_xyz( + grid.x as _, + grid.y as _, + tile.tile.z_level as _, + ); + + tile.entity = Some(commands.spawn((sprite, sprite_pos, WorldTransform::default()))); + debug!("Spawned tile at ({}, {})", grid.x, grid.y); + } else { + error!( + "Invalid atlas index '{}' for tile at pos '{:?}'", + tile.tile.atlas_index, tile.tile.position + ); + } + } + } + } + } + + Ok(()) +} + +fn system_relative_tile_position_update( + world: &mut World, + view: View<(Entities, &RelativeToTile)>, +) -> anyhow::Result<()> { + let mut to_relate = VecDeque::new(); + + for (en, rel) in view.into_iter() { + match world + .view_one::<(&TileMap, &Transform)>(rel.tilemap_entity) + .get() + { + Some((map, pos)) => { + let layer = map.layers.last().unwrap(); + + if let Some(tile_en) = layer + .tiles + .iter() + .find(|t| t.tile.position == rel.position) + .and_then(|t| t.entity) + { + to_relate.push_back((en, tile_en, Transform::from_translation(Vec3::new(0.0, 0.0, rel.z_level as _)))); + } + } + None => { + error!( + "Unknown tilemap for relative tile position of {:?}", + rel.position + ); + } + } + } + + while let Some((from, to, pos)) = to_relate.pop_front() { + if world.view_one::<&WorldTransform>(from).get().is_none() { + world.add_relation(from, ChildOf, to); + world.insert(from, (pos, WorldTransform::default())); + } + } + + Ok(()) +} + +#[derive(Default)] +pub struct TileMapPlugin; + +impl Plugin for TileMapPlugin { + fn setup(&mut self, app: &mut crate::game::App) { + // insert in postupdate to apply changes that other systems may have made to the tilemap + app.add_system_to_stage( + GameStages::PostUpdate, + "tilemap_update", + system_tilemap_update, + &[], + ); + app.add_system_to_stage( + GameStages::PostUpdate, + "relative_tile_position_update", + system_relative_tile_position_update, + &[], + ); + } +} diff --git a/examples/2d/Cargo.toml b/examples/2d/Cargo.toml index 0e9182c..ecbd39a 100644 --- a/examples/2d/Cargo.toml +++ b/examples/2d/Cargo.toml @@ -7,4 +7,5 @@ edition = "2021" lyra-engine = { path = "../../", features = ["tracy"] } anyhow = "1.0.75" async-std = "1.12.0" -tracing = "0.1.37" \ No newline at end of file +tracing = "0.1.37" +rand = "0.8.5" diff --git a/examples/2d/src/main.rs b/examples/2d/src/main.rs index d0aaaf4..b641771 100644 --- a/examples/2d/src/main.rs +++ b/examples/2d/src/main.rs @@ -1,13 +1,15 @@ use lyra_engine::{ assets::{Image, ResourceManager}, - ecs::query::{Res, ResMut, View}, + ecs::{ + query::{Res, ResMut, View}, Commands, Component, Entity, World + }, game::App, gltf::Gltf, input::{ Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource, InputActionPlugin, KeyCode, LayoutId, }, - math::{self, Rect, Transform, URect, UVec2, Vec2, Vec3}, + math::{self, Rect, Transform, URect, UVec2, UVec3, Vec2, Vec3}, render::light::directional::DirectionalLight, scene::{ system_update_world_transforms, Camera2dBundle, CameraProjection, OrthographicProjection, @@ -15,9 +17,16 @@ use lyra_engine::{ ACTLBL_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN, ACTLBL_MOVE_FORWARD_BACKWARD, ACTLBL_MOVE_LEFT_RIGHT, ACTLBL_MOVE_UP_DOWN, }, - sprite::{self, AtlasAnimations, AtlasSprite, Pivot, Sprite, SpriteAnimation, TextureAtlas}, + sprite::{ + self, AtlasAnimations, AtlasSprite, Pivot, RelativeToTile, Sprite, SpriteAnimation, + TextureAtlas, TileMap, TileMapPlugin, + }, DeltaTime, }; +use rand::{ + distributions::{Distribution, WeightedIndex}, + Rng, +}; use tracing::debug; #[async_std::main] @@ -95,42 +104,35 @@ async fn main() { a.with_plugin(lyra_engine::DefaultPlugins) .with_plugin(setup_scene_plugin) .with_plugin(action_handler_plugin) + .with_plugin(TileMapPlugin) //.with_plugin(camera_debug_plugin) .with_plugin(TopDown2dCameraPlugin) .with_system( - "system_update_world_transforms", + "update_world_transforms", system_update_world_transforms, &[], + ).with_system( + "spawn_egg", + system_spawn_egg, + &[], + ).with_system( + "egg_location", + system_egg_location, + &[], ); a.run(); } fn setup_scene_plugin(app: &mut App) { - //app.add_resource(Timer(0.0)); - //app.with_system("sprite_change", sprite_change, &[]); - app.with_system( + /* app.with_system( "sprite_atlas_animation", sprite::system_sprite_atlas_animation, &[], - ); + ); */ let world = &mut app.world; let resman = world.get_resource_mut::().unwrap(); - /* let camera_gltf = resman - .request::("../assets/AntiqueCamera.glb") - .unwrap(); - - camera_gltf.wait_recurse_dependencies_load(); - let camera_mesh = &camera_gltf.data_ref().unwrap().scenes[0]; - drop(resman); - - world.spawn(( - camera_mesh.clone(), - WorldTransform::default(), - Transform::from_xyz(0.0, -5.0, -2.0), - )); */ - let cube_gltf = resman .request::("../assets/cube-texture-embedded.gltf") .unwrap(); @@ -138,33 +140,48 @@ fn setup_scene_plugin(app: &mut App) { cube_gltf.wait_recurse_dependencies_load().unwrap(); let cube_mesh = &cube_gltf.data_ref().unwrap().scenes[0]; - let image = resman.request::("../assets/Egg_item.png").unwrap(); - image.wait_recurse_dependencies_load().unwrap(); - - let soldier = resman - .request::( - "../assets/tiny_rpg_characters/Characters(100x100)/Soldier/Soldier/Soldier.png", - ) + let grass_tileset = resman + .request::("../assets/sprout_lands/Tilesets/Grass.png") .unwrap(); - soldier.wait_recurse_dependencies_load().unwrap(); + grass_tileset.wait_recurse_dependencies_load().unwrap(); + let tile_size = UVec2::new(16, 16); let atlas = resman.store_new(TextureAtlas::from_grid( - soldier, - UVec2::new(9, 7), - UVec2::new(100, 100), + grass_tileset, + UVec2::new(11, 7), + tile_size, )); - let animations = AtlasAnimations::new(atlas, &[("soldier_run", 0.1, 9..=16)]); - let run_anim = animations.get_active("soldier_run"); - let animations = resman.store_new(animations); + let map_size = UVec2::new(32, 16); + let mut tilemap = TileMap::new(atlas, map_size, 1, tile_size); + + let textures = [ + 12, // flat grass + 55, // tall grass + 56, // small two grass + 57, // small three grass + 58, // water puddle + 60, // three flower + ]; + let weights = [80, 15, 20, 20, 2, 10]; + + let dist = WeightedIndex::new(&weights).unwrap(); + let mut rng = rand::thread_rng(); + + for y in 0..map_size.y { + for x in 0..map_size.x { + let tex = textures[dist.sample(&mut rng)]; + tilemap.insert_tile(0, tex as _, x, y); + } + } drop(resman); - world.spawn(( - animations, - run_anim, + let en = world.spawn(( + tilemap, WorldTransform::default(), Transform::from_xyz(0.0, 0.0, -10.0), )); + world.add_resource(GroundTileMap(en)); { let mut light_tran = Transform::from_xyz(1.5, 2.5, 0.0); @@ -185,15 +202,70 @@ fn setup_scene_plugin(app: &mut App) { Camera2dBundle { projection: CameraProjection::Orthographic(OrthographicProjection { scale_mode: ScaleMode::Height(180.0), + scale: 2.0, ..Default::default() }), ..Default::default() }, - Transform::from_xyz(0.0, 0.0, 0.0), + Transform::from_xyz( + (map_size.x * tile_size.x) as f32 * 0.5, + (map_size.y * tile_size.y) as f32 * 0.5, + 0.0, + ), TopDown2dCamera { zoom_speed: Some(0.2), - speed: 14.0, + speed: 34.0, + min_zoom: 10.0, ..Default::default() }, )); } + +struct GroundTileMap(Entity); + +#[derive(Component)] +struct EggEntity; + +fn system_egg_location(view: View<(&WorldTransform, &EggEntity)>) -> anyhow::Result<()> { + for (pos, _) in view.into_iter() { + println!("Found egg at world pos {:?}", **pos); + } + + Ok(()) +} + +fn system_spawn_egg( + mut commands: Commands, + inputs: Res, + tile_map: Res, + resman: Res, +) -> anyhow::Result<()> { + let debug_state = inputs.get_action_state("Debug").unwrap(); + + if inputs.was_action_just_pressed("Debug").unwrap() { + let egg = resman + .request::("../assets/sprout_lands/Objects/Egg_item.png") + .unwrap(); + + let x = rand::thread_rng().gen_range(0..32); + let y = rand::thread_rng().gen_range(0..16); + + let rtt = RelativeToTile { + tilemap_entity: tile_map.0, + position: UVec2::new(x, y), + z_level: -9, + }; + + commands.spawn(( + Sprite { + texture: egg, + color: Vec3::ONE, + pivot: Pivot::TopLeft, + }, + rtt, + EggEntity + )); + } + + Ok(()) +} diff --git a/examples/assets/sprout_lands/.gitignore b/examples/assets/sprout_lands/.gitignore new file mode 100644 index 0000000..3de37e8 --- /dev/null +++ b/examples/assets/sprout_lands/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!source.txt \ No newline at end of file diff --git a/examples/assets/sprout_lands/source.txt b/examples/assets/sprout_lands/source.txt new file mode 100644 index 0000000..d3e346c --- /dev/null +++ b/examples/assets/sprout_lands/source.txt @@ -0,0 +1 @@ +https://cupnooble.itch.io/sprout-lands-asset-pack