perf: SmallVec inline storage, TOML drop tables, seeded deterministic RNG
- SmallVec<[DropEntry; 2]> replaces Vec in DropTable: zero heap allocation for
all current tiles (0 or 1 entries), spills to heap only at 3+
- DropEntry fields packed to u8 (chance_pct, min_count, max_count): ~12 bytes ->
4 bytes; derives Copy so no extra clones
- Replaced SystemTime pseudo-RNG with dig_rng(pos): PCG-style hash of world SEED
+ tile position. Deterministic per world, same dig = same roll every time
- TOML-driven drop tables: assets/drop_tables.toml (grass=5%/1-2, rock=10%/1,
dirt/air=none). TOML parsed at startup into DropTableRegistry resource
- OnceLock global map for async terrain generation tasks to access drop tables
without Bevy resource borrowing
- terrain.rs: DropTableRegistry::global_get("tile") replaces hardcoded drop_table_for
This commit is contained in:
@@ -1,46 +1,50 @@
|
||||
use crate::constants::SEED;
|
||||
use crate::entities::item::prefabs::misc::misc_prefabs::MiscPrefab;
|
||||
use bevy::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use smallvec::SmallVec;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
static DROP_TABLE_GLOBAL: OnceLock<HashMap<String, DropTable>> = OnceLock::new();
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct DropEntry {
|
||||
pub prefab: MiscPrefab,
|
||||
pub chance: f32,
|
||||
pub min_count: u32,
|
||||
pub max_count: u32,
|
||||
pub chance_pct: u8,
|
||||
pub min_count: u8,
|
||||
pub max_count: u8,
|
||||
}
|
||||
|
||||
impl DropEntry {
|
||||
pub fn always(prefab: MiscPrefab) -> Self {
|
||||
Self {
|
||||
prefab,
|
||||
chance: 1.0,
|
||||
chance_pct: 100,
|
||||
min_count: 1,
|
||||
max_count: 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn chance(prefab: MiscPrefab, chance: f32, min_count: u32, max_count: u32) -> Self {
|
||||
pub fn chance(prefab: MiscPrefab, chance_pct: u8, min_count: u8, max_count: u8) -> Self {
|
||||
Self {
|
||||
prefab,
|
||||
chance,
|
||||
chance_pct,
|
||||
min_count,
|
||||
max_count,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn roll(&self, pos: IVec3) -> u32 {
|
||||
if self.chance < 1.0 {
|
||||
let r = pseudo_rand_f32(pos);
|
||||
if r >= self.chance {
|
||||
return 0;
|
||||
}
|
||||
pub fn roll(&self, rng_val: u32) -> u8 {
|
||||
if self.chance_pct < 100 && ((rng_val % 100) as u8) >= self.chance_pct {
|
||||
return 0;
|
||||
}
|
||||
pseudo_rand_u32(pos) % (self.max_count - self.min_count + 1) + self.min_count
|
||||
self.min_count + (((rng_val >> 8) % ((self.max_count - self.min_count + 1) as u32)) as u8)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct DropTable(pub Vec<DropEntry>);
|
||||
pub struct DropTable(pub SmallVec<[DropEntry; 2]>);
|
||||
|
||||
impl DropTable {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
@@ -48,26 +52,79 @@ impl DropTable {
|
||||
}
|
||||
}
|
||||
|
||||
fn hash3(pos: IVec3) -> u32 {
|
||||
let mut h: u32 = 0;
|
||||
h = h.wrapping_mul(374761393).wrapping_add(pos.x as u32);
|
||||
h = h.wrapping_mul(374761393).wrapping_add(pos.y as u32);
|
||||
h = h.wrapping_mul(374761393).wrapping_add(pos.z as u32);
|
||||
h ^= h >> 13;
|
||||
h = h.wrapping_mul(1274126177);
|
||||
h ^= h >> 16;
|
||||
h
|
||||
pub fn dig_rng(pos: IVec3) -> u32 {
|
||||
let mut h = (SEED as u64)
|
||||
.wrapping_add(pos.x as u64)
|
||||
.wrapping_mul(0x9e3779b97f4a7c15)
|
||||
^ (pos.y as u64).wrapping_mul(0x6c62272e07bb0142)
|
||||
^ (pos.z as u64).wrapping_mul(0x94d049bb133111eb);
|
||||
let h32 = (h ^ (h >> 32)) as u32;
|
||||
h32 ^ (h32 >> 16)
|
||||
}
|
||||
|
||||
fn pseudo_rand_u32(pos: IVec3) -> u32 {
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos() as u32;
|
||||
hash3(pos) ^ timestamp
|
||||
#[derive(Deserialize)]
|
||||
struct DropEntryToml {
|
||||
pub prefab: String,
|
||||
pub chance_pct: u8,
|
||||
pub min_count: u8,
|
||||
#[serde(rename = "max_max")]
|
||||
pub max_count: u8,
|
||||
}
|
||||
|
||||
fn pseudo_rand_f32(pos: IVec3) -> f32 {
|
||||
let val = pseudo_rand_u32(pos);
|
||||
(val as f32) / (u32::MAX as f32)
|
||||
#[derive(Deserialize)]
|
||||
struct DropTableToml {
|
||||
#[serde(default)]
|
||||
pub drops: Vec<DropEntryToml>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DropTablesFile {
|
||||
#[serde(flatten)]
|
||||
pub tiles: HashMap<String, DropTableToml>,
|
||||
}
|
||||
|
||||
fn prefab_from_str(s: &str) -> MiscPrefab {
|
||||
match s {
|
||||
"Coin" => MiscPrefab::Coin,
|
||||
"RawMeat" => MiscPrefab::RawMeat,
|
||||
other => panic!("Unknown prefab in drop_tables.toml: \"{}\"", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct DropTableRegistry(pub HashMap<String, DropTable>);
|
||||
|
||||
impl DropTableRegistry {
|
||||
pub fn load() -> Self {
|
||||
let src = std::fs::read_to_string("assets/drop_tables.toml")
|
||||
.expect("assets/drop_tables.toml not found");
|
||||
let file: DropTablesFile =
|
||||
toml::from_str(&src).expect("Failed to parse assets/drop_tables.toml");
|
||||
let mut map = HashMap::new();
|
||||
for (tile_name, table_toml) in file.tiles {
|
||||
let entries: SmallVec<[DropEntry; 2]> = table_toml
|
||||
.drops
|
||||
.iter()
|
||||
.map(|e| DropEntry {
|
||||
prefab: prefab_from_str(&e.prefab),
|
||||
chance_pct: e.chance_pct,
|
||||
min_count: e.min_count,
|
||||
max_count: e.max_count,
|
||||
})
|
||||
.collect();
|
||||
map.insert(tile_name, DropTable(entries));
|
||||
}
|
||||
Self(map)
|
||||
}
|
||||
|
||||
pub fn init_global(registry: &DropTableRegistry) {
|
||||
DROP_TABLE_GLOBAL.get_or_init(|| registry.0.clone());
|
||||
}
|
||||
|
||||
pub fn global_get(tile_name: &str) -> DropTable {
|
||||
DROP_TABLE_GLOBAL
|
||||
.get()
|
||||
.and_then(|m| m.get(tile_name).cloned())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::constants::ITILE_SIZE;
|
||||
use crate::entities::item::drop_table::dig_rng;
|
||||
use crate::entities::item::prefabs::misc::misc_prefabs::spawn_prefab;
|
||||
use crate::world::chunks::{Z_ABOVE, Z_BELOW};
|
||||
use crate::world::tiles::tile_changed::TileChangedEvent;
|
||||
use crate::world::tiles::visibility::TileOcclusionEvent;
|
||||
use crate::world::tiles::TileMap;
|
||||
use bevy::prelude::*;
|
||||
use rand::RngExt;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Digger {
|
||||
@@ -52,6 +52,8 @@ pub fn dig_system(
|
||||
continue;
|
||||
}
|
||||
|
||||
let rng_val = dig_rng(below_pos);
|
||||
|
||||
let drop_table = tilemap
|
||||
.floor_tiles
|
||||
.get(&below_pos)
|
||||
@@ -60,12 +62,12 @@ pub fn dig_system(
|
||||
if let Some(_removed) = tilemap.dig_floor(&below_pos) {
|
||||
if let Some(table) = drop_table {
|
||||
for entry in table.0 {
|
||||
let count = entry.roll(below_pos);
|
||||
for _ in 0..count {
|
||||
let count = entry.roll(rng_val);
|
||||
for _ in 0..u32::from(count) {
|
||||
spawn_prefab(
|
||||
&mut commands,
|
||||
&asset_server,
|
||||
entry.prefab.clone(),
|
||||
entry.prefab,
|
||||
below_pos.as_vec3(),
|
||||
&mut tilemap,
|
||||
);
|
||||
|
||||
@@ -4,8 +4,7 @@ use bevy_platform::time::Instant;
|
||||
use noise::{NoiseFn, Perlin};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::entities::item::drop_table::{DropEntry, DropTable};
|
||||
use crate::entities::item::prefabs::misc::misc_prefabs::MiscPrefab;
|
||||
use crate::entities::item::drop_table::DropTableRegistry;
|
||||
|
||||
use crate::{
|
||||
config::TileRegistry,
|
||||
@@ -17,19 +16,6 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
fn drop_table_for(tile_name: &str) -> DropTable {
|
||||
match tile_name {
|
||||
"grass" => DropTable(vec![
|
||||
DropEntry::chance(MiscPrefab::Coin, 0.05, 1, 2),
|
||||
]),
|
||||
"dirt" => DropTable::default(),
|
||||
"rock" => DropTable(vec![
|
||||
DropEntry::chance(MiscPrefab::Coin, 0.1, 1, 1),
|
||||
]),
|
||||
_ => DropTable::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread-safe storage for completed terrain blobs.
|
||||
/// Uses type erasure to avoid Debug bounds on TerrainBlob.
|
||||
type BlobStorage = Arc<Mutex<Box<dyn Send + Sync>>>;
|
||||
@@ -130,7 +116,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
||||
tile.transparent,
|
||||
tile.astar_weight,
|
||||
[0; 8],
|
||||
drop_table_for("air"),
|
||||
DropTableRegistry::global_get("air"),
|
||||
),
|
||||
));
|
||||
chunk_data.set_floor_tile(
|
||||
@@ -153,7 +139,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
||||
tile.transparent,
|
||||
tile.astar_weight,
|
||||
[0; 8],
|
||||
drop_table_for("rock"),
|
||||
DropTableRegistry::global_get("rock"),
|
||||
),
|
||||
));
|
||||
chunk_data.set_floor_tile(
|
||||
@@ -176,7 +162,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
||||
tile.transparent,
|
||||
tile.astar_weight,
|
||||
[0; 8],
|
||||
drop_table_for("dirt"),
|
||||
DropTableRegistry::global_get("dirt"),
|
||||
),
|
||||
));
|
||||
chunk_data.set_floor_tile(
|
||||
@@ -201,7 +187,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
||||
tile.transparent,
|
||||
tile.astar_weight,
|
||||
[0; 8],
|
||||
drop_table_for("grass"),
|
||||
DropTableRegistry::global_get("grass"),
|
||||
),
|
||||
));
|
||||
chunk_data.set_floor_tile(
|
||||
@@ -225,7 +211,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
||||
tile.transparent,
|
||||
tile.astar_weight,
|
||||
[0; 8],
|
||||
drop_table_for("dirt"),
|
||||
DropTableRegistry::global_get("dirt"),
|
||||
),
|
||||
));
|
||||
chunk_data.set_floor_tile(
|
||||
@@ -249,7 +235,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
||||
tile.transparent,
|
||||
tile.astar_weight,
|
||||
[0; 8],
|
||||
drop_table_for("air"),
|
||||
DropTableRegistry::global_get("air"),
|
||||
),
|
||||
));
|
||||
chunk_data.set_floor_tile(
|
||||
|
||||
+4
-1
@@ -34,7 +34,10 @@ pub struct WorldPlugin;
|
||||
|
||||
impl Plugin for WorldPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<ChunkMap>()
|
||||
let drop_registry = crate::entities::item::drop_table::DropTableRegistry::load();
|
||||
crate::entities::item::drop_table::DropTableRegistry::init_global(&drop_registry);
|
||||
app.insert_resource(drop_registry)
|
||||
.init_resource::<ChunkMap>()
|
||||
.init_resource::<TerrainBlobStorage>()
|
||||
.init_resource::<tiles::TilemapBenchmark>()
|
||||
.init_resource::<tiles::TilemapChunkRegistry>()
|
||||
|
||||
Reference in New Issue
Block a user