Add panning, zooming, and info box
011cd40da8b4a8ae3324d1b0cf6f216641d44225
This commit is contained in:
parent
6774fcd2fa
commit
26d3e9b4a6
7 changed files with 179 additions and 29 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -427,6 +427,15 @@ dependencies = [
|
|||
"glam",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_pancam"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ebc698de3f4e824a67f517fe9cca35f08d5fdaab99ed8e141102c582768fc62"
|
||||
dependencies = [
|
||||
"bevy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_pbr"
|
||||
version = "0.8.1"
|
||||
|
@ -2645,6 +2654,7 @@ name = "worlds-sim-rust"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bevy",
|
||||
"bevy_pancam",
|
||||
"save",
|
||||
]
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ resolver = "2"
|
|||
|
||||
[features]
|
||||
debug = ["save/debug"]
|
||||
render = ["bevy/bevy_asset", "bevy/bevy_winit", "bevy/render", "save/render"]
|
||||
render = ["bevy/bevy_asset", "bevy/bevy_winit", "bevy/render", "save/render", "dep:bevy_pancam"]
|
||||
default = ["render", "debug"]
|
||||
|
||||
[dependencies.save]
|
||||
|
@ -15,3 +15,7 @@ path = "save"
|
|||
[dependencies.bevy]
|
||||
version = "0.8"
|
||||
default-features = false
|
||||
|
||||
[dependencies.bevy_pancam]
|
||||
version = "0.6.1"
|
||||
optional = true
|
|
@ -4,7 +4,9 @@ use std::{
|
|||
fmt::{Debug, Display},
|
||||
};
|
||||
|
||||
use bevy::{math::Vec3A, prelude::Vec2, utils::default};
|
||||
// TODO: Logging doesn't seem to work here? Figure out why and fix
|
||||
|
||||
use bevy::{log::info, math::Vec3A, prelude::Vec2, utils::default};
|
||||
use noise::{NoiseFn, Perlin, Seedable};
|
||||
use rand::Rng;
|
||||
|
||||
|
@ -145,11 +147,14 @@ impl World {
|
|||
}
|
||||
|
||||
fn generate_continents(&mut self) {
|
||||
info!("Generating continents");
|
||||
let mut rng = rand::thread_rng();
|
||||
let width = self.width as f32;
|
||||
let height = self.height as f32;
|
||||
|
||||
for i in 0..Self::NUM_CONTINENTS {
|
||||
info!("{}/{}", i, Self::NUM_CONTINENTS);
|
||||
|
||||
self.continent_offsets[i as usize].x = rng
|
||||
.gen_range(width * i as f32 * 2.0 / 5.0..width * (i as f32 + 2.0) * 2.0 / 5.0)
|
||||
.repeat(width);
|
||||
|
@ -159,6 +164,7 @@ impl World {
|
|||
self.continent_widths[i as usize] =
|
||||
rng.gen_range(Self::CONTINENT_MIN_WIDTH_FACTOR..Self::CONTINENT_MAX_WIDTH_FACTOR);
|
||||
}
|
||||
info!("Done generating continents");
|
||||
}
|
||||
|
||||
fn continent_modifier(&self, x: usize, y: usize) -> f32 {
|
||||
|
@ -195,6 +201,7 @@ impl World {
|
|||
}
|
||||
|
||||
fn generate_altitude(&mut self) -> Result<(), CartesianError> {
|
||||
info!("Generating altitude");
|
||||
self.generate_continents();
|
||||
|
||||
const RADIUS_1: f32 = 0.5;
|
||||
|
@ -212,6 +219,8 @@ impl World {
|
|||
for y in 0..self.terrain.len() {
|
||||
let alpha = (y as f32 / self.height as f32) * PI;
|
||||
|
||||
info!("{}/{}", y, self.terrain.len());
|
||||
|
||||
for x in 0..self.terrain[y].len() {
|
||||
let beta = (x as f32 / self.width as f32) * TAU;
|
||||
|
||||
|
@ -250,6 +259,7 @@ impl World {
|
|||
self.terrain[y][x].altitude = Self::calculate_altitude(raw_altitude);
|
||||
}
|
||||
}
|
||||
info!("Done generating altitude");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -67,6 +67,9 @@ impl WorldManager {
|
|||
pub fn get_world(&self) -> Option<&World> {
|
||||
self.world.as_ref()
|
||||
}
|
||||
pub fn world(&self) -> &World {
|
||||
self.get_world().unwrap()
|
||||
}
|
||||
|
||||
pub fn new_world(&mut self) -> Result<&World, WorldGenError> {
|
||||
let seed = random();
|
||||
|
@ -142,7 +145,7 @@ impl WorldManager {
|
|||
let mut shade_value = 1.0;
|
||||
|
||||
while shade_value > altitude / World::MAX_ALTITUDE {
|
||||
shade_value -= 0.1;
|
||||
shade_value -= 0.05;
|
||||
}
|
||||
|
||||
Color::rgb(shade_value, shade_value, shade_value)
|
||||
|
|
158
src/main.rs
158
src/main.rs
|
@ -32,7 +32,9 @@
|
|||
#![warn(unused_results)]
|
||||
#![warn(variant_size_differences)]
|
||||
|
||||
mod world_plugins;
|
||||
mod plugins;
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use bevy::{
|
||||
app::App,
|
||||
|
@ -42,7 +44,7 @@ use bevy::{
|
|||
#[cfg(feature = "render")]
|
||||
use bevy::{
|
||||
asset::{AssetServer, Assets},
|
||||
core_pipeline::core_2d::Camera2dBundle,
|
||||
core_pipeline::core_2d::{Camera2d, Camera2dBundle},
|
||||
ecs::{
|
||||
change_detection::ResMut,
|
||||
component::Component,
|
||||
|
@ -50,24 +52,31 @@ use bevy::{
|
|||
system::{Commands, Query, Res},
|
||||
},
|
||||
hierarchy::BuildChildren,
|
||||
prelude::Vec2,
|
||||
render::{
|
||||
camera::{Camera, RenderTarget},
|
||||
color::Color,
|
||||
render_resource::{
|
||||
Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
|
||||
},
|
||||
texture::{Image, ImageSettings},
|
||||
},
|
||||
sprite::{Sprite, SpriteBundle},
|
||||
text::Text,
|
||||
transform::components::GlobalTransform,
|
||||
ui::{
|
||||
entity::{ButtonBundle, ImageBundle, NodeBundle, TextBundle},
|
||||
entity::{ButtonBundle, NodeBundle, TextBundle},
|
||||
widget::Button,
|
||||
AlignItems, FocusPolicy, Interaction, JustifyContent, PositionType, Size, Style, UiColor,
|
||||
UiImage, UiRect, Val,
|
||||
UiRect, Val,
|
||||
},
|
||||
utils::default,
|
||||
window::{CursorIcon, WindowDescriptor, Windows},
|
||||
winit::WinitSettings,
|
||||
};
|
||||
use world_plugins::WorldPlugins;
|
||||
#[cfg(feature = "render")]
|
||||
use plugins::PanCam;
|
||||
use plugins::WorldPlugins;
|
||||
use save::*;
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
|
@ -78,17 +87,33 @@ fn refresh_world_texture(images: &mut Assets<Image>, world_manager: &WorldManage
|
|||
}
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
#[derive(Component, Default)]
|
||||
#[derive(Component)]
|
||||
struct RainfallButton;
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
#[derive(Component, Default)]
|
||||
#[derive(Component)]
|
||||
struct TemperatureButton;
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
#[derive(Component, Default)]
|
||||
#[derive(Component)]
|
||||
struct ContoursButton;
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
#[derive(Component)]
|
||||
struct InfoPanel;
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
#[derive(Default, Debug)]
|
||||
struct CursorMapPosition {
|
||||
x: i32,
|
||||
y: i32,
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
const NORMAL_BUTTON: Color = Color::rgb(0.15, 0.15, 0.15);
|
||||
const HOVERED_BUTTON: Color = Color::rgb(0.25, 0.25, 0.25);
|
||||
const PRESSED_BUTTON: Color = Color::rgb(0.35, 0.60, 0.35);
|
||||
|
@ -191,6 +216,59 @@ fn handle_contours_button(
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
fn update_cursor_map_position(
|
||||
mut cursor_map_position: ResMut<'_, CursorMapPosition>,
|
||||
transform: Query<'_, '_, (&Camera, &GlobalTransform), With<Camera2d>>,
|
||||
windows: Res<'_, Windows>,
|
||||
world_manager: Res<'_, WorldManager>,
|
||||
) {
|
||||
let (camera, transform) = transform.single();
|
||||
|
||||
let window = match camera.target {
|
||||
RenderTarget::Window(window_id) => windows.get(window_id).unwrap(),
|
||||
RenderTarget::Image(_) => windows.primary(),
|
||||
};
|
||||
|
||||
if let Some(screen_position) = window.cursor_position() {
|
||||
let window_size = Vec2::new(window.width(), window.height());
|
||||
|
||||
// GPU coordinates [-1..1]
|
||||
let ndc = (screen_position / window_size) * 2.0;
|
||||
|
||||
// Matrix to reverse camera transform
|
||||
let ndc_to_world = transform.compute_matrix() * camera.projection_matrix().inverse();
|
||||
|
||||
let world_position =
|
||||
ndc_to_world.project_point3(ndc.extend(-1.0)).truncate() / WORLD_SCALE as f32;
|
||||
|
||||
cursor_map_position.x = world_position.x.round() as i32;
|
||||
cursor_map_position.y = world_manager.world().height - world_position.y.round() as i32;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
fn update_info_panel(
|
||||
cursor_position: Res<'_, CursorMapPosition>,
|
||||
world_manager: Res<'_, WorldManager>,
|
||||
mut text: Query<'_, '_, &mut Text, With<InfoPanel>>,
|
||||
) {
|
||||
let world = world_manager.world();
|
||||
text.single_mut().sections[0].value = if cursor_position.x >= 0
|
||||
&& cursor_position.x < world.width
|
||||
&& cursor_position.y >= 0
|
||||
&& cursor_position.y < world.height
|
||||
{
|
||||
let cell = &world.terrain[cursor_position.y as usize][cursor_position.x as usize];
|
||||
format!(
|
||||
"Mouse position: {}\nAltitude: {}\nRainfall: {}\nTemperature: {}",
|
||||
*cursor_position, cell.altitude, cell.rainfall, cell.temperature
|
||||
)
|
||||
} else {
|
||||
format!("Mouse position: {}\nOut of bounds", *cursor_position)
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "render")]
|
||||
fn generate_graphics(
|
||||
mut commands: Commands<'_, '_>,
|
||||
|
@ -198,7 +276,13 @@ fn generate_graphics(
|
|||
mut world_manager: ResMut<'_, WorldManager>,
|
||||
asset_server: Res<'_, AssetServer>,
|
||||
) {
|
||||
let world = world_manager.get_world().unwrap();
|
||||
use bevy::ui::AlignSelf;
|
||||
|
||||
let world = world_manager.world();
|
||||
let custom_sprite_size = Vec2 {
|
||||
x: (WORLD_SCALE * world.width) as f32,
|
||||
y: (WORLD_SCALE * world.height) as f32,
|
||||
};
|
||||
|
||||
let image_handle = images.add(Image {
|
||||
data: world_manager.world_color_bytes(),
|
||||
|
@ -219,7 +303,17 @@ fn generate_graphics(
|
|||
});
|
||||
world_manager.image_handle_id = image_handle.id;
|
||||
|
||||
_ = commands.spawn_bundle(Camera2dBundle::default());
|
||||
_ = commands
|
||||
.spawn_bundle(Camera2dBundle::default())
|
||||
.insert(PanCam::default());
|
||||
_ = commands.spawn_bundle(SpriteBundle {
|
||||
texture: images.get_handle(world_manager.image_handle_id),
|
||||
sprite: Sprite {
|
||||
custom_size: Some(custom_sprite_size),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
});
|
||||
_ = commands
|
||||
.spawn_bundle(NodeBundle {
|
||||
style: Style {
|
||||
|
@ -230,15 +324,33 @@ fn generate_graphics(
|
|||
..default()
|
||||
})
|
||||
.with_children(|root_node| {
|
||||
_ = root_node.spawn_bundle(ImageBundle {
|
||||
_ = root_node
|
||||
.spawn_bundle(NodeBundle {
|
||||
style: Style {
|
||||
size: Size::new(Val::Percent(100.0), Val::Auto),
|
||||
// align_items: AlignItems::FlexEnd,
|
||||
align_self: AlignSelf::FlexEnd,
|
||||
padding: UiRect::all(Val::Px(2.0)),
|
||||
..default()
|
||||
},
|
||||
image: UiImage(image_handle),
|
||||
color: Color::rgba(1.0, 1.0, 1.0, 0.05).into(),
|
||||
focus_policy: FocusPolicy::Pass,
|
||||
..default()
|
||||
})
|
||||
.with_children(|info_panel| {
|
||||
_ = info_panel
|
||||
.spawn_bundle(TextBundle {
|
||||
text: Text::from_section(
|
||||
"Info Panel",
|
||||
bevy::text::TextStyle {
|
||||
font: asset_server.load("JuliaMono.ttf"),
|
||||
font_size: 15.0,
|
||||
color: Color::WHITE,
|
||||
},
|
||||
),
|
||||
..default()
|
||||
})
|
||||
.insert(InfoPanel);
|
||||
});
|
||||
|
||||
_ = root_node
|
||||
.spawn_bundle(NodeBundle {
|
||||
style: Style {
|
||||
|
@ -264,7 +376,7 @@ fn generate_graphics(
|
|||
color: NORMAL_BUTTON.into(),
|
||||
..default()
|
||||
})
|
||||
.insert(RainfallButton::default())
|
||||
.insert(RainfallButton)
|
||||
.with_children(|button| {
|
||||
_ = button.spawn_bundle(TextBundle {
|
||||
text: bevy::text::Text::from_section(
|
||||
|
@ -289,7 +401,7 @@ fn generate_graphics(
|
|||
color: NORMAL_BUTTON.into(),
|
||||
..default()
|
||||
})
|
||||
.insert(TemperatureButton::default())
|
||||
.insert(TemperatureButton)
|
||||
.with_children(|button| {
|
||||
_ = button.spawn_bundle(TextBundle {
|
||||
text: bevy::text::Text::from_section(
|
||||
|
@ -314,7 +426,7 @@ fn generate_graphics(
|
|||
color: NORMAL_BUTTON.into(),
|
||||
..default()
|
||||
})
|
||||
.insert(ContoursButton::default())
|
||||
.insert(ContoursButton)
|
||||
.with_children(|button| {
|
||||
_ = button.spawn_bundle(TextBundle {
|
||||
text: bevy::text::Text::from_section(
|
||||
|
@ -332,6 +444,7 @@ fn generate_graphics(
|
|||
});
|
||||
}
|
||||
|
||||
const WORLD_SCALE: i32 = 3;
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut app = App::new();
|
||||
let mut manager = WorldManager::new();
|
||||
|
@ -344,16 +457,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
// Use nearest-neighbor rendering for cripsier pixels
|
||||
.insert_resource(ImageSettings::default_nearest())
|
||||
.insert_resource(WindowDescriptor {
|
||||
width: (2 * world.width) as f32,
|
||||
height: (2 * world.height) as f32,
|
||||
width: (WORLD_SCALE * world.width) as f32,
|
||||
height: (WORLD_SCALE * world.height) as f32,
|
||||
title: String::from("World-RS"),
|
||||
resizable: true,
|
||||
..default()
|
||||
})
|
||||
.insert_resource(CursorMapPosition::default())
|
||||
.add_startup_system(generate_graphics)
|
||||
.add_system(handle_rainfall_button)
|
||||
.add_system(handle_temperature_button)
|
||||
.add_system(handle_contours_button);
|
||||
.add_system(handle_contours_button)
|
||||
.add_system(update_cursor_map_position)
|
||||
.add_system(update_info_panel);
|
||||
}
|
||||
#[cfg(not(feature = "render"))]
|
||||
{
|
||||
|
@ -375,7 +491,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
});
|
||||
}
|
||||
|
||||
app.insert_resource(manager).add_plugins(WorldPlugins).run();
|
||||
app.add_plugins(WorldPlugins).insert_resource(manager).run();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
4
src/plugins/mod.rs
Normal file
4
src/plugins/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub(crate) mod world_plugins;
|
||||
pub(crate) use world_plugins::WorldPlugins;
|
||||
|
||||
pub(crate) use bevy_pancam::PanCam;
|
|
@ -22,6 +22,8 @@ impl PluginGroup for WorldPlugins {
|
|||
input::InputPlugin, render::RenderPlugin, sprite::SpritePlugin, text::TextPlugin,
|
||||
transform::TransformPlugin, ui::UiPlugin, window::WindowPlugin, winit::WinitPlugin,
|
||||
};
|
||||
use bevy_pancam::PanCamPlugin;
|
||||
|
||||
_ = group
|
||||
.add(TransformPlugin::default())
|
||||
// hierarchy
|
||||
|
@ -34,7 +36,8 @@ impl PluginGroup for WorldPlugins {
|
|||
.add(CorePipelinePlugin::default())
|
||||
.add(SpritePlugin::default())
|
||||
.add(TextPlugin::default())
|
||||
.add(UiPlugin::default());
|
||||
.add(UiPlugin::default())
|
||||
.add(PanCamPlugin::default());
|
||||
}
|
||||
#[cfg(not(feature = "render"))]
|
||||
{
|
Loading…
Reference in a new issue