From 6c74ed92723e369e75bc495d7f0919eaa1885395 Mon Sep 17 00:00:00 2001 From: Arkitu <85173315+Arkitu@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:32:12 +0200 Subject: [PATCH] grow thread + communications (not tested) --- Cargo.lock | 1 + Cargo.toml | 1 + src/map.rs | 16 +++-- src/map/cells.rs | 47 +++++++++----- src/map/cells/grow.rs | 145 ++++++++++++++++++++++++++++++++++++------ src/time.rs | 13 +++- 6 files changed, 183 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d1bfdf..c6b1b7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2555,6 +2555,7 @@ name = "forestiles" version = "0.4.0" dependencies = [ "android_logger", + "async-channel", "bevy", "bevy_editor_pls", "console_error_panic_hook", diff --git a/Cargo.toml b/Cargo.toml index 3eda2c5..62e7f7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ rand = { version = "0.8", features = ["small_rng"] } voronoice = "0.2" noise = "0.9" bevy_editor_pls = { version = "0.11", git = "https://github.com/jakobhellermann/bevy_editor_pls.git", optional = true } +async-channel = "*" [target.'cfg(target_arch = "wasm32")'.dependencies] console_error_panic_hook = "0.1.6" diff --git a/src/map.rs b/src/map.rs index 5137cde..c28e224 100644 --- a/src/map.rs +++ b/src/map.rs @@ -25,7 +25,10 @@ use cells::{ }; use picking::*; -use crate::map::cells::grow::{grow_thread, GrowThread}; +use crate::{ + map::cells::grow::{grow_thread, GrowThread}, + time::GameTime, +}; pub struct Plugin; impl bevy::prelude::Plugin for Plugin { @@ -80,6 +83,7 @@ fn setup( // mut materials: ResMut>, mut materials: ResMut>, seed: Res, + gt: Res, ) { let mut rng = rand::rngs::SmallRng::seed_from_u64(seed.0 as u64); let mut sites = Vec::with_capacity(CELLS); @@ -246,11 +250,15 @@ fn setup( }); } + cmds.insert_resource(GrowThread::new( + voronoi.clone(), + gt.secs.clone(), + cells, + rng, + )); + cmds.insert_resource(Voronoi(voronoi)); cmds.insert_resource(CellsEntities(cells_entities)); - - let task_pool = AsyncComputeTaskPool::get(); - cmds.insert_resource(GrowThread(task_pool.spawn(grow_thread()))); } // TODO: update this to take chunks into account diff --git a/src/map/cells.rs b/src/map/cells.rs index bd49262..4cad066 100644 --- a/src/map/cells.rs +++ b/src/map/cells.rs @@ -39,32 +39,47 @@ pub enum CellKind { Grass { wealth: WealthType }, } impl CellKind { - pub const fn regen_full_growth_duration(&self) -> Duration { + pub const FOREST: CellKind = CellKind::Forest { wealth: 0 }; + pub const GRASS: CellKind = CellKind::Grass { wealth: 0 }; + pub const fn growth_duration(&self) -> u64 { match self { - CellKind::Sea | CellKind::Beach | CellKind::Dirt | CellKind::Stone => unreachable!(), - CellKind::Forest { .. } => Duration::from_secs(100 * 365 * 24 * 60 * 60), // Let's say that a forest takes 100 years to mature - CellKind::Grass { .. } => Duration::from_secs(7 * 7 * 24 * 60 * 60), // Let's say that grass takes 7 weeks to reach its max + CellKind::Forest { .. } => 100 * 365 * 24 * 60 * 60, // a forest takes 100 years to mature + CellKind::Grass { .. } => (7 * 7 * 24 * 60 * 60), // grass takes 7 weeks to reach its max + _ => unreachable!(), } } + /// Time before trying to extend to one neighbor + pub const fn extend_duration(&self) -> usize { + match self { + CellKind::Grass { wealth } => { + // "Grass can expand into adjacent dirt at a rate of 2.5 to 7.5 cm (1 to 3 inches) per month.", DeepSeek + 59_388_377 + * (CELL_AREA as usize).isqrt() + * WealthType::MAX.saturating_div(*wealth) as usize + } + CellKind::Forest { wealth } => { + // Arbitrary value : 1m per 10 years + (10 * 365 * 24 * 3600) + * (CELL_AREA as usize).isqrt() + * WealthType::MAX.saturating_div(*wealth) as usize + } + _ => unreachable!(), + } + } + pub const fn can_place_animal(&self, kind: AnimalKind) -> bool { match self { CellKind::Sea => false, _ => true, } } - pub const fn grass_wealth(&self) -> Option { + pub const fn grass_wealth(&self) -> Option { match self { CellKind::Grass { wealth } => Some(*wealth), - CellKind::Forest { wealth } => Some( - wealth.saturating_mul( - (CellKind::Forest { wealth: 0 } - .regen_full_growth_duration() - .as_secs() - / CellKind::Grass { wealth: 0 } - .regen_full_growth_duration() - .as_secs()) as usize, - ), - ), + CellKind::Forest { wealth } => Some(wealth.saturating_mul( + (CellKind::FOREST.growth_duration() / CellKind::GRASS.growth_duration()) + as WealthType, + )), _ => None, } } @@ -105,7 +120,7 @@ impl Cell { // pub chunk: // }; -pub type WealthType = usize; +pub type WealthType = u16; const fn wealth_to_unit(wealth: WealthType) -> f32 { wealth as f32 / WealthType::MAX as f32 } diff --git a/src/map/cells/grow.rs b/src/map/cells/grow.rs index de5e1b4..71890b4 100644 --- a/src/map/cells/grow.rs +++ b/src/map/cells/grow.rs @@ -1,11 +1,16 @@ +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use bevy::prelude::*; use bevy::tasks::futures_lite::{future, FutureExt}; use bevy::tasks::{block_on, AsyncComputeTaskPool, IoTaskPool, Task}; +use rand::rngs::SmallRng; +use rand::seq::IteratorRandom; +use rand::Rng; use voronoice::Voronoi; -use crate::map::{CellsEntities, Chunk, MeshNeedsUpdate}; +use crate::map::cells::{wealth_to_unit, WealthType}; +use crate::map::{CellKind, CellsEntities, Chunk, MeshNeedsUpdate}; use super::Cell; @@ -16,7 +21,23 @@ impl bevy::prelude::Plugin for Plugin { // Background task that manages terrain #[derive(Resource)] -pub struct GrowThread(pub Task<()>); +pub struct GrowThread( + pub Task<()>, + pub async_channel::Sender, + pub async_channel::Receiver, +); +impl GrowThread { + pub fn new(voronoi: Voronoi, gt: Arc, cells: Vec, mut rng: SmallRng) -> Self { + let task_pool = AsyncComputeTaskPool::get(); + let (tx_control, rx_control) = async_channel::unbounded(); + let (tx_update, rx_update) = async_channel::unbounded(); + Self( + task_pool.spawn(grow_thread(voronoi, gt, cells, rng, rx_control, tx_update)), + tx_control, + rx_update, + ) + } +} #[derive(Component)] struct GetChunkTask(Task>); @@ -24,27 +45,115 @@ struct GetChunkTask(Task>); fn start_grow_task(mut cmds: Commands) {} fn rx_chunks_updates( - mut chunks: Query<(&Chunk, &mut GetChunkTask, &mut MeshNeedsUpdate)>, - mut cells: Query<&mut Cell>, + mut chunks: Query<(&mut MeshNeedsUpdate)>, + mut cells: Query<(&mut Cell, &Parent)>, cells_entities: Res, + grow_thread: Res, ) { - for (ch_id, mut task, mut needs_update) in chunks.iter_mut() { - let res = block_on(future::poll_once(&mut task.0)); - if let Some(updated_cells) = res { - if updated_cells.len() > 0 { - needs_update.0 = true; - } - for c in updated_cells.into_iter() { - let id = c.voronoi_id; - *cells.get_mut(cells_entities.0[id]).unwrap() = c; - } - let task_pool = IoTaskPool::get(); - task.0 = task_pool.spawn(async { Vec::new() }) - } + while let Ok(c) = grow_thread.2.try_recv() { + let id = c.voronoi_id; + let (mut cell, parent) = cells.get_mut(cells_entities.0[id]).unwrap(); + *cell = c; + chunks.get(parent.get()).unwrap().0 = true; } } -pub async fn grow_thread(voronoi: Voronoi, current_frame: Arc) {} +const GRASS_EXTEND_STEP: u64 = CellKind::GRASS.growth_duration(); +const FOREST_EXTEND_STEP: u64 = CellKind::FOREST.growth_duration(); +pub async fn grow_thread( + voronoi: Voronoi, + gt: Arc, + mut cells: Vec, + mut rng: SmallRng, + mut rx_control: async_channel::Receiver, + mut tx_update: async_channel::Sender, +) { + // Add "modified" flag + let mut cells: Vec<(Cell, bool)> = cells.into_iter().map(|c| (c, false)).collect(); + let mut last_grass_grow = 0; + let mut last_forest_grow = 0; + let mut last_grass_extend = 0; + let mut last_forest_extend = 0; + loop { + // Receive control from the game + while let Ok(c) = rx_control.try_recv() { + let id = c.voronoi_id; + cells[id] = (c, false); + } + + let gt = gt.load(Ordering::Acquire); + + // Update step + let grass_grow = ((gt - last_grass_grow) / CellKind::GRASS.growth_duration()) as usize; + let forest_grow = ((gt - last_forest_grow) / CellKind::FOREST.growth_duration()) as usize; + if grass_grow > 0 || forest_grow > 0 { + for (c, modified) in cells.iter_mut() { + if grass_grow > 0 { + if let CellKind::Grass { mut wealth } = c.kind { + wealth = wealth.saturating_add(grass_grow as WealthType); + *modified = true; + } + } + if forest_grow > 0 { + if let CellKind::Forest { mut wealth } = c.kind { + wealth = wealth.saturating_add(forest_grow as WealthType); + *modified = true; + } + } + } + // Don't make multiple updates at once to avoid problems and lags + } else if gt - last_grass_extend > GRASS_EXTEND_STEP { + for i in 0..cells.len() { + if !cells[i].1 { + if let Some(wealth) = cells[i].0.kind.grass_wealth() { + if rng.gen_bool(wealth_to_unit(wealth) as f64) { + let mut target = &mut cells + [voronoi.cell(i).iter_neighbors().choose(&mut rng).unwrap()]; + + if matches!(target.0.kind, CellKind::Dirt) { + target.0.kind == CellKind::Grass { wealth: 0 }; + target.1 = true; + } + } + } + } + } + } else if gt - last_forest_extend > FOREST_EXTEND_STEP { + for i in 0..cells.len() { + if !cells[i].1 { + if let CellKind::Forest { wealth } = cells[i].0.kind { + if rng.gen_bool(wealth_to_unit(wealth) as f64) { + let mut target = &mut cells + [voronoi.cell(i).iter_neighbors().choose(&mut rng).unwrap()]; + match target.0.kind { + CellKind::Dirt => { + target.0.kind == CellKind::Forest { wealth: 0 }; + target.1 = true; + } + CellKind::Grass { wealth: w } => { + target.0.kind + == CellKind::Forest { + wealth: (w as f32 + * CellKind::GRASS.growth_duration() as f32 + / CellKind::FOREST.growth_duration() as f32) + as WealthType, + }; + target.1 = true; + } + _ => {} + } + } + } + } + } + } + + // Send modifications + for (c, _) in cells.iter().filter(|(_, m)| *m) { + tx_update.send(c.clone()).await; + } + } +} // fn begin_generating_map_chunks(mut my_tasks: ResMut) { // let task_pool = AsyncComputeTaskPool::get(); diff --git a/src/time.rs b/src/time.rs index f948828..5683041 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1,4 +1,10 @@ -use std::time::Duration; +use std::{ + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, + time::Duration, +}; use bevy::prelude::*; @@ -7,7 +13,8 @@ impl bevy::prelude::Plugin for Plugin { fn build(&self, app: &mut App) { app.insert_resource(GameTime { current: Duration::ZERO, - speed: 24. * 60. * 60. * 10. * 10., + secs: Arc::new(AtomicU64::new(0)), + speed: 3600. * 24. * 365., }) .add_systems(PreUpdate, update_time); } @@ -16,10 +23,12 @@ impl bevy::prelude::Plugin for Plugin { #[derive(Resource)] pub struct GameTime { pub current: Duration, + pub secs: Arc, pub speed: f32, // = game time / real time } fn update_time(mut gt: ResMut, time: Res