wip: started implementing terrain system

fix/collision-refresh
hheik 2022-12-01 20:14:51 +02:00
parent 954ac51bd6
commit cf66379a23
16 changed files with 480 additions and 64 deletions

1
Cargo.lock generated
View File

@ -2061,6 +2061,7 @@ dependencies = [
"bevy", "bevy",
"bevy-inspector-egui", "bevy-inspector-egui",
"bevy_rapier2d", "bevy_rapier2d",
"lazy_static",
] ]
[[package]] [[package]]

View File

@ -9,6 +9,7 @@ edition = "2021"
bevy = { version = "0.9.0", features = ["dynamic"] } bevy = { version = "0.9.0", features = ["dynamic"] }
bevy-inspector-egui = "0.14.0" bevy-inspector-egui = "0.14.0"
bevy_rapier2d = { path = "../bevy_rapier/bevy_rapier2d" } bevy_rapier2d = { path = "../bevy_rapier/bevy_rapier2d" }
lazy_static = "1.4.0"
# Enable a small amount of optimization in debug mode # Enable a small amount of optimization in debug mode
[profile.dev] [profile.dev]

View File

@ -16,12 +16,12 @@ pub fn init() {
.add_plugin(WorldInspectorPlugin::new()) .add_plugin(WorldInspectorPlugin::new())
.add_plugin(KinematicPlugin) .add_plugin(KinematicPlugin)
.add_plugin(GameCameraPlugin) .add_plugin(GameCameraPlugin)
.add_plugin(PlayerPlugin) // .add_plugin(PlayerPlugin)
.add_startup_system(setup) // .add_startup_system(setup_debug_ground)
.run(); .run();
} }
fn setup(mut commands: Commands) { fn setup_debug_ground(mut commands: Commands) {
// Static ground // Static ground
commands commands
.spawn(()) .spawn(())

View File

@ -37,10 +37,10 @@ fn camera_setup(mut commands: Commands) {
commands.spawn(( commands.spawn((
Name::new("Camera"), Name::new("Camera"),
Camera2dBundle { Camera2dBundle {
projection: OrthographicProjection { // projection: OrthographicProjection {
scaling_mode: ScalingMode::FixedHorizontal(320.0), // scaling_mode: ScalingMode::FixedHorizontal(320.0),
..default() // ..default()
}, // },
..default() ..default()
}, },
)); ));

View File

@ -27,7 +27,6 @@ pub struct KinematicBundle {
pub events: ActiveEvents, pub events: ActiveEvents,
pub collisions: ActiveCollisionTypes, pub collisions: ActiveCollisionTypes,
pub properties: KinematicProperties, pub properties: KinematicProperties,
#[bundle]
pub transform: TransformBundle, pub transform: TransformBundle,
} }
@ -71,10 +70,7 @@ impl KinematicState {
} }
pub fn can_jump(&self) -> bool { pub fn can_jump(&self) -> bool {
if self.on_ground && !self.did_jump { self.on_ground && !self.did_jump
return true;
}
false
} }
} }

View File

@ -43,7 +43,7 @@ pub fn player_system(
}; };
kinematic_input.movement = movement; 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 { fn input_to_axis(negative: bool, positive: bool) -> f32 {

View File

@ -1,4 +1,5 @@
pub mod game; pub mod game;
pub mod terrain2d;
pub mod util; pub mod util;
fn main() { fn main() {

View File

View File

@ -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<Box2D>;
pub type UnsafeBody = UnsafeSendSync<BodyPtr<UserData>>;
#[derive(Clone, Copy, Default)]
pub struct UserData;
impl UserDataType for UserData {
type Body = Option<u32>;
type Fixture = u32;
type Joint = ();
}
pub struct Box2D {
pub gravity: B2vec2,
pub world_ptr: B2worldPtr<UserData>,
}
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<UserData> = 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()
}
}

155
src/terrain2d.rs Normal file
View File

@ -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<Terrain2D>,
mut terrain_events: EventWriter<TerrainEvent>,
) {
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<ChunkIndex, Chunk>,
events: Vec<TerrainEvent>,
}
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<ChunkIndex, Chunk> {
self.chunk_map.iter()
}
pub fn chunk_iter_mut(&mut self) -> IterMut<ChunkIndex, Chunk> {
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<Texel> {
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<Texel> {
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<usize> {
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
}

94
src/terrain2d/chunk2d.rs Normal file
View File

@ -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<ChunkRect>,
}
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<Texel> {
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 => (),
}
}
}
}
}

38
src/terrain2d/texel2d.rs Normal file
View File

@ -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<Vector2I, u8> = {
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
}
}

View File

@ -1,5 +1,12 @@
use bevy::prelude::*; 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 { pub fn lerp(a: f32, b: f32, t: f32) -> f32 {
a * (1.0 - t) + b * t a * (1.0 - t) + b * t
} }

25
src/util/math.rs Normal file
View File

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

122
src/util/vector2.rs Normal file
View File

@ -0,0 +1,122 @@
use core::{fmt, ops};
pub trait VectorComponent:
Sized
+ Copy
+ Ord
+ fmt::Display
+ ops::Add<Output = Self>
+ ops::Neg<Output = Self>
+ ops::Sub<Output = Self>
+ ops::Mul<Output = Self>
+ ops::Div<Output = Self>
{
}
impl<T> VectorComponent for T where
T: Sized
+ Copy
+ Ord
+ fmt::Display
+ ops::Neg<Output = T>
+ ops::Add<Output = T>
+ ops::Sub<Output = T>
+ ops::Mul<Output = T>
+ ops::Div<Output = T>
{
}
#[derive(PartialEq, Eq, Hash, Clone, Copy, Default, Debug)]
pub struct Vector2<T: VectorComponent> {
pub x: T,
pub y: T,
}
impl<T: VectorComponent> Vector2<T> {
pub fn min(&self, other: &Vector2<T>) -> Vector2<T> {
Vector2 {
x: Ord::min(self.x, other.x),
y: Ord::min(self.y, other.y),
}
}
pub fn max(&self, other: &Vector2<T>) -> Vector2<T> {
Vector2 {
x: Ord::max(self.x, other.x),
y: Ord::max(self.y, other.y),
}
}
}
impl<T: VectorComponent> fmt::Display for Vector2<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
impl<T: VectorComponent> ops::Add<Vector2<T>> for Vector2<T> {
type Output = Vector2<T>;
fn add(self, rhs: Vector2<T>) -> Self::Output {
Vector2 {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
impl<T: VectorComponent> ops::Neg for Vector2<T> {
type Output = Vector2<T>;
fn neg(self) -> Self::Output {
Vector2 {
x: -self.x,
y: -self.y,
}
}
}
impl<T: VectorComponent> ops::Sub<Vector2<T>> for Vector2<T> {
type Output = Vector2<T>;
fn sub(self, rhs: Vector2<T>) -> Self::Output {
self + (-rhs)
}
}
impl<T: VectorComponent> ops::Mul<Vector2<T>> for Vector2<T> {
type Output = Vector2<T>;
fn mul(self, rhs: Vector2<T>) -> Self::Output {
Vector2 {
x: self.x * rhs.x,
y: self.y * rhs.y,
}
}
}
impl<T: VectorComponent> ops::Mul<T> for Vector2<T> {
type Output = Vector2<T>;
fn mul(self, rhs: T) -> Self::Output {
Vector2 {
x: self.x * rhs,
y: self.y * rhs,
}
}
}
impl<T: VectorComponent> ops::Div<Vector2<T>> for Vector2<T> {
type Output = Vector2<T>;
fn div(self, rhs: Vector2<T>) -> Self::Output {
Vector2 {
x: self.x / rhs.x,
y: self.y / rhs.y,
}
}
}
impl<T: VectorComponent> ops::Div<T> for Vector2<T> {
type Output = Vector2<T>;
fn div(self, rhs: T) -> Self::Output {
Vector2 {
x: self.x / rhs,
y: self.y / rhs,
}
}
}

27
src/util/vector2_i32.rs Normal file
View File

@ -0,0 +1,27 @@
use bevy::prelude::Vec2;
use super::Vector2;
pub type Vector2I = Vector2<i32>;
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<Vector2I> for Vec2 {
fn from(vec: Vector2I) -> Self {
Vec2 {
x: vec.x as f32,
y: vec.y as f32,
}
}
}