Created
February 7, 2023 11:49
-
-
Save eqs/de09e1306b5059daed8e762648c43840 to your computer and use it in GitHub Desktop.
An implementation of Boids algorithm with Bevy 0.9.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use rand::{thread_rng, Rng}; | |
use bevy::prelude::*; | |
use bevy::time::FixedTimestep; | |
const WIN_WIDTH: u32 = 640; | |
const WIN_HEIGHT: u32 = 640; | |
fn main() { | |
App::new() | |
.insert_resource(ClearColor(Color::rgb(0.04, 0.04, 0.04))) | |
.insert_resource(Boids::default()) | |
.add_startup_system(setup_camera) | |
.add_startup_system(init_boids) | |
.add_plugins(DefaultPlugins.set( | |
WindowPlugin { | |
window: WindowDescriptor { | |
title: "Bevy Game".to_string(), | |
width: WIN_WIDTH as f32, | |
height: WIN_HEIGHT as f32, | |
..default() | |
}, | |
..default() | |
} | |
)) | |
.add_plugin(PhysicsPlugin::default()) | |
.add_plugin(FlockingPlugin::default()) | |
.add_plugin(BoundaryPlugin) | |
.add_system(spawn_boid_input.before(FlockingSystemLabel)) | |
.add_system(update_transform.after(PhysicsSystemLabel)) | |
.add_system(bevy::window::close_on_esc) | |
.run(); | |
} | |
fn setup_camera(mut commands: Commands) { | |
commands.spawn(Camera2dBundle::default()); | |
} | |
fn init_boids(mut commands: Commands, mut boids: ResMut<Boids>) { | |
let spawn_width = WIN_WIDTH as f32 / 8.0; | |
let spawn_height = WIN_HEIGHT as f32 / 8.0; | |
*boids = Boids((0..64).map(|_k| { | |
let mut rng = thread_rng(); | |
let x: f32 = rng.gen_range(-spawn_width..spawn_width); | |
let y: f32 = rng.gen_range(-spawn_height..spawn_height); | |
spawn_boid(&mut commands, x, y) | |
}).collect::<Vec<_>>()); | |
} | |
fn spawn_boid_input( | |
mut commands: Commands, | |
mut boids: ResMut<Boids>, | |
windows: Res<Windows>, | |
buttons: Res<Input<MouseButton>> | |
) { | |
if buttons.just_pressed(MouseButton::Left) { | |
if let Some(window) = windows.get_primary() { | |
if let Some(position) = window.cursor_position() { | |
let boid = spawn_boid( | |
&mut commands, | |
position.x - WIN_WIDTH as f32 / 2.0, | |
position.y - WIN_HEIGHT as f32 / 2.0 | |
); | |
boids.push(boid); | |
} | |
} | |
} | |
} | |
fn spawn_boid(commands: &mut Commands, x: f32, y: f32) -> Entity { | |
let mut rng = thread_rng(); | |
let vx: f32 = rng.gen_range(-64.0..64.0); | |
let vy: f32 = rng.gen_range(-64.0..64.0); | |
let ax: f32 = rng.gen_range(-64.0..64.0); | |
let ay: f32 = rng.gen_range(-64.0..64.0); | |
commands.spawn(SpriteBundle { | |
sprite: Sprite { | |
color: Color::rgb(0.2, 0.8, 0.4), | |
..default() | |
}, | |
transform: Transform { | |
translation: Vec3::new(x, y, 16.0), | |
scale: Vec3::new(8.0, 16.0, 16.0), | |
..default() | |
}, | |
..default() | |
}) | |
.insert(Boid::default()) | |
.insert(Separation(1.0)) | |
.insert(Alignment(1.5)) | |
.insert(Cohesion(1.0)) | |
.insert(Position(Vec3::new(x, y, 0.0))) | |
.insert(Velocity(Vec3::new(vx, vy, 0.0))) | |
.insert(Acceleration(Vec3::new(ax, ay, 0.0))) | |
.insert(SpeedLimit(128.0)) | |
.insert(Bounding(8.0)) | |
.insert(BoundaryWrap) | |
.id() | |
} | |
fn update_transform(mut q: Query<(&Position, Option<&Velocity>, &mut Transform), With<Boid>>) { | |
for (pos, velocity, mut transform) in q.iter_mut() { | |
transform.translation = pos.0; | |
if let Some(vel) = velocity { | |
let angle = vel.0.y.atan2(vel.0.x) + std::f32::consts::PI / 2.0; | |
transform.rotation = Quat::from_rotation_z(angle); | |
} | |
} | |
} | |
pub struct PhysicsPlugin { | |
time_step: f32, | |
} | |
impl PhysicsPlugin { | |
pub fn with_fixed_time_step(time_step: f32) -> Self { | |
Self { time_step } | |
} | |
} | |
impl Default for PhysicsPlugin { | |
fn default() -> Self { | |
Self::with_fixed_time_step(1.0 / 60.0) | |
} | |
} | |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SystemLabel)] | |
pub struct PhysicsSystemLabel; | |
#[derive(Resource)] | |
pub struct TimeStep(pub f32); | |
impl Plugin for PhysicsPlugin { | |
fn build(&self, app: &mut App) { | |
app | |
.insert_resource(TimeStep(self.time_step)) | |
.add_system_set( | |
SystemSet::new() | |
.with_run_criteria(FixedTimestep::step(self.time_step.into())) | |
.label(PhysicsSystemLabel) | |
.with_system(update_physics) | |
.with_system(speed_limit_system), | |
); | |
} | |
} | |
#[derive(Component, Default, PartialEq, Copy, Clone)] | |
pub struct Position(Vec3); | |
#[derive(Component, Default, PartialEq, Copy, Clone)] | |
pub struct Velocity(Vec3); | |
#[derive(Component, Default, PartialEq, Copy, Clone)] | |
pub struct SpeedLimit(f32); | |
#[derive(Component, Default, PartialEq, Copy, Clone)] | |
pub struct Acceleration(Vec3); | |
pub fn update_physics( | |
time_step: Res<TimeStep>, | |
mut q: Query<(&mut Position, Option<&mut Velocity>, Option<&Acceleration>)>, | |
) { | |
for (mut position, velocity, acceleration) in q.iter_mut() { | |
if let Some(mut v) = velocity { | |
position.0.x += v.0.x * time_step.0; | |
position.0.y += v.0.y * time_step.0; | |
if let Some(a) = acceleration { | |
v.0.x += a.0.x * time_step.0; | |
v.0.y += a.0.y * time_step.0; | |
} | |
} | |
} | |
} | |
pub fn speed_limit_system(mut q: Query<(&SpeedLimit, &mut Velocity)>) { | |
for (speed_limit, mut velocity) in q.iter_mut() { | |
velocity.0 = velocity.0.clamp_length_max(speed_limit.0); | |
} | |
} | |
pub struct BoundaryPlugin; | |
impl Plugin for BoundaryPlugin { | |
fn build(&self, app: &mut App) { | |
app.add_system_set_to_stage( | |
CoreStage::PostUpdate, | |
SystemSet::new() | |
.with_system(boundary_wrap_system), | |
); | |
} | |
} | |
#[derive(Component, Default, PartialEq, Copy, Clone)] | |
pub struct Bounding(f32); | |
#[derive(Component)] | |
pub struct BoundaryWrap; | |
pub fn boundary_wrap_system( | |
windows: ResMut<Windows>, | |
mut q: Query<(&mut Position, &Bounding), With<BoundaryWrap>>, | |
) { | |
if let Some(window) = windows.get_primary() { | |
let half_width = window.width() / 2.0; | |
let half_height = window.height() / 2.0; | |
for (mut pos, bounding) in q.iter_mut() { | |
let x = pos.0.x; | |
let y = pos.0.y; | |
let radius = bounding.0; | |
if x + radius * 2.0 < -half_width { | |
pos.0.x = half_width + radius * 2.0; | |
} else if x - radius * 2.0 > half_width { | |
pos.0.x = -half_width - radius * 2.0; | |
} | |
if y + radius * 2.0 < -half_height { | |
pos.0.y = half_height + radius * 2.0; | |
} else if y - radius * 2.0 > half_height { | |
pos.0.y = -half_height - radius * 2.0; | |
} | |
} | |
} | |
} | |
pub struct FlockingPlugin { | |
time_step: f32, | |
desired_separation: f32, | |
neighbor_distance: f32, | |
max_force: f32, | |
max_speed: f32, | |
} | |
impl FlockingPlugin { | |
pub fn with_fixed_time_step(time_step: f32) -> Self { | |
Self { | |
time_step, | |
..default() | |
} | |
} | |
} | |
impl Default for FlockingPlugin { | |
fn default() -> Self { | |
Self { | |
time_step: 1.0 / 60.0, | |
desired_separation: 25.0, | |
neighbor_distance: 50.0, | |
max_force: 128.0, | |
max_speed: 128.0, | |
} | |
} | |
} | |
impl Plugin for FlockingPlugin { | |
fn build(&self, app: &mut App) { | |
app | |
.insert_resource(TimeStep(self.time_step)) | |
.insert_resource(DesiredSeparation(self.desired_separation)) | |
.insert_resource(NeighborDistance(self.neighbor_distance)) | |
.insert_resource(MaxForce(self.max_force)) | |
.insert_resource(MaxSpeed(self.max_speed)) | |
.add_system_set( | |
SystemSet::new() | |
.with_run_criteria(FixedTimestep::step(self.time_step.into())) | |
.label(FlockingSystemLabel) | |
.with_system(reset_force_system) | |
.with_system(separation_system.label(FlockingRuleLabel).after(reset_force_system)) | |
.with_system(alignment_system.label(FlockingRuleLabel).after(reset_force_system)) | |
.with_system(cohesion_system.label(FlockingRuleLabel).after(reset_force_system)) | |
.with_system(apply_force_system), | |
); | |
} | |
} | |
#[derive(Component, Default)] | |
pub struct Boid { | |
force: Vec3, | |
} | |
#[derive(Resource, Default, Deref, DerefMut)] | |
pub struct Boids(Vec<Entity>); | |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SystemLabel)] | |
pub struct FlockingSystemLabel; | |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SystemLabel)] | |
pub struct FlockingRuleLabel; | |
#[derive(Resource)] | |
pub struct NeighborDistance(f32); | |
#[derive(Resource)] | |
pub struct DesiredSeparation(f32); | |
#[derive(Resource)] | |
pub struct MaxForce(f32); | |
#[derive(Resource)] | |
pub struct MaxSpeed(f32); | |
#[derive(Component, PartialEq, Copy, Clone)] | |
pub struct Separation(f32); | |
impl Default for Separation { | |
fn default() -> Self { | |
Self { 0: 1.0 } | |
} | |
} | |
#[derive(Component, PartialEq, Copy, Clone)] | |
pub struct Alignment(f32); | |
impl Default for Alignment { | |
fn default() -> Self { | |
Self { 0: 1.5 } | |
} | |
} | |
#[derive(Component, PartialEq, Copy, Clone)] | |
pub struct Cohesion(f32); | |
impl Default for Cohesion { | |
fn default() -> Self { | |
Self { 0: 1.0 } | |
} | |
} | |
fn reset_force_system(mut q: Query<&mut Boid>) { | |
for mut boid in q.iter_mut() { | |
boid.force = Vec3::new(0.0, 0.0, 0.0); | |
} | |
} | |
fn separation_system( | |
boids: Res<Boids>, | |
desired_separation: Res<DesiredSeparation>, | |
max_force: Res<MaxForce>, | |
max_speed: Res<MaxSpeed>, | |
mut q: Query<(Entity, &mut Boid, &Position, &Velocity, &Separation)> | |
) { | |
let boid_positions = boids.iter() | |
.filter_map(|e| { | |
if let Ok(row) = q.get_mut(*e) { | |
Some((row.0, *row.2, *row.3)) // Get entity, and dereferenced position and velocity | |
} else { | |
None | |
} | |
}) | |
.collect::<Vec<(Entity, Position, Velocity)>>(); | |
for (_entity, mut boid, position, velocity, separation) in q.iter_mut() { | |
let mut steer: Vec3 = Vec3::new(0.0, 0.0, 0.0); | |
let mut count = 0; | |
for (_other_entity, other_position, _other_velocity) in boid_positions.iter() { | |
let dist = position.0.distance(other_position.0); | |
if dist > 0.0 && dist < desired_separation.0 { | |
let diff = position.0 - other_position.0; | |
steer += diff.normalize() / dist; | |
count += 1; | |
} | |
} | |
if count > 0 { | |
steer /= count as f32; | |
} | |
if steer.length_squared() > 0.0 { | |
steer = (steer.normalize() * max_speed.0 - velocity.0).clamp_length_max(max_force.0); | |
boid.force += steer * separation.0; | |
} | |
} | |
} | |
fn alignment_system( | |
boids: Res<Boids>, | |
neighbor_distance: Res<NeighborDistance>, | |
max_force: Res<MaxForce>, | |
max_speed: Res<MaxSpeed>, | |
mut q: Query<(Entity, &mut Boid, &Position, &Velocity, &Alignment)> | |
) { | |
let boid_positions = boids.iter() | |
.filter_map(|e| { | |
if let Ok(row) = q.get_mut(*e) { | |
Some((row.0, *row.2, *row.3)) // Get entity, and dereferenced position and velocity | |
} else { | |
None | |
} | |
}) | |
.collect::<Vec<(Entity, Position, Velocity)>>(); | |
for (_entity, mut boid, position, velocity, alignment) in q.iter_mut() { | |
let mut vel_sum: Vec3 = Vec3::new(0.0, 0.0, 0.0); | |
let mut count = 0; | |
for (_other_entity, other_position, other_velocity) in boid_positions.iter() { | |
let dist = position.0.distance(other_position.0); | |
if dist > 0.0 && dist < neighbor_distance.0 { | |
vel_sum += other_velocity.0; | |
count += 1; | |
} | |
} | |
if count > 0 { | |
vel_sum /= count as f32; | |
vel_sum = vel_sum.normalize() * max_speed.0; | |
let steer = (vel_sum - velocity.0).clamp_length_max(max_force.0); | |
boid.force += steer * alignment.0; | |
} | |
} | |
} | |
fn cohesion_system( | |
boids: Res<Boids>, | |
neighbor_distance: Res<NeighborDistance>, | |
max_force: Res<MaxForce>, | |
max_speed: Res<MaxSpeed>, | |
mut q: Query<(Entity, &mut Boid, &Position, &Velocity, &Cohesion)> | |
) { | |
let boid_positions = boids.iter() | |
.filter_map(|e| { | |
if let Ok(row) = q.get_mut(*e) { | |
Some((row.0, *row.2, *row.3)) // Get entity, and dereferenced position and velocity | |
} else { | |
None | |
} | |
}) | |
.collect::<Vec<(Entity, Position, Velocity)>>(); | |
for (_entity, mut boid, position, velocity, separation) in q.iter_mut() { | |
let mut pos_sum: Vec3 = Vec3::new(0.0, 0.0, 0.0); | |
let mut count = 0; | |
for (_other_entity, other_position, _other_velocity) in boid_positions.iter() { | |
let dist = position.0.distance(other_position.0); | |
if dist > 0.0 && dist < neighbor_distance.0 { | |
pos_sum += other_position.0; | |
count += 1; | |
} | |
} | |
if count > 0 { | |
pos_sum /= count as f32; | |
let desired = (pos_sum - position.0).normalize() * max_speed.0; | |
let steer = (desired - velocity.0).clamp_length_max(max_force.0); | |
boid.force += steer * separation.0; | |
} | |
} | |
} | |
fn apply_force_system(time_step: Res<TimeStep>, mut q: Query<(&Boid, &mut Velocity)>) { | |
for (boid, mut velocity) in q.iter_mut() { | |
velocity.0 += boid.force * time_step.0; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment