refactor: switch NodeId to u16, use I8Vec2 for grid coordinates

This commit is contained in:
Ryan Walters
2025-09-01 12:37:44 -05:00
parent 1206cf9ad1
commit 0cbd6f1aac
11 changed files with 94 additions and 80 deletions

View File

@@ -11,7 +11,7 @@ use crate::systems::blinking::Blinking;
use crate::systems::movement::{BufferedDirection, Position, Velocity};
use crate::systems::profiling::SystemId;
use crate::systems::render::RenderDirty;
use crate::systems::{self, ghost_collision_system, present_system, Hidden, MovementModifiers};
use crate::systems::{self, ghost_collision_system, present_system, Hidden, MovementModifiers, NodeId};
use crate::systems::{
audio_system, blinking_system, collision_system, debug_render_system, directional_render_system, dirty_render_system,
eaten_ghost_system, ghost_movement_system, ghost_state_animation_system, hud_render_system, item_system, profile,
@@ -303,7 +303,7 @@ impl Game {
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("maze/energizer.png".to_string())))?;
// Build a list of item entities to spawn from the map
let nodes: Vec<(usize, EntityType, AtlasTile, f32)> = world
let nodes: Vec<(NodeId, EntityType, AtlasTile, f32)> = world
.resource::<Map>()
.iter_nodes()
.filter_map(|(id, tile)| match tile {

View File

@@ -5,7 +5,7 @@ use crate::map::graph::{Graph, Node, TraversalFlags};
use crate::map::parser::MapTileParser;
use crate::systems::movement::NodeId;
use bevy_ecs::resource::Resource;
use glam::{IVec2, Vec2};
use glam::{I8Vec2, IVec2, Vec2};
use std::collections::{HashMap, VecDeque};
use tracing::debug;
@@ -38,7 +38,7 @@ pub struct Map {
/// Connected graph of navigable positions.
pub graph: Graph,
/// Bidirectional mapping between 2D grid coordinates and graph node indices.
pub grid_to_node: HashMap<IVec2, NodeId>,
pub grid_to_node: HashMap<I8Vec2, NodeId>,
/// Predetermined spawn locations for all game entities
pub start_positions: NodePositions,
/// 2D array of tile types for collision detection and rendering
@@ -76,8 +76,8 @@ impl Map {
let mut queue = VecDeque::new();
queue.push_back(start_pos);
let pos = Vec2::new(
(start_pos.x * CELL_SIZE as i32) as f32,
(start_pos.y * CELL_SIZE as i32) as f32,
(start_pos.x as i32 * CELL_SIZE as i32) as f32,
(start_pos.y as i32 * CELL_SIZE as i32) as f32,
) + cell_offset;
let node_id = graph.add_node(Node { position: pos });
grid_to_node.insert(start_pos, node_id);
@@ -89,9 +89,9 @@ impl Map {
// Skip if the new position is out of bounds
if new_position.x < 0
|| new_position.x >= BOARD_CELL_SIZE.x as i32
|| new_position.x as i32 >= BOARD_CELL_SIZE.x as i32
|| new_position.y < 0
|| new_position.y >= BOARD_CELL_SIZE.y as i32
|| new_position.y as i32 >= BOARD_CELL_SIZE.y as i32
{
continue;
}
@@ -108,8 +108,8 @@ impl Map {
) {
// Add the new position to the graph/queue
let pos = Vec2::new(
(new_position.x * CELL_SIZE as i32) as f32,
(new_position.y * CELL_SIZE as i32) as f32,
(new_position.x as i32 * CELL_SIZE as i32) as f32,
(new_position.y as i32 * CELL_SIZE as i32) as f32,
) + cell_offset;
let new_node_id = graph.add_node(Node { position: pos });
grid_to_node.insert(new_position, new_node_id);
@@ -132,7 +132,7 @@ impl Map {
for (grid_pos, &node_id) in &grid_to_node {
for dir in Direction::DIRECTIONS {
// If the node doesn't have an edge in this direction, look for a neighbor in that direction
if graph.adjacency_list[node_id].get(dir).is_none() {
if graph.adjacency_list[node_id as usize].get(dir).is_none() {
let neighbor = grid_pos + dir.as_ivec2();
// If the neighbor exists, connect the node to it
if let Some(&neighbor_id) = grid_to_node.get(&neighbor) {
@@ -199,9 +199,9 @@ impl Map {
/// representing the four key positions within the ghost house structure.
fn build_house(
graph: &mut Graph,
grid_to_node: &HashMap<IVec2, NodeId>,
house_door: &[Option<IVec2>; 2],
) -> GameResult<(usize, usize, usize, usize)> {
grid_to_node: &HashMap<I8Vec2, NodeId>,
house_door: &[Option<I8Vec2>; 2],
) -> GameResult<(NodeId, NodeId, NodeId, NodeId)> {
// Calculate the position of the house entrance node
let (house_entrance_node_id, house_entrance_node_position) = {
// Translate the grid positions to the actual node ids
@@ -222,10 +222,13 @@ impl Map {
// Calculate the position of the house node
let (node_id, node_position) = {
let left_pos = graph.get_node(*left_node).ok_or(MapError::NodeNotFound(*left_node))?.position;
let left_pos = graph
.get_node(*left_node)
.ok_or(MapError::NodeNotFound(*left_node as usize))?
.position;
let right_pos = graph
.get_node(*right_node)
.ok_or(MapError::NodeNotFound(*right_node))?
.ok_or(MapError::NodeNotFound(*right_node as usize))?
.position;
let house_node = graph.add_node(Node {
position: left_pos.lerp(right_pos, 0.5),
@@ -249,10 +252,10 @@ impl Map {
// Place the nodes at, above, and below the center position
let center_node_id = graph.add_node(Node { position: center_pos });
let top_node_id = graph.add_node(Node {
position: center_pos + (Direction::Up.as_ivec2() * (CELL_SIZE as i32 / 2)).as_vec2(),
position: center_pos + IVec2::from(Direction::Up.as_ivec2()).as_vec2() * (CELL_SIZE as f32 / 2.0),
});
let bottom_node_id = graph.add_node(Node {
position: center_pos + (Direction::Down.as_ivec2() * (CELL_SIZE as i32 / 2)).as_vec2(),
position: center_pos + IVec2::from(Direction::Down.as_ivec2()).as_vec2() * (CELL_SIZE as f32 / 2.0),
});
// Connect the center node to the top and bottom nodes
@@ -268,7 +271,7 @@ impl Map {
// Calculate the position of the center line's center node
let center_line_center_position =
house_entrance_node_position + (Direction::Down.as_ivec2() * (3 * CELL_SIZE as i32)).as_vec2();
house_entrance_node_position + IVec2::from(Direction::Down.as_ivec2()).as_vec2() * (3.0 * CELL_SIZE as f32);
// Create the center line
let (center_center_node_id, center_top_node_id) = create_house_line(graph, center_line_center_position)?;
@@ -300,13 +303,13 @@ impl Map {
// Create the left line
let (left_center_node_id, _) = create_house_line(
graph,
center_line_center_position + (Direction::Left.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
center_line_center_position + IVec2::from(Direction::Left.as_ivec2()).as_vec2() * (CELL_SIZE as f32 * 2.0),
)?;
// Create the right line
let (right_center_node_id, _) = create_house_line(
graph,
center_line_center_position + (Direction::Right.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
center_line_center_position + IVec2::from(Direction::Right.as_ivec2()).as_vec2() * (CELL_SIZE as f32 * 2.0),
)?;
debug!("Left center node id: {left_center_node_id}");
@@ -336,8 +339,8 @@ impl Map {
/// Creates hidden intermediate nodes beyond the visible tunnel entrances and connects them with zero-distance edges for instantaneous traversal.
fn build_tunnels(
graph: &mut Graph,
grid_to_node: &HashMap<IVec2, NodeId>,
tunnel_ends: &[Option<IVec2>; 2],
grid_to_node: &HashMap<I8Vec2, NodeId>,
tunnel_ends: &[Option<I8Vec2>; 2],
) -> GameResult<()> {
// Create the hidden tunnel nodes
let left_tunnel_hidden_node_id = {
@@ -353,7 +356,7 @@ impl Map {
Direction::Left,
Node {
position: left_tunnel_entrance_node.position
+ (Direction::Left.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
+ IVec2::from(Direction::Left.as_ivec2()).as_vec2() * (CELL_SIZE as f32 * 2.0),
},
)
.map_err(|e| {
@@ -378,7 +381,7 @@ impl Map {
Direction::Right,
Node {
position: right_tunnel_entrance_node.position
+ (Direction::Right.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
+ IVec2::from(Direction::Right.as_ivec2()).as_vec2() * (CELL_SIZE as f32 * 2.0),
},
)
.map_err(|e| {

View File

@@ -1,4 +1,4 @@
use glam::IVec2;
use glam::I8Vec2;
use strum_macros::AsRefStr;
/// The four cardinal directions.
@@ -28,8 +28,8 @@ impl Direction {
}
}
/// Returns the direction as an IVec2.
pub fn as_ivec2(self) -> IVec2 {
/// Returns the direction as an I8Vec2.
pub fn as_ivec2(self) -> I8Vec2 {
self.into()
}
@@ -45,13 +45,13 @@ impl Direction {
}
}
impl From<Direction> for IVec2 {
impl From<Direction> for I8Vec2 {
fn from(dir: Direction) -> Self {
match dir {
Direction::Up => -IVec2::Y,
Direction::Down => IVec2::Y,
Direction::Left => -IVec2::X,
Direction::Right => IVec2::X,
Direction::Up => -I8Vec2::Y,
Direction::Down => I8Vec2::Y,
Direction::Left => -I8Vec2::X,
Direction::Right => I8Vec2::X,
}
}
}

View File

@@ -107,7 +107,7 @@ impl Graph {
/// Adds a new node with the given data to the graph and returns its ID.
pub fn add_node(&mut self, data: Node) -> NodeId {
let id = self.nodes.len();
let id = self.nodes.len() as NodeId;
self.nodes.push(data);
self.adjacency_list.push(Intersection::default());
id
@@ -129,10 +129,10 @@ impl Graph {
distance: Option<f32>,
direction: Direction,
) -> Result<(), &'static str> {
if from >= self.adjacency_list.len() {
if from as usize >= self.adjacency_list.len() {
return Err("From node does not exist.");
}
if to >= self.adjacency_list.len() {
if to as usize >= self.adjacency_list.len() {
return Err("To node does not exist.");
}
@@ -178,8 +178,8 @@ impl Graph {
}
None => {
// If no distance is provided, calculate it based on the positions of the nodes
let from_pos = self.nodes[from].position;
let to_pos = self.nodes[to].position;
let from_pos = self.nodes[from as usize].position;
let to_pos = self.nodes[to as usize].position;
from_pos.distance(to_pos)
}
},
@@ -187,11 +187,11 @@ impl Graph {
traversal_flags,
};
if from >= self.adjacency_list.len() {
if from as usize >= self.adjacency_list.len() {
return Err("From node does not exist.");
}
let adjacency_list = &mut self.adjacency_list[from];
let adjacency_list = &mut self.adjacency_list[from as usize];
// Check if the edge already exists in this direction or to the same target
if let Some(err) = adjacency_list.edges().find_map(|e| {
@@ -215,7 +215,7 @@ impl Graph {
/// Retrieves an immutable reference to a node's data.
pub fn get_node(&self, id: NodeId) -> Option<&Node> {
self.nodes.get(id)
self.nodes.get(id as usize)
}
/// Returns an iterator over all nodes in the graph.
@@ -228,17 +228,17 @@ impl Graph {
self.adjacency_list
.iter()
.enumerate()
.flat_map(|(node_id, intersection)| intersection.edges().map(move |edge| (node_id, edge)))
.flat_map(|(node_id, intersection)| intersection.edges().map(move |edge| (node_id as NodeId, edge)))
}
/// Finds a specific edge from a source node to a target node.
pub fn find_edge(&self, from: NodeId, to: NodeId) -> Option<Edge> {
self.adjacency_list.get(from)?.edges().find(|edge| edge.target == to)
self.adjacency_list.get(from as usize)?.edges().find(|edge| edge.target == to)
}
/// Finds an edge originating from a given node that follows a specific direction.
pub fn find_edge_in_direction(&self, from: NodeId, direction: Direction) -> Option<Edge> {
self.adjacency_list.get(from)?.get(direction)
self.adjacency_list.get(from as usize)?.get(direction)
}
}

View File

@@ -2,7 +2,7 @@
use crate::constants::{MapTile, BOARD_CELL_SIZE};
use crate::error::ParseError;
use glam::IVec2;
use glam::I8Vec2;
/// Structured representation of parsed ASCII board layout with extracted special positions.
///
@@ -15,11 +15,11 @@ pub struct ParsedMap {
/// 2D array of tiles converted from ASCII characters
pub tiles: [[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize],
/// Two positions marking the ghost house entrance (represented by '=' characters)
pub house_door: [Option<IVec2>; 2],
pub house_door: [Option<I8Vec2>; 2],
/// Two positions marking tunnel portals for wraparound teleportation ('T' characters)
pub tunnel_ends: [Option<IVec2>; 2],
pub tunnel_ends: [Option<I8Vec2>; 2],
/// Starting position for Pac-Man (marked by 'X' character in the layout)
pub pacman_start: Option<IVec2>,
pub pacman_start: Option<I8Vec2>,
}
/// Parser for converting raw board layouts into structured map data.
@@ -88,7 +88,7 @@ impl MapTileParser {
let mut tiles = [[MapTile::Empty; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize];
let mut house_door = [None; 2];
let mut tunnel_ends = [None; 2];
let mut pacman_start: Option<IVec2> = None;
let mut pacman_start: Option<I8Vec2> = None;
for (y, line) in raw_board.iter().enumerate().take(BOARD_CELL_SIZE.y as usize) {
for (x, character) in line.chars().enumerate().take(BOARD_CELL_SIZE.x as usize) {
@@ -98,16 +98,16 @@ impl MapTileParser {
match tile {
MapTile::Tunnel => {
if tunnel_ends[0].is_none() {
tunnel_ends[0] = Some(IVec2::new(x as i32, y as i32));
tunnel_ends[0] = Some(I8Vec2::new(x as i8, y as i8));
} else {
tunnel_ends[1] = Some(IVec2::new(x as i32, y as i32));
tunnel_ends[1] = Some(I8Vec2::new(x as i8, y as i8));
}
}
MapTile::Wall if character == '=' => {
if house_door[0].is_none() {
house_door[0] = Some(IVec2::new(x as i32, y as i32));
house_door[0] = Some(I8Vec2::new(x as i8, y as i8));
} else {
house_door[1] = Some(IVec2::new(x as i32, y as i32));
house_door[1] = Some(I8Vec2::new(x as i8, y as i8));
}
}
_ => {}
@@ -115,7 +115,7 @@ impl MapTileParser {
// Track Pac-Man's starting position
if character == 'X' {
pacman_start = Some(IVec2::new(x as i32, y as i32));
pacman_start = Some(I8Vec2::new(x as i8, y as i8));
}
tiles[x][y] = tile;

View File

@@ -3,7 +3,7 @@ use std::cmp::Ordering;
use crate::constants::BOARD_PIXEL_OFFSET;
use crate::map::builder::Map;
use crate::systems::{Collider, CursorPosition, Position, SystemTimings};
use crate::systems::{Collider, CursorPosition, NodeId, Position, SystemTimings};
use bevy_ecs::resource::Resource;
use bevy_ecs::system::{NonSendMut, Query, Res};
use glam::{IVec2, UVec2, Vec2};
@@ -185,7 +185,7 @@ pub fn debug_render_system(
// Render node ID if a node is highlighted
if let Some(closest_node_id) = closest_node {
let node = map.graph.get_node(closest_node_id).unwrap();
let node = map.graph.get_node(closest_node_id as NodeId).unwrap();
let pos = transform_position_with_offset(node.position, scale);
let surface = font

View File

@@ -36,7 +36,7 @@ pub fn ghost_movement_system(
loop {
match *position {
Position::Stopped { node: current_node } => {
let intersection = &map.graph.adjacency_list[current_node];
let intersection = &map.graph.adjacency_list[current_node as usize];
let opposite = velocity.direction.opposite();
let mut non_opposite_options: SmallVec<[Edge; 3]> = SmallVec::new();
@@ -159,8 +159,11 @@ pub fn eaten_ghost_system(
velocity.direction = direction;
*position = Position::Moving {
from: current_node,
to: map.graph.adjacency_list[current_node].get(direction).unwrap().target,
remaining_distance: map.graph.adjacency_list[current_node].get(direction).unwrap().distance,
to: map.graph.adjacency_list[current_node as usize].get(direction).unwrap().target,
remaining_distance: map.graph.adjacency_list[current_node as usize]
.get(direction)
.unwrap()
.distance,
};
}
}
@@ -186,8 +189,8 @@ pub fn eaten_ghost_system(
velocity.direction = next_direction;
*position = Position::Moving {
from: to,
to: map.graph.adjacency_list[to].get(next_direction).unwrap().target,
remaining_distance: map.graph.adjacency_list[to].get(next_direction).unwrap().distance,
to: map.graph.adjacency_list[to as usize].get(next_direction).unwrap().target,
remaining_distance: map.graph.adjacency_list[to as usize].get(next_direction).unwrap().distance,
};
}
}
@@ -202,7 +205,11 @@ pub fn eaten_ghost_system(
/// Helper function to find the direction from a node towards a target node.
/// Uses simple greedy pathfinding - prefers straight lines when possible.
fn find_direction_to_target(map: &Map, from_node: usize, target_node: usize) -> Option<Direction> {
fn find_direction_to_target(
map: &Map,
from_node: crate::systems::movement::NodeId,
target_node: crate::systems::movement::NodeId,
) -> Option<Direction> {
let from_pos = map.graph.get_node(from_node).unwrap().position;
let target_pos = map.graph.get_node(target_node).unwrap().position;
@@ -224,7 +231,7 @@ fn find_direction_to_target(map: &Map, from_node: usize, target_node: usize) ->
// Return first available direction towards target
for direction in preferred_dirs {
if let Some(edge) = map.graph.adjacency_list[from_node].get(direction) {
if let Some(edge) = map.graph.adjacency_list[from_node as usize].get(direction) {
if edge.traversal_flags.contains(TraversalFlags::GHOST) {
return Some(direction);
}

View File

@@ -8,7 +8,7 @@ use glam::Vec2;
///
/// Nodes represent discrete movement targets in the maze. The index directly corresponds to the node's position in the
/// graph's internal storage arrays.
pub type NodeId = usize;
pub type NodeId = u16;
/// A component that represents the speed and cardinal direction of an entity.
/// Speed is static, only applied when the entity has an edge to traverse.
@@ -57,7 +57,7 @@ impl Position {
let pos = match &self {
Position::Stopped { node } => {
// Entity is stationary at a node
let node = graph.get_node(*node).ok_or(EntityError::NodeNotFound(*node))?;
let node = graph.get_node(*node).ok_or(EntityError::NodeNotFound(*node as usize))?;
node.position
}
Position::Moving {
@@ -66,11 +66,12 @@ impl Position {
remaining_distance,
} => {
// Entity is traveling between nodes
let from_node = graph.get_node(*from).ok_or(EntityError::NodeNotFound(*from))?;
let to_node = graph.get_node(*to).ok_or(EntityError::NodeNotFound(*to))?;
let edge = graph
.find_edge(*from, *to)
.ok_or(EntityError::EdgeNotFound { from: *from, to: *to })?;
let from_node = graph.get_node(*from).ok_or(EntityError::NodeNotFound(*from as usize))?;
let to_node = graph.get_node(*to).ok_or(EntityError::NodeNotFound(*to as usize))?;
let edge = graph.find_edge(*from, *to).ok_or(EntityError::EdgeNotFound {
from: *from as usize,
to: *to as usize,
})?;
// For zero-distance edges (tunnels), progress >= 1.0 means we're at the target
if edge.distance == 0.0 {

View File

@@ -5,7 +5,8 @@ use pacman::{
events::GameEvent,
map::builder::Map,
systems::{
check_collision, collision_system, Collider, EntityType, Ghost, GhostCollider, ItemCollider, PacmanCollider, Position,
check_collision, collision_system, Collider, EntityType, Ghost, GhostCollider, ItemCollider, NodeId, PacmanCollider,
Position,
},
};
@@ -59,7 +60,7 @@ fn spawn_test_ghost(world: &mut World) -> Entity {
fn spawn_test_ghost_at_node(world: &mut World, node: usize) -> Entity {
world
.spawn((
Position::Stopped { node },
Position::Stopped { node: node as NodeId },
Collider { size: 12.0 },
GhostCollider,
Ghost::Blinky,

View File

@@ -1,4 +1,4 @@
use glam::IVec2;
use glam::I8Vec2;
use pacman::map::direction::*;
#[test]
@@ -18,14 +18,14 @@ fn test_direction_opposite() {
#[test]
fn test_direction_as_ivec2() {
let test_cases = [
(Direction::Up, -IVec2::Y),
(Direction::Down, IVec2::Y),
(Direction::Left, -IVec2::X),
(Direction::Right, IVec2::X),
(Direction::Up, -I8Vec2::Y),
(Direction::Down, I8Vec2::Y),
(Direction::Left, -I8Vec2::X),
(Direction::Right, I8Vec2::X),
];
for (dir, expected) in test_cases {
assert_eq!(dir.as_ivec2(), expected);
assert_eq!(IVec2::from(dir), expected);
assert_eq!(I8Vec2::from(dir), expected);
}
}

View File

@@ -26,8 +26,10 @@ fn test_map_node_positions() {
for (grid_pos, &node_id) in &map.grid_to_node {
let node = map.graph.get_node(node_id).unwrap();
let expected_pos = Vec2::new((grid_pos.x * CELL_SIZE as i32) as f32, (grid_pos.y * CELL_SIZE as i32) as f32)
+ Vec2::splat(CELL_SIZE as f32 / 2.0);
let expected_pos = Vec2::new(
(grid_pos.x as i32 * CELL_SIZE as i32) as f32,
(grid_pos.y as i32 * CELL_SIZE as i32) as f32,
) + Vec2::splat(CELL_SIZE as f32 / 2.0);
assert_eq!(node.position, expected_pos);
}