Make world generation asynchronous; Update dependencies (Bevy 0.9, ayyy)

This commit is contained in:
Tobias Berger 2022-11-13 21:22:42 +01:00
parent 030f590d79
commit f65f29e6b0
Signed by: toby
GPG key ID: 2D05EFAB764D6A88
17 changed files with 535 additions and 503 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
/target
.fleet/run.json

614
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "worlds-history-sim-rs"
version = "0.2.1"
version = "0.3.0"
edition = "2021"
resolver = "2"
@ -35,7 +35,7 @@ path = "planet"
default-features = false
[dependencies.bevy]
version = "0.8.1"
version = "0.9.0"
default-features = false
[dependencies.fxhash]
@ -43,7 +43,7 @@ version = "0.2.1"
optional = true
[dependencies.bevy_egui]
version = "0.16.1"
version = "0.17"
optional = true
default-features = false
features = ["manage_clipboard"]
@ -51,3 +51,7 @@ features = ["manage_clipboard"]
[dependencies.tinyfiledialogs]
version = "3.9.1"
optional = true
[dependencies.futures-lite]
version = "1.12.0"
default-features = false

View file

@ -1,6 +1,6 @@
[package]
name = "planet"
version = "0.2.1"
version = "0.3.0"
edition = "2021"
[profile]
@ -15,7 +15,7 @@ default = ["logging", "render"]
version = "0.8.5"
[dependencies.bevy]
version = "0.8"
version = "0.9.0"
default-features = false
[dependencies.serde]
@ -24,4 +24,4 @@ default-features = false
features = ["derive"]
[dependencies.ron]
version = "0.7.1"
version = "0.8.0"

View file

@ -1,4 +1,3 @@
// TODO: Logging doesn't seem to work here? Figure out why and fix
use {
crate::{
math_util::{
@ -149,6 +148,27 @@ impl World {
}
}
pub fn async_new(width: u32, height: u32, seed: u32) -> World {
World {
width,
height,
seed,
terrain: vec![
vec![TerrainCell::default(); width.try_into().unwrap()];
height.try_into().unwrap()
],
continent_offsets: [default(); World::NUM_CONTINENTS as usize],
continent_sizes: [default(); World::NUM_CONTINENTS as usize],
max_altitude: World::MIN_ALTITUDE,
min_altitude: World::MAX_ALTITUDE,
max_rainfall: World::MIN_RAINFALL,
min_rainfall: World::MAX_RAINFALL,
max_temperature: World::MIN_TEMPERATURE,
min_temperature: World::MAX_TEMPERATURE,
rng: StdRng::seed_from_u64(seed as u64),
}
}
pub fn generate(&mut self) -> Result<(), WorldGenError> {
if let Err(err) = self.generate_altitude() {
return Err(WorldGenError::CartesianError(err));

View file

@ -1,6 +1,11 @@
use {
crate::{World, WorldGenError},
bevy::{log::warn, utils::default},
bevy::{
log::warn,
prelude::Resource,
tasks::{AsyncComputeTaskPool, Task},
utils::default,
},
rand::random,
std::{
error::Error,
@ -14,7 +19,7 @@ use {
#[derive(Debug)]
pub enum LoadError {
MissingSave(io::Error),
InvalidSave(ron::Error),
InvalidSave(ron::error::SpannedError),
}
impl Error for LoadError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
@ -74,12 +79,15 @@ impl Display for SaveError {
}
}
#[derive(Debug, Default)]
#[derive(Debug, Default, Resource)]
pub struct WorldManager {
world: Option<World>,
}
impl WorldManager {
const NEW_WORLD_HEIGHT: u32 = 200;
const NEW_WORLD_WIDTH: u32 = 400;
#[must_use]
pub fn new() -> WorldManager {
default()
@ -160,11 +168,34 @@ impl WorldManager {
self.get_world().unwrap()
}
pub fn set_world(&mut self, world: World) {
self.world = Some(world);
}
pub fn new_world(&mut self, seed: Option<u32>) -> Result<&World, WorldGenError> {
let seed = seed.unwrap_or_else(random);
let mut new_world = World::new(400, 200, seed);
let mut new_world = World::new(
WorldManager::NEW_WORLD_WIDTH,
WorldManager::NEW_WORLD_HEIGHT,
seed,
);
new_world.generate()?;
self.world = Some(new_world);
Ok(self.get_world().unwrap())
}
pub fn new_world_async(&mut self, seed: Option<u32>) -> Task<Result<World, WorldGenError>> {
AsyncComputeTaskPool::get().spawn(async move {
let seed = seed.unwrap_or_else(random);
let mut new_world = World::async_new(
WorldManager::NEW_WORLD_WIDTH,
WorldManager::NEW_WORLD_HEIGHT,
seed,
);
match new_world.generate() {
Ok(()) => Ok(new_world),
Err(err) => Err(err),
}
})
}
}

View file

@ -1,2 +1 @@
#[cfg(feature = "render")]
pub(crate) mod panning;

View file

@ -1,12 +0,0 @@
use bevy::ecs::component::Component;
#[derive(Component)]
pub(crate) struct Pan2d {
pub(crate) enabled: bool,
}
impl Pan2d {
#[must_use]
pub(crate) const fn new() -> Pan2d {
Pan2d { enabled: true }
}
}

View file

@ -6,6 +6,7 @@ use {
world::World,
},
log::debug,
prelude::*,
utils::HashMap,
},
bevy_egui::egui::Ui,
@ -44,8 +45,8 @@ pub(crate) fn widget<S: 'static + WidgetSystem>(world: &mut World, ui: &mut Ui,
/// A UI widget may have multiple instances. We need to ensure the local state
/// of these instances is not shared. This hashmap allows us to dynamically
/// store instance states.
#[derive(Default)]
struct StateInstances<T: WidgetSystem> {
#[derive(Default, Resource)]
struct StateInstances<T: WidgetSystem + 'static> {
instances: HashMap<WidgetId, SystemState<T>>,
}

View file

@ -7,7 +7,7 @@ use {
WidgetSystem,
},
macros::iterable_enum,
resources::{OpenedWindows, ShouldRedraw},
resources::{GenerateWorldTask, OpenedWindows},
},
bevy::{
ecs::{
@ -34,10 +34,11 @@ impl ToolbarButton {
world.resource_scope(|world, mut world_manager: Mut<'_, WorldManager>| {
match self {
ToolbarButton::GenerateWorld => {
if let Err(err) = world_manager.new_world(None) {
eprintln!("Failed to generate world: {}", err);
let generate_world_task = &mut world.resource_mut::<GenerateWorldTask>();
if generate_world_task.0.is_some() {
debug!("Already generating new world")
} else {
world.resource_mut::<ShouldRedraw>().0 = true;
generate_world_task.0 = Some(world_manager.new_world_async(None))
}
},
ToolbarButton::SaveLoad => {

View file

@ -8,6 +8,7 @@ use {
world::World,
},
log::debug,
prelude::Resource,
utils::HashMap,
},
bevy_egui::egui::{Context, Ui, Window},
@ -80,8 +81,8 @@ fn window<S: 'static + WindowSystem>(world: &mut World, ctx: &Context) {
});
}
#[derive(Default)]
struct StateInstances<T: WindowSystem> {
#[derive(Default, Resource)]
struct StateInstances<T: WindowSystem + 'static> {
instances: HashMap<WindowId, SystemState<T>>,
}

View file

@ -1,5 +1,10 @@
#![cfg_attr(not(feature = "logging"), windows_subsystem = "windows")]
use {
crate::resources::GenerateWorldTask,
futures_lite::future::{block_on, poll_once},
};
pub(crate) mod components;
#[cfg(feature = "render")]
pub(crate) mod gui;
@ -7,52 +12,16 @@ pub(crate) mod macros;
#[cfg(feature = "render")]
pub(crate) mod planet_renderer;
pub(crate) mod plugins;
#[cfg(feature = "render")]
pub(crate) mod resources;
use {
bevy::{
app::App,
log::LogSettings,
utils::{default, tracing::Level},
},
planet::WorldManager,
plugins::WorldPlugins,
};
use {bevy::prelude::*, planet::WorldManager, plugins::WorldPlugins};
#[cfg(feature = "render")]
use {
bevy::{
asset::Assets,
core_pipeline::core_2d::{Camera2d, Camera2dBundle},
ecs::{
change_detection::{Mut, ResMut},
query::With,
system::{Commands, IntoExclusiveSystem, Query, Res},
world::World,
},
input::{keyboard::KeyCode, Input},
prelude::Vec2,
render::{
camera::{Camera, RenderTarget},
render_resource::{
Extent3d,
TextureDescriptor,
TextureDimension,
TextureFormat,
TextureUsages,
},
texture::{Image, ImageSettings},
},
sprite::{Sprite, SpriteBundle},
transform::components::GlobalTransform,
window::{WindowDescriptor, Windows},
winit::WinitSettings,
},
bevy::render::camera::RenderTarget,
bevy_egui::{
egui::{FontData, FontDefinitions, FontFamily},
EguiContext,
},
components::panning::Pan2d,
gui::{render_windows, widget, widgets::ToolbarWidget, window::open_window, windows::TileInfo},
planet_renderer::{WorldRenderSettings, WorldRenderer},
resources::{CursorMapPosition, OpenedWindows, ShouldRedraw},
@ -98,16 +67,45 @@ fn update_cursor_map_position(
}
}
fn handle_generate_world_task(
mut generate_world_task: ResMut<'_, GenerateWorldTask>,
mut world_manager: ResMut<'_, WorldManager>,
#[cfg(feature = "render")] mut should_redraw: ResMut<'_, ShouldRedraw>,
) {
if let Some(task) = &mut generate_world_task.0 {
if task.is_finished() {
debug!("Done");
if let Some(result) = block_on(poll_once(task)) {
match result {
Ok(world) => {
world_manager.set_world(world);
#[cfg(feature = "render")]
{
should_redraw.0 = true;
}
},
Err(err) => error!("{err:#?}"),
}
}
generate_world_task.0 = None;
} else {
debug!("Working")
}
}
}
#[cfg(feature = "render")]
fn generate_graphics(
mut commands: Commands<'_, '_>,
world_manager: ResMut<'_, WorldManager>,
mut images: ResMut<'_, Assets<Image>>,
mut egui_context: ResMut<'_, EguiContext>,
mut render_settings: ResMut<'_, WorldRenderSettings>,
images: ResMut<'_, Assets<Image>>,
egui_context: ResMut<'_, EguiContext>,
render_settings: ResMut<'_, WorldRenderSettings>,
) {
// Add Julia-Mono font to egui
{
let egui_context = egui_context.into_inner();
let ctx = egui_context.ctx_mut();
let mut fonts = FontDefinitions::default();
const FONT_NAME: &str = "Julia-Mono";
@ -143,6 +141,14 @@ fn generate_graphics(
};
// Set up 2D map mode
{
use bevy::render::render_resource::{
TextureDescriptor,
TextureDimension,
TextureFormat,
TextureUsages,
};
let images = images.into_inner();
let mut render_settings = render_settings.into_inner();
let map_image_handle = images.add(Image {
data: vec![],
texture_descriptor: TextureDescriptor {
@ -156,14 +162,13 @@ fn generate_graphics(
},
..default()
});
render_settings.map_image_handle_id = Some(map_image_handle.id);
_ = commands
.spawn_bundle(Camera2dBundle::default())
.insert(Pan2d::new());
render_settings.map_image_handle_id = Some(map_image_handle.id());
_ = commands.spawn(Camera2dBundle::default());
// TODO: Switch to egui
_ = commands.spawn_bundle(SpriteBundle {
texture: images.get_handle(map_image_handle.id),
_ = commands.spawn(SpriteBundle {
texture: images
.get_handle(unsafe { render_settings.map_image_handle_id.unwrap_unchecked() }),
sprite: Sprite {
custom_size: Some(custom_sprite_size),
..default()
@ -234,7 +239,7 @@ fn redraw_map(
let map_image = images
.get_mut(&map_image_handle)
.expect("Map image handle pointing to non-existing image");
map_image.resize(Extent3d {
map_image.resize(bevy::render::render_resource::Extent3d {
width: world_manager.world().width,
height: world_manager.world().height,
depth_or_array_layers: 1,
@ -246,48 +251,49 @@ fn redraw_map(
}
#[cfg(feature = "render")]
const WORLD_SCALE: i32 = 4;
const WORLD_SCALE: i32 = 2;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut app = App::new();
let mut manager = WorldManager::new();
#[cfg(feature = "render")]
{
let world = manager.new_world(None)?;
use bevy::winit::WinitSettings;
let world = manager.new_world(Some(0))?;
_ = app
.insert_resource(WinitSettings::game())
// Use nearest-neighbor rendering for cripsier pixels
.insert_resource(ImageSettings::default_nearest())
.insert_resource(WindowDescriptor {
.insert_resource(CursorMapPosition::default())
.insert_resource(OpenedWindows::default())
.insert_resource(WorldRenderSettings::default())
.insert_resource(ShouldRedraw::default())
.insert_resource(GenerateWorldTask::default())
.add_startup_system(generate_graphics)
.add_system(update_gui)
.add_system(update_cursor_map_position)
.add_system(open_tile_info)
.add_system(redraw_map);
app.add_plugins(WorldPlugins.set(WindowPlugin {
window: WindowDescriptor {
width: (WORLD_SCALE * world.width as i32) as f32,
height: (WORLD_SCALE * world.height as i32) as f32,
title: String::from("World-RS"),
resizable: true,
..default()
})
.insert_resource(CursorMapPosition::default())
.insert_resource(OpenedWindows::default())
.insert_resource(WorldRenderSettings::default())
.insert_resource(ShouldRedraw::default())
.add_startup_system(generate_graphics)
.add_system(update_gui.exclusive_system())
.add_system(update_cursor_map_position)
.add_system(open_tile_info)
.add_system(redraw_map);
},
..default()
}));
}
#[cfg(not(feature = "render"))]
{
_ = manager.new_world()?
_ = manager.new_world(Some(0))?;
app.add_plugins(WorldPlugins);
}
_ = app.insert_resource(LogSettings {
#[cfg(feature = "logging")]
level: Level::DEBUG,
#[cfg(not(feature = "logging"))]
level: Level::WARN,
..default()
});
app.add_plugins(WorldPlugins).insert_resource(manager).run();
app.add_system(handle_generate_world_task)
.insert_resource(manager)
.run();
Ok(())
}

View file

@ -1,6 +1,10 @@
use {
crate::macros::iterable_enum_stringify,
bevy::{asset::HandleId, prelude::Color, utils::HashSet},
bevy::{
asset::HandleId,
prelude::{Color, Resource},
utils::HashSet,
},
planet::{BiomeStats, TerrainCell, World, WorldManager},
};
@ -15,7 +19,7 @@ iterable_enum_stringify!(WorldOverlay {
});
#[cfg(feature = "render")]
#[derive(Debug, Default)]
#[derive(Debug, Default, Resource)]
pub struct WorldRenderSettings {
pub map_image_handle_id: Option<HandleId>,

View file

@ -1,4 +1,2 @@
#[cfg(feature = "render")]
pub(crate) mod panning_plugin;
pub(crate) mod world_plugins;
pub(crate) use world_plugins::WorldPlugins;

View file

@ -1,47 +0,0 @@
use {
crate::components::panning::Pan2d,
bevy::{
app::{App, Plugin},
ecs::{
event::EventReader,
query::With,
system::{Query, Res},
},
input::{
mouse::{MouseButton, MouseMotion},
Input,
},
sprite::Sprite,
transform::components::Transform,
},
};
#[derive(Default)]
pub(crate) struct PanningPlugin;
impl Plugin for PanningPlugin {
fn build(&self, app: &mut App) {
_ = app.add_system(panning_system_2d);
}
}
fn panning_system_2d(
mut query: Query<'_, '_, (&mut Transform, &Pan2d), With<Sprite>>,
mut mouse_motion_events: EventReader<'_, '_, MouseMotion>,
input_mouse: Res<'_, Input<MouseButton>>,
) {
if !input_mouse.pressed(MouseButton::Left) {
return;
}
let mut horizontal = 0.0;
for movement in mouse_motion_events.iter() {
horizontal += movement.delta.x;
}
query.for_each_mut(|(mut transform, pan)| {
if pan.enabled {
transform.translation.x += horizontal;
}
});
}

View file

@ -1,25 +1,21 @@
pub(crate) struct WorldPlugins;
#[cfg(not(feature = "render"))]
use bevy::app::ScheduleRunnerPlugin;
#[cfg(all(feature = "logging"))]
use bevy::diagnostic::{DiagnosticsPlugin, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin};
use bevy::{
app::{PluginGroup, PluginGroupBuilder},
core::CorePlugin,
diagnostic::{DiagnosticsPlugin, LogDiagnosticsPlugin},
log::LogPlugin,
log::{Level, LogPlugin},
prelude::*,
time::TimePlugin,
};
impl PluginGroup for WorldPlugins {
fn build(&mut self, group: &mut PluginGroupBuilder) {
_ = group.add(LogPlugin).add(CorePlugin).add(TimePlugin);
#[cfg(feature = "render")]
{
use {
crate::plugins::panning_plugin::PanningPlugin,
#[cfg(feature = "render")]
use {
bevy::{
asset::AssetPlugin,
core_pipeline::CorePipelinePlugin,
hierarchy::HierarchyPlugin,
input::InputPlugin,
render::RenderPlugin,
sprite::SpritePlugin,
@ -30,36 +26,50 @@ impl PluginGroup for WorldPlugins {
winit::WinitPlugin,
},
bevy_egui::EguiPlugin,
};
};
_ = group
impl PluginGroup for WorldPlugins {
fn build(self) -> PluginGroupBuilder {
let mut group_builder = PluginGroupBuilder::start::<Self>()
.add(LogPlugin {
#[cfg(feature = "logging")]
level: Level::DEBUG,
#[cfg(not(feature = "logging"))]
level: Level::WARN,
..default()
})
.add(CorePlugin::default()) // sets compute pool config
.add(TimePlugin);
#[cfg(feature = "render")]
{
group_builder = group_builder
.add(TransformPlugin)
// hierarchy
.add(InputPlugin)
.add(WindowPlugin)
.add(AssetPlugin)
.add(HierarchyPlugin)
.add(WinitPlugin)
.add(WindowPlugin::default())
.add(AssetPlugin::default())
.add(RenderPlugin)
.add(ImagePlugin::default_nearest())
.add(WinitPlugin)
.add(CorePipelinePlugin)
.add(SpritePlugin)
.add(TextPlugin)
.add(UiPlugin)
.add(PanningPlugin)
.add(EguiPlugin);
}
#[cfg(not(feature = "render"))]
{
use bevy::app::ScheduleRunnerPlugin;
_ = group.add(ScheduleRunnerPlugin);
group_builder = group_builder.add(ScheduleRunnerPlugin);
}
_ = group.add(DiagnosticsPlugin);
#[cfg(all(feature = "logging"))]
#[cfg(feature = "logging")]
{
use bevy::diagnostic::FrameTimeDiagnosticsPlugin;
_ = group.add(FrameTimeDiagnosticsPlugin);
group_builder = group_builder
.add(DiagnosticsPlugin)
.add(FrameTimeDiagnosticsPlugin)
.add(LogDiagnosticsPlugin::default());
}
_ = group.add(LogDiagnosticsPlugin::default());
group_builder
}
}

View file

@ -1,26 +1,38 @@
#[cfg(feature = "render")]
use {crate::gui::WindowId, bevy::utils::HashSet, std::fmt::Display};
use {
bevy::{prelude::Resource, tasks::Task},
planet::{World, WorldGenError},
};
#[derive(Default, Debug)]
#[cfg(feature = "render")]
#[derive(Default, Debug, Resource)]
pub(crate) struct CursorMapPosition {
pub(crate) x: i32,
pub(crate) y: i32,
}
#[cfg(feature = "render")]
impl Display for CursorMapPosition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("x: {}, y: {}", self.x, self.y))
}
}
#[cfg(feature = "render")]
#[derive(Resource)]
pub(crate) struct ShouldRedraw(pub(crate) bool);
#[cfg(feature = "render")]
impl Default for ShouldRedraw {
fn default() -> Self {
Self(true)
}
}
#[derive(Default)]
#[cfg(feature = "render")]
#[derive(Default, Resource)]
pub(crate) struct OpenedWindows(HashSet<WindowId>);
#[cfg(feature = "render")]
impl OpenedWindows {
pub(crate) fn open(&mut self, id: WindowId) {
// Ignore opening already opened windows
@ -36,3 +48,6 @@ impl OpenedWindows {
self.0.contains(id)
}
}
#[derive(Default, Resource)]
pub struct GenerateWorldTask(pub Option<Task<Result<World, WorldGenError>>>);