diff --git a/Cargo.lock b/Cargo.lock index 158c3e8..22a781f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2061,6 +2061,7 @@ dependencies = [ "bevy", "bevy-inspector-egui", "bevy_rapier2d", + "lazy_static", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7c96aaa..63d3671 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" bevy = { version = "0.9.0", features = ["dynamic"] } bevy-inspector-egui = "0.14.0" bevy_rapier2d = { path = "../bevy_rapier/bevy_rapier2d" } +lazy_static = "1.4.0" # Enable a small amount of optimization in debug mode [profile.dev] diff --git a/src/game.rs b/src/game.rs index 305f338..b4b0226 100644 --- a/src/game.rs +++ b/src/game.rs @@ -16,12 +16,12 @@ pub fn init() { .add_plugin(WorldInspectorPlugin::new()) .add_plugin(KinematicPlugin) .add_plugin(GameCameraPlugin) - .add_plugin(PlayerPlugin) - .add_startup_system(setup) + // .add_plugin(PlayerPlugin) + // .add_startup_system(setup_debug_ground) .run(); } -fn setup(mut commands: Commands) { +fn setup_debug_ground(mut commands: Commands) { // Static ground commands .spawn(()) diff --git a/src/game/camera.rs b/src/game/camera.rs index c4c81b4..6368e14 100644 --- a/src/game/camera.rs +++ b/src/game/camera.rs @@ -37,10 +37,10 @@ fn camera_setup(mut commands: Commands) { commands.spawn(( Name::new("Camera"), Camera2dBundle { - projection: OrthographicProjection { - scaling_mode: ScalingMode::FixedHorizontal(320.0), - ..default() - }, + // projection: OrthographicProjection { + // scaling_mode: ScalingMode::FixedHorizontal(320.0), + // ..default() + // }, ..default() }, )); diff --git a/src/game/kinematic.rs b/src/game/kinematic.rs index 51ffdee..bad778b 100644 --- a/src/game/kinematic.rs +++ b/src/game/kinematic.rs @@ -27,7 +27,6 @@ pub struct KinematicBundle { pub events: ActiveEvents, pub collisions: ActiveCollisionTypes, pub properties: KinematicProperties, - #[bundle] pub transform: TransformBundle, } @@ -71,10 +70,7 @@ impl KinematicState { } pub fn can_jump(&self) -> bool { - if self.on_ground && !self.did_jump { - return true; - } - false + self.on_ground && !self.did_jump } } diff --git a/src/game/player.rs b/src/game/player.rs index 34b58d0..3615b2d 100644 --- a/src/game/player.rs +++ b/src/game/player.rs @@ -43,7 +43,7 @@ pub fn player_system( }; kinematic_input.movement = movement; - kinematic_input.want_jump = input.pressed(KeyCode::Space) + kinematic_input.want_jump = input.just_pressed(KeyCode::Space) } fn input_to_axis(negative: bool, positive: bool) -> f32 { diff --git a/src/main.rs b/src/main.rs index 6696f98..ea412e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ pub mod game; +pub mod terrain2d; pub mod util; fn main() { diff --git a/src/mst.rs b/src/mst.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/mst/world.rs b/src/mst/world.rs deleted file mode 100644 index 2738da2..0000000 --- a/src/mst/world.rs +++ /dev/null @@ -1,51 +0,0 @@ -use box2d_rs::{ - b2_body::BodyPtr, - b2_math::B2vec2, - b2_world::{B2world, B2worldPtr}, - b2rs_common::UserDataType, -}; -use unsafe_send_sync::UnsafeSendSync; - -pub type UnsafeBox2D = UnsafeSendSync; -pub type UnsafeBody = UnsafeSendSync>; - -#[derive(Clone, Copy, Default)] -pub struct UserData; -impl UserDataType for UserData { - type Body = Option; - type Fixture = u32; - type Joint = (); -} - -pub struct Box2D { - pub gravity: B2vec2, - pub world_ptr: B2worldPtr, -} - -impl Box2D { - pub const METERS_TO_TEXELS: f32 = 4.0; - pub const TEXELS_TO_METERS: f32 = 1.0 / Self::METERS_TO_TEXELS; - pub const INIT_POS: B2vec2 = B2vec2 { - x: -1000.0, - y: -1000.0, - }; - - fn new() -> Box2D { - let gravity: B2vec2 = B2vec2 { x: 0.0, y: 100.0 }; - // let gravity: B2vec2 = B2vec2 { x: 0.0, y: 1.0 }; - // let gravity: B2vec2 = B2vec2 { x: 0.0, y: 0.0 }; - let world_ptr: B2worldPtr = B2world::new(gravity); - - Box2D { gravity, world_ptr } - } - - pub fn new_unsafe() -> UnsafeBox2D { - UnsafeBox2D::new(Self::new()) - } -} - -impl Default for Box2D { - fn default() -> Self { - Self::new() - } -} diff --git a/src/terrain2d.rs b/src/terrain2d.rs new file mode 100644 index 0000000..efaeb0b --- /dev/null +++ b/src/terrain2d.rs @@ -0,0 +1,155 @@ +use std::collections::{ + hash_map::{Iter, IterMut}, + HashMap, +}; + +use bevy::prelude::*; + +mod chunk2d; +mod texel2d; + +pub use chunk2d::*; +pub use texel2d::*; + +use crate::util::{math::*, Vector2I}; + +pub struct Terrain2DPlugin; + +impl Plugin for Terrain2DPlugin { + fn build(&self, app: &mut App) { + app.insert_resource(Terrain2D::new()) + .add_system(emit_terrain_events); + } +} + +fn emit_terrain_events( + mut terrain: ResMut, + mut terrain_events: EventWriter, +) { + for event in terrain.events.drain(..) { + terrain_events.send(event) + } + for (chunk_index, mut chunk) in terrain.chunk_iter_mut() { + if let Some(rect) = &chunk.dirty_rect { + terrain_events.send(TerrainEvent::TexelsUpdated(*chunk_index, *rect)); + chunk.dirty_rect = None; + } + } +} + +pub enum TerrainEvent { + ChunkAdded(ChunkIndex), + ChunkRemoved(ChunkIndex), + TexelsUpdated(ChunkIndex, ChunkRect), +} + +#[derive(Default, Resource)] +pub struct Terrain2D { + chunk_map: HashMap, + events: Vec, +} + +impl Terrain2D { + pub fn new() -> Terrain2D { + Terrain2D { + chunk_map: HashMap::new(), + events: Vec::new(), + } + } + + pub fn add_chunk(&mut self, index: ChunkIndex, chunk: Chunk) { + self.chunk_map.insert(index, chunk); + self.events.push(TerrainEvent::ChunkAdded(index)) + } + + pub fn remove_chunk(&mut self, index: ChunkIndex) { + self.events.push(TerrainEvent::ChunkRemoved(index)); + self.chunk_map.remove(&index); + } + + pub fn chunk_iter(&self) -> Iter { + self.chunk_map.iter() + } + + pub fn chunk_iter_mut(&mut self) -> IterMut { + self.chunk_map.iter_mut() + } + + pub fn index_to_chunk(&self, index: &ChunkIndex) -> Option<&Chunk> { + self.chunk_map.get(index) + } + + pub fn index_to_chunk_mut(&mut self, index: &ChunkIndex) -> Option<&mut Chunk> { + self.chunk_map.get_mut(index) + } + + pub fn global_to_chunk(&self, global: &Vector2I) -> Option<&Chunk> { + self.index_to_chunk(&global_to_chunk_index(global)) + } + + pub fn global_to_chunk_mut(&mut self, global: &Vector2I) -> Option<&mut Chunk> { + self.index_to_chunk_mut(&global_to_chunk_index(global)) + } + + pub fn global_to_texel(&self, global: &Vector2I) -> Option { + match self.global_to_chunk(global) { + Some(chunk) => chunk.get_texel(&global_to_local(global)), + None => None, + } + } + + pub fn global_to_texel_mut(&mut self, global: &Vector2I) -> Option { + match self.global_to_chunk(global) { + Some(chunk) => chunk.get_texel(&global_to_local(global)), + None => None, + } + } + + pub fn set_texel(&mut self, global: &Vector2I, id: TexelID) { + let index = global_to_chunk_index(global); + match self.index_to_chunk_mut(&index) { + Some(chunk) => chunk.set_texel(&global_to_local(global), id), + None => { + let mut chunk = Chunk::new(); + chunk.set_texel(&global_to_local(global), id); + self.add_chunk(index, chunk); + } + } + } +} + +pub fn local_to_texel_index(position: &Vector2I) -> Option { + match position.x >= 0 + && position.y >= 0 + && position.x < Chunk::SIZE.x + && position.y < Chunk::SIZE.y + { + true => Some(position.y as usize * Chunk::SIZE_X + position.x as usize), + false => None, + } +} + +pub fn texel_index_to_local(i: usize) -> Vector2I { + Vector2I { + x: i as i32 % Chunk::SIZE.x, + y: i as i32 / Chunk::SIZE.y, + } +} + +pub fn global_to_local(position: &Vector2I) -> Vector2I { + Vector2I { + x: wrapping_remainder(position.x, Chunk::SIZE.x), + y: wrapping_remainder(position.y, Chunk::SIZE.y), + } +} + +pub fn global_to_chunk_index(position: &Vector2I) -> ChunkIndex { + Vector2I { + x: wrapping_quotient(position.x, Chunk::SIZE.x), + y: wrapping_quotient(position.y, Chunk::SIZE.y), + } +} + +pub fn chunk_index_to_global(chunk_pos: &ChunkIndex) -> Vector2I { + *chunk_pos * Chunk::SIZE +} diff --git a/src/terrain2d/chunk2d.rs b/src/terrain2d/chunk2d.rs new file mode 100644 index 0000000..d66217d --- /dev/null +++ b/src/terrain2d/chunk2d.rs @@ -0,0 +1,94 @@ +use super::{local_to_texel_index, Texel, TexelID, NEIGHBOUR_INDEX_MAP}; +use crate::util::Vector2I; + +pub type ChunkIndex = Vector2I; + +#[derive(Clone, Copy)] +pub struct ChunkRect { + pub min: Vector2I, + pub max: Vector2I, +} + +pub struct Chunk { + pub texels: [Texel; (Self::SIZE_X * Self::SIZE_Y) as usize], + // TODO: handle multiple dirty rects + pub dirty_rect: Option, +} + +impl Chunk { + pub const SIZE_X: usize = 64; + pub const SIZE_Y: usize = 64; + pub const SIZE: Vector2I = Vector2I { + x: Self::SIZE_X as i32, + y: Self::SIZE_Y as i32, + }; + + pub fn new() -> Chunk { + Chunk { + texels: Self::new_texel_array(), + dirty_rect: None, + } + } + + pub fn new_texel_array() -> [Texel; Self::SIZE_X * Self::SIZE_Y] { + [Texel::default(); Self::SIZE_X * Self::SIZE_Y] + } + + pub fn mark_all_dirty(&mut self) { + self.dirty_rect = Some(ChunkRect { + min: Vector2I::ZERO, + max: Self::SIZE, + }); + } + + pub fn mark_dirty(&mut self, position: &Vector2I) { + match &self.dirty_rect { + Some(rect) => { + self.dirty_rect = Some(ChunkRect { + min: Vector2I::min(&rect.min, position), + max: Vector2I::max(&rect.max, position), + }) + } + None => { + self.dirty_rect = Some(ChunkRect { + min: *position, + max: *position, + }) + } + } + } + + pub fn get_texel(&self, position: &Vector2I) -> Option { + local_to_texel_index(position).map(|i| self.texels[i]) + } + + pub fn get_texel_option_mut(&mut self, position: &Vector2I) -> Option<&mut Texel> { + local_to_texel_index(position).map(|i| &mut self.texels[i]) + } + + pub fn set_texel(&mut self, position: &Vector2I, id: TexelID) { + let i = local_to_texel_index(position).expect("Texel index out of range"); + if self.texels[i].id != id { + self.mark_dirty(position); + } + let update_neighbours = self.texels[i].is_empty() + != (Texel { + id, + ..self.texels[i] + }) + .is_empty(); + self.texels[i].id = id; + // Update neighbour mask + if update_neighbours { + for offset in Texel::NEIGHBOUR_OFFSET_VECTORS { + // Flip neighbour's bit + match self.get_texel_option_mut(&(*position + offset)) { + Some(mut neighbour) => { + neighbour.neighbour_mask ^= 1 << NEIGHBOUR_INDEX_MAP[&-offset]; + } + None => (), + } + } + } + } +} diff --git a/src/terrain2d/texel2d.rs b/src/terrain2d/texel2d.rs new file mode 100644 index 0000000..d5c8fc6 --- /dev/null +++ b/src/terrain2d/texel2d.rs @@ -0,0 +1,38 @@ +use lazy_static::lazy_static; +use std::collections::HashMap; + +pub use u8 as TexelID; +pub use u8 as NeighbourMask; + +use crate::util::Vector2I; + +#[derive(Clone, Copy, Default)] +pub struct Texel { + pub id: TexelID, + /// bitmask of empty/non-empty neighbours, see NEIGHBOUR_OFFSET_VECTORS for the order + pub neighbour_mask: NeighbourMask, +} + +lazy_static! { + pub static ref NEIGHBOUR_INDEX_MAP: HashMap = { + let mut map = HashMap::new(); + for i in 0..Texel::NEIGHBOUR_OFFSET_VECTORS.len() { + map.insert(Texel::NEIGHBOUR_OFFSET_VECTORS[i], i as u8); + } + map + }; +} + +impl Texel { + pub const EMPTY: TexelID = 0; + pub const NEIGHBOUR_OFFSET_VECTORS: [Vector2I; 4] = [ + Vector2I { x: 0, y: 1 }, + Vector2I { x: 1, y: 0 }, + Vector2I { x: 0, y: -1 }, + Vector2I { x: -1, y: 0 }, + ]; + + pub fn is_empty(&self) -> bool { + self.id == 0 + } +} diff --git a/src/util.rs b/src/util.rs index 1c9de20..192db92 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,5 +1,12 @@ use bevy::prelude::*; +pub mod math; +mod vector2; +mod vector2_i32; + +pub use vector2::*; +pub use vector2_i32::*; + pub fn lerp(a: f32, b: f32, t: f32) -> f32 { a * (1.0 - t) + b * t } diff --git a/src/util/math.rs b/src/util/math.rs new file mode 100644 index 0000000..417917e --- /dev/null +++ b/src/util/math.rs @@ -0,0 +1,25 @@ +pub fn lerp(a: f32, b: f32, t: f32) -> f32 { + a * (1.0 - t) + b * t +} + +/// Calculate quotient, but take into account negative values so that they continue the cycle seamlessly. +/// e.g. (0, 4) -> 0, (-4, 4) -> -1, (-5, 4) -> -2 +pub fn wrapping_quotient(dividend: i32, divisor: i32) -> i32 { + let res = (if dividend < 0 { dividend + 1 } else { dividend }) / divisor; + if dividend < 0 { + res - 1 + } else { + res + } +} + +/// Calculate remainder, but take into account negative values so that they continue the cycle seamlessly. +/// e.g. (0, 4) -> 0, (-4, 4) -> 0, (-5, 4) -> 3 +pub fn wrapping_remainder(dividend: i32, divisor: i32) -> i32 { + let res = dividend % divisor; + if dividend < 0 { + (divisor + res) % divisor + } else { + res + } +} diff --git a/src/util/vector2.rs b/src/util/vector2.rs new file mode 100644 index 0000000..95074ec --- /dev/null +++ b/src/util/vector2.rs @@ -0,0 +1,122 @@ +use core::{fmt, ops}; + +pub trait VectorComponent: + Sized + + Copy + + Ord + + fmt::Display + + ops::Add + + ops::Neg + + ops::Sub + + ops::Mul + + ops::Div +{ +} + +impl VectorComponent for T where + T: Sized + + Copy + + Ord + + fmt::Display + + ops::Neg + + ops::Add + + ops::Sub + + ops::Mul + + ops::Div +{ +} + +#[derive(PartialEq, Eq, Hash, Clone, Copy, Default, Debug)] +pub struct Vector2 { + pub x: T, + pub y: T, +} + +impl Vector2 { + pub fn min(&self, other: &Vector2) -> Vector2 { + Vector2 { + x: Ord::min(self.x, other.x), + y: Ord::min(self.y, other.y), + } + } + + pub fn max(&self, other: &Vector2) -> Vector2 { + Vector2 { + x: Ord::max(self.x, other.x), + y: Ord::max(self.y, other.y), + } + } +} + +impl fmt::Display for Vector2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "({}, {})", self.x, self.y) + } +} + +impl ops::Add> for Vector2 { + type Output = Vector2; + fn add(self, rhs: Vector2) -> Self::Output { + Vector2 { + x: self.x + rhs.x, + y: self.y + rhs.y, + } + } +} + +impl ops::Neg for Vector2 { + type Output = Vector2; + fn neg(self) -> Self::Output { + Vector2 { + x: -self.x, + y: -self.y, + } + } +} + +impl ops::Sub> for Vector2 { + type Output = Vector2; + fn sub(self, rhs: Vector2) -> Self::Output { + self + (-rhs) + } +} + +impl ops::Mul> for Vector2 { + type Output = Vector2; + fn mul(self, rhs: Vector2) -> Self::Output { + Vector2 { + x: self.x * rhs.x, + y: self.y * rhs.y, + } + } +} + +impl ops::Mul for Vector2 { + type Output = Vector2; + fn mul(self, rhs: T) -> Self::Output { + Vector2 { + x: self.x * rhs, + y: self.y * rhs, + } + } +} + +impl ops::Div> for Vector2 { + type Output = Vector2; + fn div(self, rhs: Vector2) -> Self::Output { + Vector2 { + x: self.x / rhs.x, + y: self.y / rhs.y, + } + } +} + +impl ops::Div for Vector2 { + type Output = Vector2; + fn div(self, rhs: T) -> Self::Output { + Vector2 { + x: self.x / rhs, + y: self.y / rhs, + } + } +} diff --git a/src/util/vector2_i32.rs b/src/util/vector2_i32.rs new file mode 100644 index 0000000..66d22f0 --- /dev/null +++ b/src/util/vector2_i32.rs @@ -0,0 +1,27 @@ +use bevy::prelude::Vec2; + +use super::Vector2; + +pub type Vector2I = Vector2; + +impl Vector2I { + pub const ZERO: Vector2I = Vector2I { x: 0, y: 0 }; + pub const ONE: Vector2I = Vector2I { x: 1, y: 1 }; + pub const UP: Vector2I = Vector2I { x: 0, y: 1 }; + pub const DOWN: Vector2I = Vector2I { x: 0, y: -1 }; + pub const LEFT: Vector2I = Vector2I { x: -1, y: 0 }; + pub const RIGHT: Vector2I = Vector2I { x: 1, y: 0 }; + + pub fn angle(&self) -> f32 { + (self.y as f32).atan2(self.x as f32) + } +} + +impl From for Vec2 { + fn from(vec: Vector2I) -> Self { + Vec2 { + x: vec.x as f32, + y: vec.y as f32, + } + } +}