Compare commits

...

2 Commits

Author SHA1 Message Date
Arkitu
00f75f2de6 fixed update for regen and expand 2025-06-12 15:07:16 +02:00
Arkitu
22f59b059e better expansion (optimization) 2025-06-11 21:16:18 +02:00
2 changed files with 120 additions and 55 deletions

View File

@ -26,21 +26,27 @@ use picking::*;
pub struct Plugin; pub struct Plugin;
impl bevy::prelude::Plugin for Plugin { impl bevy::prelude::Plugin for Plugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems(Startup, setup) app.add_plugins(cells::Plugin)
.add_systems(Startup, setup)
.add_systems(PreUpdate, picking_backend.in_set(PickSet::Backend)) .add_systems(PreUpdate, picking_backend.in_set(PickSet::Backend))
.add_systems(Update, (update_map_mesh, cells_regeneration, expand)) .add_systems(Update, update_map_mesh)
.insert_resource(ClearColor(Color::srgb(0., 0., 1.))) .insert_resource(ClearColor(Color::srgb(0., 0., 1.)))
.insert_resource(Seed(thread_rng().gen())) .insert_resource(Seed(thread_rng().gen()));
.add_plugins(MaterialPlugin::<CellMaterial>::default());
} }
} }
pub const HEIGHT: f32 = 16.; /// Determined empirically
pub const WIDTH: f32 = 16.; pub const AVERAGE_NEIGHBORS_NUMBER: f64 = 6.;
pub const HEIGHT: f64 = 16.;
pub const WIDTH: f64 = 16.;
pub const REAL_HEIGHT: f32 = 8000.; pub const REAL_HEIGHT: f32 = 8000.;
pub const REAL_WIDTH: f32 = 8000.; pub const REAL_WIDTH: f32 = 8000.;
pub const CELL_AREA: f32 = REAL_HEIGHT * REAL_WIDTH / SIZE as f32; pub const CELL_AREA: f32 = REAL_HEIGHT * REAL_WIDTH / CELLS as f32;
pub const SIZE: usize = 100_000; pub const CELLS_TARGET_NUMBER: usize = 100_000;
pub const CHUNKS_RESOLUTION: usize = 2;
pub const CELLS_PER_CHUNK: usize = CELLS_TARGET_NUMBER / CHUNKS_RESOLUTION.pow(2);
pub const CELLS: usize = CELLS_PER_CHUNK * CHUNKS_RESOLUTION.pow(2);
#[derive(Resource)] #[derive(Resource)]
struct Seed(u32); struct Seed(u32);
@ -68,24 +74,41 @@ fn setup(
seed: Res<Seed>, seed: Res<Seed>,
) { ) {
let mut rng = rand::rngs::SmallRng::seed_from_u64(seed.0 as u64); let mut rng = rand::rngs::SmallRng::seed_from_u64(seed.0 as u64);
let mut sites = Vec::with_capacity(SIZE); let mut sites = Vec::with_capacity(CELLS);
for _ in 0..SIZE { for chunk_x in 0..CHUNKS_RESOLUTION {
sites.push(Point { let min_x = WIDTH / 2. * ((2. * (chunk_x as f64) / (CHUNKS_RESOLUTION as f64)) - 1.);
x: rng.gen_range(-WIDTH / 2.0..WIDTH / 2.0) as f64, let max_x = min_x + (WIDTH as f64 / CHUNKS_RESOLUTION as f64);
y: rng.gen_range(-HEIGHT / 2.0..HEIGHT / 2.0) as f64, for chunk_y in 0..CHUNKS_RESOLUTION {
}) let min_y = HEIGHT / 2. * ((2. * (chunk_y as f64) / (CHUNKS_RESOLUTION as f64)) - 1.);
let max_y = min_y + (HEIGHT as f64 / CHUNKS_RESOLUTION as f64);
println!(
"[{},{}] x:[{}, {}], y:[{}, {}]",
chunk_x, chunk_y, min_x, max_x, min_y, max_y
);
for _ in 0..CELLS_PER_CHUNK {
sites.push(Point {
x: rng.gen_range(min_x..max_x),
y: rng.gen_range(min_y..max_y),
});
}
}
} }
// dbg!(sites.len());
let voronoi = VoronoiBuilder::default() let voronoi = VoronoiBuilder::default()
.set_sites(sites) .set_sites(sites)
.set_bounding_box(BoundingBox::new_centered(WIDTH as f64, HEIGHT as f64)) .set_bounding_box(BoundingBox::new_centered(WIDTH, HEIGHT))
.set_lloyd_relaxation_iterations(3) .set_lloyd_relaxation_iterations(3)
.build() .build()
.unwrap(); .unwrap();
let mut cells = Vec::with_capacity(SIZE);
let mut cells = Vec::with_capacity(CELLS);
let z_noise = Fbm::<Perlin>::new(seed.0); let z_noise = Fbm::<Perlin>::new(seed.0);
let moisture_noise = Fbm::<Perlin>::new(seed.0 + 1).set_frequency(2.); let moisture_noise = Fbm::<Perlin>::new(seed.0 + 1).set_frequency(2.);
for i in 0..SIZE { let mut res = 0;
for i in 0..CELLS {
let c = voronoi.cell(i); let c = voronoi.cell(i);
res += c.iter_neighbors().count();
let site = c.site_position(); let site = c.site_position();
let z = get_altitude(&z_noise, &[site.x as f32, site.y as f32]); let z = get_altitude(&z_noise, &[site.x as f32, site.y as f32]);
let _m = ( let _m = (
@ -108,6 +131,7 @@ fn setup(
vertices: vec![], vertices: vec![],
}); });
} }
dbg!(res as f64 / CELLS as f64);
let mut poss = Vec::new(); let mut poss = Vec::new();
let mut indices = Vec::new(); let mut indices = Vec::new();
@ -181,27 +205,27 @@ fn setup(
MapMeshColors(colors), MapMeshColors(colors),
MeshNeedsUpdate(true), MeshNeedsUpdate(true),
MapMarker, MapMarker,
)) ));
.with_children(|parent| {
for cell in cells { for cell in cells {
let kind = cell.kind; let kind = cell.kind;
let mut cmd = parent.spawn(cell); let mut cmd = cmds.spawn(cell);
match kind { match kind {
CellKind::Grass | CellKind::Forest => { CellKind::Grass | CellKind::Forest => {
cmd.insert(( cmd.insert((
Wealth(0), Wealth(0),
Regeneration { Regeneration {
last_update: Duration::ZERO, last_update: Duration::ZERO,
full_growth_duration: kind.regen_full_growth_duration(), full_growth_duration: kind.regen_full_growth_duration(),
}, },
)); ));
}
_ => {}
} }
cmd.observe(self::cells::on_click); _ => {}
cells_entities.push(cmd.id());
} }
}); cmd.observe(self::cells::on_click);
cells_entities.push(cmd.id());
}
cmds.insert_resource(Voronoi(voronoi)); cmds.insert_resource(Voronoi(voronoi));
cmds.insert_resource(CellsEntities(cells_entities)); cmds.insert_resource(CellsEntities(cells_entities));
} }
@ -219,10 +243,12 @@ fn update_map_mesh(
for (cell, wealth) in cells.iter() { for (cell, wealth) in cells.iter() {
// let col: [f32; 4] = [rng.gen(), rng.gen(), rng.gen(), 1.]; // let col: [f32; 4] = [rng.gen(), rng.gen(), rng.gen(), 1.];
let col = cell.color(wealth.map(|w| w.0).unwrap_or_default()); let col = cell.color(wealth.map(|w| w.0).unwrap_or_default());
for id in cell.vertices.iter() { modified = modified || cols.0[cell.voronoi_id] != col;
modified = modified || cols.0[*id] != col; cols.0[cell.voronoi_id] = col;
cols.0[*id] = col.clone(); // for id in cell.vertices.iter() {
} // modified = modified || cols.0[*id] != col;
// cols.0[*id] = col.clone();
// }
} }
if modified { if modified {
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, cols.0.clone()); mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, cols.0.clone());

View File

@ -1,13 +1,31 @@
use std::time::Duration; use std::time::Duration;
use bevy::prelude::*; use bevy::{prelude::*, time::common_conditions::on_timer};
use rand::{seq::IteratorRandom, thread_rng, Rng}; use rand::{seq::IteratorRandom, thread_rng, Rng};
use crate::{time::GameTime, ui::CurrentAction}; use crate::{time::GameTime, ui::CurrentAction};
use super::{animals::Animal, AnimalKind, CellsEntities, MapMarker, MeshNeedsUpdate, Voronoi}; use super::{
animals::Animal, AnimalKind, CellsEntities, MapMarker, MeshNeedsUpdate, Voronoi,
AVERAGE_NEIGHBORS_NUMBER, CELL_AREA,
};
pub mod material; pub mod material;
use material::CellMaterial;
pub struct Plugin;
impl bevy::prelude::Plugin for Plugin {
fn build(&self, app: &mut App) {
app.add_systems(
FixedUpdate,
(
cells_regeneration.run_if(on_timer(Duration::from_secs(1))),
expand.run_if(on_timer(Duration::from_secs(1))),
),
)
.add_plugins(MaterialPlugin::<CellMaterial>::default());
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CellKind { pub enum CellKind {
@ -79,6 +97,7 @@ pub struct Regeneration {
pub full_growth_duration: Duration, pub full_growth_duration: Duration,
} }
const STEP: u8 = u8::MAX / 4;
pub fn cells_regeneration( pub fn cells_regeneration(
mut cells: Query<(&mut Regeneration, &mut Wealth)>, mut cells: Query<(&mut Regeneration, &mut Wealth)>,
mut map_needs_update: Query<&mut MeshNeedsUpdate, With<MapMarker>>, mut map_needs_update: Query<&mut MeshNeedsUpdate, With<MapMarker>>,
@ -86,10 +105,15 @@ pub fn cells_regeneration(
) { ) {
let mut map_needs_update = map_needs_update.single_mut(); let mut map_needs_update = map_needs_update.single_mut();
for (mut regen, mut wealth) in cells.iter_mut() { for (mut regen, mut wealth) in cells.iter_mut() {
if gt.current - regen.last_update > regen.full_growth_duration / u8::MAX as u32 { while gt.current - regen.last_update > regen.full_growth_duration / u8::MAX as u32 {
regen.last_update = gt.current; regen.last_update = gt
.current
.min(regen.last_update + (regen.full_growth_duration / u8::MAX as u32));
wealth.0 = wealth.0.saturating_add(1); wealth.0 = wealth.0.saturating_add(1);
map_needs_update.0 = true; // Not update map each time for optimization
if wealth.0 % STEP == 0 {
map_needs_update.0 = true;
}
} }
} }
} }
@ -113,28 +137,43 @@ pub fn expand(
wealth.unwrap().0 as f64 / 255. wealth.unwrap().0 as f64 / 255.
}; };
if random.gen_bool( if random.gen_bool(
(t.elapsed_secs_f64() * (gt.speed as f64) * wealth / (60. * 60. * 24. * 30.)) dbg!(
.min(1.), t.elapsed_secs_f64() * (gt.speed as f64) * wealth * 1e-7
/ (CELL_AREA as f64).sqrt()
)
.min(1.),
) { ) {
// Let say that grass takes 1 months to expand // "Grass can expand into adjacent dirt at a rate of 2.5 to 7.5 cm (1 to 3 inches) per month.", DeepSeek
// With some (way too complicated and probably false) computation, we get for a good grass an extension rate of (1e-7 / sqrt(cell_area)) cells per seconds
let target = voronoi let target = voronoi
.0 .0
.cell(cell.voronoi_id) .cell(cell.voronoi_id)
.iter_neighbors() .iter_neighbors()
.choose(&mut random) .choose(&mut random)
.unwrap(); .unwrap();
changes.push((target, CellKind::Grass)); changes.extend(
voronoi
.0
.cell(cell.voronoi_id)
.iter_neighbors()
.map(|t| (t, CellKind::Grass)),
);
} }
} }
if cell.kind == CellKind::Forest { if cell.kind == CellKind::Forest {
if random.gen_bool( if random.gen_bool(
(t.elapsed_secs_f64() dbg!(
* (gt.speed as f64) t.elapsed_secs_f64()
* (wealth.unwrap().0 as f64 / 255.).sqrt() * (gt.speed as f64)
/ (60. * 60. * 24. * 365. * 5.)) * (wealth.unwrap().0 as f64 / 255.).sqrt()
.min(1.), * 5.6e-7
/ (CELL_AREA as f64).sqrt()
* AVERAGE_NEIGHBORS_NUMBER
)
.min(1.),
) { ) {
// Let say that forest takes 5 years to expand // "Forests can expand at a rate of 1 to 10 meters per year", DeepSeek
// Same computations as above : (5.6e-7 / sqrt(cell_area)) cells per seconds
let target = voronoi let target = voronoi
.0 .0
.cell(cell.voronoi_id) .cell(cell.voronoi_id)