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:
Generated
+1
@@ -2444,6 +2444,7 @@ dependencies = [
|
|||||||
"rayon",
|
"rayon",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
"serde",
|
"serde",
|
||||||
|
"smallvec",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
toml = "0.9.8"
|
toml = "0.9.8"
|
||||||
rayon = "1.11.0"
|
rayon = "1.11.0"
|
||||||
rustc-hash = "2.1.1"
|
rustc-hash = "2.1.1"
|
||||||
|
smallvec = { version = "1", features = ["union"] }
|
||||||
ahash = "0.8.12"
|
ahash = "0.8.12"
|
||||||
nohash-hasher = "0.2.0"
|
nohash-hasher = "0.2.0"
|
||||||
futures-lite = "2.6.1"
|
futures-lite = "2.6.1"
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[grass]
|
||||||
|
drops = [
|
||||||
|
{ prefab = "Coin", chance_pct = 5, min_count = 1, max_count = 2 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[dirt]
|
||||||
|
drops = []
|
||||||
|
|
||||||
|
[rock]
|
||||||
|
drops = [
|
||||||
|
{ prefab = "Coin", chance_pct = 10, min_count = 1, max_max = 1 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[air]
|
||||||
|
drops = []
|
||||||
@@ -1,46 +1,50 @@
|
|||||||
|
use crate::constants::SEED;
|
||||||
use crate::entities::item::prefabs::misc::misc_prefabs::MiscPrefab;
|
use crate::entities::item::prefabs::misc::misc_prefabs::MiscPrefab;
|
||||||
use bevy::prelude::*;
|
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 struct DropEntry {
|
||||||
pub prefab: MiscPrefab,
|
pub prefab: MiscPrefab,
|
||||||
pub chance: f32,
|
pub chance_pct: u8,
|
||||||
pub min_count: u32,
|
pub min_count: u8,
|
||||||
pub max_count: u32,
|
pub max_count: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DropEntry {
|
impl DropEntry {
|
||||||
pub fn always(prefab: MiscPrefab) -> Self {
|
pub fn always(prefab: MiscPrefab) -> Self {
|
||||||
Self {
|
Self {
|
||||||
prefab,
|
prefab,
|
||||||
chance: 1.0,
|
chance_pct: 100,
|
||||||
min_count: 1,
|
min_count: 1,
|
||||||
max_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 {
|
Self {
|
||||||
prefab,
|
prefab,
|
||||||
chance,
|
chance_pct,
|
||||||
min_count,
|
min_count,
|
||||||
max_count,
|
max_count,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn roll(&self, pos: IVec3) -> u32 {
|
pub fn roll(&self, rng_val: u32) -> u8 {
|
||||||
if self.chance < 1.0 {
|
if self.chance_pct < 100 && ((rng_val % 100) as u8) >= self.chance_pct {
|
||||||
let r = pseudo_rand_f32(pos);
|
return 0;
|
||||||
if r >= self.chance {
|
|
||||||
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)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct DropTable(pub Vec<DropEntry>);
|
pub struct DropTable(pub SmallVec<[DropEntry; 2]>);
|
||||||
|
|
||||||
impl DropTable {
|
impl DropTable {
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
@@ -48,26 +52,79 @@ impl DropTable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hash3(pos: IVec3) -> u32 {
|
pub fn dig_rng(pos: IVec3) -> u32 {
|
||||||
let mut h: u32 = 0;
|
let mut h = (SEED as u64)
|
||||||
h = h.wrapping_mul(374761393).wrapping_add(pos.x as u32);
|
.wrapping_add(pos.x as u64)
|
||||||
h = h.wrapping_mul(374761393).wrapping_add(pos.y as u32);
|
.wrapping_mul(0x9e3779b97f4a7c15)
|
||||||
h = h.wrapping_mul(374761393).wrapping_add(pos.z as u32);
|
^ (pos.y as u64).wrapping_mul(0x6c62272e07bb0142)
|
||||||
h ^= h >> 13;
|
^ (pos.z as u64).wrapping_mul(0x94d049bb133111eb);
|
||||||
h = h.wrapping_mul(1274126177);
|
let h32 = (h ^ (h >> 32)) as u32;
|
||||||
h ^= h >> 16;
|
h32 ^ (h32 >> 16)
|
||||||
h
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pseudo_rand_u32(pos: IVec3) -> u32 {
|
#[derive(Deserialize)]
|
||||||
let timestamp = std::time::SystemTime::now()
|
struct DropEntryToml {
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
pub prefab: String,
|
||||||
.unwrap()
|
pub chance_pct: u8,
|
||||||
.as_nanos() as u32;
|
pub min_count: u8,
|
||||||
hash3(pos) ^ timestamp
|
#[serde(rename = "max_max")]
|
||||||
|
pub max_count: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pseudo_rand_f32(pos: IVec3) -> f32 {
|
#[derive(Deserialize)]
|
||||||
let val = pseudo_rand_u32(pos);
|
struct DropTableToml {
|
||||||
(val as f32) / (u32::MAX as f32)
|
#[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::constants::ITILE_SIZE;
|
||||||
|
use crate::entities::item::drop_table::dig_rng;
|
||||||
use crate::entities::item::prefabs::misc::misc_prefabs::spawn_prefab;
|
use crate::entities::item::prefabs::misc::misc_prefabs::spawn_prefab;
|
||||||
use crate::world::chunks::{Z_ABOVE, Z_BELOW};
|
use crate::world::chunks::{Z_ABOVE, Z_BELOW};
|
||||||
use crate::world::tiles::tile_changed::TileChangedEvent;
|
use crate::world::tiles::tile_changed::TileChangedEvent;
|
||||||
use crate::world::tiles::visibility::TileOcclusionEvent;
|
use crate::world::tiles::visibility::TileOcclusionEvent;
|
||||||
use crate::world::tiles::TileMap;
|
use crate::world::tiles::TileMap;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use rand::RngExt;
|
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct Digger {
|
pub struct Digger {
|
||||||
@@ -52,6 +52,8 @@ pub fn dig_system(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let rng_val = dig_rng(below_pos);
|
||||||
|
|
||||||
let drop_table = tilemap
|
let drop_table = tilemap
|
||||||
.floor_tiles
|
.floor_tiles
|
||||||
.get(&below_pos)
|
.get(&below_pos)
|
||||||
@@ -60,12 +62,12 @@ pub fn dig_system(
|
|||||||
if let Some(_removed) = tilemap.dig_floor(&below_pos) {
|
if let Some(_removed) = tilemap.dig_floor(&below_pos) {
|
||||||
if let Some(table) = drop_table {
|
if let Some(table) = drop_table {
|
||||||
for entry in table.0 {
|
for entry in table.0 {
|
||||||
let count = entry.roll(below_pos);
|
let count = entry.roll(rng_val);
|
||||||
for _ in 0..count {
|
for _ in 0..u32::from(count) {
|
||||||
spawn_prefab(
|
spawn_prefab(
|
||||||
&mut commands,
|
&mut commands,
|
||||||
&asset_server,
|
&asset_server,
|
||||||
entry.prefab.clone(),
|
entry.prefab,
|
||||||
below_pos.as_vec3(),
|
below_pos.as_vec3(),
|
||||||
&mut tilemap,
|
&mut tilemap,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ use bevy_platform::time::Instant;
|
|||||||
use noise::{NoiseFn, Perlin};
|
use noise::{NoiseFn, Perlin};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use crate::entities::item::drop_table::{DropEntry, DropTable};
|
use crate::entities::item::drop_table::DropTableRegistry;
|
||||||
use crate::entities::item::prefabs::misc::misc_prefabs::MiscPrefab;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::TileRegistry,
|
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.
|
/// Thread-safe storage for completed terrain blobs.
|
||||||
/// Uses type erasure to avoid Debug bounds on TerrainBlob.
|
/// Uses type erasure to avoid Debug bounds on TerrainBlob.
|
||||||
type BlobStorage = Arc<Mutex<Box<dyn Send + Sync>>>;
|
type BlobStorage = Arc<Mutex<Box<dyn Send + Sync>>>;
|
||||||
@@ -130,7 +116,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
|||||||
tile.transparent,
|
tile.transparent,
|
||||||
tile.astar_weight,
|
tile.astar_weight,
|
||||||
[0; 8],
|
[0; 8],
|
||||||
drop_table_for("air"),
|
DropTableRegistry::global_get("air"),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
chunk_data.set_floor_tile(
|
chunk_data.set_floor_tile(
|
||||||
@@ -153,7 +139,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
|||||||
tile.transparent,
|
tile.transparent,
|
||||||
tile.astar_weight,
|
tile.astar_weight,
|
||||||
[0; 8],
|
[0; 8],
|
||||||
drop_table_for("rock"),
|
DropTableRegistry::global_get("rock"),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
chunk_data.set_floor_tile(
|
chunk_data.set_floor_tile(
|
||||||
@@ -176,7 +162,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
|||||||
tile.transparent,
|
tile.transparent,
|
||||||
tile.astar_weight,
|
tile.astar_weight,
|
||||||
[0; 8],
|
[0; 8],
|
||||||
drop_table_for("dirt"),
|
DropTableRegistry::global_get("dirt"),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
chunk_data.set_floor_tile(
|
chunk_data.set_floor_tile(
|
||||||
@@ -201,7 +187,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
|||||||
tile.transparent,
|
tile.transparent,
|
||||||
tile.astar_weight,
|
tile.astar_weight,
|
||||||
[0; 8],
|
[0; 8],
|
||||||
drop_table_for("grass"),
|
DropTableRegistry::global_get("grass"),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
chunk_data.set_floor_tile(
|
chunk_data.set_floor_tile(
|
||||||
@@ -225,7 +211,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
|||||||
tile.transparent,
|
tile.transparent,
|
||||||
tile.astar_weight,
|
tile.astar_weight,
|
||||||
[0; 8],
|
[0; 8],
|
||||||
drop_table_for("dirt"),
|
DropTableRegistry::global_get("dirt"),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
chunk_data.set_floor_tile(
|
chunk_data.set_floor_tile(
|
||||||
@@ -249,7 +235,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
|||||||
tile.transparent,
|
tile.transparent,
|
||||||
tile.astar_weight,
|
tile.astar_weight,
|
||||||
[0; 8],
|
[0; 8],
|
||||||
drop_table_for("air"),
|
DropTableRegistry::global_get("air"),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
chunk_data.set_floor_tile(
|
chunk_data.set_floor_tile(
|
||||||
|
|||||||
+4
-1
@@ -34,7 +34,10 @@ pub struct WorldPlugin;
|
|||||||
|
|
||||||
impl Plugin for WorldPlugin {
|
impl Plugin for WorldPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
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::<TerrainBlobStorage>()
|
||||||
.init_resource::<tiles::TilemapBenchmark>()
|
.init_resource::<tiles::TilemapBenchmark>()
|
||||||
.init_resource::<tiles::TilemapChunkRegistry>()
|
.init_resource::<tiles::TilemapChunkRegistry>()
|
||||||
|
|||||||
Reference in New Issue
Block a user