feat: ecs keyboard interactions

This commit is contained in:
2025-08-14 18:17:58 -05:00
parent b270318640
commit 0aa056a0ae
4 changed files with 295 additions and 173 deletions

View File

@@ -1,16 +1,144 @@
use bevy_ecs::{ use bevy_ecs::{
event::{EventReader, EventWriter}, event::{EventReader, EventWriter},
query::With, query::With,
system::{Query, ResMut}, system::{Query, Res, ResMut},
}; };
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<(&PlayerControlled, &mut Velocity, &mut Position)>,
mut errors: EventWriter<GameError>,
) {
for (player, mut velocity, mut position) in entities.iter_mut() {
let distance = velocity.speed.unwrap_or(0.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 edge.permissions.can_traverse(edge) {
// // Start moving in that direction
*position = Position::BetweenNodes {
from: node_id,
to: edge.target,
traversed: distance,
};
velocity.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 {
errors.write(
EntityError::InvalidMovement(format!(
"No edge found in direction {:?} from node {}",
next_direction, node_id
))
.into(),
);
}
velocity.next_direction = None; // Consume the buffered direction regardless of whether we started moving with it
}
}
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 edge.permissions.can_traverse(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 edge.permissions.can_traverse(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;
}
}
}
}
}
}
}
// Handles // Handles
pub fn interact_system( pub fn interact_system(
mut events: EventReader<GameEvent>, mut events: EventReader<GameEvent>,

View File

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

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: Some(1.0),
},
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);