Merge pull request #5 from kristoferssolo/fix/maze-generation

Fix/maze generation
This commit is contained in:
Kristofers Solo 2024-12-08 19:39:01 +02:00 committed by GitHub
commit 3a7f8b6401
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 746 additions and 1374 deletions

447
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
[package]
name = "maze-ascension"
authors = ["Kristofers Solo <dev@kristofers.xyz>"]
version = "0.0.5"
version = "0.0.6"
edition = "2021"
[dependencies]
@ -18,8 +18,10 @@ tracing = { version = "0.1", features = [
"release_max_level_warn",
] }
hexx = { version = "0.18", features = ["bevy_reflect", "grid"] }
bevy_prototype_lyon = "0.12"
hexlab = { version = "0.1", features = ["bevy"] }
bevy-inspector-egui = { version = "0.27", optional = true }
bevy_egui = { version = "0.30", optional = true }
thiserror = "2.0"
[features]
@ -31,7 +33,8 @@ dev = [
# Improve compile times for dev builds by linking Bevy as a dynamic library.
"bevy/dynamic_linking",
"bevy/bevy_dev_tools",
"bevy-inspector-egui",
"dep:bevy-inspector-egui",
"dep:bevy_egui",
]
dev_native = [
"dev",
@ -40,7 +43,6 @@ dev_native = [
# Enable embedded asset hot reloading for native dev builds.
"bevy/embedded_watcher",
]
demo = []
# Idiomatic Bevy code often triggers these lints, and the CI workflow treats them as errors.

View File

@ -1,131 +0,0 @@
_Brought to you by the Bevy Jam working group._
# Bevy Quickstart
This template is a great way to get started on a new [Bevy](https://bevyengine.org/) game—especially for a game jam!
Start with a [basic project structure](#write-your-game) and [CI / CD](#release-your-game) that can deploy to [itch.io](https://itch.io).
You can [try this template in your web browser!](https://the-bevy-flock.itch.io/bevy-quickstart)
[@ChristopherBiscardi](https://github.com/ChristopherBiscardi) made a video on how to use this template from start to finish:
[<img src="./docs/img/thumbnail.png" width=40% height=40% alt="A video tutorial for bevy_quickstart"/>](https://www.youtube.com/watch?v=ESBRyXClaYc)
## Prerequisites
We assume that you know how to use Bevy already and have seen the [official Quick Start Guide](https://bevyengine.org/learn/quick-start/introduction/).
If you're new to Bevy, the patterns used in this template may look a bit weird at first glance.
See our [Design Document](./docs/design.md) for more information on how we structured the code and why.
## Create a new game
Install [`cargo-generate`](https://github.com/cargo-generate/cargo-generate) and run the following command:
```sh
cargo generate TheBevyFlock/bevy_quickstart --branch cargo-generate
```
Then navigate to the newly generated directory and run the following commands:
```sh
git branch --move main
cargo update
git commit -am 'Initial commit'
```
Then [create a GitHub repository](https://github.com/new) and push your local repository to it.
<details>
<summary>This template can also be set up manually.</summary>
Navigate to the top of [this GitHub repository](https://github.com/TheBevyFlock/bevy_quickstart/) and select `Use this template > Create a new repository`:
![UI demonstration](./docs/img/readme-manual-setup.png)
Clone your new Github repository to a local repository and push a commit with the following changes:
- Delete `LICENSE`, `README`, and `docs/` files.
- Search for and replace instances of `bevy_quickstart` with the name of your project.
- Adjust the `env` variables in [`.github/workflows/release.yaml`](./.github/workflows/release.yaml).
</details>
## Write your game
The best way to get started is to play around with what you find in [`src/demo/`](./src/demo).
This template comes with a basic project structure that you may find useful:
| Path | Description |
| -------------------------------------------------- | ------------------------------------------------------------------ |
| [`src/lib.rs`](./src/lib.rs) | App setup |
| [`src/asset_tracking.rs`](./src/asset_tracking.rs) | A high-level way to load collections of asset handles as resources |
| [`src/audio/`](./src/audio) | Marker components for sound effects and music |
| [`src/demo/`](./src/demo) | Example game mechanics & content (replace with your own code) |
| [`src/dev_tools.rs`](./src/dev_tools.rs) | Dev tools for dev builds (press \` aka backtick to toggle) |
| [`src/screens/`](./src/screens) | Splash screen, title screen, gameplay screen, etc. |
| [`src/theme/`](./src/theme) | Reusable UI widgets & theming |
Feel free to move things around however you want, though.
> [!Tip]
> Be sure to check out the [3rd-party tools](./docs/tooling.md) we recommend!
## Run your game
Running your game locally is very simple:
- Use `cargo run` to run a native dev build.
- Use [`trunk serve`](https://trunkrs.dev/) to run a web dev build.
If you're using [VS Code](https://code.visualstudio.com/), this template comes with a [`.vscode/tasks.json`](./.vscode/tasks.json) file.
<details>
<summary>Run release builds</summary>
- Use `cargo run --profile release-native --no-default-features` to run a native release build.
- Use `trunk serve --release --no-default-features` to run a web release build.
</details>
<details>
<summary>Linux dependencies</summary>
If you are using Linux, make sure you take a look at Bevy's [Linux dependencies](https://github.com/bevyengine/bevy/blob/main/docs/linux_dependencies.md).
Note that this template enables Wayland support, which requires additional dependencies as detailed in the link above.
Wayland is activated by using the `bevy/wayland` feature in the [`Cargo.toml`](./Cargo.toml).
</details>
<details>
<summary>(Optional) Improve your compile times</summary>
[`.cargo/config_fast_builds.toml`](./.cargo/config_fast_builds.toml) contains documentation on how to set up your environment to improve compile times.
After you've fiddled with it, rename it to `.cargo/config.toml` to enable it.
</details>
## Release your game
This template uses [GitHub workflows](https://docs.github.com/en/actions/using-workflows) to run tests and build releases.
See [Workflows](./docs/workflows.md) for more information.
## Known Issues
There are some known issues in Bevy that require some arcane workarounds.
To keep this template simple, we have opted not to include those workarounds.
You can read about them in the [Known Issues](./docs/known-issues.md) document.
## License
The source code in this repository is licensed under any of the following at your option:
- [CC0-1.0 License](./LICENSE-CC0-1.0.txt)
- [MIT License](./LICENSE-MIT.txt)
- [Apache License, Version 2.0](./LICENSE-Apache-2.0.txt)
The CC0 license explicitly does not waive patent rights, but we confirm that we hold no patent rights to anything presented in this repository.
## Credits
The [assets](./assets) in this repository are all 3rd-party. See the [credits screen](./src/screens/credits.rs) for more information.

View File

@ -1,177 +0,0 @@
//! Player sprite animation.
//! This is based on multiple examples and may be very different for your game.
//! - [Sprite flipping](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_flipping.rs)
//! - [Sprite animation](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
//! - [Timers](https://github.com/bevyengine/bevy/blob/latest/examples/time/timers.rs)
use bevy::prelude::*;
use rand::prelude::*;
use std::time::Duration;
use crate::{
audio::SoundEffect,
demo::{movement::MovementController, player::PlayerAssets},
AppSet,
};
pub(super) fn plugin(app: &mut App) {
// Animate and play sound effects based on controls.
app.register_type::<PlayerAnimation>();
app.add_systems(
Update,
(
update_animation_timer.in_set(AppSet::TickTimers),
(
update_animation_movement,
update_animation_atlas,
trigger_step_sound_effect,
)
.chain()
.run_if(resource_exists::<PlayerAssets>)
.in_set(AppSet::Update),
),
);
}
/// Update the sprite direction and animation state (idling/walking).
fn update_animation_movement(
mut player_query: Query<(&MovementController, &mut Sprite, &mut PlayerAnimation)>,
) {
for (controller, mut sprite, mut animation) in &mut player_query {
let dx = controller.intent.x;
if dx != 0.0 {
sprite.flip_x = dx < 0.0;
}
let animation_state = if controller.intent == Vec2::ZERO {
PlayerAnimationState::Idling
} else {
PlayerAnimationState::Walking
};
animation.update_state(animation_state);
}
}
/// Update the animation timer.
fn update_animation_timer(time: Res<Time>, mut query: Query<&mut PlayerAnimation>) {
for mut animation in &mut query {
animation.update_timer(time.delta());
}
}
/// Update the texture atlas to reflect changes in the animation.
fn update_animation_atlas(mut query: Query<(&PlayerAnimation, &mut TextureAtlas)>) {
for (animation, mut atlas) in &mut query {
if animation.changed() {
atlas.index = animation.get_atlas_index();
}
}
}
/// If the player is moving, play a step sound effect synchronized with the
/// animation.
fn trigger_step_sound_effect(
mut commands: Commands,
player_assets: Res<PlayerAssets>,
mut step_query: Query<&PlayerAnimation>,
) {
for animation in &mut step_query {
if animation.state == PlayerAnimationState::Walking
&& animation.changed()
&& (animation.frame == 2 || animation.frame == 5)
{
let rng = &mut rand::thread_rng();
let random_step = player_assets.steps.choose(rng).unwrap();
commands.spawn((
AudioBundle {
source: random_step.clone(),
settings: PlaybackSettings::DESPAWN,
},
SoundEffect,
));
}
}
}
/// Component that tracks player's animation state.
/// It is tightly bound to the texture atlas we use.
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct PlayerAnimation {
timer: Timer,
frame: usize,
state: PlayerAnimationState,
}
#[derive(Reflect, PartialEq)]
pub enum PlayerAnimationState {
Idling,
Walking,
}
impl PlayerAnimation {
/// The number of idle frames.
const IDLE_FRAMES: usize = 2;
/// The duration of each idle frame.
const IDLE_INTERVAL: Duration = Duration::from_millis(500);
/// The number of walking frames.
const WALKING_FRAMES: usize = 6;
/// The duration of each walking frame.
const WALKING_INTERVAL: Duration = Duration::from_millis(50);
fn idling() -> Self {
Self {
timer: Timer::new(Self::IDLE_INTERVAL, TimerMode::Repeating),
frame: 0,
state: PlayerAnimationState::Idling,
}
}
fn walking() -> Self {
Self {
timer: Timer::new(Self::WALKING_INTERVAL, TimerMode::Repeating),
frame: 0,
state: PlayerAnimationState::Walking,
}
}
pub fn new() -> Self {
Self::idling()
}
/// Update animation timers.
pub fn update_timer(&mut self, delta: Duration) {
self.timer.tick(delta);
if !self.timer.finished() {
return;
}
self.frame = (self.frame + 1)
% match self.state {
PlayerAnimationState::Idling => Self::IDLE_FRAMES,
PlayerAnimationState::Walking => Self::WALKING_FRAMES,
};
}
/// Update animation state if it changes.
pub fn update_state(&mut self, state: PlayerAnimationState) {
if self.state != state {
match state {
PlayerAnimationState::Idling => *self = Self::idling(),
PlayerAnimationState::Walking => *self = Self::walking(),
}
}
}
/// Whether animation changed this tick.
pub fn changed(&self) -> bool {
self.timer.finished()
}
/// Return sprite index in the atlas.
pub fn get_atlas_index(&self) -> usize {
match self.state {
PlayerAnimationState::Idling => self.frame,
PlayerAnimationState::Walking => 6 + self.frame,
}
}
}

View File

@ -1,20 +0,0 @@
//! Spawn the main level.
use bevy::{ecs::world::Command, prelude::*};
use crate::demo::player::SpawnPlayer;
pub(super) fn plugin(_app: &mut App) {
// No setup required for this plugin.
// It's still good to have a function here so that we can add some setup
// later if needed.
}
/// A [`Command`] to spawn the level.
/// Functions that accept only `&mut World` as their parameter implement [`Command`].
/// We use this style when a command requires no configuration.
pub fn spawn_level(world: &mut World) {
// The only thing we have in our level is a player,
// but add things like walls etc. here.
SpawnPlayer { max_speed: 400.0 }.apply(world);
}

View File

@ -1,20 +0,0 @@
//! Demo gameplay. All of these modules are only intended for demonstration
//! purposes and should be replaced with your own game logic.
//! Feel free to change the logic found here if you feel like tinkering around
//! to get a feeling for the template.
use bevy::prelude::*;
mod animation;
pub mod level;
mod movement;
pub mod player;
pub(super) fn plugin(app: &mut App) {
app.add_plugins((
animation::plugin,
movement::plugin,
player::plugin,
level::plugin,
));
}

View File

@ -1,84 +0,0 @@
//! Handle player input and translate it into movement through a character
//! controller. A character controller is the collection of systems that govern
//! the movement of characters.
//!
//! In our case, the character controller has the following logic:
//! - Set [`MovementController`] intent based on directional keyboard input.
//! This is done in the `player` module, as it is specific to the player
//! character.
//! - Apply movement based on [`MovementController`] intent and maximum speed.
//! - Wrap the character within the window.
//!
//! Note that the implementation used here is limited for demonstration
//! purposes. If you want to move the player in a smoother way,
//! consider using a [fixed timestep](https://github.com/bevyengine/bevy/blob/main/examples/movement/physics_in_fixed_timestep.rs).
use bevy::{prelude::*, window::PrimaryWindow};
use crate::AppSet;
pub(super) fn plugin(app: &mut App) {
app.register_type::<(MovementController, ScreenWrap)>();
app.add_systems(
Update,
(apply_movement, apply_screen_wrap)
.chain()
.in_set(AppSet::Update),
);
}
/// These are the movement parameters for our character controller.
/// For now, this is only used for a single player, but it could power NPCs or
/// other players as well.
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct MovementController {
/// The direction the character wants to move in.
pub intent: Vec2,
/// Maximum speed in world units per second.
/// 1 world unit = 1 pixel when using the default 2D camera and no physics
/// engine.
pub max_speed: f32,
}
impl Default for MovementController {
fn default() -> Self {
Self {
intent: Vec2::ZERO,
// 400 pixels per second is a nice default, but we can still vary this per character.
max_speed: 400.0,
}
}
}
fn apply_movement(
time: Res<Time>,
mut movement_query: Query<(&MovementController, &mut Transform)>,
) {
for (controller, mut transform) in &mut movement_query {
let velocity = controller.max_speed * controller.intent;
transform.translation += velocity.extend(0.0) * time.delta_seconds();
}
}
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct ScreenWrap;
fn apply_screen_wrap(
window_query: Query<&Window, With<PrimaryWindow>>,
mut wrap_query: Query<&mut Transform, With<ScreenWrap>>,
) {
let Ok(window) = window_query.get_single() else {
return;
};
let size = window.size() + 256.0;
let half_size = size / 2.0;
for mut transform in &mut wrap_query {
let position = transform.translation.xy();
let wrapped = (position + half_size).rem_euclid(size) - half_size;
transform.translation = wrapped.extend(transform.translation.z);
}
}

View File

@ -1,153 +0,0 @@
//! Plugin handling the player character in particular.
//! Note that this is separate from the `movement` module as that could be used
//! for other characters as well.
use bevy::{
ecs::{system::RunSystemOnce as _, world::Command},
prelude::*,
render::texture::{ImageLoaderSettings, ImageSampler},
};
use crate::{
asset_tracking::LoadResource,
demo::{
animation::PlayerAnimation,
movement::{MovementController, ScreenWrap},
},
screens::Screen,
AppSet,
};
pub(super) fn plugin(app: &mut App) {
app.register_type::<Player>();
app.load_resource::<PlayerAssets>();
// Record directional input as movement controls.
app.add_systems(
Update,
record_player_directional_input.in_set(AppSet::RecordInput),
);
}
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Default, Reflect)]
#[reflect(Component)]
pub struct Player;
/// A command to spawn the player character.
#[derive(Debug)]
pub struct SpawnPlayer {
/// See [`MovementController::max_speed`].
pub max_speed: f32,
}
impl Command for SpawnPlayer {
fn apply(self, world: &mut World) {
world.run_system_once_with(self, spawn_player);
}
}
fn spawn_player(
In(config): In<SpawnPlayer>,
mut commands: Commands,
player_assets: Res<PlayerAssets>,
mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
) {
// A texture atlas is a way to split one image with a grid into multiple
// sprites. By attaching it to a [`SpriteBundle`] and providing an index, we
// can specify which section of the image we want to see. We will use this
// to animate our player character. You can learn more about texture atlases in
// this example: https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs
let layout = TextureAtlasLayout::from_grid(UVec2::splat(32), 6, 2, Some(UVec2::splat(1)), None);
let texture_atlas_layout = texture_atlas_layouts.add(layout);
let player_animation = PlayerAnimation::new();
commands.spawn((
Name::new("Player"),
Player,
SpriteBundle {
texture: player_assets.ducky.clone(),
transform: Transform::from_scale(Vec2::splat(8.0).extend(1.0)),
..Default::default()
},
TextureAtlas {
layout: texture_atlas_layout.clone(),
index: player_animation.get_atlas_index(),
},
MovementController {
max_speed: config.max_speed,
..default()
},
ScreenWrap,
player_animation,
StateScoped(Screen::Gameplay),
));
}
fn record_player_directional_input(
input: Res<ButtonInput<KeyCode>>,
mut controller_query: Query<&mut MovementController, With<Player>>,
) {
// Collect directional input.
let mut intent = Vec2::ZERO;
if input.pressed(KeyCode::KeyW) || input.pressed(KeyCode::ArrowUp) {
intent.y += 1.0;
}
if input.pressed(KeyCode::KeyS) || input.pressed(KeyCode::ArrowDown) {
intent.y -= 1.0;
}
if input.pressed(KeyCode::KeyA) || input.pressed(KeyCode::ArrowLeft) {
intent.x -= 1.0;
}
if input.pressed(KeyCode::KeyD) || input.pressed(KeyCode::ArrowRight) {
intent.x += 1.0;
}
// Normalize so that diagonal movement has the same speed as
// horizontal and vertical movement.
// This should be omitted if the input comes from an analog stick instead.
let intent = intent.normalize_or_zero();
// Apply movement intent to controllers.
for mut controller in &mut controller_query {
controller.intent = intent;
}
}
#[derive(Resource, Asset, Reflect, Clone)]
pub struct PlayerAssets {
// This #[dependency] attribute marks the field as a dependency of the Asset.
// This means that it will not finish loading until the labeled asset is also loaded.
#[dependency]
pub ducky: Handle<Image>,
#[dependency]
pub steps: Vec<Handle<AudioSource>>,
}
impl PlayerAssets {
pub const PATH_DUCKY: &'static str = "images/ducky.png";
pub const PATH_STEP_1: &'static str = "audio/sound_effects/step1.ogg";
pub const PATH_STEP_2: &'static str = "audio/sound_effects/step2.ogg";
pub const PATH_STEP_3: &'static str = "audio/sound_effects/step3.ogg";
pub const PATH_STEP_4: &'static str = "audio/sound_effects/step4.ogg";
}
impl FromWorld for PlayerAssets {
fn from_world(world: &mut World) -> Self {
let assets = world.resource::<AssetServer>();
Self {
ducky: assets.load_with_settings(
PlayerAssets::PATH_DUCKY,
|settings: &mut ImageLoaderSettings| {
// Use `nearest` image sampling to preserve the pixel art style.
settings.sampler = ImageSampler::nearest();
},
),
steps: vec![
assets.load(PlayerAssets::PATH_STEP_1),
assets.load(PlayerAssets::PATH_STEP_2),
assets.load(PlayerAssets::PATH_STEP_3),
assets.load(PlayerAssets::PATH_STEP_4),
],
}
}
}

View File

@ -1,33 +0,0 @@
//! Development tools for the game. This plugin is only enabled in dev builds.
use bevy::{
dev_tools::{
states::log_transitions,
ui_debug_overlay::{DebugUiPlugin, UiDebugOptions},
},
input::common_conditions::input_just_pressed,
prelude::*,
};
use bevy_inspector_egui::quick::WorldInspectorPlugin;
use crate::screens::Screen;
pub(super) fn plugin(app: &mut App) {
// Log `Screen` state transitions.
app.add_systems(Update, log_transitions::<Screen>);
// Toggle the debug overlay for UI.
app.add_plugins(DebugUiPlugin);
app.add_plugins(WorldInspectorPlugin::default());
app.add_systems(
Update,
toggle_debug_ui.run_if(input_just_pressed(TOGGLE_KEY)),
);
}
const TOGGLE_KEY: KeyCode = KeyCode::Backquote;
fn toggle_debug_ui(mut options: ResMut<UiDebugOptions>) {
options.toggle();
}

4
src/dev_tools/mod.rs Normal file
View File

@ -0,0 +1,4 @@
mod plugin;
mod ui;
pub use plugin::DevToolsPlugin;

36
src/dev_tools/plugin.rs Normal file
View File

@ -0,0 +1,36 @@
use crate::screens::Screen;
use bevy::{
dev_tools::{
states::log_transitions,
ui_debug_overlay::{DebugUiPlugin, UiDebugOptions},
},
input::common_conditions::input_just_pressed,
prelude::*,
};
use bevy_egui::EguiPlugin;
use bevy_inspector_egui::quick::WorldInspectorPlugin;
use super::ui::maze_controls_ui;
#[derive(Debug)]
pub struct DevToolsPlugin;
impl Plugin for DevToolsPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, log_transitions::<Screen>)
.add_plugins(EguiPlugin)
.add_plugins(WorldInspectorPlugin::new())
.add_plugins(DebugUiPlugin)
.add_systems(Update, maze_controls_ui)
.add_systems(
Update,
toggle_debug_ui.run_if(input_just_pressed(TOGGLE_KEY)),
);
}
}
const TOGGLE_KEY: KeyCode = KeyCode::Backquote;
fn toggle_debug_ui(mut options: ResMut<UiDebugOptions>) {
options.toggle();
}

View File

@ -0,0 +1,143 @@
use std::ops::RangeInclusive;
use bevy::{prelude::*, window::PrimaryWindow};
use hexx::{Hex, HexOrientation};
use rand::{thread_rng, Rng};
use crate::maze::{events::RecreateMazeEvent, MazeConfig, MazePluginLoaded};
use bevy_egui::{
egui::{self, emath::Numeric, DragValue, TextEdit, Ui},
EguiContext,
};
pub(crate) fn maze_controls_ui(world: &mut World) {
if world.get_resource::<MazePluginLoaded>().is_none() {
return;
}
let Ok(egui_context) = world
.query_filtered::<&mut EguiContext, With<PrimaryWindow>>()
.get_single(world)
else {
return;
};
let mut egui_context = egui_context.clone();
egui::Window::new("Maze Controls").show(egui_context.get_mut(), |ui| {
if let Some(mut maze_config) = world.get_resource_mut::<MazeConfig>() {
let mut changed = false;
ui.heading("Maze Configuration");
changed |= add_seed_control(ui, &mut maze_config.seed);
changed |= add_drag_value_control(ui, "Radius:", &mut maze_config.radius, 1.0, 1..=100);
changed |=
add_drag_value_control(ui, "Height:", &mut maze_config.height, 0.5, 1.0..=50.0);
changed |= add_drag_value_control(
ui,
"Hex Size:",
&mut maze_config.hex_size,
1.0,
1.0..=100.0,
);
changed |= add_orientation_control(ui, &mut maze_config.layout.orientation);
changed |= add_position_control(ui, "Start Position:", &mut maze_config.start_pos);
changed |= add_position_control(ui, "End Position:", &mut maze_config.end_pos);
// Trigger recreation if any value changed
if changed {
maze_config.update();
if let Some(mut event_writer) =
world.get_resource_mut::<Events<RecreateMazeEvent>>()
{
event_writer.send(RecreateMazeEvent { floor: 1 });
}
}
}
});
}
fn add_drag_value_control<T: Numeric>(
ui: &mut egui::Ui,
label: &str,
value: &mut T,
speed: f64,
range: RangeInclusive<T>,
) -> bool {
let mut changed = false;
ui.horizontal(|ui| {
ui.label(label);
let response = ui.add(DragValue::new(value).speed(speed).range(range));
changed = response.changed();
});
changed
}
fn add_position_control(ui: &mut Ui, label: &str, pos: &mut Hex) -> bool {
let mut changed = false;
ui.horizontal(|ui| {
ui.label(label);
let response_x = ui.add(DragValue::new(&mut pos.x).speed(1).prefix("x: "));
let response_y = ui.add(DragValue::new(&mut pos.y).speed(1).prefix("y: "));
changed = response_x.changed() || response_y.changed();
});
changed
}
fn add_seed_control(ui: &mut Ui, seed: &mut u64) -> bool {
let mut changed = false;
ui.horizontal(|ui| {
ui.label("Seed:");
let mut seed_text = seed.to_string();
let response = ui.add(
TextEdit::singleline(&mut seed_text)
.desired_width(150.0)
.hint_text("Enter seed"),
);
// Parse text input when changed
if response.changed() {
if let Ok(new_seed) = seed_text.parse::<u64>() {
*seed = new_seed;
changed = true;
}
}
// New random seed button
if ui.button("🎲").clicked() {
*seed = thread_rng().gen();
changed = true;
}
// Copy button
if ui.button("📋").clicked() {
ui.output_mut(|o| o.copied_text = seed.to_string());
}
});
changed
}
fn add_orientation_control(ui: &mut Ui, orientation: &mut HexOrientation) -> bool {
let mut changed = false;
ui.horizontal(|ui| {
ui.label("Orientation:");
let response = ui.radio_value(orientation, HexOrientation::Flat, "Flat");
changed |= response.changed();
let response = ui.radio_value(orientation, HexOrientation::Pointy, "Pointy");
changed |= response.changed();
});
changed
}

3
src/dev_tools/ui/mod.rs Normal file
View File

@ -0,0 +1,3 @@
mod maze_controls;
pub(crate) use maze_controls::maze_controls_ui;

View File

@ -1,10 +1,7 @@
mod asset_tracking;
pub mod audio;
#[cfg(feature = "demo")]
mod demo;
#[cfg(feature = "dev")]
mod dev_tools;
#[cfg(not(feature = "demo"))]
mod maze;
mod screens;
mod theme;
@ -60,9 +57,6 @@ impl Plugin for AppPlugin {
// Add other plugins.
app.add_plugins((
asset_tracking::plugin,
#[cfg(feature = "demo")]
demo::plugin,
#[cfg(not(feature = "demo"))]
maze::plugin::MazePlugin,
screens::plugin,
theme::plugin,
@ -70,7 +64,7 @@ impl Plugin for AppPlugin {
// Enable dev tools for dev builds.
#[cfg(feature = "dev")]
app.add_plugins(dev_tools::plugin);
app.add_plugins(dev_tools::DevToolsPlugin);
}
}
@ -91,7 +85,7 @@ fn spawn_camera(mut commands: Commands) {
commands.spawn((
Name::new("Camera"),
Camera3dBundle {
transform: Transform::from_xyz(0., 300., 300.).looking_at(Vec3::ZERO, Vec3::Y),
transform: Transform::from_xyz(200., 200., 0.).looking_at(Vec3::ZERO, Vec3::Y),
..default()
},
// Render all UI to this camera.

64
src/maze/assets.rs Normal file
View File

@ -0,0 +1,64 @@
use super::MazeConfig;
use bevy::prelude::*;
use std::f32::consts::FRAC_PI_2;
const WALL_OVERLAP_MODIFIER: f32 = 1.25;
const HEX_SIDES: usize = 6;
const WHITE_EMISSION_INTENSITY: f32 = 10.;
pub(crate) struct MazeAssets {
pub(crate) hex_mesh: Handle<Mesh>,
pub(crate) wall_mesh: Handle<Mesh>,
pub(crate) hex_material: Handle<StandardMaterial>,
pub(crate) wall_material: Handle<StandardMaterial>,
}
impl MazeAssets {
pub(crate) fn new(
meshes: &mut ResMut<Assets<Mesh>>,
materials: &mut ResMut<Assets<StandardMaterial>>,
config: &MazeConfig,
) -> MazeAssets {
MazeAssets {
hex_mesh: meshes.add(generate_hex_mesh(config.hex_size, config.height)),
wall_mesh: meshes.add(generate_square_mesh(
config.hex_size + config.wall_size() / WALL_OVERLAP_MODIFIER,
config.wall_size(),
)),
hex_material: materials.add(white_material()),
wall_material: materials.add(Color::BLACK),
}
}
}
fn generate_hex_mesh(radius: f32, depth: f32) -> Mesh {
let hexagon = RegularPolygon {
sides: HEX_SIDES,
circumcircle: Circle::new(radius),
};
let prism_shape = Extrusion::new(hexagon, depth);
let rotation = Quat::from_rotation_x(FRAC_PI_2);
Mesh::from(prism_shape).rotated_by(rotation)
}
fn generate_square_mesh(depth: f32, wall_size: f32) -> Mesh {
let square = Rectangle::new(wall_size, wall_size);
let rectangular_prism = Extrusion::new(square, depth);
let rotation = Quat::from_rotation_x(FRAC_PI_2);
Mesh::from(rectangular_prism).rotated_by(rotation)
}
fn white_material() -> StandardMaterial {
StandardMaterial {
base_color: Color::WHITE,
emissive: LinearRgba::new(
WHITE_EMISSION_INTENSITY,
WHITE_EMISSION_INTENSITY,
WHITE_EMISSION_INTENSITY,
WHITE_EMISSION_INTENSITY,
),
..default()
}
}

13
src/maze/components.rs Normal file
View File

@ -0,0 +1,13 @@
use bevy::prelude::*;
#[derive(Debug, Reflect, Component)]
#[reflect(Component)]
pub(crate) struct MazeFloor(pub(crate) u8);
#[derive(Debug, Reflect, Component)]
#[reflect(Component)]
pub(crate) struct MazeTile;
#[derive(Debug, Reflect, Component)]
#[reflect(Component)]
pub(crate) struct MazeWall;

6
src/maze/events.rs Normal file
View File

@ -0,0 +1,6 @@
use bevy::prelude::*;
#[derive(Debug, Event)]
pub(crate) struct RecreateMazeEvent {
pub(crate) floor: u8,
}

View File

@ -1,220 +0,0 @@
use bevy::{
color::palettes::css::{BLACK, GREEN, RED},
pbr::wireframe::{WireframeConfig, WireframePlugin},
prelude::*,
utils::hashbrown::HashMap,
};
use bevy_prototype_lyon::{
draw::{Fill, Stroke},
entity::ShapeBundle,
path::PathBuilder,
plugin::ShapePlugin,
};
use hexx::{EdgeDirection, Hex};
use rand::{prelude::SliceRandom, rngs::ThreadRng, thread_rng};
use super::{
resource::{Layout, MazeConfig, HEX_SIZE},
tile::{Tile, TileBundle, Walls},
};
pub(super) fn plugin(app: &mut App) {
app.add_plugins((ShapePlugin, WireframePlugin));
app.init_resource::<MazeConfig>();
app.init_resource::<Layout>();
app.insert_resource(WireframeConfig {
global: false,
..default()
});
}
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: Vec3,
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.xy();
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).extend(0.);
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,
HEX_SIZE,
tile,
walls,
fill_color,
&layout,
);
}
}

View File

@ -1,11 +1,14 @@
use bevy::{ecs::world::Command, prelude::*};
use plugin::MazePlugin;
pub mod grid;
mod assets;
mod components;
pub mod events;
pub mod plugin;
pub mod prism;
pub mod resource;
pub mod tile;
mod resources;
mod systems;
pub fn spawn_grid(world: &mut World) {
pub use resources::{MazeConfig, MazePluginLoaded};
pub fn spawn_maze(world: &mut World) {
MazePlugin.apply(world);
}

View File

@ -3,27 +3,26 @@ use bevy::{
prelude::*,
};
use super::{grid, prism};
use super::{
events::RecreateMazeEvent,
systems::{self, recreation::handle_maze_recreation_event},
MazeConfig, MazePluginLoaded,
};
#[derive(Default)]
pub(crate) struct MazePlugin;
impl Plugin for MazePlugin {
fn build(&self, app: &mut App) {
app.add_plugins(prism::plugin);
app.add_plugins(grid::plugin);
// app.insert_resource(AmbientLight {
// brightness: f32::MAX,
// color: Color::WHITE,
// });
app.init_resource::<MazeConfig>()
.add_event::<RecreateMazeEvent>()
.add_systems(Update, handle_maze_recreation_event);
}
}
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);
world.run_system_once(prism::setup);
world.insert_resource(MazePluginLoaded);
world.run_system_once(systems::setup::setup);
}
}

View File

@ -1,146 +0,0 @@
use bevy::prelude::*;
use core::f32;
use std::f32::consts::{FRAC_PI_2, FRAC_PI_3};
use super::{
resource::{Layout, MazeConfig, HEX_SIZE},
tile::Tile,
};
pub(super) fn plugin(_app: &mut App) {}
const WALL_SIZE: f32 = 1.0;
pub(super) fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
config: Res<MazeConfig>,
layout: Res<Layout>,
) {
let radius = config.radius as i32;
let assets = create_base_assets(&mut meshes, &mut materials, &config);
// spawn_single_hex_tile(&mut commands, &assets, &config);
commands
.spawn((
Name::new("Floor"),
SpatialBundle {
transform: Transform::from_translation(Vec3::ZERO),
..default()
},
))
.with_children(|parent| {
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);
spawn_single_hex_tile(parent, &tile, &layout, &assets, &config);
}
}
});
}
fn spawn_single_hex_tile(
parent: &mut ChildBuilder,
tile: &Tile,
layout: &Res<Layout>,
assets: &MazeAssets,
config: &Res<MazeConfig>,
) {
let pos = tile.to_vec3(layout);
parent
.spawn((
Name::new(format!("Hex {}", &tile.to_string())),
PbrBundle {
mesh: assets.hex_mesh.clone(),
material: assets.hex_material.clone(),
transform: Transform::from_translation(pos),
..default()
},
))
.with_children(|parent| spawn_walls(parent, assets, config));
}
fn spawn_walls(parent: &mut ChildBuilder, asstets: &MazeAssets, config: &Res<MazeConfig>) {
let y_offset = config.height / 2.;
let z_rotation = Quat::from_rotation_z(-FRAC_PI_2);
for i in 0..6 {
let wall_angle = FRAC_PI_3 * i as f32;
let x_offset = (HEX_SIZE - WALL_SIZE) * f32::cos(wall_angle);
let z_offset = (HEX_SIZE - WALL_SIZE) * f32::sin(wall_angle);
let pos = Vec3::new(x_offset, y_offset, z_offset);
let x_rotation = Quat::from_rotation_x(wall_angle + FRAC_PI_2);
let final_rotation = z_rotation * x_rotation;
spawn_single_wall(parent, asstets, final_rotation, pos);
}
}
fn spawn_single_wall(
parent: &mut ChildBuilder,
asstets: &MazeAssets,
rotation: Quat,
offset: Vec3,
) {
parent.spawn((
Name::new("Wall"),
PbrBundle {
mesh: asstets.wall_mesh.clone(),
material: asstets.wall_material.clone(),
transform: Transform::from_translation(offset).with_rotation(rotation),
..default()
},
));
}
fn create_base_assets(
meshes: &mut ResMut<Assets<Mesh>>,
materials: &mut ResMut<Assets<StandardMaterial>>,
config: &Res<MazeConfig>,
) -> MazeAssets {
MazeAssets {
hex_mesh: meshes.add(generate_hex_mesh(HEX_SIZE, config.height)),
wall_mesh: meshes.add(generate_square_mesh(HEX_SIZE)),
hex_material: materials.add(white_material()),
wall_material: materials.add(Color::BLACK),
}
}
fn generate_hex_mesh(radius: f32, depth: f32) -> Mesh {
let hexagon = RegularPolygon {
sides: 6,
circumcircle: Circle::new(radius),
};
let prism_shape = Extrusion::new(hexagon, depth);
let rotation = Quat::from_rotation_x(FRAC_PI_2);
Mesh::from(prism_shape).rotated_by(rotation)
}
fn generate_square_mesh(depth: f32) -> Mesh {
let square = Rectangle::new(WALL_SIZE, WALL_SIZE);
let rectangular_prism = Extrusion::new(square, depth);
let rotation = Quat::from_rotation_x(FRAC_PI_2);
Mesh::from(rectangular_prism).rotated_by(rotation)
}
fn white_material() -> StandardMaterial {
let val = 10.;
StandardMaterial {
base_color: Color::WHITE,
emissive: LinearRgba::new(val, val, val, val),
..default()
}
}
struct MazeAssets {
hex_mesh: Handle<Mesh>,
wall_mesh: Handle<Mesh>,
hex_material: Handle<StandardMaterial>,
wall_material: Handle<StandardMaterial>,
}

View File

@ -1,51 +0,0 @@
use bevy::prelude::*;
use hexx::{Hex, HexLayout, HexOrientation};
use rand::{thread_rng, Rng};
pub(crate) const HEX_SIZE: f32 = 6.;
#[derive(Debug, Reflect, Resource)]
#[reflect(Resource)]
pub struct MazeConfig {
pub radius: u32,
pub height: f32,
pub start_pos: Hex,
pub end_pos: Hex,
}
impl Default for MazeConfig {
fn default() -> Self {
let mut rng = thread_rng();
let radius = 11;
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,
height: 20.,
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 {
Self(HexLayout {
orientation: HexOrientation::Pointy,
hex_size: Vec2::splat(HEX_SIZE),
..default()
})
}
}

100
src/maze/resources.rs Normal file
View File

@ -0,0 +1,100 @@
use std::num::TryFromIntError;
use bevy::prelude::*;
use hexx::{Hex, HexLayout, HexOrientation};
use rand::{rngs::StdRng, thread_rng, Rng, SeedableRng};
use thiserror::Error;
#[derive(Debug, Default, Reflect, Resource)]
#[reflect(Resource)]
pub struct MazePluginLoaded;
#[derive(Debug, Error)]
pub enum MazeConfigError {
#[error("Failed to convert radius from u32 to i32: {0}")]
RadiusConverions(#[from] TryFromIntError),
}
#[derive(Debug, Reflect, Resource)]
#[reflect(Resource)]
pub struct MazeConfig {
pub radius: u32,
pub height: f32,
pub hex_size: f32,
pub start_pos: Hex,
pub end_pos: Hex,
pub seed: u64,
pub layout: HexLayout,
}
impl MazeConfig {
fn new(
radius: u32,
height: f32,
hex_size: f32,
orientation: HexOrientation,
seed: Option<u64>,
) -> Result<Self, MazeConfigError> {
let seed = seed.unwrap_or_else(|| thread_rng().gen());
let mut rng = StdRng::seed_from_u64(seed);
let start_pos = generate_pos(radius, &mut rng)?;
let end_pos = generate_pos(radius, &mut rng)?;
debug!("Start pos: ({},{})", start_pos.x, start_pos.y);
debug!("End pos: ({},{})", end_pos.x, end_pos.y);
let layout = HexLayout {
orientation,
hex_size: Vec2::splat(hex_size),
..default()
};
Ok(Self {
radius,
height,
hex_size,
start_pos,
end_pos,
seed,
layout,
})
}
pub fn new_unchecked(
radius: u32,
height: f32,
hex_size: f32,
orientation: HexOrientation,
seed: Option<u64>,
) -> Self {
Self::new(radius, height, hex_size, orientation, seed)
.expect("Failed to create MazeConfig with supposedly safe values")
}
pub fn wall_size(&self) -> f32 {
self.hex_size / 6.
}
pub fn wall_offset(&self) -> f32 {
self.hex_size - self.wall_size()
}
pub fn update(&mut self) {
self.layout.hex_size = Vec2::splat(self.hex_size);
}
}
impl Default for MazeConfig {
fn default() -> Self {
Self::new_unchecked(7, 20., 6., HexOrientation::Flat, None)
}
}
fn generate_pos<R: Rng>(radius: u32, rng: &mut R) -> Result<Hex, MazeConfigError> {
let radius = i32::try_from(radius)?;
Ok(Hex::new(
rng.gen_range(-radius..radius),
rng.gen_range(-radius..radius),
))
}

View File

@ -0,0 +1,3 @@
use bevy::prelude::*;
use crate::maze::components::MazeFloor;

3
src/maze/systems/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod recreation;
pub mod setup;
mod spawn;

View File

@ -0,0 +1,27 @@
use bevy::prelude::*;
use crate::maze::{components::MazeFloor, events::RecreateMazeEvent, MazeConfig};
use super::setup::setup_maze;
pub(crate) fn handle_maze_recreation_event(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
config: Res<MazeConfig>,
query: Query<(Entity, &MazeFloor)>,
mut event_reader: EventReader<RecreateMazeEvent>,
) {
for event in event_reader.read() {
despawn_floor(&mut commands, &query, event.floor);
setup_maze(&mut commands, &mut meshes, &mut materials, &config);
}
}
fn despawn_floor(commands: &mut Commands, query: &Query<(Entity, &MazeFloor)>, floor_num: u8) {
for (entity, maze_floor) in query.iter() {
if maze_floor.0 == floor_num {
commands.entity(entity).despawn_recursive();
}
}
}

45
src/maze/systems/setup.rs Normal file
View File

@ -0,0 +1,45 @@
use bevy::prelude::*;
use hexlab::{GeneratorType, MazeBuilder};
use crate::maze::{assets::MazeAssets, components::MazeFloor, MazeConfig};
use super::spawn::spawn_single_hex_tile;
pub(crate) fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
config: Res<MazeConfig>,
) {
setup_maze(&mut commands, &mut meshes, &mut materials, &config);
}
pub(super) fn setup_maze(
commands: &mut Commands,
meshes: &mut ResMut<Assets<Mesh>>,
materials: &mut ResMut<Assets<StandardMaterial>>,
config: &MazeConfig,
) {
let maze = MazeBuilder::new()
.with_radius(config.radius)
.with_seed(config.seed)
.with_generator(GeneratorType::RecursiveBacktracking)
.build()
.expect("Something went wrong while creating maze");
let assets = MazeAssets::new(meshes, materials, config);
commands
.spawn((
Name::new("Floor"),
MazeFloor(1),
SpatialBundle {
transform: Transform::from_translation(Vec3::ZERO),
..default()
},
))
.with_children(|parent| {
for tile in maze.values() {
spawn_single_hex_tile(parent, &assets, tile, config)
}
});
}

77
src/maze/systems/spawn.rs Normal file
View File

@ -0,0 +1,77 @@
use std::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_6};
use bevy::prelude::*;
use hexlab::prelude::*;
use hexx::HexOrientation;
use crate::maze::{
assets::MazeAssets,
components::{MazeTile, MazeWall},
MazeConfig,
};
pub(super) fn spawn_single_hex_tile(
parent: &mut ChildBuilder,
assets: &MazeAssets,
tile: &HexTile,
config: &MazeConfig,
) {
let world_pos = tile.to_vec3(&config.layout);
let rotation = match config.layout.orientation {
HexOrientation::Pointy => Quat::from_rotation_y(0.0),
HexOrientation::Flat => Quat::from_rotation_y(FRAC_PI_6), // 30 degrees rotation
};
parent
.spawn((
Name::new(format!("Hex {}", tile)),
MazeTile,
PbrBundle {
mesh: assets.hex_mesh.clone(),
material: assets.hex_material.clone(),
transform: Transform::from_translation(world_pos).with_rotation(rotation),
..default()
},
))
.with_children(|parent| spawn_walls(parent, assets, config, tile.walls()));
}
fn spawn_walls(parent: &mut ChildBuilder, assets: &MazeAssets, config: &MazeConfig, walls: &Walls) {
let z_rotation = Quat::from_rotation_z(-FRAC_PI_2);
let y_offset = config.height / 2.;
for i in 0..6 {
if !walls.contains(i) {
continue;
}
let wall_angle = -FRAC_PI_3 * i as f32;
let x_offset = config.wall_offset() * f32::cos(wall_angle);
let z_offset = config.wall_offset() * f32::sin(wall_angle);
let pos = Vec3::new(x_offset, y_offset, z_offset);
let x_rotation = Quat::from_rotation_x(wall_angle + FRAC_PI_2);
let final_rotation = z_rotation * x_rotation;
spawn_single_wall(parent, assets, final_rotation, pos);
}
}
fn spawn_single_wall(
parent: &mut ChildBuilder,
asstets: &MazeAssets,
rotation: Quat,
offset: Vec3,
) {
parent.spawn((
Name::new("Wall"),
MazeWall,
PbrBundle {
mesh: asstets.wall_mesh.clone(),
material: asstets.wall_material.clone(),
transform: Transform::from_translation(offset).with_rotation(rotation),
..default()
},
));
}

View File

@ -1,55 +0,0 @@
use std::fmt::Display;
use bevy::prelude::*;
use hexx::{Hex, HexLayout};
#[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;
}
pub fn to_vec2(&self, layout: &HexLayout) -> Vec2 {
layout.hex_to_world_pos(self.hex)
}
pub fn to_vec3(&self, layout: &HexLayout) -> Vec3 {
let pos = self.to_vec2(layout);
Vec3::new(pos.x, 0., pos.y)
}
}
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

@ -2,10 +2,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::maze::spawn_grid as spawn_level_command;
use crate::maze::spawn_maze as spawn_level_command;
use crate::{asset_tracking::LoadResource, audio::Music, screens::Screen};
pub(super) fn plugin(app: &mut App) {