Skip to content

Instantly share code, notes, and snippets.

@eqs
Created February 7, 2023 11:49
Show Gist options
  • Save eqs/de09e1306b5059daed8e762648c43840 to your computer and use it in GitHub Desktop.
Save eqs/de09e1306b5059daed8e762648c43840 to your computer and use it in GitHub Desktop.
An implementation of Boids algorithm with Bevy 0.9.
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