initial commit

This commit is contained in:
Marc Planard 2024-02-29 16:23:04 +01:00
commit be5e225a1e
10 changed files with 4048 additions and 0 deletions

3506
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

10
Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[package]
name = "boids"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bevy = { version = "0.10.1", features = [ "trace" ] }
rand = "0.8.5"

BIN
assets/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 KiB

BIN
assets/boid.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
assets/fonts/DejaVuSans.ttf Normal file

Binary file not shown.

185
src/boids.rs Normal file
View File

@ -0,0 +1,185 @@
use bevy::prelude::*;
use crate::map::Map;
use crate::Params;
pub const NBR : usize = 10_000;
pub const SIZE : f32 = 100.0;
pub const VIEW_DIST : f32 = 1.0;
pub const BORDER : f32 = 1.0;
#[derive(Bundle, Default)]
pub struct BoidBundle {
mesh: SpriteBundle,
pos: Pos,
vel: Vel,
dir_vec: DirVec,
prox_cache: ProxCache
}
impl BoidBundle {
pub fn new(
pos: Vec2, vel: Vec2,
mesh: SpriteBundle
) -> Self {
BoidBundle {
mesh,
pos: Pos(pos),
vel: Vel(vel),
..default()
}
}
}
pub struct BoidPlugin;
impl Plugin for BoidPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(Map::new(SIZE as usize));
app.add_systems((
boids_proxcache,
boids_sep,
boids_ali,
boids_coh,
boids_dir,
boids_move,
mesh_update
));
}
}
#[derive(Component, Default, Debug, Clone, Copy)]
pub struct Pos(pub Vec2);
#[derive(Component, Default, Debug, Clone, Copy)]
pub struct Vel(pub Vec2);
#[derive(Component, Default, Debug)]
pub struct DirVec(Vec<Vec2>);
#[derive(Component, Default, Debug)]
pub struct ProxCache(Vec<(Entity,Vec2,f32)>);
fn boids_proxcache(
map: Res<Map>,
mut query0: Query<(Entity, &Pos, &mut ProxCache)>,
query1: Query<&Pos>
) {
query0.par_iter_mut()
.for_each_mut(|(e0, Pos(p0), mut prox_cache)| {
prox_cache.0.clear();
for e1 in map.get(p0, VIEW_DIST) {
if e0 == e1 { continue }
let Ok(Pos(p1)) = query1.get(e1) else {
error!("entity not found! should never occure!");
continue;
};
let diff = *p0 - *p1;
let d = diff.length_squared();
if d > VIEW_DIST*VIEW_DIST || d == 0.0 { continue }
prox_cache.0.push((e1, diff, d));
}
});
}
fn boids_sep(
params: Res<Params>,
mut query: Query<(&Pos, &ProxCache, &mut DirVec)>,
) {
for (Pos(p0), ProxCache(prox_cache), mut dir_vec) in &mut query {
let mut sep = Vec2::default();
for (_,diff,d) in prox_cache {
sep += *diff / *d;
}
dir_vec.0.push(sep * params.sep);
dir_vec.0.push(border_vel(p0) * 0.2);
}
}
fn boids_coh(
params: Res<Params>,
mut query: Query<(&ProxCache, &mut DirVec)>,
) {
for (ProxCache(prox_cache), mut dir_vec) in &mut query {
let mut coh = Vec2::default();
for (_,diff,_) in prox_cache {
coh -= *diff;
}
dir_vec.0.push(coh * params.coh);
}
}
fn boids_ali(
params: Res<Params>,
mut query: Query<(&ProxCache, &mut DirVec)>,
query2: Query<&Vel>
) {
for (ProxCache(prox_cache), mut dir_vec) in &mut query {
let mut ali = Vec2::default();
for (e1,_,_) in prox_cache {
let Ok(Vel(v1)) = query2.get(*e1) else {
error!("entity not found! should never occure!");
continue;
};
ali += *v1;
}
dir_vec.0.push(ali * params.ali);
}
}
fn border_vel(p0: &Vec2) -> Vec2 {
Vec2::new(
match p0.x {
_ if p0.x < -SIZE+BORDER => 1.0,
_ if p0.x > SIZE-BORDER => -1.0,
_ => 0.0
},
match p0.y {
_ if p0.y < -SIZE+BORDER => 1.0,
_ if p0.y > SIZE-BORDER => -1.0,
_ => 0.0
}
)
}
fn boids_dir(mut query: Query<(&mut DirVec, &mut Vel)>) {
for (mut dir_vec, mut vel) in &mut query {
for v in &dir_vec.0 {
vel.0 += *v;
}
vel.0 = vel.0.normalize();
dir_vec.0.clear();
}
}
fn boids_move(
time: Res<Time>,
params: Res<Params>,
mut map: ResMut<Map>,
mut query: Query<(Entity, &mut Pos, &Vel)>
) {
let dt = time.delta_seconds();
for (e, mut pos, vel) in &mut query {
let new_pos = pos.0 + vel.0 * dt * params.speed;
let new_pos = Vec2::new(
new_pos.x.clamp(-SIZE, SIZE),
new_pos.y.clamp(-SIZE, SIZE)
);
if pos.0 != new_pos {
map.update(&pos.0, &new_pos, &e);
}
pos.0 = new_pos;
}
}
fn mesh_update(mut query: Query<(&mut Transform, &Pos, &Vel)>) {
for (mut transform, Pos(pos), Vel(vel)) in &mut query {
let pos3 = Vec3::new(pos.x, pos.y, 0.0);
let angle = Vec2::X.angle_between(*vel);
*transform = Transform::from_translation(pos3)
.with_rotation(Quat::from_rotation_z(angle));
}
}

59
src/camera.rs Normal file
View File

@ -0,0 +1,59 @@
use bevy::prelude::*;
use bevy::input::mouse::{MouseWheel,MouseMotion};
#[derive(Bundle, Default)]
pub struct CamBundle {
cam: Camera2dBundle,
state: CameraState
}
impl CamBundle {
pub fn new(scale: f32) -> Self {
CamBundle {
cam: Camera2dBundle {
transform: Transform::from_scale(Vec3::new(scale, scale, 1.0)),
..default()
},
state: CameraState { scale, ..default() }
}
}
}
#[derive(Component, Default)]
pub struct CameraState {
scale: f32,
pos: Vec2
}
pub struct CameraPlugin;
impl Plugin for CameraPlugin {
fn build(&self, app: &mut App) {
app.add_system(mouse_move_events);
}
}
fn mouse_move_events(
buttons: Res<Input<MouseButton>>,
mut mouse_wheel_events: EventReader<MouseWheel>,
mut mouse_motion_events: EventReader<MouseMotion>,
mut query: Query<(&mut Transform, &mut CameraState)>
) {
let (mut transform, mut state) = query.single_mut();
for event in mouse_wheel_events.iter() {
let scale = state.scale;
state.scale = (scale + event.y * 0.25 * scale)
.clamp(0.001, 1.0);
*transform = transform.with_scale(Vec3::new(state.scale,
state.scale, 1.0));
}
if buttons.pressed(MouseButton::Left) {
let scale = state.scale;
for event in mouse_motion_events.iter() {
state.pos -= event.delta * scale;
}
transform.translation = Vec3::new(state.pos.x, -state.pos.y, 0.0);
}
}

148
src/main.rs Normal file
View File

@ -0,0 +1,148 @@
mod map;
mod boids;
mod camera;
use bevy::prelude::*;
use bevy::diagnostic::{ Diagnostics, FrameTimeDiagnosticsPlugin };
use rand::{thread_rng, Rng};
use std::time::Duration;
use boids::{NBR,BORDER,SIZE,BoidPlugin,BoidBundle};
use map::Map;
use camera::{CameraPlugin,CamBundle};
#[derive(Resource,Debug)]
pub struct Params {
sep: f32,
ali: f32,
coh: f32,
speed: f32
}
impl Default for Params {
fn default() -> Self {
Params {
sep: 0.02,
ali: 0.2,
coh: 0.02,
speed: 1.0
}
}
}
#[derive(Component)]
struct StatsText;
#[derive(Resource)]
struct FpsTimer(Timer);
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Bevy :: Boids".into(),
resolution: (900., 900.).into(),
..default()
}),
..default()
}))
.add_plugin(FrameTimeDiagnosticsPlugin::default())
.init_resource::<Params>()
.add_plugin(BoidPlugin)
.add_plugin(CameraPlugin)
.insert_resource(FpsTimer(Timer::new(Duration::from_secs_f32(0.5),
TimerMode::Repeating)))
.add_system(update_stats)
.add_startup_system(setup)
.run();
}
fn setup(
mut commands: Commands,
mut map: ResMut<Map>,
asset_server: Res<AssetServer>
) {
// Camera
commands.spawn(CamBundle::new(0.1));
//BG
commands.spawn(SpriteBundle {
texture: asset_server.load("bg.png"),
transform: Transform::from_scale(Vec3::new(SIZE as f32 / 512.0,
SIZE as f32 / 512.0, 1.0))
.with_translation(Vec3::new(0.0, 0.0, -1.0)),
..default()
});
let tex = asset_server.load("boid.png");
let mut rng = thread_rng();
for _ in 0..NBR {
let pos = Vec2::new(rng.gen_range(-SIZE+BORDER..SIZE-BORDER),
rng.gen_range(-SIZE+BORDER..SIZE-BORDER));
let v = Vec2::new(rng.gen_range(-1.0..1.0),
rng.gen_range(-1.0..1.0)).normalize();
let sprite = SpriteBundle {
sprite: Sprite {
custom_size: Some(Vec2::new(2.0, 2.0)),
..default()
},
texture: tex.clone(),
..default()
};
let entity = commands.spawn(BoidBundle::new(pos, v, sprite)).id();
map.insert(&pos, &entity);
}
// Unashamely stolen from the website examples...
let text_section = move |color, value: &str| {
TextSection::new(
value,
TextStyle {
font: asset_server.load("fonts/DejaVuSans.ttf"),
font_size: 20.0,
color,
},
)
};
commands.spawn((
TextBundle::from_sections([
text_section(Color::GREEN, "Boid Count: "),
text_section(Color::CYAN, &format!("{}", NBR)),
text_section(Color::GREEN, "\nFPS: "),
text_section(Color::CYAN, ""),
])
.with_style(Style {
position_type: PositionType::Absolute,
position: UiRect {
top: Val::Px(5.0),
left: Val::Px(5.0),
..default()
},
..default()
}),
StatsText,
));
}
fn update_stats(
diagnostics: Res<Diagnostics>,
mut query: Query<&mut Text, With<StatsText>>,
mut fps_timer: ResMut<FpsTimer>,
time: Res<Time>
) {
fps_timer.0.tick(time.delta());
if !fps_timer.0.finished() {
return;
}
let mut text = query.single_mut();
if let Some(fps) = diagnostics.get(FrameTimeDiagnosticsPlugin::FPS) {
if let Some(ema) = fps.smoothed() {
text.sections[3].value = format!("{ema:.2}");
}
}
}

88
src/map.rs Normal file
View File

@ -0,0 +1,88 @@
use bevy::prelude::*;
use std::collections::HashSet;
#[derive(Resource,Default,Debug)]
pub struct Map { size: usize,
map: Vec<Vec<HashSet<Entity>>>
}
impl Map {
pub fn new(size: usize) -> Self {
Map { size,
map: vec! [ vec![ HashSet::default() ; size*2 +1 ] ; size*2 + 1]
}
}
pub fn insert(&mut self, pos: &Vec2, entity: &Entity) {
let (x, y) = self.get_key(pos);
self.map[y][x].insert(*entity);
}
pub fn update(&mut self, old_pos: &Vec2, new_pos: &Vec2, entity: &Entity) {
let (old_x, old_y) = self.get_key(old_pos);
let (new_x, new_y) = self.get_key(new_pos);
self.map[old_y][old_x].remove(entity);
self.map[new_y][new_x].insert(*entity);
}
pub fn get(&self, pos: &Vec2, view_dist: f32) -> Vec<Entity> {
let vd = Vec2::new(view_dist, view_dist);
let (from_x, from_y) = self.get_key(&(*pos - vd));
let (to_x, to_y) = self.get_key(&(*pos + vd));
let mut v = vec![];
for y in from_y..=to_y {
for x in from_x..=to_x {
v.extend(self.map[y][x].iter());
}
}
v
}
fn get_key(&self, pos: &Vec2) -> (usize, usize) {
let size = self.size as i32;
((pos.x as i32 + size)
.clamp(0, size*2) as usize,
(pos.y as i32 + size)
.clamp(0, size*2) as usize)
}
}
/*
use std::collections::{HashMap,HashSet};
#[derive(Resource,Default,Debug)]
pub struct Map(HashMap<(i32,i32), HashSet<Entity>>);
impl Map {
pub fn update(&mut self, old_pos: &Vec2, new_pos: &Vec2, entity: &Entity) {
let old_p = (old_pos.x as i32, old_pos.y as i32);
let new_p = (new_pos.x as i32, new_pos.y as i32);
self.0.entry(old_p)
.and_modify(| set | { set.remove(entity); });
self.0.entry(new_p)
.and_modify(| set | { set.insert(*entity); })
.or_insert([*entity].into());
}
pub fn get(&self, pos: &Vec2, view_dist: f32) -> Vec<Entity> {
let from_x = (pos.x - view_dist) as i32;
let from_y = (pos.y - view_dist) as i32;
let to_x = (pos.x + view_dist) as i32;
let to_y = (pos.y + view_dist) as i32;
let mut v = vec![];
for y in from_y..=to_y {
for x in from_x..=to_x {
if let Some(set) = self.0.get(&(x,y)) {
v.extend(set.iter());
}
}
}
v
}
}
*/

52
src/mouse_inputs.rs Normal file
View File

@ -0,0 +1,52 @@
use bevy::prelude::*;
//use bevy::input::mouse::{MouseButtonInput,MouseWheel,MouseMotion};
use bevy::input::mouse::{MouseWheel,MouseMotion};
//use bevy::input::ButtonState;
use crate::CameraState;
pub fn mouse_move_events(
buttons: Res<Input<MouseButton>>,
mut mouse_wheel_events: EventReader<MouseWheel>,
mut mouse_motion_events: EventReader<MouseMotion>,
mut query: Query<(&mut Transform, &mut CameraState)>
) {
let (mut transform, mut state) = query.single_mut();
for event in mouse_wheel_events.iter() {
let scale = state.scale;
state.scale = (scale + event.y * scale)
.clamp(0.001, 0.1);
*transform = transform.with_scale(Vec3::new(state.scale,
state.scale, 1.0));
}
if buttons.pressed(MouseButton::Left) {
let scale = state.scale;
for event in mouse_motion_events.iter() {
state.pos -= event.delta * scale;
}
transform.translation = Vec3::new(state.pos.x, -state.pos.y, 0.0);
}
}
/*
pub fn mouse_button_events(
mut commands: Commands,
mut mouse_button_events: EventReader<MouseButtonInput>
) {
for _event in mouse_button_events.iter() {
if event.button == MouseButton::Left &&
event.state == ButtonState::Pressed {
} else if event.button == MouseButton::Right &&
event.state == ButtonState::Pressed {
for (entity,_) in &query {
commands.entity(entity).despawn();
terrain_events
.send(GenTerrainEvent { seed: rand::random() });
}
}
}
}
*/