feat: added TerrainStages and moved terrain debug systems to DebugPlugin

feat/simulation
hheik 2022-12-22 19:41:51 +02:00
parent 7edca9d472
commit 5f2b2b9d06
6 changed files with 283 additions and 197 deletions

View File

@ -22,10 +22,10 @@ pub fn init() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugin(RapierPhysicsPlugin::<NoUserData>::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)

View File

@ -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);
}
}

164
src/game/debug/terrain.rs Normal file
View File

@ -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<Terrain2D>,
mut debug_draw: ResMut<DebugLines>,
mut brush: ResMut<TerrainBrush2D>,
windows: Res<Windows>,
mouse_input: Res<Input<MouseButton>>,
key_input: Res<Input<KeyCode>>,
mut mouse_wheel: EventReader<MouseWheel>,
camera_query: Query<(&Camera, &GlobalTransform), With<GameCamera>>,
) {
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<Terrain2D>, mut debug_draw: ResMut<DebugLines>) {
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,
);
}
}
}

View File

@ -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::<TerrainChunk2D>()
.insert_resource(Terrain2D::new())
.insert_resource(TerrainBrush2D::default())
.add_event::<TerrainEvent2D>()
.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<Terrain2D>,
mut debug_draw: ResMut<DebugLines>,
mut brush: ResMut<TerrainBrush2D>,
windows: Res<Windows>,
mouse_input: Res<Input<MouseButton>>,
key_input: Res<Input<KeyCode>>,
mut mouse_wheel: EventReader<MouseWheel>,
camera_query: Query<(&Camera, &GlobalTransform), With<GameCamera>>,
) {
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<Terrain2D>, mut debug_draw: ResMut<DebugLines>) {
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(

View File

@ -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<Segment2I>;
lazy_static! {
pub static ref COLOR_MAP: HashMap<TexelID, [u8; 4]> = {
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<u8> {
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<u8> = 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

View File

@ -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<TexelID, TexelBehaviour2D> = {
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<f32>,
// pub gravity: Option<f32>,
pub form: TexelForm,
pub color: Color,
}
impl TexelBehaviour2D {
pub fn from_id(id: &TexelID) -> Option<Self> {
ID_MAP.get(id).copied()
}
}