Add progress bar for world generation.

Also remove a bunch of spammy logging
This commit is contained in:
Tobias Berger 2022-11-15 19:04:02 +01:00
parent 1a3b4bb72c
commit 9581a5d206
Signed by: toby
GPG key ID: 2D05EFAB764D6A88
21 changed files with 221 additions and 89 deletions

2
Cargo.lock generated
View file

@ -2087,6 +2087,7 @@ name = "planet"
version = "0.3.0"
dependencies = [
"bevy",
"crossbeam-channel",
"postcard",
"rand",
"serde",
@ -3087,6 +3088,7 @@ version = "0.3.0"
dependencies = [
"bevy",
"bevy_egui",
"crossbeam-channel",
"futures-lite",
"fxhash",
"planet",

View file

@ -54,4 +54,9 @@ optional = true
[dependencies.futures-lite]
version = "1.12.0"
default-features = false
default-features = false
[dependencies.crossbeam-channel]
version = "0.5.6"
default-features = false
features = ["std"]

View file

@ -26,4 +26,9 @@ features = ["derive"]
[dependencies.postcard]
version = "1.0.2"
default-features = false
features = ["use-std"]
features = ["use-std"]
[dependencies.crossbeam-channel]
version = "0.5.6"
default-features = false
features = ["std"]

View file

@ -4,7 +4,7 @@ pub mod biome;
pub use biome::{BiomeStats, BiomeType};
pub mod world_manager;
pub use world_manager::WorldManager;
pub(crate) mod macros;
pub mod macros;
pub mod math_util;
pub mod perlin;
pub mod saving;

View file

@ -17,6 +17,7 @@ use {
prelude::Vec2,
utils::{default, HashMap},
},
crossbeam_channel::Sender,
rand::{rngs::StdRng, Rng, SeedableRng},
serde::{Deserialize, Serialize},
std::{
@ -169,18 +170,25 @@ impl World {
}
}
pub fn generate(&mut self) -> Result<(), WorldGenError> {
if let Err(err) = self.generate_altitude() {
pub fn generate(
&mut self,
progress_sender: &Sender<(f32, String)>,
) -> Result<(), WorldGenError> {
send_progress(progress_sender, 0.0, "Generating altitude");
if let Err(err) = self.generate_altitude(progress_sender) {
return Err(WorldGenError::CartesianError(err));
}
if let Err(err) = self.generate_rainfall() {
send_progress(progress_sender, 0.0, "Generating rainfall");
if let Err(err) = self.generate_rainfall(progress_sender) {
return Err(WorldGenError::CartesianError(err));
}
if let Err(err) = self.generate_temperature() {
send_progress(progress_sender, 0.0, "Generating temperature");
if let Err(err) = self.generate_temperature(progress_sender) {
return Err(WorldGenError::CartesianError(err));
}
self.generate_biomes();
send_progress(progress_sender, 0.0, "Generating biomes");
self.generate_biomes(progress_sender);
Ok(())
}
@ -192,8 +200,8 @@ impl World {
let height = self.height as f32;
for i in 0..World::NUM_CONTINENTS {
#[cfg(feature = "logging")]
info!("Continents {}/{}", i, World::NUM_CONTINENTS);
// #[cfg(feature = "logging")]
// info!("Continents: {}/{}", i, World::NUM_CONTINENTS);
self.continent_offsets[i as usize].x = self
.rng
@ -254,7 +262,10 @@ impl World {
max_value
}
fn generate_altitude(&mut self) -> Result<(), CartesianError> {
fn generate_altitude(
&mut self,
progress_sender: &Sender<(f32, String)>,
) -> Result<(), CartesianError> {
info!("Generating altitude");
self.generate_continents();
@ -270,13 +281,20 @@ impl World {
let offset_4 = World::random_offset_vector(&mut self.rng);
let offset_5 = World::random_offset_vector(&mut self.rng);
for y in 0..self.terrain.len() {
#[cfg(feature = "logging")]
info!("Altitude: {}/{}", y, self.terrain.len());
let height = self.terrain.len();
for y in 0..height {
let alpha = (y as f32 / self.height as f32) * PI;
for x in 0..self.terrain[y].len() {
let width = self.terrain[y].len();
let size = height * width;
for x in 0..width {
let index = y * width + x;
send_progress(
progress_sender,
index as f32 / size as f32,
format!("Generating topography: {index}/{size}"),
);
let beta = (x as f32 / self.width as f32) * TAU;
let value_1 =
@ -385,20 +403,27 @@ impl World {
World::MIN_ALTITUDE + (raw_altitude * World::ALTITUDE_SPAN)
}
fn generate_rainfall(&mut self) -> Result<(), CartesianError> {
#[cfg(feature = "logging")]
fn generate_rainfall(
&mut self,
progress_sender: &Sender<(f32, String)>,
) -> Result<(), CartesianError> {
info!("Generating rainfall");
const RADIUS: f32 = 2.0;
let offset = World::random_offset_vector(&mut self.rng);
let height = self.terrain.len();
for y in 0..height {
#[cfg(feature = "logging")]
info!("Rainfall: {}/{}", y, height);
let alpha = (y as f32 / self.height as f32) * PI;
let width = self.terrain[y].len();
let size = width * height;
for x in 0..width {
let index = y * width + x;
send_progress(
progress_sender,
index as f32 / size as f32,
format!("Generating rainfall: {index}/{size}"),
);
let beta = (x as f32 / self.width as f32) * TAU;
let random_noise =
@ -459,17 +484,28 @@ impl World {
)
}
fn generate_temperature(&mut self) -> Result<(), CartesianError> {
#[cfg(feature = "logging")]
fn generate_temperature(
&mut self,
progress_sender: &Sender<(f32, String)>,
) -> Result<(), CartesianError> {
info!("Generating temperature");
let offset = World::random_offset_vector(&mut self.rng);
const RADIUS: f32 = 2.0;
for y in 0..self.terrain.len() {
#[cfg(feature = "logging")]
info!("Temperature: {}/{}", y, self.terrain.len());
let height = self.terrain.len();
for y in 0..height {
let alpha = (y as f32 / self.height as f32) * PI;
for x in 0..self.terrain[y].len() {
let width = self.terrain[y].len();
let size = width * height;
for x in 0..width {
let index = y * width + x;
send_progress(
progress_sender,
index as f32 / size as f32,
format!("Generating temperature: {index}/{size}"),
);
let beta = (x as f32 / self.width as f32) * TAU;
let random_noise =
@ -496,6 +532,7 @@ impl World {
}
}
info!("Done generating temperature");
Ok(())
}
@ -507,12 +544,19 @@ impl World {
)
}
fn generate_biomes(&mut self) {
fn generate_biomes(&mut self, progress_sender: &Sender<(f32, String)>) {
info!("Generating biomes");
for y in 0..self.terrain.len() {
#[cfg(feature = "logging")]
info!("Biomes: {}/{}", y, self.terrain.len());
for x in 0..self.terrain[y].len() {
let height = self.terrain.len();
for y in 0..height {
let width = self.terrain[y].len();
let size = height * width;
for x in 0..width {
let index = y * width + x;
send_progress(
progress_sender,
index as f32 / size as f32,
format!("Generating biomes: {index}/{size}"),
);
let cell = &self.terrain[y][x];
let mut total_presence = 0.0;
@ -534,6 +578,7 @@ impl World {
.collect();
}
}
info!("Done generating biomes");
}
fn biome_presence(&self, cell: &TerrainCell, biome: &BiomeStats) -> f32 {
@ -767,3 +812,15 @@ impl World {
return false;
}
}
fn send_progress<T: Into<String>>(
progress_sender: &Sender<(f32, String)>,
progress: f32,
progress_text: T,
) {
if let Err(_) = progress_sender.try_send((progress, progress_text.into())) {
// Quietly ignore the error, it's not critical, and logging is slow.
// debug!("Failed to send world generation progress. {err}");
}
}

View file

@ -6,6 +6,7 @@ use {
tasks::{AsyncComputeTaskPool, Task},
utils::default,
},
crossbeam_channel::Sender,
rand::random,
std::{
error::Error,
@ -99,7 +100,6 @@ impl WorldManager {
return Err(SaveError::MissingWorld);
};
let serialized = match postcard::to_stdvec(world) {
Ok(serialized) => serialized,
Err(err) => {
@ -124,7 +124,7 @@ impl WorldManager {
if let Err(err) = file.read_to_end(&mut buf) {
return Err(LoadError::MissingSave(err));
};
match postcard::from_bytes(buf.as_slice()) {
Ok(world) => {
self.world = Some(world);
@ -139,11 +139,15 @@ impl WorldManager {
self.world.as_ref()
}
pub fn set_world(&mut self, world: World) {
pub fn set_world(&mut self, world: World) {
self.world = Some(world);
}
pub fn new_world_async(&mut self, seed: Option<u32>) -> Task<Result<World, WorldGenError>> {
pub fn new_world_async(
&mut self,
seed: Option<u32>,
progress_sender: Sender<(f32, String)>,
) -> Task<Result<World, WorldGenError>> {
AsyncComputeTaskPool::get().spawn(async move {
let seed = seed.unwrap_or_else(random);
let mut new_world = World::async_new(
@ -151,7 +155,18 @@ impl WorldManager {
WorldManager::NEW_WORLD_HEIGHT,
seed,
);
match new_world.generate() {
if let Err(_) =
progress_sender.try_send((0.0, String::from("Generating new world...")))
{
// Quietly ignore. It's not critical and logging is slow.
}
let result = new_world.generate(&progress_sender);
if let Err(_) =
progress_sender.try_send((1.0, String::from("Done generating world!")))
{
// Quietly ignore. See above
}
match result {
Ok(()) => Ok(new_world),
Err(err) => Err(err),
}

View file

@ -1,9 +1,9 @@
pub(crate) mod widget;
pub(crate) use widget::*;
pub(crate) mod window;
pub(crate) use window::*;
pub mod widget;
pub use widget::*;
pub mod window;
pub use window::*;
pub(crate) mod widgets;
pub(crate) mod windows;
pub mod widgets;
pub mod windows;
use crate::gui::{open_window, WidgetId, WidgetSystem};

View file

@ -14,11 +14,11 @@ use {
std::hash::Hasher,
};
pub(crate) trait WidgetSystem: SystemParam {
pub trait WidgetSystem: SystemParam {
fn render(world: &mut World, state: &mut SystemState<Self>, ui: &mut Ui, id: WidgetId);
}
pub(crate) fn widget<S: 'static + WidgetSystem>(world: &mut World, ui: &mut Ui, id: WidgetId) {
pub fn widget<S: 'static + WidgetSystem>(world: &mut World, ui: &mut Ui, id: WidgetId) {
// We need to cache `SystemState` to allow for a system's locally tracked state
if !world.contains_resource::<StateInstances<S>>() {
// Note, this message should only appear once! If you see it twice in the logs,
@ -51,10 +51,10 @@ struct StateInstances<T: WidgetSystem + 'static> {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct WidgetId(pub(crate) u64);
pub struct WidgetId(pub u64);
impl WidgetId {
#[must_use]
pub(crate) fn new(name: &str) -> Self {
pub fn new(name: &str) -> Self {
let bytes = name.as_bytes();
let mut hasher = FxHasher32::default();
hasher.write(bytes);
@ -62,7 +62,7 @@ impl WidgetId {
}
// #[must_use]
// pub(crate) fn with(&self, name: &str) -> Self {
// pub fn with(&self, name: &str) -> Self {
// Self::new(&format!("{}{name}", self.0))
// }
}

View file

@ -1,2 +1,2 @@
mod toolbar;
pub(crate) use toolbar::ToolbarWidget;
pub use toolbar::ToolbarWidget;

View file

@ -7,7 +7,7 @@ use {
WidgetSystem,
},
macros::iterable_enum,
resources::{GenerateWorldTask, OpenedWindows},
resources::{GenerateWorldProgressChannel, GenerateWorldTask, OpenedWindows},
},
bevy::{
ecs::{
@ -34,11 +34,13 @@ impl ToolbarButton {
world.resource_scope(|world, mut world_manager: Mut<WorldManager>| {
match self {
ToolbarButton::GenerateWorld => {
let progress_sender = world.resource::<GenerateWorldProgressChannel>().sender();
let generate_world_task = &mut world.resource_mut::<GenerateWorldTask>();
if generate_world_task.0.is_some() {
debug!("Already generating new world")
} else {
generate_world_task.0 = Some(world_manager.new_world_async(None))
generate_world_task.0 =
Some(world_manager.new_world_async(None, progress_sender))
}
},
ToolbarButton::SaveLoad => {
@ -87,7 +89,7 @@ impl From<&ToolbarButton> for String {
}
#[derive(SystemParam)]
pub(crate) struct ToolbarWidget<'w, 's> {
pub struct ToolbarWidget<'w, 's> {
#[system_param(ignore)]
_phantom: PhantomData<(&'w (), &'s ())>,
}

View file

@ -16,13 +16,13 @@ use {
std::hash::Hasher,
};
pub(crate) trait WindowSystem: SystemParam {
pub trait WindowSystem: SystemParam {
fn draw_contents(world: &mut World, state: &mut SystemState<Self>, ui: &mut Ui);
fn name() -> &'static str;
fn resizable() -> bool;
}
pub(crate) fn render_windows(world: &mut World, ctx: &Context) {
pub fn render_windows(world: &mut World, ctx: &Context) {
// TODO: Windows are hard-coded here instead of being iterable, and allows
// creating new windows that are never rendered.
// Is that good enough?
@ -32,10 +32,10 @@ pub(crate) fn render_windows(world: &mut World, ctx: &Context) {
window::<windows::SaveLoad>(world, ctx);
}
pub(crate) fn open_window<S: 'static + WindowSystem>(windows: &mut OpenedWindows) {
pub fn open_window<S: 'static + WindowSystem>(windows: &mut OpenedWindows) {
windows.open(S::name().into());
}
pub(crate) fn close_window<S: 'static + WindowSystem>(windows: &mut OpenedWindows) {
pub fn close_window<S: 'static + WindowSystem>(windows: &mut OpenedWindows) {
windows.close(&S::name().into());
}
@ -87,10 +87,10 @@ struct StateInstances<T: WindowSystem + 'static> {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct WindowId(pub(crate) u64);
pub struct WindowId(pub u64);
impl WindowId {
#[must_use]
pub(crate) fn new(name: &str) -> Self {
pub fn new(name: &str) -> Self {
let bytes = name.as_bytes();
let mut hasher = FxHasher32::default();
hasher.write(bytes);
@ -98,7 +98,7 @@ impl WindowId {
}
// #[must_use]
// pub(crate) fn with(&self, name: &str) -> Self {
// pub fn with(&self, name: &str) -> Self {
// Self::new(&format!("{}{name}", self.0))
// }
}

View file

@ -1,8 +1,8 @@
mod tile_info;
pub(crate) use tile_info::TileInfo;
pub use tile_info::TileInfo;
mod world_view_selection;
pub(crate) use world_view_selection::WorldViewSelection;
pub use world_view_selection::WorldViewSelection;
mod world_overlay_selection;
pub(crate) use world_overlay_selection::WorldOverlaySelection;
pub use world_overlay_selection::WorldOverlaySelection;
mod save_load;
pub(crate) use save_load::SaveLoad;
pub use save_load::SaveLoad;

View file

@ -14,7 +14,7 @@ use {
};
#[derive(SystemParam)]
pub(crate) struct SaveLoad<'w, 's> {
pub struct SaveLoad<'w, 's> {
pub file_name: Local<'s, String>,
#[system_param(ignore)]
_phantom: PhantomData<(&'w (), &'s ())>,

View file

@ -10,7 +10,7 @@ use {
};
#[derive(SystemParam)]
pub(crate) struct TileInfo<'w, 's> {
pub struct TileInfo<'w, 's> {
#[system_param(ignore)]
_phantom: PhantomData<(&'w (), &'s ())>,
}

View file

@ -14,7 +14,7 @@ use {
};
#[derive(SystemParam)]
pub(crate) struct WorldOverlaySelection<'w, 's> {
pub struct WorldOverlaySelection<'w, 's> {
#[system_param(ignore)]
_phantom: PhantomData<(&'w (), &'s ())>,
}

View file

@ -14,7 +14,7 @@ use {
};
#[derive(SystemParam)]
pub(crate) struct WorldViewSelection<'w, 's> {
pub struct WorldViewSelection<'w, 's> {
#[system_param(ignore)]
_phantom: PhantomData<(&'w (), &'s ())>,
}

View file

@ -3,23 +3,24 @@
use {
crate::resources::GenerateWorldTask,
futures_lite::future::{block_on, poll_once},
resources::GenerateWorldProgressChannel,
};
pub(crate) mod components;
pub mod components;
#[cfg(feature = "render")]
pub(crate) mod gui;
pub(crate) mod macros;
pub mod gui;
pub mod macros;
#[cfg(feature = "render")]
pub(crate) mod planet_renderer;
pub(crate) mod plugins;
pub(crate) mod resources;
pub mod planet_renderer;
pub mod plugins;
pub mod resources;
use {bevy::prelude::*, planet::WorldManager, plugins::WorldPlugins};
#[cfg(feature = "render")]
use {
bevy::render::camera::RenderTarget,
bevy_egui::{
egui::{FontData, FontDefinitions, FontFamily},
egui::{FontData, FontDefinitions, FontFamily, ProgressBar},
EguiContext,
},
gui::{render_windows, widget, widgets::ToolbarWidget, window::open_window, windows::TileInfo},
@ -73,6 +74,9 @@ fn handle_generate_world_task(
mut generate_world_task: ResMut<GenerateWorldTask>,
mut world_manager: ResMut<WorldManager>,
#[cfg(feature = "render")] mut should_redraw: ResMut<ShouldRedraw>,
#[cfg(feature = "render")] mut egui_ctx: ResMut<'_, EguiContext>,
#[cfg(feature = "render")] progress_channel: Res<'_, GenerateWorldProgressChannel>,
#[cfg(feature = "render")] mut progress: Local<(f32, String)>,
) {
if let Some(task) = &mut generate_world_task.0 {
if task.is_finished() {
@ -92,8 +96,24 @@ fn handle_generate_world_task(
}
}
generate_world_task.0 = None;
#[cfg(feature = "render")]
{
*progress = (0.0, String::from("Generating world..."));
}
} else {
debug!("Still generating world")
debug!("Still generating world");
#[cfg(feature = "render")]
{
if let Ok(new_progress) = progress_channel.receiver().try_recv() {
*progress = new_progress;
}
_ = bevy_egui::egui::TopBottomPanel::bottom("Generating World ProgressBar")
.default_height(8.0)
.show(egui_ctx.ctx_mut(), |ui| {
ui.add(ProgressBar::new(progress.0).text(progress.1.as_str()));
});
}
}
}
}
@ -293,9 +313,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
app.insert_resource(WorldManager::new())
.insert_resource(GenerateWorldTask(
/* Some(manager.new_world_async(Some(0))) */ None,
))
.insert_resource(GenerateWorldProgressChannel::new())
.insert_resource(GenerateWorldTask(None))
.add_system(handle_generate_world_task)
.run();

View file

@ -166,7 +166,7 @@ fn coastline_color(world: &World, cell: &TerrainCell) -> Color {
COASTLINE_PALETTE[0]
}
}
pub(crate) trait WorldRenderer {
pub trait WorldRenderer {
fn map_color_bytes(&self, render_settings: &WorldRenderSettings) -> Vec<u8>;
fn generate_color(&self, cell: &TerrainCell, render_settings: &WorldRenderSettings) -> Color;
}

View file

@ -1,2 +1,2 @@
pub(crate) mod world_plugins;
pub(crate) use world_plugins::WorldPlugins;
pub mod world_plugins;
pub use world_plugins::WorldPlugins;

View file

@ -1,4 +1,4 @@
pub(crate) struct WorldPlugins;
pub struct WorldPlugins;
#[cfg(not(feature = "render"))]
use bevy::app::ScheduleRunnerPlugin;

View file

@ -2,14 +2,15 @@
use {crate::gui::WindowId, bevy::utils::HashSet, std::fmt::Display};
use {
bevy::{prelude::Resource, tasks::Task},
crossbeam_channel::{bounded, Receiver, Sender},
planet::{World, WorldGenError},
};
#[cfg(feature = "render")]
#[derive(Default, Debug, Resource)]
pub(crate) struct CursorMapPosition {
pub(crate) x: i32,
pub(crate) y: i32,
pub struct CursorMapPosition {
pub x: i32,
pub y: i32,
}
#[cfg(feature = "render")]
impl Display for CursorMapPosition {
@ -20,27 +21,53 @@ impl Display for CursorMapPosition {
#[cfg(feature = "render")]
#[derive(Resource, Default)]
pub(crate) struct ShouldRedraw(pub(crate) bool);
pub struct ShouldRedraw(pub bool);
#[cfg(feature = "render")]
#[derive(Default, Resource)]
pub(crate) struct OpenedWindows(HashSet<WindowId>);
pub struct OpenedWindows(HashSet<WindowId>);
#[cfg(feature = "render")]
impl OpenedWindows {
pub(crate) fn open(&mut self, id: WindowId) {
pub fn open(&mut self, id: WindowId) {
// Ignore opening already opened windows
_ = self.0.insert(id);
}
pub(crate) fn close(&mut self, id: &WindowId) {
pub fn close(&mut self, id: &WindowId) {
// Ignore closing already closed windows
_ = self.0.remove(id);
}
pub(crate) fn is_open(&self, id: &WindowId) -> bool {
pub fn is_open(&self, id: &WindowId) -> bool {
self.0.contains(id)
}
}
#[derive(Resource)]
pub struct GenerateWorldProgressChannel(Sender<(f32, String)>, Receiver<(f32, String)>);
impl GenerateWorldProgressChannel {
pub fn new() -> Self {
bounded(1).into()
}
pub fn sender(&self) -> Sender<(f32, String)> {
self.0.clone()
}
pub fn receiver(&self) -> &Receiver<(f32, String)> {
&self.1
}
}
impl Default for GenerateWorldProgressChannel {
fn default() -> Self {
Self::new()
}
}
impl From<(Sender<(f32, String)>, Receiver<(f32, String)>)> for GenerateWorldProgressChannel {
fn from(value: (Sender<(f32, String)>, Receiver<(f32, String)>)) -> Self {
Self(value.0, value.1)
}
}
#[derive(Default, Resource)]
pub struct GenerateWorldTask(pub Option<Task<Result<World, WorldGenError>>>);