feat: directional rendering, interactivity

This commit is contained in:
2025-08-14 15:44:07 -05:00
parent bc759f1ed4
commit b270318640
5 changed files with 118 additions and 28 deletions

44
src/ecs/interact.rs Normal file
View File

@@ -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<GameEvent>,
mut state: ResMut<GlobalState>,
mut players: Query<(&PlayerControlled, &mut Velocity)>,
mut errors: EventWriter<GameError>,
) {
// 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;
}
_ => {}
},
}
}
}

View File

@@ -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<AnimatedTexture>; 4],
pub stopped_textures: [Option<AnimatedTexture>; 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<f32>,
}
#[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;

View File

@@ -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<DeltaTime>,
mut renderables: Query<(&Velocity, &mut DirectionalAnimated, &mut Renderable)>,
mut errors: EventWriter<GameError>,
) {
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<BackbufferResource>,
mut atlas: NonSendMut<SpriteAtlas>,
map: Res<Map>,
renderables: Query<(Entity, &Renderable, &Position)>,
mut renderables: Query<(Entity, &mut Renderable, &Position)>,
mut errors: EventWriter<GameError>,
) {
// 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()));
}

View File

@@ -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);

View File

@@ -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<Bindings>, mut commands: Commands, mut pump: NonSendMut<&'static mut EventPump>) {
pub fn handle_input(bindings: Res<Bindings>, mut writer: EventWriter<GameEvent>, 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));
}
}
_ => {}