From b25bc5f5194172a9e5743704f353df6a56fc88df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Maita?= <47983254+mnmaita@users.noreply.github.com> Date: Tue, 14 Nov 2023 22:00:25 -0300 Subject: [PATCH] Random Level Generation (#63) Issue: ============== Closes #54 Closes #9 What was done: ============== * Added a random level generator that uses Perlin noise. * Constrains the camera within the level boundaries. * Added Tile and BorderTile components. --- Cargo.lock | 96 +++++++++++++++++++++++++++++-- Cargo.toml | 2 + src/camera.rs | 60 +++++++++++++++++++- src/game/plugin.rs | 12 +--- src/level.rs | 137 +++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 3 + 6 files changed, 292 insertions(+), 18 deletions(-) create mode 100644 src/level.rs diff --git a/Cargo.lock b/Cargo.lock index 3fc3dda..e56e343 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,7 +93,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.11", "once_cell", "version_check", "zerocopy", @@ -878,7 +878,7 @@ checksum = "c8e75d4a34ef0b15dffd1ee9079ef1f0f5139527e192b9d5708b3e158777c753" dependencies = [ "ahash", "bevy_utils_proc_macros", - "getrandom", + "getrandom 0.2.11", "hashbrown 0.14.2", "instant", "nonmax", @@ -1531,6 +1531,19 @@ name = "game-off-2023" version = "0.1.0" dependencies = [ "bevy", + "noise", + "rand", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] @@ -1542,7 +1555,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1955,7 +1968,7 @@ checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -2040,6 +2053,17 @@ dependencies = [ "libc", ] +[[package]] +name = "noise" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba869e17168793186c10ca82c7079a4ffdeac4f1a7d9e755b9491c028180e40" +dependencies = [ + "num-traits", + "rand", + "rand_xorshift", +] + [[package]] name = "nom" version = "7.1.3" @@ -2376,6 +2400,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -2416,6 +2446,56 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17fd96390ed3feda12e1dfe2645ed587e0bea749e319333f104a33ff62f77a0b" +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_xorshift" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77d416b86801d23dde1aa643023b775c3a462efc0ed96443add11546cdf1dca8" +dependencies = [ + "rand_core", +] + [[package]] name = "range-alloc" version = "0.1.3" @@ -2888,7 +2968,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" dependencies = [ - "getrandom", + "getrandom 0.2.11", "serde", ] @@ -2920,6 +3000,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 0f04c8d..a167e2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,8 @@ bevy = { version = "0.12.0", default-features = false, features = [ "vorbis", "x11", ] } +noise = "0.8.2" +rand = "0.7.3" [profile.dev.package."*"] opt-level = 3 diff --git a/src/camera.rs b/src/camera.rs index 1d9ea82..25e839e 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -1,6 +1,9 @@ use bevy::prelude::*; -use crate::game::Player; +use crate::{ + game::Player, + level::{GRID_SIZE, HALF_TILE_SIZE, TILE_SIZE}, +}; pub struct CameraPlugin; @@ -8,7 +11,14 @@ impl Plugin for CameraPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, setup_camera); - app.add_systems(Update, update_camera.run_if(any_with_component::())); + app.add_systems( + Update, + ( + update_camera.run_if(any_with_component::()), + constrain_camera_position_to_level, + ) + .chain(), + ); } } @@ -26,3 +36,49 @@ fn update_camera( camera_transform.translation.x = player_transform.translation.x; camera_transform.translation.y = player_transform.translation.y; } + +fn constrain_camera_position_to_level( + mut camera_query: Query<(&Camera, &mut Transform), With>, + #[cfg(debug_assertions)] mut gizmos: Gizmos, +) { + let (camera, mut camera_transform) = camera_query.single_mut(); + + if let Some(viewport_size) = camera.logical_viewport_size() { + let level_dimensions = GRID_SIZE * TILE_SIZE; + let viewport_size_remainder = viewport_size % TILE_SIZE; + let camera_boundary_size = (level_dimensions + - (viewport_size - viewport_size_remainder) + - viewport_size_remainder) + .clamp(Vec2::ZERO, Vec2::splat(f32::MAX)); + let camera_boundary = Rect::from_center_size(-HALF_TILE_SIZE, camera_boundary_size); + + if camera_boundary.is_empty() { + if viewport_size.x > level_dimensions.x { + camera_transform.translation.x = 0.0; + } + if viewport_size.y > level_dimensions.y { + camera_transform.translation.y = 0.0; + } + } + + if camera_boundary.size() != Vec2::ZERO + && !camera_boundary.contains(camera_transform.translation.truncate()) + { + let (min_x, max_x) = (camera_boundary.min.x, camera_boundary.max.x); + let (min_y, max_y) = (camera_boundary.min.y, camera_boundary.max.y); + camera_transform.translation.x = camera_transform.translation.x.clamp(min_x, max_x); + camera_transform.translation.y = camera_transform.translation.y.clamp(min_y, max_y); + } + + #[cfg(debug_assertions)] + { + gizmos.rect_2d( + camera_boundary.center(), + 0., + camera_boundary.size(), + Color::RED, + ); + gizmos.circle_2d(camera_transform.translation.truncate(), 10., Color::RED); + } + } +} diff --git a/src/game/plugin.rs b/src/game/plugin.rs index 5777d71..981d194 100644 --- a/src/game/plugin.rs +++ b/src/game/plugin.rs @@ -8,16 +8,6 @@ pub struct GamePlugin; impl Plugin for GamePlugin { fn build(&self, app: &mut App) { - app.add_systems(OnEnter(AppState::InGame), (draw_background, spawn_player)); + app.add_systems(OnEnter(AppState::InGame), spawn_player); } } - -fn draw_background(mut commands: Commands, asset_server: Res) { - let texture = asset_server - .get_handle("textures/background.png") - .unwrap_or_default(); - commands.spawn(SpriteBundle { - texture, - ..default() - }); -} diff --git a/src/level.rs b/src/level.rs new file mode 100644 index 0000000..3bb8b2b --- /dev/null +++ b/src/level.rs @@ -0,0 +1,137 @@ +use bevy::prelude::*; +use noise::{NoiseFn, Perlin}; +use rand::random; + +use crate::AppState; + +pub const TILE_SIZE: Vec2 = Vec2::new(32., 32.); +pub const GRID_SIZE: Vec2 = Vec2::new(100., 100.); +pub const HALF_TILE_SIZE: Vec2 = Vec2::new(TILE_SIZE.x * 0.5, TILE_SIZE.y * 0.5); +pub const HALF_GRID_SIZE: Vec2 = Vec2::new(GRID_SIZE.x * 0.5, GRID_SIZE.y * 0.5); + +pub struct LevelPlugin; + +impl Plugin for LevelPlugin { + fn build(&self, app: &mut App) { + app.add_systems(OnEnter(AppState::InGame), generate_level); + + #[cfg(debug_assertions)] + { + app.add_systems(Update, debug_draw_tiles.after(generate_level)); + } + } +} + +#[cfg(debug_assertions)] +fn debug_draw_tiles( + query: Query<(&Transform, Option<&BorderTile>), With>, + mut gizmos: Gizmos, +) { + for (transform, border_tile) in &query { + gizmos.rect_2d( + transform.translation.truncate(), + 0., + TILE_SIZE, + if border_tile.is_some() { + Color::BLACK + } else { + Color::FUCHSIA + }, + ); + } +} + +fn generate_level(mut commands: Commands) { + const MAP_OFFSET_X: f64 = 0.; + const MAP_OFFSET_Y: f64 = 0.; + const MAP_SCALE: f64 = 20.; + + let seed = random(); + let perlin = Perlin::new(seed); + let tile_count = Tile::_LAST as u8; + + for y in 0..GRID_SIZE.y as i32 { + for x in 0..GRID_SIZE.x as i32 { + let point = [ + (x as f64 - MAP_OFFSET_X) / MAP_SCALE, + (y as f64 - MAP_OFFSET_Y) / MAP_SCALE, + ]; + let noise_value = perlin.get(point).clamp(0., 1.); + let scaled_noise_value = + (noise_value * tile_count as f64).clamp(0., tile_count as f64 - 1.); + let int_noise_value = scaled_noise_value.floor() as u8; + let tile: Tile = int_noise_value.into(); + let color = tile.into(); + let custom_size = Some(TILE_SIZE); + let position = (Vec2::new(x as f32, y as f32) - HALF_GRID_SIZE) * TILE_SIZE; + let translation = position.extend(0.0); + let transform = Transform::from_translation(translation); + + let mut tile_entity = commands.spawn(TileBundle { + sprite: SpriteBundle { + sprite: Sprite { + color, + custom_size, + ..default() + }, + transform, + ..default() + }, + tile, + }); + + if y == 0 || x == 0 || y == GRID_SIZE.y as i32 - 1 || x == GRID_SIZE.x as i32 - 1 { + tile_entity.insert(BorderTile); + } + } + } +} + +#[derive(Component)] +struct BorderTile; + +#[derive(Bundle)] +pub struct TileBundle { + pub sprite: SpriteBundle, + pub tile: Tile, +} + +#[derive(Component, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Tile { + Water, + Sand, + Grass, + Hills, + Mountains, + _LAST, +} + +impl From for Tile { + fn from(value: u8) -> Self { + // For every new type added to the enum, a new match arm should be added here. + match value { + 0 => Self::Water, + 1 => Self::Sand, + 2 => Self::Grass, + 3 => Self::Hills, + 4 => Self::Mountains, + #[cfg(debug_assertions)] + _ => panic!("From for Tile: Missing match arm!"), + #[cfg(not(debug_assertions))] + _ => Self::Water, + } + } +} + +impl From for Color { + fn from(value: Tile) -> Self { + match value { + Tile::Grass => Self::DARK_GREEN, + Tile::Hills => Self::GRAY, + Tile::Mountains => Self::DARK_GRAY, + Tile::Water => Self::BLUE, + Tile::Sand => Self::BEIGE, + Tile::_LAST => Self::default(), + } + } +} diff --git a/src/main.rs b/src/main.rs index d7e8286..61563dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ use bevy::{ use camera::CameraPlugin; use game::GamePlugin; use input::InputPlugin; +use level::LevelPlugin; use textures::{texture_assets_loaded, TexturesPlugin}; mod animation; @@ -17,6 +18,7 @@ mod audio; mod camera; mod game; mod input; +mod level; mod textures; fn main() { @@ -36,6 +38,7 @@ fn main() { CameraPlugin, GamePlugin, InputPlugin, + LevelPlugin, TexturesPlugin, ));