diff --git a/docs/devlog.md b/docs/devlog.md index 1d993af..c3245f9 100644 --- a/docs/devlog.md +++ b/docs/devlog.md @@ -1,5 +1,16 @@ # development log +## 2024-11-28 + +I tinkered with camera controls and the new `require` attribute. Now the camera +follows the balloon. I also fixed the toggles for debug gizmos! + +I added controls for changing the physics time multiplier (and a debug ui) and +it is correctly changing the physics clock's relative speed, but the physics +breaks when the physics clock's relative speed goes above 1.5. Maybe it has +something to do with schedules, but more likely the timestep is simply too +large. + ## 2024-11-27 Some of my dependencies may now have Bevy 0.15 support. diff --git a/src/app3d/camera.rs b/src/app3d/camera.rs index 1f8f018..bf13ffe 100644 --- a/src/app3d/camera.rs +++ b/src/app3d/camera.rs @@ -1,20 +1,114 @@ -use bevy::prelude::*; +use bevy::{ + input::mouse::{MouseScrollUnit, MouseWheel}, + prelude::*, +}; -// use crate::controls::CameraControls; +use super::controls::KeyBindingsConfig; +use crate::simulator::Balloon; + +const INVERT_ZOOM: bool = true; pub struct CameraPlugin; impl Plugin for CameraPlugin { fn build(&self, app: &mut App) { + app.init_resource::(); app.add_systems(Startup, setup); + app.add_systems(Update, zoom_camera); + app.add_plugins(CameraFollowPlugin); } } +#[derive(Component, Default)] +#[require(Camera3d, PerspectiveProjection)] +struct MainCamera; + +/// A resource that stores the currently selected camera target. +#[derive(Resource)] +struct CameraSelection { + entity: Entity, + offset: Vec3, +} + +impl Default for CameraSelection { + fn default() -> Self { + Self { + entity: Entity::PLACEHOLDER, + offset: Vec3::new(0., 0., 10.), + } + } +} + +/// A marker component for entities that can be selected as a camera target. +#[derive(Component, Default, Reflect)] +pub struct CameraTarget; + fn setup(mut commands: Commands) { commands.spawn(( - // Note we're setting the initial position below with yaw, pitch, and radius, hence - // we don't set transform on the camera. + Name::new("Main Camera"), + MainCamera, Camera3d::default(), Transform::from_xyz(0.0, 20., 50.0).looking_at(Vec3::new(0., 20., 0.), Vec3::Y), )); } + +fn zoom_camera( + mut camera: Query<&mut PerspectiveProjection, (With, With)>, + mut evr_scroll: EventReader, + key_bindings: Res, +) { + let mut projection = camera.single_mut(); + let ctrl = &key_bindings.camera_controls; + let direction = if INVERT_ZOOM { -1.0 } else { 1.0 }; + for ev in evr_scroll.read() { + match ev.unit { + MouseScrollUnit::Line => { + projection.fov = projection.fov.clamp(ctrl.min_fov, ctrl.max_fov) + + ev.y * ctrl.zoom_step * direction; + } + MouseScrollUnit::Pixel => { + projection.fov = projection.fov.clamp(ctrl.min_fov, ctrl.max_fov) + + ev.y * ctrl.zoom_step * direction; + } + } + } +} + +struct CameraFollowPlugin; + +impl Plugin for CameraFollowPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Update, (mark_new_targets, follow_selected_target)); + } +} + +fn mark_new_targets( + mut commands: Commands, + balloons: Query>, + mut selection: ResMut, +) { + for entity in &balloons { + commands.entity(entity).insert(CameraTarget); + // Focus on the newest balloon + selection.entity = entity; + } +} + +fn follow_selected_target( + selection: Res, + targets: Query<&Transform, (With, Without)>, + mut camera: Query<&mut Transform, With>, +) { + let mut cam = camera.single_mut(); + match targets.get(selection.entity) { + Ok(t) => { + // If the target exists, move the camera next to it + cam.translation = t.translation + selection.offset; + // Look at the target position + cam.look_at(t.translation, Vec3::Y); + } + Err(_) => { + // If there is no selected entity, stay where you are + } + } +} diff --git a/src/app3d/controls.rs b/src/app3d/controls.rs index 117667d..c6c8225 100644 --- a/src/app3d/controls.rs +++ b/src/app3d/controls.rs @@ -18,10 +18,13 @@ pub struct KeyBindingsConfig { #[derive(Reflect)] pub struct CameraControls { - pub modifier_pan: Option, + pub cycle_target: KeyCode, + pub modifier_pan: KeyCode, pub button_pan: MouseButton, pub button_orbit: MouseButton, - pub toggle_zoom_direction: KeyCode, + pub zoom_step: f32, + pub max_fov: f32, + pub min_fov: f32, } #[derive(Reflect)] @@ -36,6 +39,10 @@ pub struct DebugControls { #[derive(Reflect)] pub struct TimeControls { pub toggle_pause: KeyCode, + pub faster: KeyCode, + pub slower: KeyCode, + pub reset_speed: KeyCode, + pub scale_step: f32, } // ============================ DEFAULT KEYBINDINGS ============================ @@ -44,10 +51,13 @@ pub struct TimeControls { impl Default for CameraControls { fn default() -> Self { Self { - modifier_pan: Some(KeyCode::ShiftLeft), + cycle_target: KeyCode::Tab, + modifier_pan: KeyCode::ShiftLeft, button_pan: MouseButton::Middle, button_orbit: MouseButton::Middle, - toggle_zoom_direction: KeyCode::KeyZ, + zoom_step: 0.01, + max_fov: 1.0, + min_fov: 0.01, } } } @@ -68,6 +78,10 @@ impl Default for TimeControls { fn default() -> Self { Self { toggle_pause: KeyCode::Space, + faster: KeyCode::ArrowUp, + slower: KeyCode::ArrowDown, + reset_speed: KeyCode::Backspace, + scale_step: 0.1, } } } diff --git a/src/app3d/dev_tools.rs b/src/app3d/dev_tools.rs index 1fd3b32..9e82f41 100644 --- a/src/app3d/dev_tools.rs +++ b/src/app3d/dev_tools.rs @@ -14,7 +14,9 @@ use bevy::{ prelude::*, }; -use crate::{app3d::controls::KeyBindingsConfig, simulator::SimState}; +use crate::simulator::SimState; + +use super::{controls::KeyBindingsConfig, gizmos::ForceGizmos}; pub struct DevToolsPlugin; @@ -34,8 +36,11 @@ impl Plugin for DevToolsPlugin { app.init_resource::(); - app.add_systems(Update, log_transitions::); - app.add_systems(Update, show_physics_gizmos); + app.add_systems(Update, ( + log_transitions::, + show_force_gizmos, + show_physics_gizmos, + )); // Wireframe doesn't work on WASM #[cfg(not(target_arch = "wasm32"))] @@ -103,11 +108,25 @@ fn toggle_debug_ui( } } +fn show_force_gizmos( + debug_state: Res, + mut gizmo_store: ResMut +) { + if debug_state.is_changed() { + let (_, force_config) = gizmo_store.config_mut::(); + if debug_state.forces { + *force_config = ForceGizmos::all(); + } else { + *force_config = ForceGizmos::none(); + } + } +} + fn show_physics_gizmos( debug_state: Res, mut gizmo_store: ResMut ) { - if gizmo_store.is_changed() { + if debug_state.is_changed() { let (_, physics_config) = gizmo_store.config_mut::(); if debug_state.physics { *physics_config = PhysicsGizmos::all(); diff --git a/src/app3d/gizmos.rs b/src/app3d/gizmos.rs index 83aade3..c30d914 100644 --- a/src/app3d/gizmos.rs +++ b/src/app3d/gizmos.rs @@ -1,6 +1,6 @@ use bevy::{color::palettes::basic::*, prelude::*}; -use crate::simulator::{forces::Force, SimState}; +use crate::simulator::forces::Force; const ARROW_SCALE: f32 = 0.1; @@ -8,17 +8,76 @@ pub struct ForceArrowsPlugin; impl Plugin for ForceArrowsPlugin { fn build(&self, app: &mut App) { - app.add_systems(PostUpdate, force_arrows); + app.init_gizmo_group::(); + app.register_type::(); + app.add_systems( + PostUpdate, + force_arrows.run_if( + |store: Res| { + store.config::().0.enabled + }), + ); } } -fn force_arrows(query: Query<&dyn Force>, mut gizmos: Gizmos) { +fn force_arrows( + query: Query<&dyn Force>, + mut gizmos: Gizmos, +) { for forces in query.iter() { for force in forces.iter() { let start = force.point_of_application(); let end = start + force.force() * ARROW_SCALE; - let color = force.color().unwrap_or(RED.into()); - gizmos.arrow(start, end, color).with_tip_length(0.1); + let color = match force.color() { + Some(c) => c, + None => RED.into(), + }; + gizmos.arrow(start, end, color).with_tip_length(0.3); + } + } +} + +#[derive(Reflect, GizmoConfigGroup)] +pub struct ForceGizmos { + /// The scale of the force arrows. + pub arrow_scale: Option, + /// The color of the force arrows. If `None`, the arrows will not be rendered. + pub arrow_color: Option, + /// The length of the arrow tips. + pub tip_length: Option, + /// Determines if the forces should be hidden when not active. + pub enabled: bool, +} + +impl Default for ForceGizmos { + fn default() -> Self { + Self { + arrow_scale: Some(0.1), + arrow_color: Some(RED.into()), + tip_length: Some(0.3), + enabled: false, + } + } +} + +impl ForceGizmos { + /// Creates a [`ForceGizmos`] configuration with all rendering options enabled. + pub fn all() -> Self { + Self { + arrow_scale: Some(0.1), + arrow_color: Some(RED.into()), + tip_length: Some(0.3), + enabled: true, + } + } + + /// Creates a [`ForceGizmos`] configuration with debug rendering enabled but all options turned off. + pub fn none() -> Self { + Self { + arrow_scale: None, + arrow_color: None, + tip_length: None, + enabled: false, } } } diff --git a/src/app3d/mod.rs b/src/app3d/mod.rs index 4790e1c..8df3737 100644 --- a/src/app3d/mod.rs +++ b/src/app3d/mod.rs @@ -14,7 +14,7 @@ use monitors::MonitorsPlugin; use bevy::{app::{PluginGroup, PluginGroupBuilder}, prelude::*}; -use crate::simulator::SimState; +use crate::simulator::{SimState, time::TimeScaleOptions}; pub struct App3dPlugins; @@ -35,6 +35,7 @@ impl Plugin for InterfacePlugin { fn build(&self, app: &mut App) { app.add_plugins(( PausePlayPlugin, + ChangeTimeScalePlugin, ForceArrowsPlugin, MonitorsPlugin, #[cfg(feature = "dev")] @@ -43,7 +44,7 @@ impl Plugin for InterfacePlugin { } } -pub struct PausePlayPlugin; +struct PausePlayPlugin; impl Plugin for PausePlayPlugin { fn build(&self, app: &mut App) { @@ -65,3 +66,27 @@ fn toggle_pause( } } } + +struct ChangeTimeScalePlugin; + +impl Plugin for ChangeTimeScalePlugin { + fn build(&self, app: &mut App) { + app.add_systems(PreUpdate, modify_time_scale); + } +} + +fn modify_time_scale( + mut time_options: ResMut, + key_input: Res>, + key_bindings: Res, +) { + if key_input.just_pressed(key_bindings.time_controls.faster) { + time_options.multiplier += key_bindings.time_controls.scale_step; + } + if key_input.just_pressed(key_bindings.time_controls.slower) { + time_options.multiplier -= key_bindings.time_controls.scale_step; + } + if key_input.just_pressed(key_bindings.time_controls.reset_speed) { + time_options.multiplier = 1.0; + } +} diff --git a/src/app3d/monitors.rs b/src/app3d/monitors.rs index 2a03d32..7e99306 100644 --- a/src/app3d/monitors.rs +++ b/src/app3d/monitors.rs @@ -1,16 +1,20 @@ //! UI for monitoring the simulation. #![allow(unused_imports)] use bevy::{ - ecs::system::{SystemParam, lifetimeless::{SQuery, SRes}}, + ecs::system::{ + lifetimeless::{SQuery, SRes}, + SystemParam, + }, prelude::*, }; +use avian3d::prelude::*; use iyes_perf_ui::{entry::PerfUiEntry, prelude::*, utils::format_pretty_float}; use crate::simulator::{ + balloon::Balloon, forces::{Buoyancy, Drag, Force, Weight}, - SimState, SimulatedBody, ideal_gas::IdealGas, - balloon::Balloon, + SimState, }; pub struct MonitorsPlugin; @@ -21,6 +25,7 @@ impl Plugin for MonitorsPlugin { app.add_perf_ui_simple_entry::(); app.add_perf_ui_simple_entry::(); app.add_perf_ui_simple_entry::(); + app.add_perf_ui_simple_entry::(); app.add_systems(Startup, spawn_monitors); } } @@ -34,6 +39,7 @@ fn spawn_monitors(mut commands: Commands) { SimStateMonitor::default(), ForceMonitor::default(), GasMonitor::default(), + TimeScaleMonitor::default(), )); } @@ -126,7 +132,7 @@ impl PerfUiEntry for SimStateMonitor { // (optional) Called every frame to determine if the value should be highlighted fn value_highlight(&self, value: &Self::Value) -> bool { self.threshold_highlight - .map(|_| value == &SimState::Stopped) + .map(|_| value == &SimState::Faulted) .unwrap_or(false) } } @@ -167,7 +173,7 @@ impl Default for ForceMonitor { impl PerfUiEntry for ForceMonitor { type Value = (f32, f32, f32); - type SystemParam = SQuery<(&'static Weight, &'static Buoyancy, &'static Drag), With>; + type SystemParam = SQuery<(&'static Weight, &'static Buoyancy, &'static Drag), With>; fn label(&self) -> &str { if self.label.is_empty() { @@ -200,11 +206,7 @@ impl PerfUiEntry for ForceMonitor { force_resources: &mut ::Item<'_, '_>, ) -> Option { for (weight, buoyancy, drag) in force_resources.iter() { - return Some(( - weight.force().y, - buoyancy.force().y, - drag.force().y, - )) + return Some((weight.force().y, buoyancy.force().y, drag.force().y)); } None } @@ -252,7 +254,7 @@ impl Default for GasMonitor { threshold_highlight: Some(10.0), color_gradient: ColorGradient::new_preset_gyr(0.0, 10.0, 100.0).unwrap(), digits: 5, - precision: 2, + precision: 3, sort_key: iyes_perf_ui::utils::next_sort_key(), } } @@ -281,7 +283,7 @@ impl PerfUiEntry for GasMonitor { let mut temperature = format_pretty_float(self.digits, self.precision, value.2 as f64); let mut density = format_pretty_float(self.digits, self.precision, value.3 as f64); let mut mass = format_pretty_float(self.digits, self.precision, value.4 as f64); - let mut species = value.5.clone(); + let species = value.5.clone(); // (and append units to it) if self.display_units { @@ -290,9 +292,11 @@ impl PerfUiEntry for GasMonitor { temperature.push_str(" K"); density.push_str(" kg/m3"); mass.push_str(" kg"); - species.push_str(""); } - format!("{}\n{}\n{}\n{}\n{}\n{}", species, volume, pressure, temperature, density, mass) + format!( + "{}\n{}\n{}\n{}\n{}\n{}", + species, volume, pressure, temperature, density, mass + ) } fn update_value( @@ -324,3 +328,101 @@ impl PerfUiEntry for GasMonitor { } } } + +#[derive(Component)] +struct TimeScaleMonitor { + /// The label text to display, to allow customization + pub label: String, + /// Should we display units? + pub display_units: bool, + /// Highlight the value if it goes above this threshold + #[allow(dead_code)] + pub threshold_highlight: Option, + /// Support color gradients! + #[allow(dead_code)] + pub color_gradient: ColorGradient, + /// Width for formatting the string + pub digits: u8, + /// Precision for formatting the string + pub precision: u8, + /// Required to ensure the entry appears in the correct place in the Perf UI + pub sort_key: i32, +} + +impl Default for TimeScaleMonitor { + fn default() -> Self { + let rygyr_gradient = ColorGradient::new().with_stops(vec![ + (0.01, Color::srgb(1.0, 0.0, 0.0).into()), + (0.5, Color::srgb(1.0, 1.0, 0.0).into()), + (1.0, Color::srgb(0.0, 1.0, 0.0).into()), + (1.5, Color::srgb(1.0, 1.0, 0.0).into()), + (10.0, Color::srgb(1.0, 0.0, 0.0).into()), + ]); + TimeScaleMonitor { + label: String::new(), + display_units: true, + threshold_highlight: Some(10.0), + color_gradient: rygyr_gradient, + digits: 5, + precision: 3, + sort_key: iyes_perf_ui::utils::next_sort_key(), + } + } +} + +impl PerfUiEntry for TimeScaleMonitor { + type Value = (f32, f32, f32); + type SystemParam = ( + SRes>, + SRes>, + ); + + fn label(&self) -> &str { + if self.label.is_empty() { + "Time" + } else { + &self.label + } + } + + fn sort_key(&self) -> i32 { + self.sort_key + } + + fn format_value(&self, value: &Self::Value) -> String { + let mut virtual_time = format_pretty_float(self.digits, self.precision, value.0 as f64); + let mut physics_time = format_pretty_float(self.digits, self.precision, value.1 as f64); + let multiplier = format_pretty_float(2, 1, value.2 as f64); + // (and append units to it) + if self.display_units { + virtual_time.push_str(" s"); + physics_time.push_str(" s"); + } + format!("real: {}\nphysics: {} (x{})", virtual_time, physics_time, multiplier) + } + + fn update_value( + &self, + (virtual_time, physics_time): &mut ::Item<'_, '_>, + ) -> Option { + Some(( + virtual_time.as_ref().elapsed_secs(), + physics_time.as_ref().elapsed_secs(), + physics_time.as_ref().relative_speed(), + )) + } + + // (optional) We should add a width hint, so that the displayed + // strings in the UI can be correctly aligned. + // This value represents the largest length the formatted string + // is expected to have. + fn width_hint(&self) -> usize { + // there is a helper we can use, since we use `format_pretty_float` + let w = iyes_perf_ui::utils::width_hint_pretty_float(self.digits, self.precision); + if self.display_units { + w + 2 + } else { + w + } + } +} diff --git a/src/app3d/scene.rs b/src/app3d/scene.rs index 647fe1c..0a0cc38 100644 --- a/src/app3d/scene.rs +++ b/src/app3d/scene.rs @@ -23,7 +23,7 @@ fn simple_scene( let ground_size = Vec3::new(50.0, 0.1, 50.0); let plane = meshes.add(Plane3d::default().mesh().size(ground_size.x, ground_size.z).subdivisions(10)); let plane_material = materials.add(StandardMaterial { - base_color: Color::srgb(0.5, 0.5, 0.5), + base_color: Color::srgb(0.5, 0.5, 0.0), perceptual_roughness: 0.5, ..default() }); @@ -47,6 +47,7 @@ fn spawn_balloon( base_color: Color::srgba(1.0, 0.0, 0.0, 0.5), perceptual_roughness: 0.0, metallic: 1.0, + alpha_mode: AlphaMode::Blend, ..default() }); let sphere = Sphere::default(); @@ -54,7 +55,6 @@ fn spawn_balloon( let species = GasSpecies::helium(); commands.spawn(( Name::new("Balloon"), - SimulatedBody, BalloonBundle { balloon: Balloon { material_properties: BalloonMaterial::default(), diff --git a/src/simulator/atmosphere.rs b/src/simulator/atmosphere.rs index f5fd6e0..d0849e0 100644 --- a/src/simulator/atmosphere.rs +++ b/src/simulator/atmosphere.rs @@ -11,7 +11,7 @@ use bevy::prelude::*; use super::{ ideal_gas::{ideal_gas_density, GasSpecies}, properties::{Density, Pressure, Temperature}, - SimulationUpdateOrder, SimState, SimulatedBody, + SimulationUpdateOrder, SimState, Balloon, }; pub struct AtmospherePlugin; @@ -26,7 +26,7 @@ impl Plugin for AtmospherePlugin { } fn pause_on_out_of_bounds( - positions: Query<&Position, With>, + positions: Query<&Position, With>, mut state: ResMut>, ) { for position in positions.iter() { diff --git a/src/simulator/balloon.rs b/src/simulator/balloon.rs index 095ab65..a6a4a8d 100644 --- a/src/simulator/balloon.rs +++ b/src/simulator/balloon.rs @@ -1,11 +1,10 @@ //! Properties, attributes and functions related to the balloon. -use avian3d::{math::PI, prelude::Position}; +use avian3d::{math::PI, prelude::*}; use bevy::prelude::*; use super::{ - ideal_gas::IdealGas, properties::sphere_radius_from_volume, SimulatedBody, - SimulationUpdateOrder, Volume, + ideal_gas::IdealGas, properties::sphere_radius_from_volume, SimulationUpdateOrder, Volume, }; pub struct BalloonPlugin; @@ -24,6 +23,7 @@ impl Plugin for BalloonPlugin { } #[derive(Component, Debug, Clone, PartialEq, Reflect)] +#[require(IdealGas, RigidBody, Mesh3d)] pub struct Balloon { pub material_properties: BalloonMaterial, pub shape: Sphere, diff --git a/src/simulator/core.rs b/src/simulator/core.rs index 81a8cee..1c9cc90 100644 --- a/src/simulator/core.rs +++ b/src/simulator/core.rs @@ -5,10 +5,6 @@ use bevy::{ prelude::*, }; -/// A marker component for entities that are simulated. -#[derive(Component, Default)] -pub struct SimulatedBody; - pub struct SimulatorPlugins; impl PluginGroup for SimulatorPlugins { @@ -29,16 +25,9 @@ impl Plugin for CorePhysicsPlugin { properties::CorePropertiesPlugin, ideal_gas::IdealGasPlugin, forces::ForcesPlugin, + time::TimeScalePlugin, )); app.init_state::(); - app.add_systems( - OnEnter(SimState::Running), - |mut time: ResMut>| time.as_mut().unpause(), - ); - app.add_systems( - OnExit(SimState::Running), - |mut time: ResMut>| time.as_mut().pause(), - ); app.configure_sets( Update, ( diff --git a/src/simulator/forces/aero.rs b/src/simulator/forces/aero.rs index b4242b6..e01c4b1 100644 --- a/src/simulator/forces/aero.rs +++ b/src/simulator/forces/aero.rs @@ -4,7 +4,7 @@ use avian3d::{math::PI, prelude::*}; use bevy::prelude::*; use bevy_trait_query::{self, RegisterExt}; -use super::{Atmosphere, Balloon, Density, ForceUpdateOrder, Force, SimulatedBody}; +use super::{Atmosphere, Balloon, Density, ForceUpdateOrder, Force}; pub struct AeroForcesPlugin; diff --git a/src/simulator/forces/body.rs b/src/simulator/forces/body.rs index 0eb60bb..3c22e57 100644 --- a/src/simulator/forces/body.rs +++ b/src/simulator/forces/body.rs @@ -4,7 +4,7 @@ use avian3d::{math::PI, prelude::*}; use bevy::prelude::*; use bevy_trait_query::{self, RegisterExt}; -use super::{Atmosphere, Balloon, Density, Force, ForceUpdateOrder, Mass, SimulatedBody, Volume}; +use super::{Atmosphere, Balloon, Density, Force, ForceUpdateOrder, Mass, Volume}; use crate::simulator::properties::{EARTH_RADIUS_M, STANDARD_G}; pub struct BodyForcesPlugin; @@ -27,7 +27,6 @@ impl Plugin for BodyForcesPlugin { /// Downward force (N) vector due to gravity as a function of altitude (m) and /// mass (kg). The direction of this force is always world-space down. #[derive(Component, Reflect)] -#[require(Position, Mass)] pub struct Weight { position: Vec3, mass: f32, @@ -74,7 +73,7 @@ pub fn weight(position: Vec3, mass: f32) -> Vec3 { } fn update_weight_parameters( - mut bodies: Query<(&mut Weight, &Position, &Mass), With>, + mut bodies: Query<(&mut Weight, &Position, &Mass), With>, ) { for (mut weight, position, mass) in bodies.iter_mut() { weight.update(position.0, mass.value()); @@ -83,7 +82,6 @@ fn update_weight_parameters( /// Upward force (N) vector due to atmosphere displaced by the given gas volume. #[derive(Component, Reflect)] -#[require(Balloon, Position)] pub struct Buoyancy { position: Vec3, displaced_volume: Volume, @@ -128,7 +126,7 @@ pub fn buoyancy(position: Vec3, displaced_volume: Volume, ambient_density: Densi fn update_buoyant_parameters( atmosphere: Res, - mut bodies: Query<(&mut Buoyancy, &Position, &Balloon), With>, + mut bodies: Query<(&mut Buoyancy, &Position, &Balloon)>, ) { for (mut buoyancy, position, balloon) in bodies.iter_mut() { let ambient_density = atmosphere.density(position.0); diff --git a/src/simulator/forces/mod.rs b/src/simulator/forces/mod.rs index 4dedef0..d68bf3a 100644 --- a/src/simulator/forces/mod.rs +++ b/src/simulator/forces/mod.rs @@ -12,7 +12,7 @@ pub use aero::Drag; #[allow(unused_imports)] pub use body::{Buoyancy, Weight}; -use super::{Atmosphere, Balloon, Density, SimulatedBody, SimulationUpdateOrder, SimState, Volume}; +use super::{Atmosphere, Balloon, Density, SimulationUpdateOrder, SimState, Volume}; pub struct ForcesPlugin; impl Plugin for ForcesPlugin { @@ -62,9 +62,12 @@ pub struct ForceBundle { drag: aero::Drag, } -fn on_simulated_body_added(mut commands: Commands, query: Query>) { - for entity in &query { - commands.entity(entity).insert(ForceBundle::default()); +fn on_simulated_body_added(mut commands: Commands, query: Query<(Entity, &RigidBody), Added>) { + for (entity, rigid_body) in &query { + let mut this_entity = commands.entity(entity); + if rigid_body.is_dynamic() { + this_entity.insert(ForceBundle::default()); + } } } @@ -102,7 +105,7 @@ pub trait Force { /// TODO: preserve the position of the total force vector and apply it at that /// point instead of the center of mass. fn update_total_external_force( - mut body_forces: Query<(&mut ExternalForce, &dyn Force, &RigidBody), With>, + mut body_forces: Query<(&mut ExternalForce, &dyn Force, &RigidBody)>, ) { // Iterate over each entity that has force vector components. for (mut physics_force_component, acting_forces, rigid_body) in body_forces.iter_mut() { diff --git a/src/simulator/mod.rs b/src/simulator/mod.rs index f0e957a..00b3486 100644 --- a/src/simulator/mod.rs +++ b/src/simulator/mod.rs @@ -6,11 +6,13 @@ pub mod forces; pub mod ideal_gas; pub mod payload; pub mod properties; +pub mod time; // Re-export the properties module at the top level. -pub use core::{SimulatorPlugins, SimState, SimulatedBody, SimulationUpdateOrder}; +pub use core::{SimulatorPlugins, SimState, SimulationUpdateOrder}; pub use properties::{Density, Pressure, Temperature, Volume, MolarMass}; pub use atmosphere::Atmosphere; pub use forces::{Weight, Buoyancy, Drag}; pub use balloon::{Balloon, BalloonBundle, BalloonMaterial}; pub use ideal_gas::{GasSpecies, IdealGas}; +pub use payload::Payload; diff --git a/src/simulator/payload.rs b/src/simulator/payload.rs index 3c107c5..ffcc056 100644 --- a/src/simulator/payload.rs +++ b/src/simulator/payload.rs @@ -12,10 +12,11 @@ impl Plugin for PayloadPlugin { } /// A thing carried by the balloon. -#[derive(Component)] +#[derive(Component, Default)] pub struct Payload; /// A tether that connects the balloon to the payload. -#[derive(Component)] +#[derive(Component, Default)] +#[require(Payload)] pub struct Tether; diff --git a/src/simulator/time.rs b/src/simulator/time.rs new file mode 100644 index 0000000..da3790c --- /dev/null +++ b/src/simulator/time.rs @@ -0,0 +1,55 @@ +use avian3d::prelude::*; +use bevy::prelude::*; + +use super::SimState; + +pub struct TimeScalePlugin; + +impl Plugin for TimeScalePlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + app.add_systems( + OnEnter(SimState::Stopped), + pause, + ); + app.add_systems( + OnExit(SimState::Stopped), + unpause, + ); + app.add_systems( + PreUpdate, + modify_time_scale.run_if(in_state(SimState::Running)), + ); + } +} + +#[derive(Resource)] +pub struct TimeScaleOptions { + pub multiplier: f32, +} + +impl Default for TimeScaleOptions { + fn default() -> Self { + Self { multiplier: 1.0 } + } +} + +fn modify_time_scale( + mut time: ResMut>, + options: Res, +) { + if options.is_changed() { + info!("setting relative speed to {}", options.multiplier); + time.as_mut().set_relative_speed(options.multiplier); + } +} + +fn pause(mut physics_time: ResMut>, mut virtual_time: ResMut>) { + physics_time.as_mut().pause(); + virtual_time.as_mut().pause(); +} + +fn unpause(mut physics_time: ResMut>, mut virtual_time: ResMut>) { + physics_time.as_mut().unpause(); + virtual_time.as_mut().unpause(); +}