kuilu/src/terrain2d/chunk2d.rs

621 lines
23 KiB
Rust

use std::collections::VecDeque;
use super::{
local_to_texel_index, texel_index_to_local, Terrain2D, TerrainEvent2D, Texel2D,
TexelBehaviour2D, TexelID, NEIGHBOUR_INDEX_MAP,
};
use crate::util::{CollisionLayers, Segment2I, Vector2I};
use bevy::{
prelude::*,
render::{render_resource::Extent3d, texture::ImageSampler},
};
use bevy_rapier2d::prelude::*;
use lazy_static::lazy_static;
type Island = VecDeque<Segment2I>;
lazy_static! {
/// Marching Square case dictionary.
///
/// Key is a bitmask of neighbouring tiles (up, right, down, left - least significant bit first).
/// Bit set to 1 means that the neighbour has collision. Only the 4 least significant bits are currently used.
///
/// Value is an array of segments that the tile should have. The segments are configured to go clockwise.
///
/// Note: This dictionary should only be used for empty tiles.
static ref MST_CASE_MAP: [Vec<Segment2I>; 16] = [
/* 0b0000 */ vec![],
/* 0b0001 */ vec![ Segment2I { from: Vector2I::ONE, to: Vector2I::UP } ],
/* 0b0010 */ vec![ Segment2I { from: Vector2I::RIGHT, to: Vector2I::ONE } ],
/* 0b0011 */ vec![ Segment2I { from: Vector2I::RIGHT, to: Vector2I::UP } ],
/* 0b0100 */ vec![ Segment2I { from: Vector2I::ZERO, to: Vector2I::RIGHT } ],
/* 0b0101 */ vec![ Segment2I { from: Vector2I::ONE, to: Vector2I::UP }, Segment2I { from: Vector2I::ZERO, to: Vector2I::RIGHT } ],
/* 0b0110 */ vec![ Segment2I { from: Vector2I::ZERO, to: Vector2I::ONE } ],
/* 0b0111 */ vec![ Segment2I { from: Vector2I::ZERO, to: Vector2I::UP } ],
/* 0b1000 */ vec![ Segment2I { from: Vector2I::UP, to: Vector2I::ZERO } ],
/* 0b1001 */ vec![ Segment2I { from: Vector2I::ONE, to: Vector2I::ZERO } ],
/* 0b1010 */ vec![ Segment2I { from: Vector2I::RIGHT, to: Vector2I::ONE }, Segment2I { from: Vector2I::UP, to: Vector2I::ZERO } ],
/* 0b1011 */ vec![ Segment2I { from: Vector2I::RIGHT, to: Vector2I::ZERO } ],
/* 0b1100 */ vec![ Segment2I { from: Vector2I::UP, to: Vector2I::RIGHT } ],
/* 0b1101 */ vec![ Segment2I { from: Vector2I::ONE, to: Vector2I::RIGHT } ],
/* 0b1110 */ vec![ Segment2I { from: Vector2I::UP, to: Vector2I::ONE } ],
/* 0b1111 */ vec![],
];
/// Version of the MS case dictionary that is used by the solid tiles at the edge of the chunk
static ref MST_EDGE_CASE_MAP: [Segment2I; 4] = [
/* up */ Segment2I { from: Vector2I::UP, to: Vector2I::ONE },
/* right */ Segment2I { from: Vector2I::ONE, to: Vector2I::RIGHT },
/* down */ Segment2I { from: Vector2I::RIGHT, to: Vector2I::ZERO },
/* left */ Segment2I { from: Vector2I::ZERO, to: Vector2I::UP },
];
}
#[derive(Reflect, Component, Default)]
#[reflect(Component)]
pub struct TerrainChunk2D {
pub index: Chunk2DIndex,
}
#[derive(Reflect, Component, Default)]
#[reflect(Component)]
pub struct TerrainChunkSpriteSync2D;
#[derive(Reflect, Component, Default)]
#[reflect(Component)]
pub struct TerrainChunkCollisionSync2D;
#[derive(Bundle, Default)]
pub struct ChunkSpriteBundle {
pub chunk: TerrainChunk2D,
pub sync_flag: TerrainChunkSpriteSync2D,
pub sprite: SpriteBundle,
}
#[derive(Bundle, Default)]
pub struct ChunkColliderBundle {
pub chunk: TerrainChunk2D,
pub sync_flag: TerrainChunkCollisionSync2D,
pub transform: TransformBundle,
}
pub type Chunk2DIndex = Vector2I;
#[derive(Clone, Copy)]
pub struct ChunkRect {
pub min: Vector2I,
pub max: Vector2I,
}
pub struct Chunk2D {
pub texels: [Texel2D; (Self::SIZE_X * Self::SIZE_Y) as usize],
// TODO: handle multiple dirty rects
pub dirty_rect: Option<ChunkRect>,
}
impl Chunk2D {
pub const SIZE_X: usize = 32;
pub const SIZE_Y: usize = 32;
pub const SIZE: Vector2I = Vector2I {
x: Self::SIZE_X as i32,
y: Self::SIZE_Y as i32,
};
pub fn new() -> Chunk2D {
Chunk2D {
texels: Self::new_texel_array(),
dirty_rect: None,
}
}
pub fn new_full() -> Chunk2D {
let mut chunk = Chunk2D {
texels: Self::new_texel_array(),
dirty_rect: None,
};
for y in 0..Self::SIZE_Y {
for x in 0..Self::SIZE_X {
chunk.set_texel(&Vector2I::new(x as i32, y as i32), 1);
}
}
chunk
}
pub fn new_half() -> Chunk2D {
let mut chunk = Chunk2D {
texels: Self::new_texel_array(),
dirty_rect: None,
};
for y in 0..Self::SIZE_Y {
for x in 0..Self::SIZE_X {
if x <= Self::SIZE_Y - y {
chunk.set_texel(&Vector2I::new(x as i32, y as i32), 1);
}
}
}
chunk
}
pub fn new_circle() -> Chunk2D {
let mut chunk = Chunk2D {
texels: Self::new_texel_array(),
dirty_rect: None,
};
let origin = Self::SIZE / 2;
let radius = Self::SIZE_X as i32 / 2;
for y in 0..Self::SIZE_Y {
for x in 0..Self::SIZE_X {
let dx = (x as i32 - origin.x).abs();
let dy = (y as i32 - origin.y).abs();
if dx * dx + dy * dy <= (radius - 1) * (radius - 1) {
chunk.set_texel(&Vector2I::new(x as i32, y as i32), 1);
}
}
}
chunk
}
pub fn new_texel_array() -> [Texel2D; Self::SIZE_X * Self::SIZE_Y] {
[Texel2D::default(); Self::SIZE_X * Self::SIZE_Y]
}
pub fn xy_vec() -> Vec<Vector2I> {
let mut result = Vec::with_capacity(Self::SIZE_X * Self::SIZE_Y);
for y in 0..Self::SIZE_Y {
for x in 0..Self::SIZE_X {
result.push(Vector2I {
x: x as i32,
y: y as i32,
});
}
}
result
}
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 mark_clean(&mut self) {
self.dirty_rect = None;
}
pub fn get_texel(&self, position: &Vector2I) -> Option<Texel2D> {
local_to_texel_index(position).map(|i| self.texels[i])
}
pub fn get_texel_mut(&mut self, position: &Vector2I) -> Option<&mut Texel2D> {
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()
!= (Texel2D {
id,
..self.texels[i]
})
.is_empty();
self.texels[i].id = id;
// Update neighbour mask
if update_neighbours {
for offset in Texel2D::NEIGHBOUR_OFFSET_VECTORS {
// Flip neighbour's bit
match self.get_texel_mut(&(*position + offset)) {
Some(mut neighbour) => {
neighbour.neighbour_mask ^= 1 << NEIGHBOUR_INDEX_MAP[&-offset];
}
None => (),
}
}
}
}
pub fn create_texture_data(&self) -> Vec<u8> {
let mut image_data = Vec::with_capacity(Chunk2D::SIZE_X * Chunk2D::SIZE_Y * 4);
for y in (0..Chunk2D::SIZE_Y).rev() {
for x in 0..Chunk2D::SIZE_X {
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
}
pub fn create_collision_data(&self) -> Vec<Vec<Vec2>> {
let mut islands: Vec<Island> = Vec::new();
for i in 0..self.texels.len() {
let local = texel_index_to_local(i);
let edge_mask: u8 = if local.y == Chunk2D::SIZE.y - 1 {
1 << 0
} else {
0
} | if local.x == Chunk2D::SIZE.x - 1 {
1 << 1
} else {
0
} | if local.y == 0 { 1 << 2 } else { 0 }
| if local.x == 0 { 1 << 3 } else { 0 };
let mut sides: Vec<Segment2I>;
if self.texels[i].is_empty() {
sides = MST_CASE_MAP[self.texels[i].neighbour_mask as usize]
.iter()
.clone()
.map(|side| Segment2I {
from: side.from + local,
to: side.to + local,
})
.collect();
} else if !self.texels[i].is_empty() && edge_mask != 0 {
sides = Vec::with_capacity(Chunk2D::SIZE_X * 2 + Chunk2D::SIZE_Y * 2);
for i in 0..MST_EDGE_CASE_MAP.len() {
if edge_mask & (1 << i) != 0 {
let edge = MST_EDGE_CASE_MAP[i];
sides.push(Segment2I {
from: edge.from + local,
to: edge.to + local,
})
}
}
} else {
continue;
}
for side in sides {
// Check if the side can be attached to any island
// The naming of front and back are kind of misleading, and come from the VecDeque type.
// You can think of the front as the beginning of the island loop, and back the end.
// Connect to an island if possible, otherwise create a new island
{
let mut connected_to: Option<&mut Island> = None;
for island in islands.iter_mut() {
if island.back().is_some() && island.back().unwrap().to == side.from {
connected_to = Some(island);
}
}
match connected_to {
Some(back) => {
back.push_back(side);
}
None => {
let mut island: Island = Island::new();
island.push_back(side);
islands.push(island);
}
}
}
// Find connected islands
loop {
let mut merge_index: Option<usize> = None;
'outer: for i in 0..islands.len() {
for j in 0..islands.len() {
if i == j {
continue;
}
if islands[i].back().is_some()
&& islands[j].front().is_some()
&& islands[i].back().unwrap().to == islands[j].front().unwrap().from
{
merge_index = Some(i);
break 'outer;
}
}
}
// Merge connected islands
match merge_index {
Some(index) => {
let mut merge_from = islands.swap_remove(index);
match islands.iter_mut().find(|island| match island.front() {
Some(front) => front.from == merge_from.back().unwrap().to,
None => false,
}) {
Some(merge_to) => loop {
match merge_from.pop_back() {
Some(segment) => merge_to.push_front(segment),
None => break,
}
},
None => (),
};
}
None => break,
}
}
}
}
let mut result: Vec<Vec<Vec2>> = Vec::with_capacity(islands.len());
for island in islands {
if island.len() < 4 {
continue;
}
let mut points: Vec<Vec2> = Vec::with_capacity(island.len() + 1);
points.push(Vec2::from(island.front().unwrap().from));
let mut current_angle: Option<f32> = None;
for side in island {
if current_angle.is_some() && (current_angle.unwrap() - side.angle()).abs() < 0.1 {
let len = points.len();
points[len - 1] = Vec2::from(side.to)
} else {
current_angle = Some(side.angle());
points.push(Vec2::from(side.to));
}
}
result.push(points);
}
result
}
}
pub fn chunk_spawner(
mut commands: Commands,
mut terrain_events: EventReader<TerrainEvent2D>,
mut images: ResMut<Assets<Image>>,
chunk_query: Query<(Entity, &TerrainChunk2D)>,
) {
for terrain_event in terrain_events.iter() {
match terrain_event {
TerrainEvent2D::ChunkAdded(chunk_index) => {
// Create unique handle for the image
let mut image = Image::new(
Extent3d {
width: Chunk2D::SIZE_X as u32,
height: Chunk2D::SIZE_Y as u32,
depth_or_array_layers: 1,
},
bevy::render::render_resource::TextureDimension::D2,
vec![0x00; Chunk2D::SIZE_X * Chunk2D::SIZE_Y * 4],
bevy::render::render_resource::TextureFormat::Rgba8Unorm,
);
image.sampler_descriptor = ImageSampler::nearest();
let texture = images.add(image);
let pos = Vec2::from(*chunk_index * Chunk2D::SIZE);
commands
.spawn(ChunkSpriteBundle {
chunk: TerrainChunk2D {
index: *chunk_index,
},
sprite: SpriteBundle {
sprite: Sprite {
custom_size: Some(Vec2::from(Chunk2D::SIZE)),
anchor: bevy::sprite::Anchor::BottomLeft,
..default()
},
texture,
transform: Transform::from_translation(Vec3::new(pos.x, pos.y, 0.0)),
..default()
},
..default()
})
.insert(Name::new(format!(
"Chunk Sprite {},{}",
chunk_index.x, chunk_index.y
)));
commands
.spawn(ChunkColliderBundle {
chunk: TerrainChunk2D {
index: *chunk_index,
},
transform: TransformBundle::from_transform(Transform::from_translation(
Vec3::new(pos.x, pos.y, 0.0),
)),
..default()
})
.insert(Name::new(format!(
"Chunk Collider {},{}",
chunk_index.x, chunk_index.y
)));
}
TerrainEvent2D::ChunkRemoved(chunk_index) => {
for (entity, chunk) in chunk_query.iter() {
if chunk.index == *chunk_index {
commands.entity(entity).despawn_recursive();
}
}
}
_ => (),
}
}
}
/**
Update the chunk sprite as needed
*/
pub fn chunk_sprite_sync(
mut terrain_events: EventReader<TerrainEvent2D>,
mut images: ResMut<Assets<Image>>,
terrain: Res<Terrain2D>,
added_chunk_query: Query<
(Entity, &TerrainChunk2D),
(With<TerrainChunkSpriteSync2D>, Changed<TerrainChunk2D>),
>,
chunk_query: Query<(Entity, &TerrainChunk2D), (With<TerrainChunkSpriteSync2D>, With<Sprite>)>,
texture_query: Query<&Handle<Image>>,
) {
let mut updated_chunks: Vec<(Entity, &TerrainChunk2D, Option<ChunkRect>)> = vec![];
// Check for added components
for (added_entity, added_chunk) in added_chunk_query.iter() {
updated_chunks.push((added_entity, added_chunk, None));
}
// Check for terrain events
for event in terrain_events.iter() {
for (entity, chunk) in chunk_query.iter() {
let (chunk_index, rect) = match event {
TerrainEvent2D::ChunkAdded(chunk_index) => {
// The entity should not have the time to react to the event since it was just made
// REM: This gets called when new chunk is instantiated with brush
// println!("[chunk_sprite_sync -> TerrainEvent2D::ChunkAdded] This probably shouldn't be firing, maybe the chunk was destroyed and immediately created? chunk: {chunk_index:?}");
(chunk_index, None)
}
TerrainEvent2D::TexelsUpdated(chunk_index, rect) => (chunk_index, Some(*rect)),
_ => continue,
};
if *chunk_index != chunk.index {
continue;
};
updated_chunks.push((entity, chunk, rect));
}
}
// Update sprite
for (entity, chunk, rect) in updated_chunks {
let chunk = terrain.index_to_chunk(&chunk.index).unwrap();
// TODO: Update only the rect
let _rect = rect.unwrap_or(ChunkRect {
min: Vector2I::ZERO,
max: Chunk2D::SIZE - Vector2I::ONE,
});
let handle = texture_query.get(entity).unwrap();
let mut image = images.get_mut(handle).unwrap();
let image_data = chunk.create_texture_data();
image.data = image_data;
}
}
/**
Create and update colliders for chunk as needed
*/
pub fn chunk_collision_sync(
mut terrain_events: EventReader<TerrainEvent2D>,
mut commands: Commands,
terrain: Res<Terrain2D>,
added_chunk_query: Query<
(Entity, &TerrainChunk2D),
(With<TerrainChunkCollisionSync2D>, Changed<TerrainChunk2D>),
>,
chunk_query: Query<(Entity, &TerrainChunk2D), With<TerrainChunkCollisionSync2D>>,
child_query: Query<&Children>,
collider_query: Query<&Collider>,
) {
let mut updated_chunks: Vec<(Entity, &TerrainChunk2D)> = vec![];
// Check for added components
for (added_entity, added_chunk) in added_chunk_query.iter() {
updated_chunks.push((added_entity, added_chunk));
}
// Check for terrain events
for event in terrain_events.iter() {
for (entity, chunk) in chunk_query.iter() {
let chunk_index = match event {
TerrainEvent2D::ChunkAdded(chunk_index) => {
// The entity should not have the time to react to the event since it was just made
// REM: This gets called when new chunk is instantiated with brush
// println!("[chunk_collision_sync -> TerrainEvent2D::ChunkAdded] This probably shouldn't be firing, maybe the chunk was destroyed and immediately created? chunk: {chunk_index:?}");
chunk_index
}
TerrainEvent2D::TexelsUpdated(chunk_index, _) => chunk_index,
_ => continue,
};
if *chunk_index != chunk.index {
continue;
};
updated_chunks.push((entity, chunk));
}
}
// let layer_membership = CollisionLayers::WORLD;
// REM: Kinda messy, partly due do how entity creation is timed
for (entity, chunk_component) in updated_chunks.iter() {
let chunk = terrain.index_to_chunk(&chunk_component.index).unwrap();
let new_islands = chunk.create_collision_data();
// Create new colliders
if let Ok(children) = child_query.get(*entity) {
// Chunk has children, new ones will be created and old ones components will be removed
for (index, island) in new_islands.iter().enumerate() {
if let Some(child) = children.get(index) {
// Replace collider
commands
.entity(*child)
.insert(Collider::polyline(island.clone(), None));
} else {
// Create new child
commands.entity(*entity).with_children(|builder| {
builder
.spawn(Collider::polyline(island.clone(), None))
.insert(TransformBundle::default())
.insert(CollisionGroups::new(CollisionLayers::WORLD, Group::ALL))
.insert(Name::new(format!("Island #{}", index)));
});
}
}
} else {
// Chunk doesn't have a Children component yet
for (index, island) in new_islands.iter().enumerate() {
commands.entity(*entity).with_children(|builder| {
builder
.spawn(Collider::polyline(island.clone(), None))
.insert(TransformBundle::default())
.insert(CollisionGroups::new(CollisionLayers::WORLD, Group::ALL))
.insert(Name::new(format!("Island #{}", index)));
});
}
};
// Remove extra children.
// Leaving them seems to cause weird problems with rapier when re-adding the collider. The collider is ignored until something else is updated.
for children in child_query.get(*entity) {
for (index, child) in children.iter().enumerate() {
if let Ok(_) = collider_query.get(*child) {
if index >= new_islands.len() {
commands.entity(*child).despawn_recursive();
}
}
}
}
}
}