feat(grid): add precidualy generated maze grid

This commit is contained in:
Kristofers Solo 2024-09-23 20:42:07 +03:00
parent f16fd51090
commit c5f8dede6d
11 changed files with 349 additions and 285 deletions

View File

@ -17,7 +17,7 @@ tracing = { version = "0.1", features = [
"max_level_debug",
"release_max_level_warn",
] }
hexx = { version = "0.18", features = ["bevy_reflect"] }
hexx = { version = "0.18", features = ["bevy_reflect", "grid"] }
bevy_prototype_lyon = "0.12"
[features]

View File

@ -1,36 +0,0 @@
use bevy::prelude::*;
use hexx::EdgeDirection;
pub(super) fn plugin(_app: &mut App) {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)]
pub enum Direction {
Top,
TopRight,
BottomRight,
Bottom,
BottomLeft,
TopLeft,
}
impl Direction {
pub const ALL: [Direction; 6] = [
Self::Top,
Self::TopRight,
Self::BottomRight,
Self::Bottom,
Self::BottomLeft,
Self::TopLeft,
];
pub fn opposite(&self) -> Self {
match self {
Self::Top => Self::Bottom,
Self::TopRight => Self::BottomLeft,
Self::BottomRight => Self::TopLeft,
Self::Bottom => Self::Top,
Self::BottomLeft => Self::TopRight,
Self::TopLeft => Self::BottomRight,
}
}
}

View File

@ -1,194 +0,0 @@
use bevy::{
color::palettes::css::{BLACK, GREEN, RED},
prelude::*,
utils::hashbrown::HashMap,
};
use bevy_prototype_lyon::{
draw::{Fill, Stroke},
entity::ShapeBundle,
path::PathBuilder,
plugin::ShapePlugin,
};
use rand::{prelude::SliceRandom, rngs::ThreadRng, thread_rng};
use super::tile::{AxialCoord, Tile};
const DIRECTIONS: [AxialCoord; 6] = [
AxialCoord { q: 1, r: 0 }, // Right
AxialCoord { q: 1, r: -1 }, // Top-right
AxialCoord { q: 0, r: -1 }, // Top-left
AxialCoord { q: -1, r: 0 }, // Left
AxialCoord { q: -1, r: 1 }, // Bottom-left
AxialCoord { q: 0, r: 1 }, // Bottom-right
];
pub struct HexGrid;
impl Plugin for HexGrid {
fn build(&self, app: &mut App) {
app.add_plugins(ShapePlugin);
app.add_systems(Startup, setup_system);
}
}
pub(super) fn setup_system(mut commands: Commands) {
let radius = 7;
let mut grid = generate_hex_grix(radius);
let start_coord = AxialCoord::new(-radius, 0);
let end_coord = AxialCoord::new(radius, 0);
let mut rng = thread_rng();
generate_maze(&mut grid, start_coord, &mut rng);
render_maze(&mut commands, &mut grid, radius, start_coord, end_coord);
}
fn generate_hex_grix(radius: i32) -> HashMap<AxialCoord, Tile> {
let mut grid = HashMap::new();
for q in -radius..=radius {
let r1 = (-radius).max(-q - radius);
let r2 = radius.min(-q + radius);
for r in r1..=r2 {
let coord = AxialCoord::new(q, r);
let tile = Tile {
position: coord,
walls: [true; 6],
visited: false,
};
grid.insert(coord, tile);
}
}
grid
}
fn add_hex_tile(
commands: &mut Commands,
position: Vec2,
size: f32,
walls: [bool; 6],
fill_color: Color,
) {
let hex_points = (0..6)
.map(|i| {
let angle_deg = 60. * i as f32 - 30.;
let angle_rad = angle_deg.to_radians();
Vec2::new(size * angle_rad.cos(), size * angle_rad.sin())
})
.collect::<Vec<Vec2>>();
let mut path_builder = PathBuilder::new();
path_builder.move_to(hex_points[0]);
for point in &hex_points[1..] {
path_builder.line_to(*point);
}
path_builder.close();
let hexagon = path_builder.build();
// Create the hexagon fill
commands.spawn((
ShapeBundle {
path: hexagon,
spatial: SpatialBundle {
transform: Transform::from_xyz(position.x, position.y, 0.),
..default()
},
..default()
},
Fill::color(fill_color),
));
// Draw walls
for i in 0..6 {
if walls[i] {
let start = hex_points[i];
let end = hex_points[(i + 1) % 6];
let mut line_builder = PathBuilder::new();
line_builder.move_to(start);
line_builder.line_to(end);
let line = line_builder.build();
commands.spawn((
ShapeBundle {
path: line,
spatial: SpatialBundle {
transform: Transform::from_xyz(position.x, position.y, 1.),
..default()
},
..default()
},
Stroke::new(BLACK, 2.),
));
}
}
}
fn generate_maze(
grid: &mut HashMap<AxialCoord, Tile>,
current_coord: AxialCoord,
rng: &mut ThreadRng,
) {
{
let current_tile = grid.get_mut(&current_coord).unwrap();
current_tile.visit();
}
let mut directions = DIRECTIONS.clone();
directions.shuffle(rng);
for (i, direction) in directions.iter().enumerate() {
let neightbor_coord = AxialCoord {
q: current_coord.q + direction.q,
r: current_coord.r + direction.r,
};
if let Some(neightbor_tile) = grid.get(&neightbor_coord) {
if !neightbor_tile.visited {
// Remove the wall between current_tile and neighbor_tile
{
let current_tile = grid.get_mut(&current_coord).unwrap();
current_tile.walls[i] = false;
}
{
let neightbor_tile = grid.get_mut(&neightbor_coord).unwrap();
neightbor_tile.walls[opposite_wall(i)] = false;
}
// Recurse with the neighbor tile
generate_maze(grid, neightbor_coord, rng);
}
}
}
}
fn render_maze(
commands: &mut Commands,
grid: &mut HashMap<AxialCoord, Tile>,
radius: i32,
start_coord: AxialCoord,
end_coord: AxialCoord,
) {
let hex_size = 30.;
let hex_height = hex_size * 2.;
let hex_width = (3.0_f32).sqrt() * hex_size;
for tile in grid.values() {
let (q, r) = (tile.position.q, tile.position.r);
let x = hex_width * (q as f32 + r as f32 / 2.);
let y = hex_height * (r as f32 * 3. / 4.);
let mut fill_color = Color::srgb(0.8, 0.8, 0.8);
if tile.position == start_coord {
fill_color = GREEN.into();
} else if tile.position == end_coord {
fill_color = RED.into();
}
add_hex_tile(commands, Vec2::new(x, y), hex_size, tile.walls, fill_color);
}
}
fn opposite_wall(index: usize) -> usize {
(index + 3) % 6
}

View File

@ -1,27 +0,0 @@
use bevy::{
ecs::{system::RunSystemOnce, world::Command},
prelude::*,
};
use bevy_prototype_lyon::plugin::ShapePlugin;
use grid::setup_system;
pub mod direction;
pub mod grid;
pub mod tile;
pub struct HexGrid;
impl Plugin for HexGrid {
fn build(&self, app: &mut App) {
app.add_plugins(ShapePlugin);
}
}
impl Command for HexGrid {
fn apply(self, world: &mut World) {
world.run_system_once(setup_system);
}
}
pub fn spawn_grid(world: &mut World) {
HexGrid.apply(world);
}

View File

@ -1,24 +0,0 @@
#[derive(Debug, Clone)]
pub struct Tile {
pub position: AxialCoord,
pub walls: [bool; 6],
pub visited: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AxialCoord {
pub q: i32,
pub r: i32,
}
impl Tile {
pub fn visit(&mut self) {
self.visited = true
}
}
impl AxialCoord {
pub fn new(q: i32, r: i32) -> Self {
Self { q, r }
}
}

View File

@ -5,7 +5,7 @@ mod demo;
#[cfg(feature = "dev")]
mod dev_tools;
#[cfg(not(feature = "demo"))]
mod hexgrid;
mod maze;
mod screens;
mod theme;
@ -63,7 +63,7 @@ impl Plugin for AppPlugin {
#[cfg(feature = "demo")]
demo::plugin,
#[cfg(not(feature = "demo"))]
hexgrid::HexGrid,
maze::MazePlugin,
screens::plugin,
theme::plugin,
));

218
src/maze/grid.rs Normal file
View File

@ -0,0 +1,218 @@
use std::usize;
use bevy::{
color::palettes::css::{BLACK, GREEN, RED},
prelude::*,
utils::hashbrown::HashMap,
};
use bevy_prototype_lyon::{
draw::{Fill, Stroke},
entity::ShapeBundle,
path::PathBuilder,
plugin::ShapePlugin,
};
use hexx::{EdgeDirection, Hex};
use log::info;
use rand::{prelude::SliceRandom, rngs::ThreadRng, thread_rng};
use super::{
resource::{Layout, MazeConfig},
tile::{Tile, TileBundle, Walls},
};
pub(super) fn plugin(app: &mut App) {
app.add_plugins(ShapePlugin);
app.init_resource::<MazeConfig>();
app.init_resource::<Layout>();
}
pub(super) fn spawn_hex_grid(mut commands: Commands, config: Res<MazeConfig>) {
let radius = config.radius as i32;
for q in -radius..=radius {
let r1 = (-radius).max(-q - radius);
let r2 = radius.min(-q + radius);
for r in r1..=r2 {
let tile = Tile::new(q, r);
commands.spawn((
Name::new(format!("Tile {}", &tile.to_string())),
TileBundle {
hex: tile,
..default()
},
));
}
}
}
pub(super) fn generate_maze(
mut commands: Commands,
query: Query<(Entity, &Tile, &Walls)>,
config: Res<MazeConfig>,
) {
let mut tiles = query
.into_iter()
.map(|(entity, tile, walls)| (tile.hex, (entity, tile.clone(), walls.clone())))
.collect();
let mut rng = thread_rng();
recursive_maze(&mut tiles, config.start_pos, &mut rng);
for (entity, tile, walls) in tiles.values() {
commands
.entity(*entity)
.insert(tile.clone())
.insert(walls.clone());
}
}
fn recursive_maze(
tiles: &mut HashMap<Hex, (Entity, Tile, Walls)>,
current_hex: Hex,
rng: &mut ThreadRng,
) {
{
let (_, tile, _) = tiles.get_mut(&current_hex).unwrap();
tile.visit();
}
let mut directions = EdgeDirection::ALL_DIRECTIONS;
directions.shuffle(rng);
for direction in directions.into_iter() {
let neighbor_hex = current_hex + direction;
if let Some((_, neighbor_tile, _)) = tiles.get(&neighbor_hex) {
if !neighbor_tile.visited {
remove_wall_between(tiles, current_hex, neighbor_hex, direction);
recursive_maze(tiles, neighbor_hex, rng);
}
}
}
}
fn remove_wall_between(
tiles: &mut HashMap<Hex, (Entity, Tile, Walls)>,
current_hex: Hex,
neighbor_hex: Hex,
direction: EdgeDirection,
) {
{
let (_, _, walls) = tiles.get_mut(&current_hex).unwrap();
walls.0[direction.index() as usize] = false;
}
{
let (_, _, walls) = tiles.get_mut(&neighbor_hex).unwrap();
walls.0[direction.const_neg().index() as usize] = false;
}
}
fn add_hex_tile(
commands: &mut Commands,
position: Vec2,
size: f32,
tile: &Tile,
walls: &Walls,
fill_color: Color,
layout: &Layout,
) {
let hex_points = tile
.hex
.all_vertices()
.into_iter()
.map(|v| {
let mut layout = layout.clone();
layout.origin = position;
layout.hex_size = Vec2::splat(size);
layout.hex_to_world_pos(v.origin + v.direction)
})
.collect::<Vec<Vec2>>();
let mut path_builder = PathBuilder::new();
path_builder.move_to(hex_points[0]);
for point in &hex_points[1..] {
path_builder.line_to(*point);
}
path_builder.close();
let hexagon = path_builder.build();
// Create the hexagon fill
commands.spawn((
ShapeBundle {
path: hexagon,
spatial: SpatialBundle {
transform: Transform::from_xyz(position.x, position.y, 0.),
..default()
},
..default()
},
Fill::color(fill_color),
));
// .with_children(|p| {
// p.spawn(Text2dBundle {
// text: Text {
// sections: vec![TextSection {
// value: tile.to_string(),
// style: TextStyle {
// font_size: 16.,
// color: Color::BLACK,
// ..default()
// },
// }],
// ..default()
// },
// transform: Transform::from_xyz(position.x * 2., position.y * 2., 1.),
// ..default()
// });
// });
// Draw walls
for direction in EdgeDirection::iter() {
let idx = direction.index() as usize;
if walls[idx] {
let start = hex_points[idx];
let end = hex_points[(idx + 1) % 6];
let mut line_builder = PathBuilder::new();
line_builder.move_to(start);
line_builder.line_to(end);
let line = line_builder.build();
commands.spawn((
ShapeBundle {
path: line,
spatial: SpatialBundle {
transform: Transform::from_xyz(position.x, position.y, 1.),
..default()
},
..default()
},
Stroke::new(BLACK, 2.),
));
}
}
}
pub(super) fn render_maze(
mut commands: Commands,
query: Query<(&Tile, &mut Walls)>,
layout: Res<Layout>,
config: Res<MazeConfig>,
) {
for (tile, walls) in query.iter() {
let world_pos = layout.hex_to_world_pos(tile.hex);
let fill_color = match tile.hex {
pos if pos == config.start_pos => GREEN.into(),
pos if pos == config.end_pos => RED.into(),
_ => Color::srgb(0.8, 0.8, 0.8),
};
add_hex_tile(
&mut commands,
world_pos,
config.size,
tile,
walls,
fill_color,
&layout,
);
}
}

28
src/maze/mod.rs Normal file
View File

@ -0,0 +1,28 @@
use bevy::{
ecs::{system::RunSystemOnce, world::Command},
prelude::*,
};
use grid::{generate_maze, plugin, render_maze, spawn_hex_grid};
pub mod grid;
pub mod resource;
pub mod tile;
pub struct MazePlugin;
impl Plugin for MazePlugin {
fn build(&self, app: &mut App) {
app.add_plugins(plugin);
}
}
impl Command for MazePlugin {
fn apply(self, world: &mut World) {
world.run_system_once(spawn_hex_grid);
world.run_system_once(generate_maze);
world.run_system_once(render_maze);
}
}
pub fn spawn_grid(world: &mut World) {
MazePlugin.apply(world);
}

53
src/maze/resource.rs Normal file
View File

@ -0,0 +1,53 @@
use bevy::prelude::*;
use hexx::{Hex, HexLayout, HexOrientation};
use rand::{thread_rng, Rng};
#[derive(Debug, Reflect, Resource)]
#[reflect(Resource)]
pub struct MazeConfig {
pub radius: u32,
pub size: f32,
pub start_pos: Hex,
pub end_pos: Hex,
}
impl Default for MazeConfig {
fn default() -> Self {
let mut rng = thread_rng();
let radius = 5;
let start_pos = Hex::new(
rng.gen_range(-radius..radius),
rng.gen_range(-radius..radius),
);
let end_pos = Hex::new(
rng.gen_range(-radius..radius),
rng.gen_range(-radius..radius),
);
debug!("Start pos: ({},{})", start_pos.x, start_pos.y);
debug!("End pos: ({},{})", end_pos.x, end_pos.y);
Self {
radius: radius as u32,
size: 10.,
start_pos,
end_pos,
}
}
}
#[derive(Debug, Reflect, Resource, Deref, DerefMut, Clone)]
#[reflect(Resource)]
pub struct Layout(pub HexLayout);
impl FromWorld for Layout {
fn from_world(world: &mut World) -> Self {
let size = world
.get_resource::<MazeConfig>()
.unwrap_or(&MazeConfig::default())
.size;
Self(HexLayout {
orientation: HexOrientation::Pointy,
hex_size: Vec2::splat(size),
..default()
})
}
}

46
src/maze/tile.rs Normal file
View File

@ -0,0 +1,46 @@
use std::fmt::Display;
use bevy::prelude::*;
use hexx::Hex;
#[derive(Debug, Reflect, Component, Default, PartialEq, Eq, Hash, Clone)]
#[reflect(Component)]
pub struct Tile {
pub hex: Hex,
pub visited: bool,
}
#[derive(Debug, Reflect, Component, Deref, DerefMut, Clone)]
#[reflect(Component)]
pub struct Walls(pub [bool; 6]);
#[derive(Debug, Reflect, Bundle, Default)]
pub struct TileBundle {
pub hex: Tile,
pub walls: Walls,
}
impl Tile {
pub fn new(q: i32, r: i32) -> Self {
Self {
hex: Hex::new(q, r),
visited: false,
}
}
pub fn visit(&mut self) {
self.visited = true;
}
}
impl Display for Tile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({},{})", self.hex.x, self.hex.y)
}
}
impl Default for Walls {
fn default() -> Self {
Self([true; 6])
}
}

View File

@ -5,7 +5,7 @@ use bevy::{input::common_conditions::input_just_pressed, prelude::*};
#[cfg(feature = "demo")]
use crate::demo::level::spawn_level as spawn_level_command;
#[cfg(not(feature = "demo"))]
use crate::hexgrid::spawn_grid as spawn_level_command;
use crate::maze::spawn_grid as spawn_level_command;
use crate::{asset_tracking::LoadResource, audio::Music, screens::Screen};
pub(super) fn plugin(app: &mut App) {