render: create TileMap with a 'RelativeToTile' component to position entities along the grid

This commit is contained in:
SeanOMik 2024-11-29 22:01:17 -05:00 committed by SeanOMik
parent 4afd518f45
commit fa6511bff1
7 changed files with 338 additions and 42 deletions

1
Cargo.lock generated
View File

@ -861,6 +861,7 @@ dependencies = [
"anyhow", "anyhow",
"async-std", "async-std",
"lyra-engine", "lyra-engine",
"rand",
"tracing", "tracing",
] ]

View File

@ -9,6 +9,9 @@ pub use texture_atlas::*;
mod animation_sheet; mod animation_sheet;
pub use animation_sheet::*; pub use animation_sheet::*;
mod tilemap;
pub use tilemap::*;
/// How the sprite is positioned and rotated relative to its [`Transform`]. /// How the sprite is positioned and rotated relative to its [`Transform`].
/// ///
/// Default pivot is `Pivot::Center`, this makes it easier to rotate the sprites. /// Default pivot is `Pivot::Center`, this makes it easier to rotate the sprites.

View File

@ -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<Entity>,
}
#[derive(Clone, Component, Reflect)]
struct Layer {
tiles: Vec<TileInstance>,
level: u32,
}
#[derive(Clone, Component, Reflect)]
pub struct TileMap {
pub atlas: ResHandle<TextureAtlas>,
/// 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<Layer>,
}
impl TileMap {
pub fn new(
atlas: ResHandle<TextureAtlas>,
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,
&[],
);
}
}

View File

@ -8,3 +8,4 @@ lyra-engine = { path = "../../", features = ["tracy"] }
anyhow = "1.0.75" anyhow = "1.0.75"
async-std = "1.12.0" async-std = "1.12.0"
tracing = "0.1.37" tracing = "0.1.37"
rand = "0.8.5"

View File

@ -1,13 +1,15 @@
use lyra_engine::{ use lyra_engine::{
assets::{Image, ResourceManager}, assets::{Image, ResourceManager},
ecs::query::{Res, ResMut, View}, ecs::{
query::{Res, ResMut, View}, Commands, Component, Entity, World
},
game::App, game::App,
gltf::Gltf, gltf::Gltf,
input::{ input::{
Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource, Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource,
InputActionPlugin, KeyCode, LayoutId, InputActionPlugin, KeyCode, LayoutId,
}, },
math::{self, Rect, Transform, URect, UVec2, Vec2, Vec3}, math::{self, Rect, Transform, URect, UVec2, UVec3, Vec2, Vec3},
render::light::directional::DirectionalLight, render::light::directional::DirectionalLight,
scene::{ scene::{
system_update_world_transforms, Camera2dBundle, CameraProjection, OrthographicProjection, 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_LOOK_ROLL, ACTLBL_LOOK_UP_DOWN, ACTLBL_MOVE_FORWARD_BACKWARD,
ACTLBL_MOVE_LEFT_RIGHT, ACTLBL_MOVE_UP_DOWN, 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, DeltaTime,
}; };
use rand::{
distributions::{Distribution, WeightedIndex},
Rng,
};
use tracing::debug; use tracing::debug;
#[async_std::main] #[async_std::main]
@ -95,42 +104,35 @@ async fn main() {
a.with_plugin(lyra_engine::DefaultPlugins) a.with_plugin(lyra_engine::DefaultPlugins)
.with_plugin(setup_scene_plugin) .with_plugin(setup_scene_plugin)
.with_plugin(action_handler_plugin) .with_plugin(action_handler_plugin)
.with_plugin(TileMapPlugin)
//.with_plugin(camera_debug_plugin) //.with_plugin(camera_debug_plugin)
.with_plugin(TopDown2dCameraPlugin) .with_plugin(TopDown2dCameraPlugin)
.with_system( .with_system(
"system_update_world_transforms", "update_world_transforms",
system_update_world_transforms, system_update_world_transforms,
&[], &[],
).with_system(
"spawn_egg",
system_spawn_egg,
&[],
).with_system(
"egg_location",
system_egg_location,
&[],
); );
a.run(); a.run();
} }
fn setup_scene_plugin(app: &mut App) { fn setup_scene_plugin(app: &mut App) {
//app.add_resource(Timer(0.0)); /* app.with_system(
//app.with_system("sprite_change", sprite_change, &[]);
app.with_system(
"sprite_atlas_animation", "sprite_atlas_animation",
sprite::system_sprite_atlas_animation, sprite::system_sprite_atlas_animation,
&[], &[],
); ); */
let world = &mut app.world; let world = &mut app.world;
let resman = world.get_resource_mut::<ResourceManager>().unwrap(); let resman = world.get_resource_mut::<ResourceManager>().unwrap();
/* let camera_gltf = resman
.request::<Gltf>("../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 let cube_gltf = resman
.request::<Gltf>("../assets/cube-texture-embedded.gltf") .request::<Gltf>("../assets/cube-texture-embedded.gltf")
.unwrap(); .unwrap();
@ -138,33 +140,48 @@ fn setup_scene_plugin(app: &mut App) {
cube_gltf.wait_recurse_dependencies_load().unwrap(); cube_gltf.wait_recurse_dependencies_load().unwrap();
let cube_mesh = &cube_gltf.data_ref().unwrap().scenes[0]; let cube_mesh = &cube_gltf.data_ref().unwrap().scenes[0];
let image = resman.request::<Image>("../assets/Egg_item.png").unwrap(); let grass_tileset = resman
image.wait_recurse_dependencies_load().unwrap(); .request::<Image>("../assets/sprout_lands/Tilesets/Grass.png")
let soldier = resman
.request::<Image>(
"../assets/tiny_rpg_characters/Characters(100x100)/Soldier/Soldier/Soldier.png",
)
.unwrap(); .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( let atlas = resman.store_new(TextureAtlas::from_grid(
soldier, grass_tileset,
UVec2::new(9, 7), UVec2::new(11, 7),
UVec2::new(100, 100), tile_size,
)); ));
let animations = AtlasAnimations::new(atlas, &[("soldier_run", 0.1, 9..=16)]); let map_size = UVec2::new(32, 16);
let run_anim = animations.get_active("soldier_run"); let mut tilemap = TileMap::new(atlas, map_size, 1, tile_size);
let animations = resman.store_new(animations);
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); drop(resman);
world.spawn(( let en = world.spawn((
animations, tilemap,
run_anim,
WorldTransform::default(), WorldTransform::default(),
Transform::from_xyz(0.0, 0.0, -10.0), 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); 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 { Camera2dBundle {
projection: CameraProjection::Orthographic(OrthographicProjection { projection: CameraProjection::Orthographic(OrthographicProjection {
scale_mode: ScaleMode::Height(180.0), scale_mode: ScaleMode::Height(180.0),
scale: 2.0,
..Default::default() ..Default::default()
}), }),
..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 { TopDown2dCamera {
zoom_speed: Some(0.2), zoom_speed: Some(0.2),
speed: 14.0, speed: 34.0,
min_zoom: 10.0,
..Default::default() ..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<ActionHandler>,
tile_map: Res<GroundTileMap>,
resman: Res<ResourceManager>,
) -> anyhow::Result<()> {
let debug_state = inputs.get_action_state("Debug").unwrap();
if inputs.was_action_just_pressed("Debug").unwrap() {
let egg = resman
.request::<Image>("../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(())
}

View File

@ -0,0 +1,3 @@
*
!.gitignore
!source.txt

View File

@ -0,0 +1 @@
https://cupnooble.itch.io/sprout-lands-asset-pack