From 5f2b2b9d06247002b4d8d5394618b6f7aea2d899 Mon Sep 17 00:00:00 2001 From: hheik <4469778+hheik@users.noreply.github.com> Date: Thu, 22 Dec 2022 19:41:51 +0200 Subject: [PATCH] feat: added TerrainStages and moved terrain debug systems to DebugPlugin --- src/game.rs | 2 +- src/game/debug.rs | 7 +- src/game/debug/terrain.rs | 164 +++++++++++++++++++++++ src/terrain2d.rs | 207 ++++++----------------------- src/terrain2d/chunk2d.rs | 45 +++---- src/terrain2d/texel_behaviour2d.rs | 55 ++++++++ 6 files changed, 283 insertions(+), 197 deletions(-) create mode 100644 src/game/debug/terrain.rs create mode 100644 src/terrain2d/texel_behaviour2d.rs diff --git a/src/game.rs b/src/game.rs index 921c6a9..21fc182 100644 --- a/src/game.rs +++ b/src/game.rs @@ -22,10 +22,10 @@ pub fn init() { App::new() .add_plugins(DefaultPlugins) .add_plugin(RapierPhysicsPlugin::::default()) + .add_plugin(Terrain2DPlugin) .add_plugin(DebugPlugin) .add_plugin(KinematicPlugin) .add_plugin(GameCameraPlugin) - .add_plugin(Terrain2DPlugin) .add_plugin(PlayerPlugin) .add_startup_system(setup_terrain) .add_startup_system(setup_window) diff --git a/src/game/debug.rs b/src/game/debug.rs index f9e8664..08b3eb6 100644 --- a/src/game/debug.rs +++ b/src/game/debug.rs @@ -3,12 +3,17 @@ use bevy_inspector_egui::*; use bevy_prototype_debug_lines::DebugLinesPlugin; use bevy_rapier2d::prelude::*; +mod terrain; + +use terrain::TerrainDebugPlugin; + pub struct DebugPlugin; impl Plugin for DebugPlugin { fn build(&self, app: &mut App) { app.add_plugin(DebugLinesPlugin::default()) .add_plugin(RapierDebugRenderPlugin::default()) - .add_plugin(WorldInspectorPlugin::new()); + .add_plugin(WorldInspectorPlugin::new()) + .add_plugin(TerrainDebugPlugin); } } diff --git a/src/game/debug/terrain.rs b/src/game/debug/terrain.rs new file mode 100644 index 0000000..47a50ee --- /dev/null +++ b/src/game/debug/terrain.rs @@ -0,0 +1,164 @@ +use crate::{game::camera::GameCamera, terrain2d::*, util::Vector2I}; +use bevy::{input::mouse::MouseWheel, prelude::*, render::camera::RenderTarget}; +use bevy_prototype_debug_lines::DebugLines; + +pub struct TerrainDebugPlugin; + +impl Plugin for TerrainDebugPlugin { + fn build(&self, app: &mut App) { + app.insert_resource(TerrainBrush2D::default()) + .add_system(debug_painter) + .add_system_to_stage( + TerrainStages::First, + dirty_rect_visualizer, + ); + } +} + +#[derive(Resource)] +struct TerrainBrush2D { + pub radius: i32, + pub tile: TexelID, +} + +impl Default for TerrainBrush2D { + fn default() -> Self { + TerrainBrush2D { radius: 7, tile: 3 } + } +} + +// REM: Dirty and hopefully temporary +fn debug_painter( + mut terrain: ResMut, + mut debug_draw: ResMut, + mut brush: ResMut, + windows: Res, + mouse_input: Res>, + key_input: Res>, + mut mouse_wheel: EventReader, + camera_query: Query<(&Camera, &GlobalTransform), With>, +) { + let allow_painting = key_input.pressed(KeyCode::LControl); + + // Change brush + for event in mouse_wheel.iter() { + if allow_painting { + brush.radius = (brush.radius + event.y.round() as i32).clamp(1, 128); + } + } + + if !allow_painting { + return; + } + + // https://bevy-cheatbook.github.io/cookbook/cursor2world.html#2d-games + // get the camera info and transform + // assuming there is exactly one main camera entity, so query::single() is OK + let (camera, camera_transform) = camera_query.single(); + + // get the window that the camera is displaying to (or the primary window) + let window = if let RenderTarget::Window(id) = camera.target { + windows.get(id).unwrap() + } else { + windows.get_primary().unwrap() + }; + + // check if the cursor is inside the window and get its position + let world_pos = if let Some(screen_pos) = window.cursor_position() { + // get the size of the window + let window_size = Vec2::new(window.width() as f32, window.height() as f32); + + // convert screen position [0..resolution] to ndc [-1..1] (gpu coordinates) + let ndc = (screen_pos / window_size) * 2.0 - Vec2::ONE; + + // matrix for undoing the projection and camera transform + let ndc_to_world = camera_transform.compute_matrix() * camera.projection_matrix().inverse(); + + // use it to convert ndc to world-space coordinates + let world_pos = ndc_to_world.project_point3(ndc.extend(-1.0)); + + // reduce it to a 2D value + world_pos.truncate() + } else { + return; + }; + + if key_input.just_pressed(KeyCode::Key1) { + brush.tile = 1; + } + if key_input.just_pressed(KeyCode::Key2) { + brush.tile = 2; + } + if key_input.just_pressed(KeyCode::Key3) { + brush.tile = 3; + } + + let colors = vec![ + Color::rgba(1.0, 0.25, 0.25, 1.0), + Color::rgba(0.25, 1.0, 0.25, 1.0), + Color::rgba(0.25, 0.25, 1.0, 1.0), + ]; + + let origin = Vector2I::from(world_pos); + let radius = brush.radius; + let color = colors[brush.tile as usize % colors.len()]; + let id = match ( + mouse_input.pressed(MouseButton::Left), + mouse_input.pressed(MouseButton::Right), + ) { + (true, false) => brush.tile, + (_, _) => 0, + }; + + for y in origin.y - (radius - 1)..origin.y + radius { + for x in origin.x - (radius - 1)..origin.x + radius { + let dx = (x - origin.x).abs(); + let dy = (y - origin.y).abs(); + + if dx * dx + dy * dy <= (radius - 1) * (radius - 1) { + let pos: Vector2I = Vector2I { x, y }; + debug_draw.line_colored( + Vec3::from(pos) + Vec3::new(0.45, 0.45, 0.0), + Vec3::from(pos) + Vec3::new(0.55, 0.55, 0.0), + 0.0, + color, + ); + if mouse_input.pressed(MouseButton::Left) || mouse_input.pressed(MouseButton::Right) + { + terrain.set_texel(&pos, id) + } + } + } + } +} + +/** + Visualize dirty rects +*/ +fn dirty_rect_visualizer(terrain: Res, mut debug_draw: ResMut) { + for (chunk_index, chunk) in terrain.chunk_iter() { + let rect = if let Some(rect) = chunk.dirty_rect { + rect + } else { + continue; + }; + + let color = Color::RED; + + let points = vec![ + Vec3::new(rect.min.x as f32, rect.min.y as f32, 0.0), + Vec3::new((rect.max.x + 1) as f32, rect.min.y as f32, 0.0), + Vec3::new((rect.max.x + 1) as f32, (rect.max.y + 1) as f32, 0.0), + Vec3::new(rect.min.x as f32, (rect.max.y + 1) as f32, 0.0), + ]; + for i in 0..points.len() { + let offset = Vec3::from(chunk_index_to_global(chunk_index)); + debug_draw.line_colored( + offset + points[i], + offset + points[(i + 1) % points.len()], + 0.0, + color, + ); + } + } +} diff --git a/src/terrain2d.rs b/src/terrain2d.rs index 8474dfe..9e14fb2 100644 --- a/src/terrain2d.rs +++ b/src/terrain2d.rs @@ -3,197 +3,68 @@ use std::collections::{ HashMap, }; -use bevy::{input::mouse::MouseWheel, prelude::*, render::camera::RenderTarget}; -use bevy_prototype_debug_lines::DebugLines; +use bevy::ecs::prelude::SystemStage; +use bevy::prelude::*; +use bevy_rapier2d::prelude::*; mod chunk2d; mod terrain_gen2d; mod texel2d; +mod texel_behaviour2d; pub use chunk2d::*; pub use terrain_gen2d::*; pub use texel2d::*; +pub use texel_behaviour2d::*; -use crate::{ - game::camera::GameCamera, - util::{math::*, Vector2I}, -}; +use crate::util::{math::*, Vector2I}; pub struct Terrain2DPlugin; impl Plugin for Terrain2DPlugin { fn build(&self, app: &mut App) { + // Add terrain stages. They should go between CoreStage::Update and Rapier's own stages + app.add_stage_before( + PhysicsStages::SyncBackend, + TerrainStages::First, + SystemStage::parallel(), + ).add_stage_after( + TerrainStages::First, + TerrainStages::EventHandler, + SystemStage::parallel(), + ).add_stage_after( + TerrainStages::EventHandler, + TerrainStages::ChunkSync, + SystemStage::parallel(), + ).add_stage_after( + TerrainStages::ChunkSync, + TerrainStages::Last, + SystemStage::parallel(), + ); + app.register_type::() .insert_resource(Terrain2D::new()) - .insert_resource(TerrainBrush2D::default()) .add_event::() - .add_system(debug_painter) + .add_system_to_stage(TerrainStages::EventHandler, emit_terrain_events) .add_system_to_stage( - CoreStage::PostUpdate, - dirty_rect_visualizer.before(emit_terrain_events), - ) - .add_system_to_stage( - CoreStage::PostUpdate, + TerrainStages::EventHandler, chunk_spawner.before(emit_terrain_events), ) - .add_system_to_stage( - CoreStage::PostUpdate, - chunk_sprite_sync.after(chunk_spawner), - ) - .add_system_to_stage( - CoreStage::PostUpdate, - chunk_collision_sync.after(chunk_spawner), - ) - .add_system_to_stage(CoreStage::PostUpdate, emit_terrain_events); + .add_system_to_stage(TerrainStages::ChunkSync, chunk_sprite_sync) + .add_system_to_stage(CoreStage::PostUpdate, chunk_collision_sync); } } -#[derive(Resource)] -struct TerrainBrush2D { - pub radius: i32, - pub tile: TexelID, -} - -impl Default for TerrainBrush2D { - fn default() -> Self { - TerrainBrush2D { radius: 7, tile: 3 } - } -} - -// REM: Dirty and hopefully temporary -fn debug_painter( - mut terrain: ResMut, - mut debug_draw: ResMut, - mut brush: ResMut, - windows: Res, - mouse_input: Res>, - key_input: Res>, - mut mouse_wheel: EventReader, - camera_query: Query<(&Camera, &GlobalTransform), With>, -) { - let allow_painting = key_input.pressed(KeyCode::LControl); - - // Change brush - for event in mouse_wheel.iter() { - if allow_painting { - brush.radius = (brush.radius + event.y.round() as i32).clamp(1, 128); - } - } - - if !allow_painting { - return; - } - - // https://bevy-cheatbook.github.io/cookbook/cursor2world.html#2d-games - // get the camera info and transform - // assuming there is exactly one main camera entity, so query::single() is OK - let (camera, camera_transform) = camera_query.single(); - - // get the window that the camera is displaying to (or the primary window) - let window = if let RenderTarget::Window(id) = camera.target { - windows.get(id).unwrap() - } else { - windows.get_primary().unwrap() - }; - - // check if the cursor is inside the window and get its position - let world_pos = if let Some(screen_pos) = window.cursor_position() { - // get the size of the window - let window_size = Vec2::new(window.width() as f32, window.height() as f32); - - // convert screen position [0..resolution] to ndc [-1..1] (gpu coordinates) - let ndc = (screen_pos / window_size) * 2.0 - Vec2::ONE; - - // matrix for undoing the projection and camera transform - let ndc_to_world = camera_transform.compute_matrix() * camera.projection_matrix().inverse(); - - // use it to convert ndc to world-space coordinates - let world_pos = ndc_to_world.project_point3(ndc.extend(-1.0)); - - // reduce it to a 2D value - world_pos.truncate() - } else { - return; - }; - - if key_input.just_pressed(KeyCode::Key1) { - brush.tile = 1; - } - if key_input.just_pressed(KeyCode::Key2) { - brush.tile = 2; - } - if key_input.just_pressed(KeyCode::Key3) { - brush.tile = 3; - } - - let colors = vec![ - Color::rgba(1.0, 0.25, 0.25, 1.0), - Color::rgba(0.25, 1.0, 0.25, 1.0), - Color::rgba(0.25, 0.25, 1.0, 1.0), - ]; - - let origin = Vector2I::from(world_pos); - let radius = brush.radius; - let color = colors[brush.tile as usize % colors.len()]; - let id = match ( - mouse_input.pressed(MouseButton::Left), - mouse_input.pressed(MouseButton::Right), - ) { - (true, false) => brush.tile, - (_, _) => 0, - }; - - for y in origin.y - (radius - 1)..origin.y + radius { - for x in origin.x - (radius - 1)..origin.x + radius { - let dx = (x - origin.x).abs(); - let dy = (y - origin.y).abs(); - - if dx * dx + dy * dy <= (radius - 1) * (radius - 1) { - let pos: Vector2I = Vector2I { x, y }; - debug_draw.line_colored( - Vec3::from(pos) + Vec3::new(0.45, 0.45, 0.0), - Vec3::from(pos) + Vec3::new(0.55, 0.55, 0.0), - 0.0, - color, - ); - if mouse_input.pressed(MouseButton::Left) || mouse_input.pressed(MouseButton::Right) - { - terrain.set_texel(&pos, id) - } - } - } - } -} - -/** - Visualize dirty rects -*/ -fn dirty_rect_visualizer(terrain: Res, mut debug_draw: ResMut) { - for (chunk_index, chunk) in terrain.chunk_iter() { - let rect = if let Some(rect) = chunk.dirty_rect { - rect - } else { - continue; - }; - - let color = Color::RED; - - let points = vec![ - Vec3::new(rect.min.x as f32, rect.min.y as f32, 0.0), - Vec3::new((rect.max.x + 1) as f32, rect.min.y as f32, 0.0), - Vec3::new((rect.max.x + 1) as f32, (rect.max.y + 1) as f32, 0.0), - Vec3::new(rect.min.x as f32, (rect.max.y + 1) as f32, 0.0), - ]; - for i in 0..points.len() { - let offset = Vec3::from(chunk_index_to_global(chunk_index)); - debug_draw.line_colored( - offset + points[i], - offset + points[(i + 1) % points.len()], - 0.0, - color, - ); - } - } +#[derive(StageLabel)] +pub enum TerrainStages { + /// First of terrain stages to be called + First, + /// The stage that Handles collected events and creates new chunk entities as needed + EventHandler, + /// Chunk sync systems (e.g. collsion and sprite) run in this stage + ChunkSync, + /// Last of terrain stages to be called + Last, } fn emit_terrain_events( diff --git a/src/terrain2d/chunk2d.rs b/src/terrain2d/chunk2d.rs index edffec9..8c745a1 100644 --- a/src/terrain2d/chunk2d.rs +++ b/src/terrain2d/chunk2d.rs @@ -1,8 +1,8 @@ -use std::collections::{HashMap, VecDeque}; +use std::collections::VecDeque; use super::{ - local_to_texel_index, texel_index_to_local, Terrain2D, TerrainEvent2D, Texel2D, TexelID, - NEIGHBOUR_INDEX_MAP, + local_to_texel_index, texel_index_to_local, Terrain2D, TerrainEvent2D, Texel2D, + TexelBehaviour2D, TexelID, NEIGHBOUR_INDEX_MAP, }; use crate::util::{CollisionLayers, Segment2I, Vector2I}; use bevy::{ @@ -15,17 +15,6 @@ use lazy_static::lazy_static; type Island = VecDeque; lazy_static! { - pub static ref COLOR_MAP: HashMap = { - let mut map = HashMap::new(); - map.insert(0, [0x00, 0x00, 0x00, 0x00]); - // map.insert(0, [0x03, 0x03, 0x03, 0xff]); - // map.insert(1, [0x47, 0x8e, 0x48, 0xff]); - map.insert(1, [0x9e, 0x7f, 0x63, 0xff]); - map.insert(2, [0x38, 0x32, 0x2d, 0xff]); - map.insert(3, [0x1e, 0x1e, 0x1e, 0xff]); - map - }; - /// Marching Square case dictionary. /// /// Key is a bitmask of neighbouring tiles (up, right, down, left - least significant bit first). @@ -247,21 +236,23 @@ impl Chunk2D { pub fn create_texture_data(&self) -> Vec { let mut image_data = Vec::with_capacity(Chunk2D::SIZE_X * Chunk2D::SIZE_Y * 4); - let fallback: [u8; 4] = [0x00, 0x00, 0x00, 0x00]; for y in (0..Chunk2D::SIZE_Y).rev() { for x in 0..Chunk2D::SIZE_X { - image_data.append( - &mut COLOR_MAP - .get( - &self - .get_texel(&Vector2I::new(x as i32, y as i32)) - .unwrap() - .id, - ) - .unwrap_or(&fallback) - .to_vec() - .clone(), - ); + let id = &self + .get_texel(&Vector2I::new(x as i32, y as i32)) + .unwrap() + .id; + let behaviour = TexelBehaviour2D::from_id(id); + let color = + behaviour.map_or(Color::rgba_u8(0, 0, 0, 0), |behaviour| behaviour.color); + let color_data = color.as_rgba_u32(); + let mut color_data: Vec = vec![ + ((color_data >> 0) & 0xff) as u8, + ((color_data >> 8) & 0xff) as u8, + ((color_data >> 16) & 0xff) as u8, + ((color_data >> 24) & 0xff) as u8, + ]; + image_data.append(&mut color_data); } } image_data diff --git a/src/terrain2d/texel_behaviour2d.rs b/src/terrain2d/texel_behaviour2d.rs new file mode 100644 index 0000000..caf1e00 --- /dev/null +++ b/src/terrain2d/texel_behaviour2d.rs @@ -0,0 +1,55 @@ +use super::TexelID; +use bevy::prelude::*; +use lazy_static::lazy_static; +use std::collections::HashMap; + +lazy_static! { + static ref ID_MAP: HashMap = { + let mut result = HashMap::new(); + + result.insert(1, TexelBehaviour2D { + color: Color::rgb(0.61, 0.49, 0.38), + ..default() + }); + + result.insert(2, TexelBehaviour2D { + color: Color::rgb(0.21, 0.19, 0.17), + ..default() + }); + + result.insert(3, TexelBehaviour2D { + color: Color::rgb(0.11, 0.11, 0.11), + ..default() + }); + + result.insert(4, TexelBehaviour2D { + color: Color::rgb(1.0, 0.0, 0.0), + form: TexelForm::Gas, + ..default() + }); + + result + }; +} + +#[derive(Clone, Copy, Default)] +pub enum TexelForm { + #[default] + Solid, + Liquid, + Gas, +} + +#[derive(Clone, Copy, Default)] +pub struct TexelBehaviour2D { + // pub flammability: Option, + // pub gravity: Option, + pub form: TexelForm, + pub color: Color, +} + +impl TexelBehaviour2D { + pub fn from_id(id: &TexelID) -> Option { + ID_MAP.get(id).copied() + } +}