grow thread + communications (not tested)

This commit is contained in:
Arkitu 2025-06-25 19:32:12 +02:00
parent 1a68ab5af1
commit 6c74ed9272
6 changed files with 183 additions and 40 deletions

1
Cargo.lock generated
View File

@ -2555,6 +2555,7 @@ name = "forestiles"
version = "0.4.0" version = "0.4.0"
dependencies = [ dependencies = [
"android_logger", "android_logger",
"async-channel",
"bevy", "bevy",
"bevy_editor_pls", "bevy_editor_pls",
"console_error_panic_hook", "console_error_panic_hook",

View File

@ -48,6 +48,7 @@ rand = { version = "0.8", features = ["small_rng"] }
voronoice = "0.2" voronoice = "0.2"
noise = "0.9" noise = "0.9"
bevy_editor_pls = { version = "0.11", git = "https://github.com/jakobhellermann/bevy_editor_pls.git", optional = true } 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] [target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.6" console_error_panic_hook = "0.1.6"

View File

@ -25,7 +25,10 @@ use cells::{
}; };
use picking::*; use picking::*;
use crate::map::cells::grow::{grow_thread, GrowThread}; use crate::{
map::cells::grow::{grow_thread, GrowThread},
time::GameTime,
};
pub struct Plugin; pub struct Plugin;
impl bevy::prelude::Plugin for Plugin { impl bevy::prelude::Plugin for Plugin {
@ -80,6 +83,7 @@ fn setup(
// mut materials: ResMut<Assets<StandardMaterial>>, // mut materials: ResMut<Assets<StandardMaterial>>,
mut materials: ResMut<Assets<CellMaterial>>, mut materials: ResMut<Assets<CellMaterial>>,
seed: Res<Seed>, seed: Res<Seed>,
gt: Res<GameTime>,
) { ) {
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(CELLS); 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(Voronoi(voronoi));
cmds.insert_resource(CellsEntities(cells_entities)); 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 // TODO: update this to take chunks into account

View File

@ -39,32 +39,47 @@ pub enum CellKind {
Grass { wealth: WealthType }, Grass { wealth: WealthType },
} }
impl CellKind { 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 { match self {
CellKind::Sea | CellKind::Beach | CellKind::Dirt | CellKind::Stone => unreachable!(), CellKind::Forest { .. } => 100 * 365 * 24 * 60 * 60, // a forest takes 100 years to mature
CellKind::Forest { .. } => Duration::from_secs(100 * 365 * 24 * 60 * 60), // Let's say that a forest takes 100 years to mature CellKind::Grass { .. } => (7 * 7 * 24 * 60 * 60), // grass takes 7 weeks to reach its max
CellKind::Grass { .. } => Duration::from_secs(7 * 7 * 24 * 60 * 60), // Let's say that 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 { pub const fn can_place_animal(&self, kind: AnimalKind) -> bool {
match self { match self {
CellKind::Sea => false, CellKind::Sea => false,
_ => true, _ => true,
} }
} }
pub const fn grass_wealth(&self) -> Option<usize> { pub const fn grass_wealth(&self) -> Option<WealthType> {
match self { match self {
CellKind::Grass { wealth } => Some(*wealth), CellKind::Grass { wealth } => Some(*wealth),
CellKind::Forest { wealth } => Some( CellKind::Forest { wealth } => Some(wealth.saturating_mul(
wealth.saturating_mul( (CellKind::FOREST.growth_duration() / CellKind::GRASS.growth_duration())
(CellKind::Forest { wealth: 0 } as WealthType,
.regen_full_growth_duration() )),
.as_secs()
/ CellKind::Grass { wealth: 0 }
.regen_full_growth_duration()
.as_secs()) as usize,
),
),
_ => None, _ => None,
} }
} }
@ -105,7 +120,7 @@ impl Cell {
// pub chunk: // pub chunk:
// }; // };
pub type WealthType = usize; pub type WealthType = u16;
const fn wealth_to_unit(wealth: WealthType) -> f32 { const fn wealth_to_unit(wealth: WealthType) -> f32 {
wealth as f32 / WealthType::MAX as f32 wealth as f32 / WealthType::MAX as f32
} }

View File

@ -1,11 +1,16 @@
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc; use std::sync::Arc;
use bevy::prelude::*; use bevy::prelude::*;
use bevy::tasks::futures_lite::{future, FutureExt}; use bevy::tasks::futures_lite::{future, FutureExt};
use bevy::tasks::{block_on, AsyncComputeTaskPool, IoTaskPool, Task}; use bevy::tasks::{block_on, AsyncComputeTaskPool, IoTaskPool, Task};
use rand::rngs::SmallRng;
use rand::seq::IteratorRandom;
use rand::Rng;
use voronoice::Voronoi; 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; use super::Cell;
@ -16,7 +21,23 @@ impl bevy::prelude::Plugin for Plugin {
// Background task that manages terrain // Background task that manages terrain
#[derive(Resource)] #[derive(Resource)]
pub struct GrowThread(pub Task<()>); pub struct GrowThread(
pub Task<()>,
pub async_channel::Sender<Cell>,
pub async_channel::Receiver<Cell>,
);
impl GrowThread {
pub fn new(voronoi: Voronoi, gt: Arc<AtomicU64>, cells: Vec<Cell>, 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)] #[derive(Component)]
struct GetChunkTask(Task<Vec<Cell>>); struct GetChunkTask(Task<Vec<Cell>>);
@ -24,27 +45,115 @@ struct GetChunkTask(Task<Vec<Cell>>);
fn start_grow_task(mut cmds: Commands) {} fn start_grow_task(mut cmds: Commands) {}
fn rx_chunks_updates( fn rx_chunks_updates(
mut chunks: Query<(&Chunk, &mut GetChunkTask, &mut MeshNeedsUpdate)>, mut chunks: Query<(&mut MeshNeedsUpdate)>,
mut cells: Query<&mut Cell>, mut cells: Query<(&mut Cell, &Parent)>,
cells_entities: Res<CellsEntities>, cells_entities: Res<CellsEntities>,
grow_thread: Res<GrowThread>,
) { ) {
for (ch_id, mut task, mut needs_update) in chunks.iter_mut() { while let Ok(c) = grow_thread.2.try_recv() {
let res = block_on(future::poll_once(&mut task.0)); let id = c.voronoi_id;
if let Some(updated_cells) = res { let (mut cell, parent) = cells.get_mut(cells_entities.0[id]).unwrap();
if updated_cells.len() > 0 { *cell = c;
needs_update.0 = true; chunks.get(parent.get()).unwrap().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() })
}
} }
} }
pub async fn grow_thread(voronoi: Voronoi, current_frame: Arc<AtomicU>) {} 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<AtomicU64>,
mut cells: Vec<Cell>,
mut rng: SmallRng,
mut rx_control: async_channel::Receiver<Cell>,
mut tx_update: async_channel::Sender<Cell>,
) {
// 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<MyMapGenTasks>) { // fn begin_generating_map_chunks(mut my_tasks: ResMut<MyMapGenTasks>) {
// let task_pool = AsyncComputeTaskPool::get(); // let task_pool = AsyncComputeTaskPool::get();

View File

@ -1,4 +1,10 @@
use std::time::Duration; use std::{
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
time::Duration,
};
use bevy::prelude::*; use bevy::prelude::*;
@ -7,7 +13,8 @@ impl bevy::prelude::Plugin for Plugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.insert_resource(GameTime { app.insert_resource(GameTime {
current: Duration::ZERO, current: Duration::ZERO,
speed: 24. * 60. * 60. * 10. * 10., secs: Arc::new(AtomicU64::new(0)),
speed: 3600. * 24. * 365.,
}) })
.add_systems(PreUpdate, update_time); .add_systems(PreUpdate, update_time);
} }
@ -16,10 +23,12 @@ impl bevy::prelude::Plugin for Plugin {
#[derive(Resource)] #[derive(Resource)]
pub struct GameTime { pub struct GameTime {
pub current: Duration, pub current: Duration,
pub secs: Arc<AtomicU64>,
pub speed: f32, // = game time / real time pub speed: f32, // = game time / real time
} }
fn update_time(mut gt: ResMut<GameTime>, time: Res<Time>) { fn update_time(mut gt: ResMut<GameTime>, time: Res<Time>) {
let speed = gt.speed; let speed = gt.speed;
gt.current += Duration::from_secs_f32(time.delta_secs() * speed); gt.current += Duration::from_secs_f32(time.delta_secs() * speed);
gt.secs.store(gt.current.as_secs(), Ordering::Release);
} }