feat: added TerrainStages and moved terrain debug systems to DebugPlugin
parent
7edca9d472
commit
5f2b2b9d06
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
207
src/terrain2d.rs
207
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::<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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue