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;
impl bevy::prelude::Plugin for Plugin {
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(Update, (update_map_mesh, cells_regeneration, expand))
.add_systems(Update, update_map_mesh)
.insert_resource(ClearColor(Color::srgb(0., 0., 1.)))
.insert_resource(Seed(thread_rng().gen()))
.add_plugins(MaterialPlugin::<CellMaterial>::default());
.insert_resource(Seed(thread_rng().gen()));
}
}
pub const HEIGHT: f32 = 16.;
pub const WIDTH: f32 = 16.;
/// Determined empirically
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_WIDTH: f32 = 8000.;
pub const CELL_AREA: f32 = REAL_HEIGHT * REAL_WIDTH / SIZE as f32;
pub const SIZE: usize = 100_000;
pub const CELL_AREA: f32 = REAL_HEIGHT * REAL_WIDTH / CELLS as f32;
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)]
struct Seed(u32);
@ -68,24 +74,41 @@ fn setup(
seed: Res<Seed>,
) {
let mut rng = rand::rngs::SmallRng::seed_from_u64(seed.0 as u64);
let mut sites = Vec::with_capacity(SIZE);
for _ in 0..SIZE {
sites.push(Point {
x: rng.gen_range(-WIDTH / 2.0..WIDTH / 2.0) as f64,
y: rng.gen_range(-HEIGHT / 2.0..HEIGHT / 2.0) as f64,
})
let mut sites = Vec::with_capacity(CELLS);
for chunk_x in 0..CHUNKS_RESOLUTION {
let min_x = WIDTH / 2. * ((2. * (chunk_x as f64) / (CHUNKS_RESOLUTION as f64)) - 1.);
let max_x = min_x + (WIDTH as f64 / CHUNKS_RESOLUTION 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()
.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)
.build()
.unwrap();
let mut cells = Vec::with_capacity(SIZE);
let mut cells = Vec::with_capacity(CELLS);
let z_noise = Fbm::<Perlin>::new(seed.0);
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);
res += c.iter_neighbors().count();
let site = c.site_position();
let z = get_altitude(&z_noise, &[site.x as f32, site.y as f32]);
let _m = (
@ -108,6 +131,7 @@ fn setup(
vertices: vec![],
});
}
dbg!(res as f64 / CELLS as f64);
let mut poss = Vec::new();
let mut indices = Vec::new();
@ -181,27 +205,27 @@ fn setup(
MapMeshColors(colors),
MeshNeedsUpdate(true),
MapMarker,
))
.with_children(|parent| {
for cell in cells {
let kind = cell.kind;
let mut cmd = parent.spawn(cell);
match kind {
CellKind::Grass | CellKind::Forest => {
cmd.insert((
Wealth(0),
Regeneration {
last_update: Duration::ZERO,
full_growth_duration: kind.regen_full_growth_duration(),
},
));
}
_ => {}
));
for cell in cells {
let kind = cell.kind;
let mut cmd = cmds.spawn(cell);
match kind {
CellKind::Grass | CellKind::Forest => {
cmd.insert((
Wealth(0),
Regeneration {
last_update: Duration::ZERO,
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(CellsEntities(cells_entities));
}
@ -219,10 +243,12 @@ fn update_map_mesh(
for (cell, wealth) in cells.iter() {
// let col: [f32; 4] = [rng.gen(), rng.gen(), rng.gen(), 1.];
let col = cell.color(wealth.map(|w| w.0).unwrap_or_default());
for id in cell.vertices.iter() {
modified = modified || cols.0[*id] != col;
cols.0[*id] = col.clone();
}
modified = modified || cols.0[cell.voronoi_id] != col;
cols.0[cell.voronoi_id] = col;
// for id in cell.vertices.iter() {
// modified = modified || cols.0[*id] != col;
// cols.0[*id] = col.clone();
// }
}
if modified {
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, cols.0.clone());

View File

@ -1,13 +1,31 @@
use std::time::Duration;
use bevy::prelude::*;
use bevy::{prelude::*, time::common_conditions::on_timer};
use rand::{seq::IteratorRandom, thread_rng, Rng};
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;
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)]
pub enum CellKind {
@ -79,6 +97,7 @@ pub struct Regeneration {
pub full_growth_duration: Duration,
}
const STEP: u8 = u8::MAX / 4;
pub fn cells_regeneration(
mut cells: Query<(&mut Regeneration, &mut Wealth)>,
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();
for (mut regen, mut wealth) in cells.iter_mut() {
if gt.current - regen.last_update > regen.full_growth_duration / u8::MAX as u32 {
regen.last_update = gt.current;
while gt.current - regen.last_update > regen.full_growth_duration / u8::MAX as u32 {
regen.last_update = gt
.current
.min(regen.last_update + (regen.full_growth_duration / u8::MAX as u32));
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.
};
if random.gen_bool(
(t.elapsed_secs_f64() * (gt.speed as f64) * wealth / (60. * 60. * 24. * 30.))
.min(1.),
dbg!(
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
.0
.cell(cell.voronoi_id)
.iter_neighbors()
.choose(&mut random)
.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 random.gen_bool(
(t.elapsed_secs_f64()
* (gt.speed as f64)
* (wealth.unwrap().0 as f64 / 255.).sqrt()
/ (60. * 60. * 24. * 365. * 5.))
.min(1.),
dbg!(
t.elapsed_secs_f64()
* (gt.speed as f64)
* (wealth.unwrap().0 as f64 / 255.).sqrt()
* 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
.0
.cell(cell.voronoi_id)