refactor file locations for readability

This commit is contained in:
2025-09-11 18:23:53 +01:00
parent e58f6804bf
commit ee7ac39ad5
25 changed files with 1103 additions and 1052 deletions
+118
View File
@@ -0,0 +1,118 @@
use bevy::prelude::*;
use bevy_platform::collections::HashMap;
use bevy_platform::sync::Mutex;
use crate::world::{tiles::TileMap, CurrentWorldSpriteState, TerrainSpriteState};
pub const CHUNK_SIZE: i32 = 8;
pub const Z_BELOW: f32 = 45.0;
pub const Z_ABOVE: f32 = 15.0;
pub const Z_TOTAL: f32 = Z_ABOVE + Z_BELOW;
const _: () = assert!(Z_TOTAL <= 255.0);
#[derive(Resource)]
pub struct ChunkMap {
pub loaded_chunks: HashMap<IVec2, (bool, i32)>,
}
impl Default for ChunkMap {
fn default() -> Self {
Self {
loaded_chunks: HashMap::new(),
}
}
}
#[derive(Event)]
pub struct GenerateChunkEvent {
pub chunk_position: IVec2,
}
#[derive(Event)]
pub struct ChunkTerrainEvent {
pub chunk_position: IVec2,
}
#[derive(Event)]
// CAlculate the weather effecrs and rivers/lakes of this chunk
pub struct ChunkWeatheringAndPrecipitationEvent {
pub chunk_position: IVec2,
}
#[derive(Event)]
pub struct ChunkForrestryEvent {
pub chunk_position: IVec2,
pub floor_tiles: Vec<(Vec3, String)>,
}
#[derive(Event)]
pub struct ChunkFoliageEvent {
pub chunk_position: IVec2,
}
#[derive(Event)]
pub struct ChunkFaunaEvent {
pub chunk_position: IVec2,
}
pub fn setup_chunk_system(mut commands: Commands) {
commands.insert_resource(TileMap::default());
commands.insert_resource(ChunkMap::default());
}
pub fn handle_chunk_events(
mut chunk_events: EventReader<GenerateChunkEvent>,
mut chunk_map: ResMut<ChunkMap>,
mut terrain_event_writer: EventWriter<ChunkTerrainEvent>,
mut weathering_event_writer: EventWriter<ChunkWeatheringAndPrecipitationEvent>,
mut foliage_event_writer: EventWriter<ChunkFoliageEvent>,
mut fauna_event_writer: EventWriter<ChunkFaunaEvent>,
mut cwss: ResMut<CurrentWorldSpriteState>,
) {
let mut any = false;
for event in chunk_events.par_read() {
let chunk_pos = event.0.chunk_position;
if let Some((true, _)) = chunk_map.loaded_chunks.get(&chunk_pos) {
continue;
}
any = true;
let chunk_map_updates: Mutex<HashMap<IVec2, bool>> = Mutex::new(HashMap::new());
// Mark chunk as loaded in our thread-safe collection
chunk_map_updates.lock().unwrap().insert(chunk_pos, true);
terrain_event_writer.write(ChunkTerrainEvent {
chunk_position: chunk_pos,
});
weathering_event_writer.write(ChunkWeatheringAndPrecipitationEvent {
chunk_position: chunk_pos,
});
foliage_event_writer.write(ChunkFoliageEvent {
chunk_position: chunk_pos,
});
fauna_event_writer.write(ChunkFaunaEvent {
chunk_position: chunk_pos,
});
// Apply all collected updates to the actual resources
for (chunk_pos, value) in chunk_map_updates.into_inner().unwrap() {
chunk_map.loaded_chunks.insert(chunk_pos, (value, 1800));
}
}
if any {
cwss.state = TerrainSpriteState::WaitingForRender;
}
}
pub fn chunkmap_despawn_timer_system(
mut chunk_map: ResMut<ChunkMap>,
mut cwss: ResMut<CurrentWorldSpriteState>,
) {
for (_, (is_loaded, timer)) in chunk_map.loaded_chunks.iter_mut() {
if !*is_loaded {
continue;
}
if *timer > 0 {
*timer -= 1;
} else {
//TODO - remove chunk from chunkmap
*is_loaded = false;
cwss.state = TerrainSpriteState::WaitingForRender;
}
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod management;
pub use management::*;
+11
View File
@@ -0,0 +1,11 @@
use bevy::prelude::*;
use crate::world::{tiles::TileMap, ChunkMap, GenerateChunkEvent};
pub fn generate_chunk_fauna(
mut commands: Commands,
mut events: EventReader<GenerateChunkEvent>,
mut chunk_map: ResMut<ChunkMap>,
mut tilemap: ResMut<TileMap>,
) {
}
+11
View File
@@ -0,0 +1,11 @@
use bevy::prelude::*;
use crate::world::{tiles::TileMap, ChunkMap, GenerateChunkEvent};
pub fn generate_chunk_foliage(
mut commands: Commands,
mut events: EventReader<GenerateChunkEvent>,
mut chunk_map: ResMut<ChunkMap>,
mut tilemap: ResMut<TileMap>,
) {
}
+176
View File
@@ -0,0 +1,176 @@
use bevy::prelude::*;
use bevy_platform::collections::HashSet;
use bevy_platform::sync::Mutex;
use bevy_platform::time::Instant;
use bevy_rand::prelude::*;
use rand::{Rng, SeedableRng};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use crate::{
constants::{SEED, TILE_SIZE},
world::{
tiles::TileMap, ChunkForrestryEvent, FixtureTilePrefab, TextureIDs, Textures,
VisibleGameEntity,
},
};
pub fn generate_chunk_forrestry(
commands: ParallelCommands<'_, '_>,
mut events: EventReader<ChunkForrestryEvent>,
mut tilemap: ResMut<TileMap>,
texture_ids: Res<TextureIDs>,
textures: Res<Textures>,
) {
let start = Instant::now();
let count = events.len();
let collected_tilemap_updates: Mutex<Vec<(IVec3, (i32, bool, bool, [u32; 8]))>> =
Mutex::new(Vec::<(IVec3, (i32, bool, bool, [u32; 8]))>::new());
events.par_read().for_each(|event| {
let floor_positions = &event.floor_tiles;
let mut tree_positions: Vec<Vec3> = Vec::new();
let min_distance = 7.0 * TILE_SIZE;
let mut hasher = DefaultHasher::new();
SEED.hash(&mut hasher);
event.chunk_position.x.hash(&mut hasher);
event.chunk_position.y.hash(&mut hasher);
let seed = hasher.finish();
let mut rng = WyRand::seed_from_u64(seed);
for (position, floor_type) in floor_positions.iter() {
let above_pos = *position + Vec3::new(0.0, 0.0, TILE_SIZE);
match floor_type.as_str() {
"grass" => {
commands.command_scope(|mut commands| {
// Track log positions to avoid leaf overlap
let mut log_positions = HashSet::new();
// Check if position is far enough from existing trees
let is_far_enough = tree_positions
.iter()
.all(|&tree_pos| above_pos.distance(tree_pos) > min_distance);
// 1 in 100 chance if far enough from other trees
if is_far_enough && rng.random::<u32>() % 100 == 0 {
// Generate trunk
let trunk_height = 4 + rng.random::<u32>() % 5;
for i in 0..trunk_height {
let trunk_pos =
above_pos + Vec3::new(0.0, 0.0, i as f32 * TILE_SIZE);
let trunk_ivec = trunk_pos.as_ivec3();
// Skip if position already has a tile
if tilemap.fixture_tiles.get(&trunk_ivec).is_some() {
continue;
}
let trunk_entity =
FixtureTilePrefab::log(trunk_pos).spawn(&mut commands);
tree_positions.push(trunk_pos);
// Add sprite component to the same entity if texture exists
if let Some(texture_id) = texture_ids.refs.get(&500004) {
if let Some(texture) = textures.handles.get(texture_id) {
let sprite = Sprite {
image: texture.clone(),
..Default::default()
};
commands
.entity(trunk_entity)
.insert((sprite, VisibleGameEntity));
// .insert(Visibility::Visible); // Override the hidden visibility
}
}
collected_tilemap_updates
.lock()
.unwrap()
.push((trunk_ivec, (1, false, true, [0; 8])));
log_positions.insert(trunk_ivec);
}
// Generate leaves - 3D spherical canopy with random shape
let base_leaf_radius = 2.25;
let leaf_center =
above_pos + Vec3::new(0.0, 0.0, trunk_height as f32 * TILE_SIZE);
for x in -base_leaf_radius as i32..=base_leaf_radius as i32 {
for y in -base_leaf_radius as i32..=base_leaf_radius as i32 {
for z in -base_leaf_radius as i32..=base_leaf_radius as i32 {
let pos = Vec3::new(
x as f32 * TILE_SIZE,
y as f32 * TILE_SIZE,
z as f32 * TILE_SIZE,
) + leaf_center;
let ivec = pos.as_ivec3();
// Skip if position already has a tile or is a log
if tilemap.fixture_tiles.get(&ivec).is_some()
|| log_positions.contains(&ivec)
{
continue;
}
// Apply random radius variation for natural shape
let x_f = x as f32;
let y_f = y as f32;
let z_f = z as f32;
let radius = base_leaf_radius
* (1.0 + (rng.random::<f32>() * 0.35 - 0.1));
if x_f * x_f + y_f * y_f + z_f * z_f <= radius * radius {
FixtureTilePrefab::leaves(pos).spawn(&mut commands);
if let Some(texture_id) = texture_ids.refs.get(&500005)
{
if let Some(texture) =
textures.handles.get(texture_id)
{
let sprite = Sprite {
image: texture.clone(),
..Default::default()
};
let leaf = commands
.spawn((
sprite,
Transform::from_xyz(
pos.x, pos.y, pos.z,
),
))
.id();
commands.entity(leaf).insert(VisibleGameEntity);
collected_tilemap_updates
.lock()
.unwrap()
.push((ivec, (5, false, true, [0; 8])));
}
}
}
}
}
}
}
});
}
_ => {}
}
}
});
let collected_updates = collected_tilemap_updates.into_inner().unwrap();
for (ivec, data) in collected_updates {
tilemap.fixture_tiles.insert(ivec, data);
}
if count > 0 {
println!(
"Forrestry update for {:?} chunks in {:.2?}",
count,
start.elapsed()
);
}
}
+9
View File
@@ -0,0 +1,9 @@
pub mod fauna;
pub mod foliage;
pub mod forestry;
pub mod terrain;
pub use fauna::*;
pub use foliage::*;
pub use forestry::*;
pub use terrain::*;
+174
View File
@@ -0,0 +1,174 @@
use bevy::prelude::*;
use bevy_platform::collections::HashMap;
use bevy_platform::sync::Mutex;
use bevy_platform::time::Instant;
use noise::{NoiseFn, Perlin};
use crate::{
constants::{SEED, TILE_SIZE},
world::{
tiles::TileMap, ChunkForrestryEvent, ChunkTerrainEvent, FloorTilePrefab,
TileOcclusionEvent, CHUNK_SIZE, Z_ABOVE, Z_BELOW,
},
};
pub fn generate_surface_terrain(x: i32, y: i32) -> f32 {
let noise = Perlin::new(SEED);
let mut noise_value = 0.0;
let mut amplitude = 1.0;
let mut frequency = 0.008;
for _ in 0..6 {
noise_value += noise.get([x as f64 * frequency, y as f64 * frequency]) * amplitude;
amplitude *= 0.6;
frequency *= 1.8;
}
(noise_value * 2.5) as f32
}
pub fn generate_chunk_terrain(
commands: ParallelCommands<'_, '_>, // Use ParallelCommands for parallel spawning
mut events: EventReader<ChunkTerrainEvent>,
mut tilemap: ResMut<TileMap>,
mut forrestry_event_writer: EventWriter<ChunkForrestryEvent>,
mut occlusion_event_writer: EventWriter<TileOcclusionEvent>,
) {
let is_empty = events.is_empty();
let start = Instant::now();
let count: usize = events.len();
let cave_noise = Perlin::new(SEED);
// Create mutexes for our shared resources
let tilemap_updates = Mutex::new(HashMap::new());
let forrestry_events = Mutex::new(Vec::new());
events.par_read().for_each(|event| {
let chunk_pos = event.chunk_position;
let start_x = chunk_pos.x * CHUNK_SIZE;
let start_y = chunk_pos.y * CHUNK_SIZE;
let mut surface_positions: Vec<(Vec3, String)> = Vec::new();
let mut local_tilemap_updates: HashMap<IVec3, (i32, bool, bool, bool, i32, [u32; 8])> =
HashMap::new();
// Generate tiles for this chunk
for local_y in 0..CHUNK_SIZE {
for local_x in 0..CHUNK_SIZE {
let world_x = start_x + local_x;
let world_y = start_y + local_y;
let noise_position = Vec3::new(
(world_x as f32 * TILE_SIZE).round(),
(world_y as f32 * TILE_SIZE).round(),
(generate_surface_terrain(world_x, world_y) * TILE_SIZE).round(),
);
// Spawn tiles and add them to tilemap
for z in -Z_BELOW as isize..=Z_ABOVE as isize {
let position = Vec3::new(
(world_x as f32 * TILE_SIZE).round(),
(world_y as f32 * TILE_SIZE).round(),
(z as f32 * TILE_SIZE).round(),
);
let pos_ivec = position.as_ivec3();
if z < -5 {
let cave_value = cave_noise.get([
world_x as f64 * 0.05,
world_y as f64 * 0.05,
z as f64 * 0.05,
]);
if cave_value < -0.75 {
commands.command_scope(|mut cmd| {
FloorTilePrefab::air(position).spawn(&mut cmd);
});
local_tilemap_updates
.insert(pos_ivec, (0, true, false, true, 0, [0; 8]));
// Air tile
} else if cave_value < 0.8 {
commands.command_scope(|mut cmd| {
FloorTilePrefab::rock(position).spawn(&mut cmd);
});
local_tilemap_updates
.insert(pos_ivec, (2, false, true, false, 50, [0; 8]));
// Rock tile
} else {
commands.command_scope(|mut cmd| {
FloorTilePrefab::dirt(position).spawn(&mut cmd);
});
local_tilemap_updates
.insert(pos_ivec, (1, false, true, false, 85, [0; 8]));
// Dirt tile
}
} else if noise_position.z > position.z {
if (generate_surface_terrain(world_x, world_y) * TILE_SIZE).round()
<= position.z + TILE_SIZE
{
commands.command_scope(|mut cmd| {
FloorTilePrefab::grass(position).spawn(&mut cmd);
});
local_tilemap_updates
.insert(pos_ivec, (1, false, true, false, 100, [0; 8])); // Dirt tile (grass)
surface_positions.push((position, ("grass").to_string()));
} else {
commands.command_scope(|mut cmd| {
FloorTilePrefab::dirt(position).spawn(&mut cmd);
});
local_tilemap_updates
.insert(pos_ivec, (1, false, true, false, 85, [0; 8]));
// Dirt tile
}
} else {
commands.command_scope(|mut cmd| {
FloorTilePrefab::air(position).spawn(&mut cmd);
});
local_tilemap_updates.insert(pos_ivec, (0, true, false, true, 0, [0; 8]));
// Air tile
}
}
}
}
// Add our local updates to the global mutexes
{
let mut tilemap_guard = tilemap_updates.lock().unwrap();
for (pos, data) in local_tilemap_updates {
tilemap_guard.insert(pos, data);
}
}
// Store forrestry event for this chunk
forrestry_events.lock().unwrap().push(ChunkForrestryEvent {
chunk_position: chunk_pos,
floor_tiles: surface_positions,
});
});
for (pos, data) in tilemap_updates.into_inner().unwrap() {
tilemap.floor_tiles.insert(pos, data);
occlusion_event_writer.write(TileOcclusionEvent { tile_position: pos });
}
// Send all forrestry events
for event in forrestry_events.into_inner().unwrap() {
forrestry_event_writer.write(event);
}
if !is_empty {
println!("{} terrain chunks loaded in {:.2?}", count, start.elapsed());
}
}
pub fn generate_chunk_weathering_and_precipitation(// mut commands: Commands,
// mut events: EventReader<GenerateChunkEvent>,
// mut chunk_map: ResMut<ChunkMap>,
// mut tilemap: ResMut<TileMap>,
) {
// TODO: Generate weathering and precipitation
// Temperature and humidity
// Erosion
// Hadley lines/cells etc
// Biomes
}
+78
View File
@@ -0,0 +1,78 @@
use crate::{
camera,
world::generation::{
generate_chunk_fauna, generate_chunk_foliage, generate_chunk_forrestry,
generate_chunk_terrain, generate_chunk_weathering_and_precipitation,
},
};
use bevy::prelude::*;
pub mod chunks;
pub mod generation;
pub mod textures;
pub mod tiles;
// Re-export commonly used items
pub use chunks::management::*;
pub use textures::management::*;
pub use tiles::{prefabs::*, rendering::*, visibility::*};
// Plugin
pub struct WorldPlugin;
impl Plugin for WorldPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<ChunkMap>()
.add_event::<GenerateChunkEvent>()
.add_event::<ChunkTerrainEvent>()
.add_event::<ChunkWeatheringAndPrecipitationEvent>()
.add_event::<ChunkForrestryEvent>()
.add_event::<ChunkFoliageEvent>()
.add_event::<ChunkFaunaEvent>()
.add_event::<TileOcclusionEvent>()
.add_systems(Startup, (setup_chunk_system, setup_initial_chunks).chain())
.add_systems(
FixedUpdate,
(
(
handle_chunk_events,
generate_chunk_terrain,
generate_chunk_weathering_and_precipitation,
generate_chunk_forrestry,
generate_chunk_foliage,
generate_chunk_fauna,
)
.chain(),
chunkmap_despawn_timer_system,
),
)
.add_systems(
Update,
(
compute_visibility_of_game_entities,
(
handle_tile_occlusion_updates,
build_quilted_terrain_sprites,
update_tile_visibility.run_if(
|cwss: Res<tiles::CurrentWorldSpriteState>,
camera_moved: Res<camera::CameraMoved>| {
cwss.state == tiles::TerrainSpriteState::RenderReady
|| camera_moved.0
},
),
)
.chain(),
),
);
}
}
fn setup_initial_chunks(mut event_writer: EventWriter<GenerateChunkEvent>) {
for x in -5..=5 {
for y in -5..=5 {
event_writer.write(GenerateChunkEvent {
chunk_position: IVec2::new(x, y),
});
}
}
}
+85
View File
@@ -0,0 +1,85 @@
use bevy::prelude::*;
use bevy_platform::collections::HashMap;
use bevy_platform::time::Instant;
pub const FLOOR_ID_OFFSET: u32 = 0;
pub const FIXTURE_ID_OFFSET: u32 = 500000;
pub const DEFAULT_TEXTURE: &str = "default.png";
#[derive(Resource)]
pub struct Textures {
pub handles: HashMap<String, Handle<Image>>,
}
#[derive(Resource)]
pub struct TextureIDs {
pub refs: HashMap<u32, String>,
}
const GRASS_FLOOR_PATH: &str = "grass_floor.png";
const DIRT_FLOOR_PATH: &str = "dirt_floor.png";
const ROCK_FLOOR_PATH: &str = "rock_floor.png";
const BEDROCK_FLOOR_PATH: &str = "bedrock_floor.png";
const SKY_PATH: &str = "sky.png";
const DIRT_WALL_PATH: &str = "dirt_wall.png";
const ROCK_WALL_PATH: &str = "rock_wall.png";
const BEDROCK_WALL_PATH: &str = "bedrock_wall.png";
const LOG_PATH: &str = "log.png";
const LEAVES_PATH: &str = "leaves.png";
pub fn initialize_textures(mut commands: Commands, asset_server: Res<AssetServer>) {
println!("");
println!("####################");
let start = Instant::now();
let mut textures: HashMap<String, Handle<Image>> = HashMap::new();
let mut texture_ids: HashMap<u32, String> = HashMap::new();
texture_ids.insert(u32::MAX, DEFAULT_TEXTURE.to_string());
texture_ids.insert(FLOOR_ID_OFFSET, SKY_PATH.to_string());
textures.insert(SKY_PATH.to_string(), asset_server.load(SKY_PATH));
texture_ids.insert(FLOOR_ID_OFFSET + 1, GRASS_FLOOR_PATH.to_string());
textures.insert(
GRASS_FLOOR_PATH.to_string(),
asset_server.load(GRASS_FLOOR_PATH),
);
texture_ids.insert(FLOOR_ID_OFFSET + 2, DIRT_FLOOR_PATH.to_string());
textures.insert(
DIRT_FLOOR_PATH.to_string(),
asset_server.load(DIRT_FLOOR_PATH),
);
texture_ids.insert(FLOOR_ID_OFFSET + 3, ROCK_FLOOR_PATH.to_string());
textures.insert(
ROCK_FLOOR_PATH.to_string(),
asset_server.load(ROCK_FLOOR_PATH),
);
texture_ids.insert(FLOOR_ID_OFFSET + 4, BEDROCK_FLOOR_PATH.to_string());
textures.insert(
BEDROCK_FLOOR_PATH.to_string(),
asset_server.load(BEDROCK_FLOOR_PATH),
);
texture_ids.insert(FIXTURE_ID_OFFSET + 1, DIRT_WALL_PATH.to_string());
textures.insert(
DIRT_WALL_PATH.to_string(),
asset_server.load(DIRT_WALL_PATH),
);
texture_ids.insert(FIXTURE_ID_OFFSET + 2, ROCK_WALL_PATH.to_string());
textures.insert(
ROCK_WALL_PATH.to_string(),
asset_server.load(ROCK_WALL_PATH),
);
texture_ids.insert(FIXTURE_ID_OFFSET + 3, BEDROCK_WALL_PATH.to_string());
textures.insert(
BEDROCK_WALL_PATH.to_string(),
asset_server.load(BEDROCK_WALL_PATH),
);
texture_ids.insert(FIXTURE_ID_OFFSET + 4, LOG_PATH.to_string());
textures.insert(LOG_PATH.to_string(), asset_server.load(LOG_PATH));
texture_ids.insert(FIXTURE_ID_OFFSET + 5, LEAVES_PATH.to_string());
textures.insert(LEAVES_PATH.to_string(), asset_server.load(LEAVES_PATH));
commands.insert_resource(Textures { handles: textures });
commands.insert_resource(TextureIDs { refs: texture_ids });
println!("Textures initialized in {:.2?}", start.elapsed());
}
+3
View File
@@ -0,0 +1,3 @@
pub mod management;
pub use management::*;
+44
View File
@@ -0,0 +1,44 @@
use bevy::prelude::*;
#[derive(Component, Clone)]
pub struct FloorTile {
pub id: u32,
pub opaque: bool,
pub walkable: bool,
pub astar_weight: u8,
pub visible_range: [u32; 8],
}
impl Default for FloorTile {
fn default() -> Self {
Self {
id: 0,
opaque: true,
walkable: true,
astar_weight: 0,
visible_range: [0; 8],
}
}
}
#[derive(Component, Clone)]
pub struct FixtureTile {
pub id: u32,
pub solid: bool,
pub visible_range: [u32; 8],
}
impl Default for FixtureTile {
fn default() -> Self {
Self {
id: 0,
solid: true,
visible_range: [0; 8],
}
}
}
#[derive(Component)]
pub struct TileState {
pub timer: Timer,
}
+11
View File
@@ -0,0 +1,11 @@
pub mod components;
pub mod prefabs;
pub mod rendering;
pub mod tilemap;
pub mod visibility;
pub use components::*;
pub use prefabs::*;
pub use rendering::*;
pub use tilemap::*;
pub use visibility::*;
+166
View File
@@ -0,0 +1,166 @@
use bevy::prelude::*;
use crate::world::{
tiles::{FixtureTile, FloorTile, TileState},
FIXTURE_ID_OFFSET,
};
#[derive(Bundle)]
pub struct FloorTilePrefab {
transform: Transform,
tile: FloorTile,
tile_state: TileState,
visibility: Visibility,
}
impl FloorTilePrefab {
pub fn grass(position: Vec3) -> Self {
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: 1,
astar_weight: 100,
..Default::default()
},
tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
},
visibility: Visibility::Hidden,
}
}
pub fn dirt(position: Vec3) -> Self {
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: 2,
astar_weight: 85,
..Default::default()
},
tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
},
visibility: Visibility::Hidden,
}
}
pub fn rock(position: Vec3) -> Self {
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: 3,
astar_weight: 50,
..Default::default()
},
tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
},
visibility: Visibility::Hidden,
}
}
pub fn air(position: Vec3) -> Self {
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: 0,
opaque: false,
walkable: false,
..Default::default()
},
tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
},
visibility: Visibility::Hidden,
}
}
pub fn bedrock(position: Vec3) -> Self {
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: 4,
astar_weight: 150,
..Default::default()
},
tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
},
visibility: Visibility::Hidden,
}
}
pub fn spawn(self, commands: &mut Commands) {
commands.spawn((self.tile, self.transform, self.tile_state, self.visibility));
}
}
#[derive(Bundle)]
pub struct FixtureTilePrefab {
transform: Transform,
tile: FixtureTile,
visibility: Visibility,
}
impl FixtureTilePrefab {
pub fn dirt_wall(position: Vec3) -> Self {
FixtureTilePrefab {
transform: Transform::from_translation(position),
tile: FixtureTile {
id: FIXTURE_ID_OFFSET + 1,
..Default::default()
},
visibility: Visibility::Hidden,
}
}
pub fn rock_wall(position: Vec3) -> Self {
FixtureTilePrefab {
transform: Transform::from_translation(position),
tile: FixtureTile {
id: FIXTURE_ID_OFFSET + 2,
..Default::default()
},
visibility: Visibility::Hidden,
}
}
pub fn log(position: Vec3) -> Self {
FixtureTilePrefab {
transform: Transform::from_translation(position),
tile: FixtureTile {
id: FIXTURE_ID_OFFSET + 4,
..Default::default()
},
visibility: Visibility::Hidden,
}
}
pub fn leaves(position: Vec3) -> Self {
FixtureTilePrefab {
transform: Transform::from_translation(position),
tile: FixtureTile {
id: FIXTURE_ID_OFFSET + 5,
..Default::default()
},
visibility: Visibility::Hidden,
}
}
pub fn bedrock_wall(position: Vec3) -> Self {
FixtureTilePrefab {
transform: Transform::from_translation(position),
tile: FixtureTile {
id: 0,
..Default::default()
},
visibility: Visibility::Hidden,
}
}
pub fn spawn(self, commands: &mut Commands) -> Entity {
commands
.spawn((self.tile, self.transform, self.visibility))
.id()
}
}
+302
View File
@@ -0,0 +1,302 @@
use bevy::{asset::RenderAssetUsages, prelude::*, render::render_resource};
use bevy_platform::collections::HashMap;
use bevy_platform::sync::Mutex;
use bevy_platform::time::Instant;
use rayon::prelude::*;
use crate::{
constants::{PIXEL_RATIO, TILE_PIXELS, TILE_SIZE},
world::{tiles::FloorTile, TextureIDs, Textures, Z_BELOW, Z_TOTAL},
};
#[derive(Debug, PartialEq)]
pub enum TerrainSpriteState {
Inactive,
WaitingForRender,
InProgress,
RenderReady,
}
#[derive(Resource)]
pub struct CurrentWorldSpriteState {
pub state: TerrainSpriteState,
}
#[derive(Component)]
pub struct TerrainSprite {
pub z_index: usize, // Store the z-index this sprite belongs to (as world_Z)
}
#[derive(Resource)]
pub struct QuiltCache {
pub dimensions: HashMap<usize, (u32, u32)>,
pub dirty_indices: Vec<usize>,
}
impl Default for QuiltCache {
fn default() -> Self {
Self {
dimensions: HashMap::new(),
dirty_indices: Vec::new(),
}
}
}
// here be dragons :(
pub fn build_quilted_terrain_sprites(
query_tiles: Query<(&FloorTile, &Transform)>,
commands: ParallelCommands<'_, '_>,
mut cwss: ResMut<CurrentWorldSpriteState>,
textures: Res<Textures>,
texture_ids: Res<TextureIDs>,
query_terrain_sprites: Query<Entity, With<TerrainSprite>>,
mut images: ResMut<Assets<Image>>,
mut quilt_cache: ResMut<QuiltCache>,
) {
if cwss.state != TerrainSpriteState::WaitingForRender {
return;
}
let now = Instant::now();
cwss.state = TerrainSpriteState::InProgress;
// Despawn existing terrain sprites
let despawn_entities: Vec<Entity> = query_terrain_sprites.iter().collect();
for entity in despawn_entities {
commands.command_scope(|mut cmd| {
cmd.entity(entity).despawn();
});
}
// Collect tiles by z-index first
let mut tiles_by_z: HashMap<usize, Vec<(Vec2, &FloorTile)>> = HashMap::new();
// Calculate bounds for all visible tiles
for (floortile, transform) in query_tiles.iter() {
let position = Vec2::new(transform.translation.x, transform.translation.y);
for z_index in 0..=Z_TOTAL as usize {
if (floortile.visible_range[z_index / 32] & (1 << (z_index % 32) as u32)) != 0 {
tiles_by_z
.entry(z_index)
.or_default()
.push((position, floortile));
}
}
}
// Thread-safe collections to store results
let dimensions_mutex = Mutex::new(HashMap::new());
let texture_handles_mutex = Mutex::new(HashMap::new());
// Process each z-level in parallel
let z_indices: Vec<usize> = tiles_by_z.keys().cloned().collect();
z_indices.into_par_iter().for_each(|z_index| {
let tiles = if let Some(tiles) = tiles_by_z.get(&z_index) {
tiles
} else {
return; // Skip empty z-levels
};
if tiles.is_empty() {
return;
}
// Calculate bounds
let min_x_aligned: f32 = ((tiles.iter().map(|(pos, _)| pos.x).reduce(f32::min).unwrap()
- TILE_SIZE / 2.0)
/ TILE_SIZE)
.floor()
* TILE_SIZE;
let min_y_aligned: f32 = ((tiles.iter().map(|(pos, _)| pos.y).reduce(f32::min).unwrap()
- TILE_SIZE / 2.0)
/ TILE_SIZE)
.floor()
* TILE_SIZE;
let max_x_aligned: f32 = ((tiles.iter().map(|(pos, _)| pos.x).reduce(f32::max).unwrap()
+ TILE_SIZE / 2.0)
/ TILE_SIZE)
.ceil()
* TILE_SIZE;
let max_y_aligned: f32 = ((tiles.iter().map(|(pos, _)| pos.y).reduce(f32::max).unwrap()
+ TILE_SIZE / 2.0)
/ TILE_SIZE)
.ceil()
* TILE_SIZE;
// Calculate terrain texture dimensions in pixels
let width_tiles = ((max_x_aligned - min_x_aligned) / TILE_SIZE) as u32;
let height_tiles = ((max_y_aligned - min_y_aligned) / TILE_SIZE) as u32;
let width_px = width_tiles * TILE_PIXELS;
let height_px = height_tiles * TILE_PIXELS;
// Store dimensions in our thread-safe map
dimensions_mutex
.lock()
.unwrap()
.insert(z_index, (width_px, height_px));
let mut texture_data = vec![0u8; (width_px * height_px * 4) as usize];
// Process tiles for this z-level
// We can't parallelize this inner loop without more complex locking on texture_data
for (pos, floortile) in tiles {
if let Some(texture_id) = texture_ids.refs.get(&floortile.id) {
if let Some(texture) = textures.handles.get(texture_id) {
let rel_x = pos.x - min_x_aligned;
let rel_y = pos.y - min_y_aligned;
let tile_x = (rel_x / TILE_SIZE).round() as u32;
let tile_y = (height_tiles as f32 - 1.0 - (rel_y / TILE_SIZE).round()) as u32;
let target_x = tile_x * TILE_PIXELS;
let target_y = tile_y * TILE_PIXELS;
let mut data: Vec<&[u8]> = vec![];
let mut base_texture: &Image = &Default::default();
// Use a thread-safe approach to access images
// In a full implementation, this would require a more sophisticated
// thread-safe access pattern to Assets<Image>
if let Some(_base_texture) = images.get(texture) {
if let Some(_data) = &_base_texture.data {
data.push(_data);
base_texture = _base_texture;
}
}
blit_texture_with_alpha(
data, // tile
&mut texture_data, // terrain
base_texture.size().x as u32,
base_texture.size().y as u32,
width_px,
height_px,
target_x,
target_y,
);
}
}
}
// Create the quilted texture
let quilted_texture = Image::new_fill(
render_resource::Extent3d {
width: width_px,
height: height_px,
depth_or_array_layers: 1,
},
render_resource::TextureDimension::D2,
&texture_data,
render_resource::TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::RENDER_WORLD,
);
// In a real implementation, we would need thread-safe access to images
// For now, we'll collect the textures and add them after parallel processing
let center_x = min_x_aligned + (max_x_aligned - min_x_aligned) / 2.0;
let center_y = min_y_aligned + (max_y_aligned - min_y_aligned) / 2.0;
// Store texture and position data for later spawning
texture_handles_mutex
.lock()
.unwrap()
.insert(z_index, (quilted_texture, center_x, center_y));
});
// Process collected textures and spawn entities
let collected_dimensions = dimensions_mutex.into_inner().unwrap();
let collected_textures = texture_handles_mutex.into_inner().unwrap();
// Update quilt_cache dimensions
for (z_index, dimensions) in collected_dimensions {
quilt_cache.dimensions.insert(z_index, dimensions);
}
// Add images and spawn entities with the collected data
for (z_index, (quilted_texture, center_x, center_y)) in collected_textures {
let texture_handle = images.add(quilted_texture);
commands.command_scope(|mut cmd| {
cmd.spawn((
Sprite {
image: texture_handle,
..Default::default()
},
Transform::from_xyz(
center_x - TILE_SIZE / 2.0,
center_y - TILE_SIZE / 2.0,
-Z_BELOW * TILE_SIZE,
)
.with_scale(Vec3::splat(PIXEL_RATIO)),
Visibility::Hidden,
TerrainSprite { z_index },
));
});
}
quilt_cache.dirty_indices.clear();
cwss.state = TerrainSpriteState::RenderReady;
println!("Terrain sprites baked in: {:.2?}", now.elapsed());
}
// Helper function to blit a texture onto another texture
fn blit_texture_with_alpha(
sources: Vec<&[u8]>,
target: &mut [u8],
source_width: u32,
source_height: u32,
target_width: u32,
target_height: u32,
offset_x: u32,
offset_y: u32,
) {
for (_, source_data) in sources.iter().rev().enumerate() {
for y in 0..source_height {
if y + offset_y >= target_height {
continue;
}
for x in 0..source_width {
if x + offset_x >= target_width {
continue;
}
let source_pixel_idx = ((y * source_width) + x) as usize * 4;
let target_pixel_idx =
(((y + offset_y) * target_width) + (x + offset_x)) as usize * 4;
let src_a = source_data[source_pixel_idx + 3];
if src_a == 0 {
continue;
}
let src_r = source_data[source_pixel_idx];
let src_g = source_data[source_pixel_idx + 1];
let src_b = source_data[source_pixel_idx + 2];
if src_a == 255 {
target[target_pixel_idx] = src_r;
target[target_pixel_idx + 1] = src_g;
target[target_pixel_idx + 2] = src_b;
target[target_pixel_idx + 3] = 255;
} else {
let dst_r = target[target_pixel_idx];
let dst_g = target[target_pixel_idx + 1];
let dst_b = target[target_pixel_idx + 2];
let alpha_factor = src_a as f32 / 255.0;
let inv_alpha = 1.0 - alpha_factor;
target[target_pixel_idx] =
(src_r as f32 * alpha_factor + dst_r as f32 * inv_alpha) as u8;
target[target_pixel_idx + 1] =
(src_g as f32 * alpha_factor + dst_g as f32 * inv_alpha) as u8;
target[target_pixel_idx + 2] =
(src_b as f32 * alpha_factor + dst_b as f32 * inv_alpha) as u8;
target[target_pixel_idx + 3] = 255;
}
}
}
}
}
+8
View File
@@ -0,0 +1,8 @@
use bevy::prelude::*;
use bevy_platform::collections::hash_map::HashMap;
#[derive(Resource, Default, Clone)]
pub struct TileMap {
pub floor_tiles: HashMap<IVec3, (i32, bool, bool, bool, i32, [u32; 8])>, //id, canStandIn, canStandOn, visiblyTransparent, astar_weight, visible_range
pub fixture_tiles: HashMap<IVec3, (i32, bool, bool, [u32; 8])>, // id, canStandIn, canStandOn, visible_range
}
+181
View File
@@ -0,0 +1,181 @@
use crate::{
constants::{ITILE_SIZE, TILE_SIZE},
game,
world::{
chunks::{Z_ABOVE, Z_BELOW, Z_TOTAL},
tiles::{CurrentWorldSpriteState, FloorTile, TerrainSprite, TerrainSpriteState, TileMap},
},
};
use bevy::prelude::*;
use bevy_platform::collections::HashMap;
use bevy_platform::time::Instant;
#[derive(Component)]
pub struct VisibleGameEntity;
pub fn compute_visibility_of_game_entities(
mut query: Query<(&Transform, &mut Visibility, &mut Sprite), With<VisibleGameEntity>>,
z_index: Res<game::ZIndex>,
) {
query
.par_iter_mut()
.for_each(|(transform, mut visibility, mut sprite)| {
let entity_z = (transform.translation.z / TILE_SIZE) - 1.0;
// if same z-level, always visible
if z_index.0 == entity_z {
sprite.color = Color::WHITE;
sprite.color.set_alpha(1.0);
*visibility = Visibility::Visible;
return;
};
// if entity too low, saturation does not matter
if z_index.0 - entity_z > 8.0 {
*visibility = Visibility::Hidden;
return;
}
// if entity too high, saturation does not matter
if entity_z > z_index.0 {
*visibility = Visibility::Hidden;
return;
}
// if visible, calculate saturation
*visibility = Visibility::Visible;
let saturation =
((z_index.0 - (transform.translation.z / TILE_SIZE) + 1.) / 8.0).clamp(0.0, 1.0);
sprite.color = Color::hsv(194.7, saturation, 1.0 - (saturation / 2.0));
sprite.color.set_alpha(1.0 - saturation);
});
}
#[derive(Event)]
pub struct TileOcclusionEvent {
pub tile_position: IVec3,
}
pub fn handle_tile_occlusion_updates(
mut tilemap: ResMut<TileMap>,
mut floor_tiles: Query<(Entity, &mut FloorTile, &Transform)>,
mut cwss: ResMut<CurrentWorldSpriteState>,
mut events: EventReader<TileOcclusionEvent>,
) {
let start = Instant::now();
let count = events.len();
// Process events in parallel
let updates: Vec<(IVec3, [u32; 8])> = events
.par_read()
.into_iter()
.map(|event| {
let pos = event.0.tile_position;
let visibility = calculate_visibility(pos, &tilemap);
(pos, visibility)
})
.collect();
// Map updates for efficient lookup
let update_map: HashMap<IVec3, [u32; 8]> = updates.into_iter().collect();
// Update only the relevant FloorTile components
for (_, mut tile, pos) in floor_tiles.iter_mut() {
if let Some(visibility) = update_map.get(&pos.translation.as_ivec3()) {
tile.visible_range = *visibility;
if let Some(tile_data) = tilemap.floor_tiles.get_mut(&pos.translation.as_ivec3()) {
tile_data.5 = *visibility;
}
}
}
if count > 0 {
cwss.state = TerrainSpriteState::WaitingForRender;
println!(
"Tile occlusion calculated for {} tiles in {:.2?}",
count,
start.elapsed()
);
}
}
pub fn calculate_visibility(pos: IVec3, tilemap: &TileMap) -> [u32; 8] {
let mut visible_range = [0u32; 8];
for mut camera_z in -Z_BELOW as i32..=Z_ABOVE as i32 {
camera_z *= ITILE_SIZE;
let mut is_visible = false;
if pos.z > camera_z {
continue;
}
let mut is_occluded = false;
let v_check_height: i32 = Z_TOTAL as i32 - (camera_z / ITILE_SIZE) + Z_BELOW as i32 + 1;
'vertical_check: for z_offset in 1..v_check_height {
let above_pos = IVec3::new(pos.x, pos.y, pos.z + (z_offset * ITILE_SIZE));
if above_pos.z <= camera_z {
if let Some(&(_, _, _, visibly_transparent, _, _)) =
tilemap.floor_tiles.get(&above_pos)
{
if !visibly_transparent {
is_occluded = true;
break 'vertical_check;
}
}
} else {
break 'vertical_check;
}
}
if !is_occluded {
'neighbor_check: for x_offset in -1..=1 {
for y_offset in -1..=1 {
for z_offset in 0..=1 {
if x_offset == 0 && y_offset == 0 && z_offset == 0 {
continue;
}
let neighbor_pos = IVec3::new(
pos.x + x_offset * ITILE_SIZE,
pos.y + y_offset * ITILE_SIZE,
pos.z + z_offset * ITILE_SIZE,
);
if let Some(&(id, _, _, _, _, _)) = tilemap.floor_tiles.get(&neighbor_pos) {
if id == 0 {
// id 0 = air tile
is_visible = true;
break 'neighbor_check;
}
}
}
}
}
}
if is_visible {
let z2 = ((camera_z / ITILE_SIZE) + Z_BELOW as i32) as usize;
visible_range[z2 / 32] |= 1 << ((z2 % 32) as u32);
}
}
visible_range
}
pub fn update_tile_visibility(
z_index: Res<game::ZIndex>,
mut query: Query<(&TerrainSprite, &mut Visibility)>,
mut cwss: ResMut<CurrentWorldSpriteState>,
) {
let now = Instant::now();
for (terrain_sprite, mut visibility) in query.iter_mut() {
*visibility = if terrain_sprite.z_index == ((z_index.0 + Z_BELOW) as usize) {
Visibility::Visible
} else {
Visibility::Hidden
};
}
cwss.state = TerrainSpriteState::Inactive;
println!("Visibility update: {:.2?}", now.elapsed());
}