Compare commits

..

2 Commits

Author SHA1 Message Date
70fb2b9503 fix: working movement again with ecs 2025-08-14 18:35:23 -05:00
0aa056a0ae feat: ecs keyboard interactions 2025-08-14 18:17:58 -05:00
5 changed files with 302 additions and 180 deletions

View File

@@ -1,16 +1,141 @@
use bevy_ecs::{ use bevy_ecs::{
event::{EventReader, EventWriter}, event::{EventReader, EventWriter},
query::With, system::{Query, Res, ResMut},
system::{Query, ResMut},
}; };
use tracing::debug;
use crate::entity::graph::EdgePermissions;
use crate::{ use crate::{
ecs::{GlobalState, PlayerControlled, Velocity}, ecs::{DeltaTime, GlobalState, PlayerControlled, Position, Velocity},
error::GameError, error::{EntityError, GameError},
game::events::GameEvent, game::events::GameEvent,
input::commands::GameCommand, input::commands::GameCommand,
map::builder::Map,
}; };
pub fn movement_system(
map: Res<Map>,
delta_time: Res<DeltaTime>,
mut entities: Query<(&mut PlayerControlled, &mut Velocity, &mut Position)>,
mut errors: EventWriter<GameError>,
) {
for (mut player, mut velocity, mut position) in entities.iter_mut() {
let distance = velocity.speed * 60.0 * delta_time.0;
// Decrement the remaining frames for the next direction
if let Some((direction, remaining)) = velocity.next_direction {
if remaining > 0 {
velocity.next_direction = Some((direction, remaining - 1));
} else {
velocity.next_direction = None;
}
}
match *position {
Position::AtNode(node_id) => {
// We're not moving, but a buffered direction is available.
if let Some((next_direction, _)) = velocity.next_direction {
if let Some(edge) = map.graph.find_edge_in_direction(node_id, next_direction) {
if can_traverse(&mut player, edge) {
// Start moving in that direction
*position = Position::BetweenNodes {
from: node_id,
to: edge.target,
traversed: distance,
};
velocity.direction = next_direction;
velocity.next_direction = None;
}
} else {
errors.write(
EntityError::InvalidMovement(format!(
"No edge found in direction {:?} from node {}",
next_direction, node_id
))
.into(),
);
}
}
}
Position::BetweenNodes { from, to, traversed } => {
// There is no point in any of the next logic if we don't travel at all
if distance <= 0.0 {
return;
}
let edge = map
.graph
.find_edge(from, to)
.ok_or_else(|| {
errors.write(
EntityError::InvalidMovement(format!(
"Inconsistent state: Traverser is on a non-existent edge from {} to {}.",
from, to
))
.into(),
);
return;
})
.unwrap();
let new_traversed = traversed + distance;
if new_traversed < edge.distance {
// Still on the same edge, just update the distance.
*position = Position::BetweenNodes {
from,
to,
traversed: new_traversed,
};
} else {
let overflow = new_traversed - edge.distance;
let mut moved = false;
// If we buffered a direction, try to find an edge in that direction
if let Some((next_dir, _)) = velocity.next_direction {
if let Some(edge) = map.graph.find_edge_in_direction(to, next_dir) {
if can_traverse(&mut player, edge) {
*position = Position::BetweenNodes {
from: to,
to: edge.target,
traversed: overflow,
};
velocity.direction = next_dir; // Remember our new direction
velocity.next_direction = None; // Consume the buffered direction
moved = true;
}
}
}
// If we didn't move, try to continue in the current direction
if !moved {
if let Some(edge) = map.graph.find_edge_in_direction(to, velocity.direction) {
if can_traverse(&mut player, edge) {
*position = Position::BetweenNodes {
from: to,
to: edge.target,
traversed: overflow,
};
} else {
*position = Position::AtNode(to);
velocity.next_direction = None;
}
} else {
*position = Position::AtNode(to);
velocity.next_direction = None;
}
}
}
}
}
}
}
fn can_traverse(_player: &mut PlayerControlled, edge: crate::entity::graph::Edge) -> bool {
matches!(edge.permissions, EdgePermissions::All)
}
// Handles // Handles
pub fn interact_system( pub fn interact_system(
mut events: EventReader<GameEvent>, mut events: EventReader<GameEvent>,
@@ -32,7 +157,7 @@ pub fn interact_system(
match event { match event {
GameEvent::Command(command) => match command { GameEvent::Command(command) => match command {
GameCommand::MovePlayer(direction) => { GameCommand::MovePlayer(direction) => {
velocity.direction = *direction; velocity.next_direction = Some((*direction, 90));
} }
GameCommand::Exit => { GameCommand::Exit => {
state.exit = true; state.exit = true;

View File

@@ -125,7 +125,8 @@ impl Position {
#[derive(Default, Component)] #[derive(Default, Component)]
pub struct Velocity { pub struct Velocity {
pub direction: Direction, pub direction: Direction,
pub speed: Option<f32>, pub next_direction: Option<(Direction, u8)>,
pub speed: f32,
} }
#[derive(Bundle)] #[derive(Bundle)]

View File

@@ -11,18 +11,21 @@ use sdl2::video::Window;
/// Updates the directional animated texture of an entity. /// Updates the directional animated texture of an entity.
pub fn directional_render_system( pub fn directional_render_system(
dt: Res<DeltaTime>, dt: Res<DeltaTime>,
mut renderables: Query<(&Velocity, &mut DirectionalAnimated, &mut Renderable)>, mut renderables: Query<(&Velocity, &mut DirectionalAnimated, &mut Renderable, &Position)>,
mut errors: EventWriter<GameError>, mut errors: EventWriter<GameError>,
) { ) {
for (velocity, mut texture, mut renderable) in renderables.iter_mut() { for (velocity, mut texture, mut renderable, position) in renderables.iter_mut() {
let texture = if velocity.speed.is_none() { let stopped = matches!(position, Position::AtNode(_));
let texture = if stopped {
texture.stopped_textures[velocity.direction.as_usize()].as_mut() texture.stopped_textures[velocity.direction.as_usize()].as_mut()
} else { } else {
texture.textures[velocity.direction.as_usize()].as_mut() texture.textures[velocity.direction.as_usize()].as_mut()
}; };
if let Some(texture) = texture { if let Some(texture) = texture {
texture.tick(dt.0); if !stopped {
texture.tick(dt.0);
}
renderable.sprite = *texture.current_tile(); renderable.sprite = *texture.current_tile();
} else { } else {
errors.write(TextureError::RenderFailed(format!("Entity has no texture")).into()); errors.write(TextureError::RenderFailed(format!("Entity has no texture")).into());

View File

@@ -1,181 +1,161 @@
// use tracing::error; use tracing::error;
// use crate::error::GameResult; use crate::ecs::{NodeId, Position};
use crate::error::GameResult;
// use super::direction::Direction; use super::direction::Direction;
// use super::graph::{Edge, Graph, NodeId}; use super::graph::{Edge, Graph};
// /// Manages an entity's movement through the graph. /// Manages an entity's movement through the graph.
// /// ///
// /// A `Traverser` encapsulates the state of an entity's position and direction, /// A `Traverser` encapsulates the state of an entity's position and direction,
// /// providing a way to advance along the graph's paths based on a given distance. /// providing a way to advance along the graph's paths based on a given distance.
// /// It also handles direction changes, buffering the next intended direction. /// It also handles direction changes, buffering the next intended direction.
// pub struct Traverser { pub struct Traverser {
// /// The current position of the traverser in the graph. /// The current position of the traverser in the graph.
// pub position: Position, pub position: Position,
// /// The current direction of movement. /// The current direction of movement.
// pub direction: Direction, pub direction: Direction,
// /// Buffered direction change with remaining frame count for timing. /// Buffered direction change with remaining frame count for timing.
// /// ///
// /// The `u8` value represents the number of frames remaining before /// The `u8` value represents the number of frames remaining before
// /// the buffered direction expires. This allows for responsive controls /// the buffered direction expires. This allows for responsive controls
// /// by storing direction changes for a limited time. /// by storing direction changes for a limited time.
// pub next_direction: Option<(Direction, u8)>, pub next_direction: Option<(Direction, u8)>,
// } }
// impl Traverser { impl Traverser {
// /// Creates a new traverser starting at the given node ID. /// Sets the next direction for the traverser to take.
// /// ///
// /// The traverser will immediately attempt to start moving in the initial direction. /// The direction is buffered and will be applied at the next opportunity,
// pub fn new<F>(graph: &Graph, start_node: NodeId, initial_direction: Direction, can_traverse: &F) -> Self /// typically when the traverser reaches a new node. This allows for responsive
// where /// controls, as the new direction is stored for a limited time.
// F: Fn(Edge) -> bool, pub fn set_next_direction(&mut self, new_direction: Direction) {
// { if self.direction != new_direction {
// let mut traverser = Traverser { self.next_direction = Some((new_direction, 30));
// position: Position::AtNode(start_node), }
// direction: initial_direction, }
// next_direction: Some((initial_direction, 1)),
// };
// // This will kickstart the traverser into motion /// Advances the traverser along the graph by a specified distance.
// if let Err(e) = traverser.advance(graph, 0.0, can_traverse) { ///
// error!("Traverser initialization error: {}", e); /// This method updates the traverser's position based on its current state
// } /// and the distance to travel.
///
/// - If at a node, it checks for a buffered direction to start moving.
/// - If between nodes, it moves along the current edge.
/// - If it reaches a node, it attempts to transition to a new edge based on
/// the buffered direction or by continuing straight.
/// - If no valid move is possible, it stops at the node.
///
/// Returns an error if the movement is invalid (e.g., trying to move in an impossible direction).
pub fn advance<F>(&mut self, graph: &Graph, distance: f32, can_traverse: &F) -> GameResult<()>
where
F: Fn(Edge) -> bool,
{
// Decrement the remaining frames for the next direction
if let Some((direction, remaining)) = self.next_direction {
if remaining > 0 {
self.next_direction = Some((direction, remaining - 1));
} else {
self.next_direction = None;
}
}
// traverser match self.position {
// } Position::AtNode(node_id) => {
// We're not moving, but a buffered direction is available.
if let Some((next_direction, _)) = self.next_direction {
if let Some(edge) = graph.find_edge_in_direction(node_id, next_direction) {
if can_traverse(edge) {
// Start moving in that direction
self.position = Position::BetweenNodes {
from: node_id,
to: edge.target,
traversed: distance.max(0.0),
};
self.direction = next_direction;
} else {
return Err(crate::error::GameError::Entity(crate::error::EntityError::InvalidMovement(
format!(
"Cannot traverse edge from {} to {} in direction {:?}",
node_id, edge.target, next_direction
),
)));
}
} else {
return Err(crate::error::GameError::Entity(crate::error::EntityError::InvalidMovement(
format!("No edge found in direction {:?} from node {}", next_direction, node_id),
)));
}
// /// Sets the next direction for the traverser to take. self.next_direction = None; // Consume the buffered direction regardless of whether we started moving with it
// /// }
// /// The direction is buffered and will be applied at the next opportunity, }
// /// typically when the traverser reaches a new node. This allows for responsive Position::BetweenNodes { from, to, traversed } => {
// /// controls, as the new direction is stored for a limited time. // There is no point in any of the next logic if we don't travel at all
// pub fn set_next_direction(&mut self, new_direction: Direction) { if distance <= 0.0 {
// if self.direction != new_direction { return Ok(());
// self.next_direction = Some((new_direction, 30)); }
// }
// }
// /// Advances the traverser along the graph by a specified distance. let edge = graph.find_edge(from, to).ok_or_else(|| {
// /// crate::error::GameError::Entity(crate::error::EntityError::InvalidMovement(format!(
// /// This method updates the traverser's position based on its current state "Inconsistent state: Traverser is on a non-existent edge from {} to {}.",
// /// and the distance to travel. from, to
// /// )))
// /// - If at a node, it checks for a buffered direction to start moving. })?;
// /// - If between nodes, it moves along the current edge.
// /// - If it reaches a node, it attempts to transition to a new edge based on
// /// the buffered direction or by continuing straight.
// /// - If no valid move is possible, it stops at the node.
// ///
// /// Returns an error if the movement is invalid (e.g., trying to move in an impossible direction).
// pub fn advance<F>(&mut self, graph: &Graph, distance: f32, can_traverse: &F) -> GameResult<()>
// where
// F: Fn(Edge) -> bool,
// {
// // Decrement the remaining frames for the next direction
// if let Some((direction, remaining)) = self.next_direction {
// if remaining > 0 {
// self.next_direction = Some((direction, remaining - 1));
// } else {
// self.next_direction = None;
// }
// }
// match self.position { let new_traversed = traversed + distance;
// Position::AtNode(node_id) => {
// // We're not moving, but a buffered direction is available.
// if let Some((next_direction, _)) = self.next_direction {
// if let Some(edge) = graph.find_edge_in_direction(node_id, next_direction) {
// if can_traverse(edge) {
// // Start moving in that direction
// self.position = Position::BetweenNodes {
// from: node_id,
// to: edge.target,
// traversed: distance.max(0.0),
// };
// self.direction = next_direction;
// } else {
// return Err(crate::error::GameError::Entity(crate::error::EntityError::InvalidMovement(
// format!(
// "Cannot traverse edge from {} to {} in direction {:?}",
// node_id, edge.target, next_direction
// ),
// )));
// }
// } else {
// return Err(crate::error::GameError::Entity(crate::error::EntityError::InvalidMovement(
// format!("No edge found in direction {:?} from node {}", next_direction, node_id),
// )));
// }
// self.next_direction = None; // Consume the buffered direction regardless of whether we started moving with it if new_traversed < edge.distance {
// } // Still on the same edge, just update the distance.
// } self.position = Position::BetweenNodes {
// Position::BetweenNodes { from, to, traversed } => { from,
// // There is no point in any of the next logic if we don't travel at all to,
// if distance <= 0.0 { traversed: new_traversed,
// return Ok(()); };
// } } else {
let overflow = new_traversed - edge.distance;
let mut moved = false;
// let edge = graph.find_edge(from, to).ok_or_else(|| { // If we buffered a direction, try to find an edge in that direction
// crate::error::GameError::Entity(crate::error::EntityError::InvalidMovement(format!( if let Some((next_dir, _)) = self.next_direction {
// "Inconsistent state: Traverser is on a non-existent edge from {} to {}.", if let Some(edge) = graph.find_edge_in_direction(to, next_dir) {
// from, to if can_traverse(edge) {
// ))) self.position = Position::BetweenNodes {
// })?; from: to,
to: edge.target,
traversed: overflow,
};
// let new_traversed = traversed + distance; self.direction = next_dir; // Remember our new direction
self.next_direction = None; // Consume the buffered direction
moved = true;
}
}
}
// if new_traversed < edge.distance { // If we didn't move, try to continue in the current direction
// // Still on the same edge, just update the distance. if !moved {
// self.position = Position::BetweenNodes { if let Some(edge) = graph.find_edge_in_direction(to, self.direction) {
// from, if can_traverse(edge) {
// to, self.position = Position::BetweenNodes {
// traversed: new_traversed, from: to,
// }; to: edge.target,
// } else { traversed: overflow,
// let overflow = new_traversed - edge.distance; };
// let mut moved = false; } else {
self.position = Position::AtNode(to);
self.next_direction = None;
}
} else {
self.position = Position::AtNode(to);
self.next_direction = None;
}
}
}
}
}
// // If we buffered a direction, try to find an edge in that direction Ok(())
// if let Some((next_dir, _)) = self.next_direction { }
// if let Some(edge) = graph.find_edge_in_direction(to, next_dir) { }
// if can_traverse(edge) {
// self.position = Position::BetweenNodes {
// from: to,
// to: edge.target,
// traversed: overflow,
// };
// self.direction = next_dir; // Remember our new direction
// self.next_direction = None; // Consume the buffered direction
// moved = true;
// }
// }
// }
// // If we didn't move, try to continue in the current direction
// if !moved {
// if let Some(edge) = graph.find_edge_in_direction(to, self.direction) {
// if can_traverse(edge) {
// self.position = Position::BetweenNodes {
// from: to,
// to: edge.target,
// traversed: overflow,
// };
// } else {
// self.position = Position::AtNode(to);
// self.next_direction = None;
// }
// } else {
// self.position = Position::AtNode(to);
// self.next_direction = None;
// }
// }
// }
// }
// }
// Ok(())
// }
// }

View File

@@ -3,7 +3,7 @@
include!(concat!(env!("OUT_DIR"), "/atlas_data.rs")); include!(concat!(env!("OUT_DIR"), "/atlas_data.rs"));
use crate::constants::CANVAS_SIZE; use crate::constants::CANVAS_SIZE;
use crate::ecs::interact::interact_system; use crate::ecs::interact::{interact_system, movement_system};
use crate::ecs::render::{directional_render_system, render_system, BackbufferResource, MapTextureResource}; use crate::ecs::render::{directional_render_system, render_system, BackbufferResource, MapTextureResource};
use crate::ecs::{DeltaTime, DirectionalAnimated, GlobalState, PlayerBundle, PlayerControlled, Position, Renderable, Velocity}; use crate::ecs::{DeltaTime, DirectionalAnimated, GlobalState, PlayerBundle, PlayerControlled, Position, Renderable, Velocity};
use crate::entity::direction::Direction; use crate::entity::direction::Direction;
@@ -132,7 +132,11 @@ impl Game {
let player = PlayerBundle { let player = PlayerBundle {
player: PlayerControlled, player: PlayerControlled,
position: Position::AtNode(pacman_start_node), position: Position::AtNode(pacman_start_node),
velocity: Velocity::default(), velocity: Velocity {
direction: Direction::Up,
next_direction: None,
speed: 1.125,
},
sprite: Renderable { sprite: Renderable {
sprite: SpriteAtlas::get_tile(&atlas, "pacman/full.png") sprite: SpriteAtlas::get_tile(&atlas, "pacman/full.png")
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("pacman/full.png".to_string())))?, .ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("pacman/full.png".to_string())))?,
@@ -164,7 +168,16 @@ impl Game {
}, },
}); });
schedule.add_systems((handle_input, interact_system, directional_render_system, render_system).chain()); schedule.add_systems(
(
handle_input,
interact_system,
movement_system,
directional_render_system,
render_system,
)
.chain(),
);
// Spawn player // Spawn player
world.spawn(player); world.spawn(player);