wip
This commit is contained in:
parent
2ce0307b3c
commit
af4065e149
7 changed files with 449 additions and 161 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -943,6 +943,8 @@ dependencies = [
|
||||||
"bevy_time",
|
"bevy_time",
|
||||||
"bevy_transform",
|
"bevy_transform",
|
||||||
"bevy_ui",
|
"bevy_ui",
|
||||||
|
"bevy_ui_render",
|
||||||
|
"bevy_ui_widgets",
|
||||||
"bevy_utils",
|
"bevy_utils",
|
||||||
"bevy_window",
|
"bevy_window",
|
||||||
"bevy_winit",
|
"bevy_winit",
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy = { version = "0.19", default-features = false, features = ["2d"] }
|
bevy = { version = "0.19", default-features = false, features = ["2d", "ui"] }
|
||||||
|
|
||||||
[profile.dev.package."*"]
|
[profile.dev.package."*"]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
|
||||||
206
src/boids.rs
Normal file
206
src/boids.rs
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
use bevy::{platform::collections::HashMap, prelude::*};
|
||||||
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::ModelConfig,
|
||||||
|
spatial_grid::{SpatialGrid, update_spatial_grid},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct BoidsPlugin;
|
||||||
|
|
||||||
|
impl Plugin for BoidsPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_resource::<SpatialGrid>()
|
||||||
|
.add_systems(Startup, setup)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
sync_boid_count.run_if(resource_changed::<ModelConfig>),
|
||||||
|
)
|
||||||
|
.add_systems(FixedUpdate, (update_spatial_grid, update_boids).chain());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Boid {
|
||||||
|
velocity: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct BoidAssets {
|
||||||
|
mesh: Mesh2d,
|
||||||
|
material: MeshMaterial2d<ColorMaterial>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup(
|
||||||
|
model_config: Res<ModelConfig>,
|
||||||
|
mut commands: Commands,
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<ColorMaterial>>,
|
||||||
|
) {
|
||||||
|
let ModelConfig {
|
||||||
|
num_boids,
|
||||||
|
max_velocity,
|
||||||
|
..
|
||||||
|
} = *model_config;
|
||||||
|
|
||||||
|
// Triangle pointing right
|
||||||
|
let assets = BoidAssets {
|
||||||
|
mesh: Mesh2d(meshes.add(Triangle2d::new(
|
||||||
|
Vec2::new(10.0, 0.0),
|
||||||
|
Vec2::new(-3.0, 3.0),
|
||||||
|
Vec2::new(-3.0, -3.0),
|
||||||
|
))),
|
||||||
|
material: MeshMaterial2d(materials.add(Color::WHITE)),
|
||||||
|
};
|
||||||
|
spawn_boids(
|
||||||
|
&mut commands,
|
||||||
|
&assets,
|
||||||
|
0..num_boids,
|
||||||
|
num_boids,
|
||||||
|
max_velocity,
|
||||||
|
);
|
||||||
|
commands.insert_resource(assets);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_boids(
|
||||||
|
commands: &mut Commands,
|
||||||
|
assets: &BoidAssets,
|
||||||
|
range: std::ops::Range<usize>,
|
||||||
|
num_boids: usize,
|
||||||
|
max_velocity: f32,
|
||||||
|
) {
|
||||||
|
let mesh = assets.mesh.clone();
|
||||||
|
let material = assets.material.clone();
|
||||||
|
commands.spawn_batch(range.map(move |n| {
|
||||||
|
let angle = 2.0 * PI / (num_boids as f32) * n as f32;
|
||||||
|
(
|
||||||
|
Boid {
|
||||||
|
velocity: Vec2::from_angle(angle) * max_velocity,
|
||||||
|
},
|
||||||
|
mesh.clone(),
|
||||||
|
material.clone(),
|
||||||
|
Transform::default().rotate(Quat::from_rotation_z(angle)),
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns or despawns boids so that their count matches `ModelConfig::num_boids`.
|
||||||
|
fn sync_boid_count(
|
||||||
|
model_config: Res<ModelConfig>,
|
||||||
|
assets: Option<Res<BoidAssets>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
boids: Query<Entity, With<Boid>>,
|
||||||
|
) {
|
||||||
|
let Some(assets) = assets else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let current = boids.iter().len();
|
||||||
|
let target = model_config.num_boids;
|
||||||
|
if target > current {
|
||||||
|
spawn_boids(
|
||||||
|
&mut commands,
|
||||||
|
&assets,
|
||||||
|
current..target,
|
||||||
|
target,
|
||||||
|
model_config.max_velocity,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
for entity in boids.iter().take(current - target) {
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Model {
|
||||||
|
separation: Vec2,
|
||||||
|
alignment: Vec2,
|
||||||
|
cohesion: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_boids(
|
||||||
|
model_config: Res<ModelConfig>,
|
||||||
|
window: Single<&Window, With<bevy::window::PrimaryWindow>>,
|
||||||
|
time: Res<Time<Fixed>>,
|
||||||
|
grid: Res<SpatialGrid>,
|
||||||
|
mut query: Query<(Entity, &mut Boid, &mut Transform)>,
|
||||||
|
) {
|
||||||
|
let dt = time.delta_secs();
|
||||||
|
let x_bound = window.width() * model_config.x_bound;
|
||||||
|
let y_bound = window.height() * model_config.y_bound;
|
||||||
|
|
||||||
|
let mut model_updates = HashMap::new();
|
||||||
|
for (entity, boid, transform) in query.iter() {
|
||||||
|
let mut nb_neighbors: usize = 0;
|
||||||
|
let mut separation = Vec2::new(0.0, 0.0);
|
||||||
|
let mut avg_speed = Vec2::new(0.0, 0.0);
|
||||||
|
let mut avg_pos = Vec2::new(0.0, 0.0);
|
||||||
|
for &(other_entity, other_position) in grid.neighbors(transform.translation.xy()) {
|
||||||
|
if entity == other_entity {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let delta = transform.translation.xy() - other_position;
|
||||||
|
let distance = delta.length();
|
||||||
|
if distance < model_config.protected_range {
|
||||||
|
if distance > f32::EPSILON {
|
||||||
|
let direction = delta / distance;
|
||||||
|
let strength = 1.0 - distance / model_config.protected_range;
|
||||||
|
separation += direction * strength;
|
||||||
|
}
|
||||||
|
} else if distance < model_config.visible_range {
|
||||||
|
let Ok((_, other_boid, _)) = query.get(other_entity) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
avg_speed += other_boid.velocity;
|
||||||
|
avg_pos += other_position;
|
||||||
|
nb_neighbors += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nb_neighbors > 0 {
|
||||||
|
avg_speed /= nb_neighbors as f32;
|
||||||
|
avg_pos /= nb_neighbors as f32;
|
||||||
|
}
|
||||||
|
model_updates.insert(
|
||||||
|
entity,
|
||||||
|
Model {
|
||||||
|
separation,
|
||||||
|
alignment: avg_speed - boid.velocity,
|
||||||
|
cohesion: avg_pos - transform.translation.xy(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (entity, mut boid, mut transform) in query.iter_mut() {
|
||||||
|
let mut out_of_bounds_acceleration = Vec2::new(0.0, 0.0);
|
||||||
|
if transform.translation.x < -x_bound / 2.0 {
|
||||||
|
out_of_bounds_acceleration.x = 1.0;
|
||||||
|
} else if transform.translation.x > x_bound / 2.0 {
|
||||||
|
out_of_bounds_acceleration.x = -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if transform.translation.y < -y_bound / 2.0 {
|
||||||
|
out_of_bounds_acceleration.y = 1.0;
|
||||||
|
} else if transform.translation.y > y_bound / 2.0 {
|
||||||
|
out_of_bounds_acceleration.y = -1.0;
|
||||||
|
}
|
||||||
|
let out_of_bounds_acceleration = out_of_bounds_acceleration.normalize_or_zero();
|
||||||
|
|
||||||
|
let model_update = model_updates.get(&entity).unwrap();
|
||||||
|
|
||||||
|
let boundary_force = out_of_bounds_acceleration * model_config.max_velocity;
|
||||||
|
let separation_force = model_update.separation * model_config.separation_factor;
|
||||||
|
let alignment_force = model_update.alignment * model_config.alignment_factor;
|
||||||
|
let cohesion_force = model_update.cohesion * model_config.cohesion_factor;
|
||||||
|
|
||||||
|
// Push each boid forward along its current heading
|
||||||
|
let self_force = boid.velocity.normalize_or_zero() * model_config.self_acceleration;
|
||||||
|
|
||||||
|
let acceleration =
|
||||||
|
boundary_force + separation_force + alignment_force + cohesion_force + self_force;
|
||||||
|
boid.velocity += acceleration * dt;
|
||||||
|
boid.velocity = boid.velocity.clamp_length_max(model_config.max_velocity);
|
||||||
|
|
||||||
|
transform.translation.x += boid.velocity.x * dt;
|
||||||
|
transform.translation.y += boid.velocity.y * dt;
|
||||||
|
transform.rotation = Quat::from_rotation_z(boid.velocity.y.atan2(boid.velocity.x));
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/config.rs
Normal file
42
src/config.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
pub struct ConfigPlugin;
|
||||||
|
|
||||||
|
impl Plugin for ConfigPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_resource::<ModelConfig>()
|
||||||
|
.register_type::<ModelConfig>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Reflect, Resource)]
|
||||||
|
#[reflect(Resource)]
|
||||||
|
pub struct ModelConfig {
|
||||||
|
pub num_boids: usize,
|
||||||
|
pub x_bound: f32,
|
||||||
|
pub y_bound: f32,
|
||||||
|
pub max_velocity: f32,
|
||||||
|
pub protected_range: f32,
|
||||||
|
pub visible_range: f32,
|
||||||
|
pub alignment_factor: f32,
|
||||||
|
pub cohesion_factor: f32,
|
||||||
|
pub separation_factor: f32,
|
||||||
|
pub self_acceleration: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ModelConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
num_boids: 300,
|
||||||
|
x_bound: 0.75,
|
||||||
|
y_bound: 0.75,
|
||||||
|
max_velocity: 250.0,
|
||||||
|
protected_range: 20.0,
|
||||||
|
visible_range: 230.0,
|
||||||
|
alignment_factor: 0.3,
|
||||||
|
cohesion_factor: 0.2,
|
||||||
|
separation_factor: 500.0,
|
||||||
|
self_acceleration: 30.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
169
src/main.rs
169
src/main.rs
|
|
@ -1,35 +1,9 @@
|
||||||
use bevy::{platform::collections::HashMap, prelude::*};
|
mod boids;
|
||||||
use std::f32::consts::PI;
|
mod config;
|
||||||
|
mod spatial_grid;
|
||||||
|
mod ui;
|
||||||
|
|
||||||
#[derive(Reflect, Resource)]
|
use bevy::prelude::*;
|
||||||
#[reflect(Resource)]
|
|
||||||
struct ModelConfig {
|
|
||||||
num_boids: usize,
|
|
||||||
x_bound: f32,
|
|
||||||
y_bound: f32,
|
|
||||||
max_velocity: f32,
|
|
||||||
protected_range: f32,
|
|
||||||
visible_range: f32,
|
|
||||||
alignment_factor: f32,
|
|
||||||
cohesion_factor: f32,
|
|
||||||
separation_factor: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ModelConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
num_boids: 50,
|
|
||||||
x_bound: 0.7,
|
|
||||||
y_bound: 0.7,
|
|
||||||
max_velocity: 200.0,
|
|
||||||
protected_range: 50.0,
|
|
||||||
visible_range: 150.0,
|
|
||||||
alignment_factor: 0.5,
|
|
||||||
cohesion_factor: 0.5,
|
|
||||||
separation_factor: 200.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
|
|
@ -44,134 +18,9 @@ fn main() {
|
||||||
}),
|
}),
|
||||||
..default()
|
..default()
|
||||||
}))
|
}))
|
||||||
.init_resource::<ModelConfig>()
|
.add_plugins((config::ConfigPlugin, boids::BoidsPlugin, ui::ConfigUiPlugin))
|
||||||
.register_type::<ModelConfig>()
|
.add_systems(Startup, |mut commands: Commands| {
|
||||||
.add_systems(Startup, setup)
|
commands.spawn(Camera2d);
|
||||||
.add_systems(FixedUpdate, update_boids)
|
})
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
struct Boid {
|
|
||||||
velocity: Vec2,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup(
|
|
||||||
model_config: Res<ModelConfig>,
|
|
||||||
mut commands: Commands,
|
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
|
||||||
mut materials: ResMut<Assets<ColorMaterial>>,
|
|
||||||
) {
|
|
||||||
let ModelConfig {
|
|
||||||
num_boids,
|
|
||||||
max_velocity,
|
|
||||||
..
|
|
||||||
} = *model_config;
|
|
||||||
|
|
||||||
commands.spawn(Camera2d);
|
|
||||||
|
|
||||||
// Triangle pointing right
|
|
||||||
let triangle_mesh = Mesh2d(meshes.add(Triangle2d::new(
|
|
||||||
Vec2::new(10.0, 0.0),
|
|
||||||
Vec2::new(-3.0, 3.0),
|
|
||||||
Vec2::new(-3.0, -3.0),
|
|
||||||
)));
|
|
||||||
let triangle_material = MeshMaterial2d(materials.add(Color::WHITE));
|
|
||||||
commands.spawn_batch((0..num_boids).map(move |n| {
|
|
||||||
let angle = 2.0 * PI / (num_boids as f32) * n as f32;
|
|
||||||
(
|
|
||||||
Boid {
|
|
||||||
velocity: Vec2::from_angle(angle) * max_velocity,
|
|
||||||
},
|
|
||||||
triangle_mesh.clone(),
|
|
||||||
triangle_material.clone(),
|
|
||||||
Transform::default().rotate(Quat::from_rotation_z(angle)),
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Model {
|
|
||||||
separation: Vec2,
|
|
||||||
alignment: Vec2,
|
|
||||||
cohesion: Vec2,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_boids(
|
|
||||||
model_config: Res<ModelConfig>,
|
|
||||||
window: Single<&Window, With<bevy::window::PrimaryWindow>>,
|
|
||||||
time: Res<Time<Fixed>>,
|
|
||||||
mut query: Query<(Entity, &mut Boid, &mut Transform)>,
|
|
||||||
) {
|
|
||||||
let dt = time.delta_secs();
|
|
||||||
let x_bound = window.width() * model_config.x_bound;
|
|
||||||
let y_bound = window.height() * model_config.y_bound;
|
|
||||||
|
|
||||||
let mut model_updates = HashMap::new();
|
|
||||||
for (entity, boid, transform) in query.iter() {
|
|
||||||
let mut nb_neighbors: usize = 0;
|
|
||||||
let mut separation = Vec2::new(0.0, 0.0);
|
|
||||||
let mut avg_speed = Vec2::new(0.0, 0.0);
|
|
||||||
let mut avg_pos = Vec2::new(0.0, 0.0);
|
|
||||||
for (other_entity, other_boid, other_transform) in query.iter() {
|
|
||||||
if entity == other_entity {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let delta = transform.translation.xy() - other_transform.translation.xy();
|
|
||||||
let distance = delta.length();
|
|
||||||
if distance < model_config.protected_range {
|
|
||||||
if distance > f32::EPSILON {
|
|
||||||
let direction = delta / distance;
|
|
||||||
let strength = 1.0 - distance / model_config.protected_range;
|
|
||||||
separation += direction * strength;
|
|
||||||
}
|
|
||||||
} else if distance < model_config.visible_range {
|
|
||||||
avg_speed += other_boid.velocity;
|
|
||||||
avg_pos += other_transform.translation.xy();
|
|
||||||
nb_neighbors += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if nb_neighbors > 0 {
|
|
||||||
avg_speed /= nb_neighbors as f32;
|
|
||||||
avg_pos /= nb_neighbors as f32;
|
|
||||||
}
|
|
||||||
model_updates.insert(
|
|
||||||
entity,
|
|
||||||
Model {
|
|
||||||
separation,
|
|
||||||
alignment: avg_speed - boid.velocity,
|
|
||||||
cohesion: avg_pos - transform.translation.xy(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (entity, mut boid, mut transform) in query.iter_mut() {
|
|
||||||
let mut out_of_bounds_acceleration = Vec2::new(0.0, 0.0);
|
|
||||||
if transform.translation.x < -x_bound / 2.0 {
|
|
||||||
out_of_bounds_acceleration.x = 1.0;
|
|
||||||
} else if transform.translation.x > x_bound / 2.0 {
|
|
||||||
out_of_bounds_acceleration.x = -1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if transform.translation.y < -y_bound / 2.0 {
|
|
||||||
out_of_bounds_acceleration.y = 1.0;
|
|
||||||
} else if transform.translation.y > y_bound / 2.0 {
|
|
||||||
out_of_bounds_acceleration.y = -1.0;
|
|
||||||
}
|
|
||||||
let out_of_bounds_acceleration = out_of_bounds_acceleration.normalize_or_zero();
|
|
||||||
|
|
||||||
let model_update = model_updates.get(&entity).unwrap();
|
|
||||||
|
|
||||||
let boundary_force = out_of_bounds_acceleration * model_config.max_velocity;
|
|
||||||
let separation_force = model_update.separation * model_config.separation_factor;
|
|
||||||
let alignment_force = model_update.alignment * model_config.alignment_factor;
|
|
||||||
let cohesion_force = model_update.cohesion * model_config.cohesion_factor;
|
|
||||||
|
|
||||||
let acceleration = boundary_force + separation_force + alignment_force + cohesion_force;
|
|
||||||
boid.velocity += acceleration * dt;
|
|
||||||
boid.velocity = boid.velocity.clamp_length_max(model_config.max_velocity);
|
|
||||||
|
|
||||||
transform.translation.x += boid.velocity.x * dt;
|
|
||||||
transform.translation.y += boid.velocity.y * dt;
|
|
||||||
transform.rotation = Quat::from_rotation_z(boid.velocity.y.atan2(boid.velocity.x));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
44
src/spatial_grid.rs
Normal file
44
src/spatial_grid.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
use bevy::{platform::collections::HashMap, prelude::*};
|
||||||
|
|
||||||
|
use crate::{boids::Boid, config::ModelConfig};
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct SpatialGrid {
|
||||||
|
pub cell_size: f32,
|
||||||
|
// Maps a grid coordinate (x, y) to the boids (and their positions) in that cell
|
||||||
|
pub cells: HashMap<(i32, i32), Vec<(Entity, Vec2)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpatialGrid {
|
||||||
|
fn cell(&self, position: Vec2) -> (i32, i32) {
|
||||||
|
let cell = (position / self.cell_size).floor();
|
||||||
|
(cell.x as i32, cell.y as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Entries in the 3x3 block of cells around `position`. With `cell_size` at
|
||||||
|
/// least the search radius, this covers every boid within that radius.
|
||||||
|
pub fn neighbors(&self, position: Vec2) -> impl Iterator<Item = &(Entity, Vec2)> {
|
||||||
|
let (cx, cy) = self.cell(position);
|
||||||
|
(cx - 1..=cx + 1)
|
||||||
|
.flat_map(move |x| (cy - 1..=cy + 1).map(move |y| (x, y)))
|
||||||
|
.filter_map(|key| self.cells.get(&key))
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_spatial_grid(
|
||||||
|
model_config: Res<ModelConfig>,
|
||||||
|
mut grid: ResMut<SpatialGrid>,
|
||||||
|
boids: Query<(Entity, &Transform), With<Boid>>,
|
||||||
|
) {
|
||||||
|
grid.cell_size = model_config
|
||||||
|
.visible_range
|
||||||
|
.max(model_config.protected_range)
|
||||||
|
.max(1.0);
|
||||||
|
grid.cells.clear();
|
||||||
|
for (entity, transform) in &boids {
|
||||||
|
let position = transform.translation.xy();
|
||||||
|
let cell = grid.cell(position);
|
||||||
|
grid.cells.entry(cell).or_default().push((entity, position));
|
||||||
|
}
|
||||||
|
}
|
||||||
145
src/ui.rs
Normal file
145
src/ui.rs
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
use bevy::{prelude::*, reflect::structs::Struct};
|
||||||
|
|
||||||
|
use crate::config::ModelConfig;
|
||||||
|
|
||||||
|
pub struct ConfigUiPlugin;
|
||||||
|
|
||||||
|
impl Plugin for ConfigUiPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(Startup, setup_ui).add_systems(
|
||||||
|
Update,
|
||||||
|
(
|
||||||
|
toggle_panel,
|
||||||
|
update_config_labels.run_if(resource_changed::<ModelConfig>),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Editable `ModelConfig` fields and the amount each -/+ click changes them by.
|
||||||
|
const CONFIG_FIELDS: &[(&str, f32)] = &[
|
||||||
|
("num_boids", 5.0),
|
||||||
|
("x_bound", 0.05),
|
||||||
|
("y_bound", 0.05),
|
||||||
|
("max_velocity", 10.0),
|
||||||
|
("protected_range", 5.0),
|
||||||
|
("visible_range", 10.0),
|
||||||
|
("alignment_factor", 0.05),
|
||||||
|
("cohesion_factor", 0.05),
|
||||||
|
("separation_factor", 10.0),
|
||||||
|
("self_acceleration", 5.0),
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct ConfigPanel;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct ConfigLabel(&'static str);
|
||||||
|
|
||||||
|
fn read_field(config: &ModelConfig, name: &str) -> String {
|
||||||
|
let field = config.field(name).unwrap();
|
||||||
|
if let Some(v) = field.try_downcast_ref::<f32>() {
|
||||||
|
format!("{v:.2}")
|
||||||
|
} else if let Some(v) = field.try_downcast_ref::<usize>() {
|
||||||
|
v.to_string()
|
||||||
|
} else {
|
||||||
|
unreachable!("unsupported ModelConfig field type for {name}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step_field(config: &mut ModelConfig, name: &str, delta: f32) {
|
||||||
|
let field = config.field_mut(name).unwrap();
|
||||||
|
if let Some(v) = field.try_downcast_mut::<f32>() {
|
||||||
|
*v = (*v + delta).max(0.0);
|
||||||
|
} else if let Some(v) = field.try_downcast_mut::<usize>() {
|
||||||
|
*v = v.saturating_add_signed(delta as isize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_ui(mut commands: Commands, model_config: Res<ModelConfig>) {
|
||||||
|
let font = TextFont::from_font_size(14.0);
|
||||||
|
commands
|
||||||
|
.spawn((
|
||||||
|
Node {
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
top: px(10),
|
||||||
|
left: px(10),
|
||||||
|
padding: UiRect::all(px(8)),
|
||||||
|
display: Display::Grid,
|
||||||
|
// name, -, value, +
|
||||||
|
grid_template_columns: vec![RepeatedGridTrack::auto(4)],
|
||||||
|
row_gap: px(4),
|
||||||
|
column_gap: px(6),
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
border_radius: BorderRadius::all(px(6)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.6)),
|
||||||
|
Visibility::Hidden,
|
||||||
|
ConfigPanel,
|
||||||
|
))
|
||||||
|
.with_children(|panel| {
|
||||||
|
for &(name, step) in CONFIG_FIELDS {
|
||||||
|
panel.spawn((Text::new(name), font.clone()));
|
||||||
|
spawn_step_button(panel, "-", name, -step, &font);
|
||||||
|
panel.spawn((
|
||||||
|
Text::new(read_field(&model_config, name)),
|
||||||
|
font.clone(),
|
||||||
|
TextLayout::justify(Justify::Center),
|
||||||
|
Node {
|
||||||
|
min_width: px(60),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
ConfigLabel(name),
|
||||||
|
));
|
||||||
|
spawn_step_button(panel, "+", name, step, &font);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_step_button(
|
||||||
|
parent: &mut ChildSpawnerCommands,
|
||||||
|
label: &str,
|
||||||
|
field: &'static str,
|
||||||
|
delta: f32,
|
||||||
|
font: &TextFont,
|
||||||
|
) {
|
||||||
|
parent
|
||||||
|
.spawn((
|
||||||
|
Button,
|
||||||
|
Node {
|
||||||
|
width: px(22),
|
||||||
|
height: px(22),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
border_radius: BorderRadius::all(px(4)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::srgb(0.25, 0.25, 0.4)),
|
||||||
|
children![(Text::new(label), font.clone())],
|
||||||
|
))
|
||||||
|
.observe(
|
||||||
|
move |_: On<Pointer<Click>>, mut config: ResMut<ModelConfig>| {
|
||||||
|
step_field(&mut config, field, delta);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows or hides the config panel when M is pressed.
|
||||||
|
fn toggle_panel(
|
||||||
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut panel: Single<&mut Visibility, With<ConfigPanel>>,
|
||||||
|
) {
|
||||||
|
if keys.just_pressed(KeyCode::KeyM) {
|
||||||
|
panel.toggle_visible_hidden();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_config_labels(
|
||||||
|
model_config: Res<ModelConfig>,
|
||||||
|
mut labels: Query<(&ConfigLabel, &mut Text)>,
|
||||||
|
) {
|
||||||
|
for (label, mut text) in &mut labels {
|
||||||
|
text.0 = read_field(&model_config, label.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue