diff --git a/src/ecs/interact.rs b/src/ecs/interact.rs new file mode 100644 index 0000000..366ff17 --- /dev/null +++ b/src/ecs/interact.rs @@ -0,0 +1,44 @@ +use bevy_ecs::{ + event::{EventReader, EventWriter}, + query::With, + system::{Query, ResMut}, +}; + +use crate::{ + ecs::{GlobalState, PlayerControlled, Velocity}, + error::GameError, + game::events::GameEvent, + input::commands::GameCommand, +}; + +// Handles +pub fn interact_system( + mut events: EventReader, + mut state: ResMut, + mut players: Query<(&PlayerControlled, &mut Velocity)>, + mut errors: EventWriter, +) { + // Get the player's velocity (handling to ensure there is only one player) + let mut velocity = match players.single_mut() { + Ok((_, velocity)) => velocity, + Err(e) => { + errors.write(GameError::InvalidState(format!("Player not found: {}", e)).into()); + return; + } + }; + + // Handle events + for event in events.read() { + match event { + GameEvent::Command(command) => match command { + GameCommand::MovePlayer(direction) => { + velocity.direction = *direction; + } + GameCommand::Exit => { + state.exit = true; + } + _ => {} + }, + } + } +} diff --git a/src/ecs/mod.rs b/src/ecs/mod.rs index 90f1d92..4ab9508 100644 --- a/src/ecs/mod.rs +++ b/src/ecs/mod.rs @@ -9,7 +9,11 @@ use glam::Vec2; use crate::{ entity::{direction::Direction, graph::Graph, traversal}, error::{EntityError, GameResult}, - texture::{directional::DirectionalAnimatedTexture, sprite::Sprite}, + texture::{ + animated::AnimatedTexture, + directional::DirectionalAnimatedTexture, + sprite::{AtlasTile, Sprite}, + }, }; /// A tag component for entities that are controlled by the player. @@ -17,12 +21,21 @@ use crate::{ pub struct PlayerControlled; /// A component for entities that have a sprite, with a layer for ordering. +/// +/// This is intended to be modified by other entities allowing animation. #[derive(Component)] pub struct Renderable { - pub sprite: Sprite, + pub sprite: AtlasTile, pub layer: u8, } +/// A component for entities that have a directional animated texture. +#[derive(Component)] +pub struct DirectionalAnimated { + pub textures: [Option; 4], + pub stopped_textures: [Option; 4], +} + /// A unique identifier for a node, represented by its index in the graph's storage. pub type NodeId = usize; @@ -112,7 +125,7 @@ impl Position { #[derive(Default, Component)] pub struct Velocity { pub direction: Direction, - pub speed: f32, + pub speed: Option, } #[derive(Bundle)] @@ -121,6 +134,7 @@ pub struct PlayerBundle { pub position: Position, pub velocity: Velocity, pub sprite: Renderable, + pub directional_animated: DirectionalAnimated, } #[derive(Resource)] @@ -131,4 +145,5 @@ pub struct GlobalState { #[derive(Resource)] pub struct DeltaTime(pub f32); +pub mod interact; pub mod render; diff --git a/src/ecs/render.rs b/src/ecs/render.rs index a3d3e2d..1b86530 100644 --- a/src/ecs/render.rs +++ b/src/ecs/render.rs @@ -1,15 +1,36 @@ -use crate::ecs::{render, Position, Renderable}; -use crate::entity::graph::Graph; +use crate::ecs::{DeltaTime, DirectionalAnimated, Position, Renderable, Velocity}; use crate::error::{EntityError, GameError, TextureError}; use crate::map::builder::Map; -use crate::texture::sprite::{Sprite, SpriteAtlas}; +use crate::texture::sprite::SpriteAtlas; use bevy_ecs::entity::Entity; use bevy_ecs::event::EventWriter; -use bevy_ecs::query::With; use bevy_ecs::system::{NonSendMut, Query, Res}; use sdl2::render::{Canvas, Texture}; use sdl2::video::Window; +/// Updates the directional animated texture of an entity. +pub fn directional_render_system( + dt: Res, + mut renderables: Query<(&Velocity, &mut DirectionalAnimated, &mut Renderable)>, + mut errors: EventWriter, +) { + for (velocity, mut texture, mut renderable) in renderables.iter_mut() { + let texture = if velocity.speed.is_none() { + texture.stopped_textures[velocity.direction.as_usize()].as_mut() + } else { + texture.textures[velocity.direction.as_usize()].as_mut() + }; + + if let Some(texture) = texture { + texture.tick(dt.0); + renderable.sprite = *texture.current_tile(); + } else { + errors.write(TextureError::RenderFailed(format!("Entity has no texture")).into()); + continue; + } + } +} + pub struct MapTextureResource(pub Texture<'static>); pub struct BackbufferResource(pub Texture<'static>); @@ -19,7 +40,7 @@ pub fn render_system( mut backbuffer: NonSendMut, mut atlas: NonSendMut, map: Res, - renderables: Query<(Entity, &Renderable, &Position)>, + mut renderables: Query<(Entity, &mut Renderable, &Position)>, mut errors: EventWriter, ) { // Clear the main canvas first @@ -40,13 +61,18 @@ pub fn render_system( .map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into())); // Render all entities to the backbuffer - for (_, renderable, position) in renderables.iter() { + for (_, mut renderable, position) in renderables.iter_mut() { let pos = position.get_pixel_pos(&map.graph); match pos { Ok(pos) => { + let dest = crate::helpers::centered_with_size( + glam::IVec2::new(pos.x as i32, pos.y as i32), + glam::UVec2::new(renderable.sprite.size.x as u32, renderable.sprite.size.y as u32), + ); + renderable .sprite - .render(backbuffer_canvas, &mut atlas, pos) + .render(backbuffer_canvas, &mut atlas, dest) .err() .map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into())); } diff --git a/src/game/mod.rs b/src/game/mod.rs index 350e3ac..2df3894 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -3,8 +3,9 @@ include!(concat!(env!("OUT_DIR"), "/atlas_data.rs")); use crate::constants::CANVAS_SIZE; -use crate::ecs::render::{render_system, BackbufferResource, MapTextureResource}; -use crate::ecs::{DeltaTime, GlobalState, PlayerBundle, PlayerControlled, Position, Renderable, Velocity}; +use crate::ecs::interact::interact_system; +use crate::ecs::render::{directional_render_system, render_system, BackbufferResource, MapTextureResource}; +use crate::ecs::{DeltaTime, DirectionalAnimated, GlobalState, PlayerBundle, PlayerControlled, Position, Renderable, Velocity}; use crate::entity::direction::Direction; use crate::entity::{graph, traversal}; use crate::error::{GameError, GameResult, TextureError}; @@ -101,18 +102,6 @@ impl Game { let map = Map::new(constants::RAW_BOARD)?; let pacman_start_node = map.start_positions.pacman; - let player = PlayerBundle { - player: PlayerControlled, - position: Position::AtNode(pacman_start_node), - velocity: Velocity::default(), - sprite: Renderable { - sprite: Sprite::new( - SpriteAtlas::get_tile(&atlas, "pacman/full.png") - .ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("pacman/full.png".to_string())))?, - ), - layer: 0, - }, - }; let mut textures = [None, None, None, None]; let mut stopped_textures = [None, None, None, None]; @@ -140,6 +129,21 @@ impl Game { stopped_textures[direction.as_usize()] = Some(AnimatedTexture::new(stopped_tiles, 0.1)?); } + let player = PlayerBundle { + player: PlayerControlled, + position: Position::AtNode(pacman_start_node), + velocity: Velocity::default(), + sprite: Renderable { + sprite: SpriteAtlas::get_tile(&atlas, "pacman/full.png") + .ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("pacman/full.png".to_string())))?, + layer: 0, + }, + directional_animated: DirectionalAnimated { + textures, + stopped_textures, + }, + }; + world.insert_non_send_resource(atlas); world.insert_non_send_resource(event_pump); world.insert_non_send_resource(canvas); @@ -160,7 +164,7 @@ impl Game { }, }); - schedule.add_systems((handle_input, render_system).chain()); + schedule.add_systems((handle_input, interact_system, directional_render_system, render_system).chain()); // Spawn player world.spawn(player); diff --git a/src/input/mod.rs b/src/input/mod.rs index 91c414c..7d1d480 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use bevy_ecs::{ + event::EventWriter, resource::Resource, system::{Commands, NonSendMut, Res}, }; @@ -41,17 +42,17 @@ impl Default for Bindings { } } -pub fn handle_input(bindings: Res, mut commands: Commands, mut pump: NonSendMut<&'static mut EventPump>) { +pub fn handle_input(bindings: Res, mut writer: EventWriter, mut pump: NonSendMut<&'static mut EventPump>) { for event in pump.poll_iter() { match event { Event::Quit { .. } => { - commands.trigger(GameEvent::Command(GameCommand::Exit)); + writer.write(GameEvent::Command(GameCommand::Exit)); } Event::KeyDown { keycode: Some(key), .. } => { let command = bindings.key_bindings.get(&key).copied(); if let Some(command) = command { tracing::info!("triggering command: {:?}", command); - commands.trigger(GameEvent::Command(command)); + writer.write(GameEvent::Command(command)); } } _ => {}