From 9823d9f5f05bdfc9f83d3404db3a44de8d694164 Mon Sep 17 00:00:00 2001 From: Austreelis Date: Tue, 14 Mar 2023 21:12:32 +0100 Subject: [PATCH 1/6] Add a "debug" visualization of signal strengths --- emergence_lib/src/signals.rs | 77 +++++++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/emergence_lib/src/signals.rs b/emergence_lib/src/signals.rs index 4514b8df9..9909af597 100644 --- a/emergence_lib/src/signals.rs +++ b/emergence_lib/src/signals.rs @@ -28,12 +28,22 @@ pub(crate) struct SignalsPlugin; impl Plugin for SignalsPlugin { fn build(&self, app: &mut App) { - app.init_resource::().add_systems( - (emit_signals, diffuse_signals, degrade_signals) - .chain() - .in_set(SimulationSet) - .in_schedule(CoreSchedule::FixedUpdate), - ); + app.init_resource::() + .init_resource::() + .insert_resource(DebugDisplayedSignal(SignalType::Push(Id::from_name( + "acacia_leaf", + )))) + .add_systems( + (emit_signals, diffuse_signals, degrade_signals) + .chain() + .in_set(SimulationSet) + .in_schedule(CoreSchedule::FixedUpdate), + ) + .add_system( + debug_display_signal_map + .run_if(debug_signal_map_enabled) + .in_base_set(CoreSet::PostUpdate), + ); } } @@ -462,6 +472,61 @@ fn degrade_signals(mut signals: ResMut) { } } +#[derive(Resource, Debug)] +struct DebugDisplayedSignal(SignalType); + +#[derive(Resource, Debug)] +struct DebugColorScheme(Vec>); + +impl FromWorld for DebugColorScheme { + fn from_world(world: &mut World) -> Self { + let mut material_assets = world.resource_mut::>(); + + let mut color_scheme = Vec::with_capacity(256); + // FIXME: This color palette is not very colorblind-friendly, even though it was inspired + // by matlab's veridis + for i in 0..256 { + let s = i as f32 / 255.0; + color_scheme.push(material_assets.add(StandardMaterial { + base_color: Color::Rgba { + red: 2.0 * s - s * s, + green: 0.8 * s.sqrt(), + blue: s * s * 0.6, + alpha: 1.0, + }, + ..Default::default() + })); + } + + color_scheme.shrink_to_fit(); + DebugColorScheme(color_scheme) + } +} + +fn debug_signal_map_enabled(displayed_signal: Option>) -> bool { + displayed_signal.is_some() +} + +fn debug_display_signal_map( + mut hexes: Query<(&TilePos, &mut Handle)>, + signals: Res, + displayed_signal: Res, + color_scheme: Res, +) { + for (tile, mut material) in &mut hexes { + let signal_strength = signals.get(displayed_signal.0, *tile).0; + // Just a simple dark red (low strength) to bright yellow (high strength) color scheme + // The scale is logarithmic, so that small nuances are still pretty visible + let scaled_strength = signal_strength / 50.0; + let color_index = if signal_strength < f32::EPSILON { + 0 + } else { + ((scaled_strength * 254.0) as usize) + 1 + }; + *material.as_mut() = color_scheme.0[color_index.min(255)].clone_weak(); + } +} + #[cfg(test)] mod tests { use super::*; From 883319e3640300e6f741ddc520ec0034d908eff8 Mon Sep 17 00:00:00 2001 From: Austreelis Date: Sun, 19 Mar 2023 07:03:44 +0100 Subject: [PATCH 2/6] Use the same approach as tile selection to display signals I'm not really pleased with the fact I had to disable the `display_tile_overlay` system when using the signal overlay but this can be cleanup later. Color scheme has been updated to look better with the new terrain and shadows, and the overlay material has been made transparent a bit. --- emergence_lib/src/graphics/mod.rs | 11 ++++++-- emergence_lib/src/signals.rs | 45 +++++++++++++++++++++---------- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/emergence_lib/src/graphics/mod.rs b/emergence_lib/src/graphics/mod.rs index 8f51f06a7..6ea5f773f 100644 --- a/emergence_lib/src/graphics/mod.rs +++ b/emergence_lib/src/graphics/mod.rs @@ -2,7 +2,10 @@ use bevy::prelude::*; -use crate::{asset_management::AssetState, player_interaction::InteractionSystem}; +use crate::{ + asset_management::AssetState, player_interaction::InteractionSystem, + signals::debug_signal_overlay_disabled, +}; use self::{ atmosphere::AtmospherePlugin, lighting::LightingPlugin, selection::display_tile_overlay, @@ -29,7 +32,11 @@ impl Plugin for GraphicsPlugin { .add_systems( (inherit_materials, remove_ghostly_shadows).in_base_set(CoreSet::PostUpdate), ) - .add_system(display_tile_overlay.after(InteractionSystem::SelectTiles)); + .add_system( + display_tile_overlay + .after(InteractionSystem::SelectTiles) + .run_if(debug_signal_overlay_disabled), + ); } } diff --git a/emergence_lib/src/signals.rs b/emergence_lib/src/signals.rs index 9909af597..33d5057c0 100644 --- a/emergence_lib/src/signals.rs +++ b/emergence_lib/src/signals.rs @@ -8,7 +8,7 @@ use core::ops::{Add, AddAssign, Mul, Sub, SubAssign}; use itertools::Itertools; use crate::asset_management::manifest::{ - Id, Item, ItemManifest, Structure, StructureManifest, Unit, UnitManifest, + Id, Item, ItemManifest, Structure, StructureManifest, Terrain, Unit, UnitManifest, }; use crate::simulation::geometry::{MapGeometry, TilePos}; use crate::simulation::SimulationSet; @@ -40,8 +40,8 @@ impl Plugin for SignalsPlugin { .in_schedule(CoreSchedule::FixedUpdate), ) .add_system( - debug_display_signal_map - .run_if(debug_signal_map_enabled) + debug_display_signal_overlay + .run_if(debug_signal_overlay_enabled) .in_base_set(CoreSet::PostUpdate), ); } @@ -473,7 +473,7 @@ fn degrade_signals(mut signals: ResMut) { } #[derive(Resource, Debug)] -struct DebugDisplayedSignal(SignalType); +pub(crate) struct DebugDisplayedSignal(SignalType); #[derive(Resource, Debug)] struct DebugColorScheme(Vec>); @@ -489,11 +489,13 @@ impl FromWorld for DebugColorScheme { let s = i as f32 / 255.0; color_scheme.push(material_assets.add(StandardMaterial { base_color: Color::Rgba { - red: 2.0 * s - s * s, + red: 0.8 * (2.0 * s - s * s), green: 0.8 * s.sqrt(), blue: s * s * 0.6, - alpha: 1.0, + alpha: 0.8, }, + unlit: true, + alpha_mode: AlphaMode::Add, ..Default::default() })); } @@ -503,27 +505,42 @@ impl FromWorld for DebugColorScheme { } } -fn debug_signal_map_enabled(displayed_signal: Option>) -> bool { +fn debug_signal_overlay_enabled(displayed_signal: Option>) -> bool { displayed_signal.is_some() } -fn debug_display_signal_map( - mut hexes: Query<(&TilePos, &mut Handle)>, +pub(crate) fn debug_signal_overlay_disabled( + displayed_signal: Option>, +) -> bool { + displayed_signal.is_none() +} + +fn debug_display_signal_overlay( + terrain_query: Query<(&TilePos, &Children), With>>, + mut overlay_query: Query<(&mut Handle, &mut Visibility)>, signals: Res, displayed_signal: Res, color_scheme: Res, ) { - for (tile, mut material) in &mut hexes { - let signal_strength = signals.get(displayed_signal.0, *tile).0; + for (tile_pos, children) in terrain_query.iter() { + // This is promised to be the correct entity in the initialization of the terrain's children + let overlay_entity = children[1]; + + let (mut overlay_material, mut overlay_visibility) = + overlay_query.get_mut(overlay_entity).unwrap(); + + let signal_strength = signals.get(displayed_signal.0, *tile_pos).0; // Just a simple dark red (low strength) to bright yellow (high strength) color scheme // The scale is logarithmic, so that small nuances are still pretty visible - let scaled_strength = signal_strength / 50.0; + let scaled_strength = signal_strength.ln_1p() / 6.0; let color_index = if signal_strength < f32::EPSILON { - 0 + *overlay_visibility = Visibility::Hidden; + continue; } else { + *overlay_visibility = Visibility::Visible; ((scaled_strength * 254.0) as usize) + 1 }; - *material.as_mut() = color_scheme.0[color_index.min(255)].clone_weak(); + *overlay_material.as_mut() = color_scheme.0[color_index.min(255)].clone_weak(); } } From 90f0fe98d20ec84054a53930fc91b03b7d5967b4 Mon Sep 17 00:00:00 2001 From: Austreelis Date: Sun, 19 Mar 2023 07:11:18 +0100 Subject: [PATCH 3/6] Implement lazy evaluation of a continuous field for signal diffusion This only takes into account sources for now. This means that it only reaches part of the goal set in #511: 1. It only tracks signal emitters. 2. It does determine an equation for each emitter. The equation simply is the solution of the diffusion equation for that emitter: `1/(1+D*t) * 1/sqrt(4*pi*k*t) * exp(d^2 / (4*k*t))` where: - `D` is a decay rate constant, and `1/(1+D*t)` is a decay term. - `k` is a diffusivity constant. - `d` is the distance between the emitter and a tile. - `t` is the time elapsed since the signal was emitted. 3. Contributions of each emitter are then integrated to compute the actual field value. This works because of the linearity of this subset of the problem, but may not hold when tackling e.g. barriers or signal modifiers (I haven't yet investigated it). 4. It lazyly looks up the field value when needed, but I suspect this is pretty costly when displaying the signal overlay. I don't know if and what we'd want to do about it yet. --- emergence_lib/src/signals.rs | 166 ++++++-------------------- emergence_lib/src/signals/equation.rs | 119 ++++++++++++++++++ 2 files changed, 157 insertions(+), 128 deletions(-) create mode 100644 emergence_lib/src/signals/equation.rs diff --git a/emergence_lib/src/signals.rs b/emergence_lib/src/signals.rs index 33d5057c0..f29424e60 100644 --- a/emergence_lib/src/signals.rs +++ b/emergence_lib/src/signals.rs @@ -3,6 +3,8 @@ //! By collecting information about the local environment into a slowly updated, tile-centric data structure, //! we can scale path-finding and decisionmaking in a clear and comprehensible way. +mod equation; + use bevy::{prelude::*, utils::HashMap}; use core::ops::{Add, AddAssign, Mul, Sub, SubAssign}; use itertools::Itertools; @@ -14,14 +16,18 @@ use crate::simulation::geometry::{MapGeometry, TilePos}; use crate::simulation::SimulationSet; use crate::units::goals::Goal; -/// The fraction of signals in each cell that will move to each of 6 neighbors each frame. -/// -/// Higher values will result in more spread out signals. -/// -/// If no neighbor exists, total diffusion will be reduced correspondingly. -/// As a result, this value *must* be below 1/6, -/// and probably should be below 1/7 to avoid weirdness. -pub const DIFFUSION_FRACTION: f32 = 0.1; +use self::equation::DiffusionEquation; + +/// The diffusivity of signals: how fast signals diffuse to neighboring tiles, in seconds per +/// tile. +pub const DIFFUSIVITY: f32 = 0.2; + +/// The decay rate of signals: how fast signals decay, in "signal strength" per second. +pub const DECAY_RATE: f32 = 2.0; + +/// The strength below which a signal is considered negligible. The total +/// quantity of a single emission is considered when trimming signals. +pub const DECAY_THRESHOLD: f32 = 0.1; /// The resources and systems need to work with signals pub(crate) struct SignalsPlugin; @@ -34,7 +40,7 @@ impl Plugin for SignalsPlugin { "acacia_leaf", )))) .add_systems( - (emit_signals, diffuse_signals, degrade_signals) + (emit_signals, update_signals) .chain() .in_set(SimulationSet) .in_schedule(CoreSchedule::FixedUpdate), @@ -50,17 +56,17 @@ impl Plugin for SignalsPlugin { /// The central resource that tracks all signals. #[derive(Resource, Debug, Default)] pub struct Signals { - /// The spatialized map for each signal - maps: HashMap, + /// The equations associated to each signal type. + signal_equations: HashMap, } impl Signals { /// Returns the signal strength of `signal_type` at the given `tile_pos`. /// /// Missing values will be filled with [`SignalStrength::ZERO`]. - pub fn get(&self, signal_type: SignalType, tile_pos: TilePos) -> SignalStrength { - match self.maps.get(&signal_type) { - Some(map) => map.get(tile_pos), + fn get(&self, signal_type: SignalType, tile_pos: TilePos) -> SignalStrength { + match self.signal_equations.get(&signal_type) { + Some(equation) => equation.evaluate_signal(tile_pos), None => SignalStrength::ZERO, } } @@ -72,12 +78,12 @@ impl Signals { tile_pos: TilePos, signal_strength: SignalStrength, ) { - match self.maps.get_mut(&signal_type) { - Some(map) => map.add_signal(tile_pos, signal_strength), + match self.signal_equations.get_mut(&signal_type) { + Some(equation) => equation.emit_signal(tile_pos, signal_strength), None => { - let mut new_map = SignalMap::default(); - new_map.add_signal(tile_pos, signal_strength); - self.maps.insert(signal_type, new_map); + let mut new_equation = DiffusionEquation::default(); + new_equation.emit_signal(tile_pos, signal_strength); + self.signal_equations.insert(signal_type, new_equation); } } } @@ -87,7 +93,7 @@ impl Signals { /// This is useful for decision-making. pub(crate) fn all_signals_at_position(&self, tile_pos: TilePos) -> LocalSignals { let mut all_signals = HashMap::new(); - for &signal_type in self.maps.keys() { + for &signal_type in self.signal_equations.keys() { let strength = self.get(signal_type, tile_pos); all_signals.insert(signal_type, strength); } @@ -193,43 +199,6 @@ impl Signals { signal_strength_map } - - /// Diffuses signals from one cell into the next - pub fn diffuse(&mut self, map_geometry: &MapGeometry, diffusion_fraction: f32) { - for original_map in self.maps.values_mut() { - let num_elements = original_map.map.len(); - let size_hint = num_elements * 6; - let mut addition_map = Vec::with_capacity(size_hint); - let mut removal_map = Vec::with_capacity(size_hint); - - for (&occupied_tile, original_strength) in original_map - .map - .iter() - .filter(|(_, signal_strength)| SignalStrength::ZERO.ne(signal_strength)) - { - let amount_to_send_to_each_neighbor = *original_strength * diffusion_fraction; - - let mut num_neighbors = 0.0; - for neighboring_tile in occupied_tile.empty_neighbors(map_geometry) { - num_neighbors += 1.0; - addition_map.push((neighboring_tile, amount_to_send_to_each_neighbor)); - } - removal_map.push(( - occupied_tile, - amount_to_send_to_each_neighbor * num_neighbors, - )); - } - - // We cannot do this in one step, as we need to avoid bizarre iteration order dependencies - for (removal_pos, removal_strength) in removal_map.into_iter() { - original_map.subtract_signal(removal_pos, removal_strength) - } - - for (addition_pos, addition_strength) in addition_map.into_iter() { - original_map.add_signal(addition_pos, addition_strength) - } - } - } } /// All of the signals on a single tile. @@ -273,41 +242,6 @@ impl LocalSignals { } } -/// Stores the [`SignalStrength`] of the given [`SignalType`] at each [`TilePos`]. -#[derive(Debug, Default)] -struct SignalMap { - /// The lookup data structure - map: HashMap, -} - -impl SignalMap { - /// Returns the signal strenth at the given [`TilePos`]. - /// - /// Missing values will be filled with [`SignalStrength::ZERO`]. - fn get(&self, tile_pos: TilePos) -> SignalStrength { - *self.map.get(&tile_pos).unwrap_or(&SignalStrength::ZERO) - } - - /// Returns a mutable reference to the signal strength at the given [`TilePos`]. - /// - /// Missing values will be inserted with [`SignalStrength::ZERO`]. - fn get_mut(&mut self, tile_pos: TilePos) -> &mut SignalStrength { - self.map.entry(tile_pos).or_insert(SignalStrength::ZERO) - } - - /// Adds the `signal_strength` to the signal at `tile_pos`. - fn add_signal(&mut self, tile_pos: TilePos, signal_strength: SignalStrength) { - *self.get_mut(tile_pos) += signal_strength - } - - /// Subtracts the `signal_strength` to the signal at `tile_pos`. - /// - /// The value is capped a minimum of [`SignalStrength::ZERO`]. - fn subtract_signal(&mut self, tile_pos: TilePos, signal_strength: SignalStrength) { - *self.get_mut(tile_pos) -= signal_strength; - } -} - /// The variety of signal. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum SignalType { @@ -432,44 +366,20 @@ fn emit_signals(mut signals: ResMut, emitter_query: Query<(&TilePos, &E } /// Spreads signals between tiles. -fn diffuse_signals(mut signals: ResMut, map_geometry: Res) { - let map_geometry = &*map_geometry; - signals.diffuse(map_geometry, DIFFUSION_FRACTION); -} - -/// Degrades signals, allowing them to approach an asymptotically constant level. -fn degrade_signals(mut signals: ResMut) { - /// The fraction of signal that will decay at each step. - /// - /// Higher values lead to faster decay and improved signal responsiveness. - /// This must always be between 0 and 1. - const DEGRADATION_FRACTION: f32 = 0.01; - - /// The value below which decayed signals are eliminated completely - /// - /// Increasing this value will: - /// - increase computational costs - /// - increase the range at which tasks can be detected - /// - increase the amount of time units will wait around for more production - const EPSILON_STRENGTH: SignalStrength = SignalStrength(1e-8); - - for signal_map in signals.maps.values_mut() { - let mut tiles_to_clear: Vec = Vec::with_capacity(signal_map.map.len()); - - for (tile_pos, signal_strength) in signal_map.map.iter_mut() { - let new_strength = *signal_strength * (1. - DEGRADATION_FRACTION); - - if new_strength > EPSILON_STRENGTH { - *signal_strength = new_strength; - } else { - tiles_to_clear.push(*tile_pos); - } - } - - for tile_to_clear in tiles_to_clear { - signal_map.map.remove(&tile_to_clear); +fn update_signals(mut signals: ResMut, time: Res