diff --git a/assets/models/bird.glb b/assets/models/bird.glb new file mode 100644 index 00000000..ccaee1a3 --- /dev/null +++ b/assets/models/bird.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:763ebf93f5352727d382ff6ed724666b97fab98ef3d3f24f9c5aa75666d816b6 +size 44284 diff --git a/native_app/src/gui/inspect/inspect_debug.rs b/native_app/src/gui/inspect/inspect_debug.rs index 8050a85f..0f7dd1ea 100644 --- a/native_app/src/gui/inspect/inspect_debug.rs +++ b/native_app/src/gui/inspect/inspect_debug.rs @@ -5,8 +5,8 @@ use egui_inspect::{Inspect, InspectArgs}; use simulation::economy::{ItemRegistry, Market}; use simulation::transportation::Location; use simulation::{ - AnyEntity, CompanyEnt, FreightStationEnt, HumanEnt, Simulation, SoulID, TrainEnt, VehicleEnt, - WagonEnt, + AnyEntity, BirdEnt, CompanyEnt, FreightStationEnt, HumanEnt, Simulation, SoulID, TrainEnt, + VehicleEnt, WagonEnt, }; /// Inspect window @@ -49,6 +49,9 @@ impl InspectRenderer { AnyEntity::HumanID(x) => { >::render(sim.get(x).unwrap(), "", ui, &args) } + AnyEntity::BirdID(x) => { + >::render(sim.get(x).unwrap(), "", ui, &args) + } } if let AnyEntity::VehicleID(id) = entity { diff --git a/native_app/src/gui/selectable.rs b/native_app/src/gui/selectable.rs index cddecf1e..97753e77 100644 --- a/native_app/src/gui/selectable.rs +++ b/native_app/src/gui/selectable.rs @@ -13,6 +13,8 @@ pub fn select_radius(id: AnyEntity) -> f32 { AnyEntity::FreightStationID(_) => 0.0, AnyEntity::CompanyID(_) => 0.0, AnyEntity::HumanID(_) => 3.0, + // TODO: make the radius smaller after finishing testing + AnyEntity::BirdID(_) => 20.0, } } diff --git a/native_app/src/rendering/entity_render.rs b/native_app/src/rendering/entity_render.rs index b942eff8..ce74c524 100644 --- a/native_app/src/rendering/entity_render.rs +++ b/native_app/src/rendering/entity_render.rs @@ -14,6 +14,7 @@ pub struct InstancedRender { pub wagons_freight: InstancedMeshBuilder, pub trucks: InstancedMeshBuilder, pub pedestrians: InstancedMeshBuilder, + pub birds: InstancedMeshBuilder, } impl InstancedRender { @@ -32,6 +33,7 @@ impl InstancedRender { wagons_passenger: InstancedMeshBuilder::new(load_mesh(gfx, "wagon.glb").unwrap()), trucks: InstancedMeshBuilder::new(load_mesh(gfx, "truck.glb").unwrap()), pedestrians: InstancedMeshBuilder::new(load_mesh(gfx, "pedestrian.glb").unwrap()), + birds: InstancedMeshBuilder::new(load_mesh(gfx, "bird.glb").unwrap()), } } @@ -40,6 +42,7 @@ impl InstancedRender { self.cars.instances.clear(); self.trucks.instances.clear(); self.pedestrians.instances.clear(); + self.birds.instances.clear(); for v in sim.world().vehicles.values() { let trans = &v.trans; let instance = MeshInstance { @@ -92,6 +95,14 @@ impl InstancedRender { } } + for bird_ent in sim.world().birds.values() { + self.birds.instances.push(MeshInstance { + pos: bird_ent.trans.position, + dir: bird_ent.trans.dir, + tint: LinearColor::WHITE, + }); + } + self.path_not_found.clear(); for (_, (trans, itin)) in sim.world().query_trans_itin() { let Some(wait) = itin.is_wait_for_reroute() else { @@ -125,6 +136,9 @@ impl InstancedRender { if let Some(x) = self.pedestrians.build(fctx.gfx) { fctx.objs.push(Box::new(x)); } + if let Some(x) = self.birds.build(fctx.gfx) { + fctx.objs.push(Box::new(x)); + } if let Some(x) = self.locomotives.build(fctx.gfx) { fctx.objs.push(Box::new(x)); } diff --git a/simulation/src/init.rs b/simulation/src/init.rs index 43e678aa..39f81ef5 100644 --- a/simulation/src/init.rs +++ b/simulation/src/init.rs @@ -17,6 +17,8 @@ use crate::transportation::train::{ }; use crate::utils::resources::Resources; use crate::utils::time::Tick; +use crate::wildlife::add_flocks_randomly; +use crate::wildlife::bird::bird_decision_system; use crate::world::{CompanyEnt, FreightStationEnt, HumanEnt, TrainEnt, VehicleEnt, WagonEnt}; use crate::World; use crate::{ @@ -33,6 +35,7 @@ pub fn init() { register_system("update_decision_system", update_decision_system); register_system("company_system", company_system); register_system("pedestrian_decision_system", pedestrian_decision_system); + register_system("bird_decision_system", bird_decision_system); register_system("coworld_synchronize", coworld_synchronize); register_system("locomotive_system", locomotive_system); register_system("vehicle_decision_system", vehicle_decision_system); @@ -46,6 +49,7 @@ pub fn init() { register_system("random_vehicles", random_vehicles_update); register_system_sim("add_souls_to_empty_buildings", add_souls_to_empty_buildings); + register_system_sim("add_flocks_randomly", add_flocks_randomly); register_resource_noserialize::(); register_resource_noserialize::(); diff --git a/simulation/src/lib.rs b/simulation/src/lib.rs index 838874a2..9dd7a852 100644 --- a/simulation/src/lib.rs +++ b/simulation/src/lib.rs @@ -46,6 +46,7 @@ pub mod souls; mod tests; pub mod transportation; pub mod utils; +pub mod wildlife; mod world; pub mod world_command; diff --git a/simulation/src/wildlife/bird.rs b/simulation/src/wildlife/bird.rs new file mode 100644 index 00000000..878e71fa --- /dev/null +++ b/simulation/src/wildlife/bird.rs @@ -0,0 +1,166 @@ +use crate::map::Map; +use crate::physics::Speed; +use crate::utils::resources::Resources; +use crate::utils::time::GameTime; +use crate::Simulation; +use crate::World; +use crate::{BirdEnt, BirdID}; +use geom::angle_lerpxy; +use geom::AABB; +use geom::{Transform, Vec3}; + +/// spawns a bird in the world +pub fn spawn_bird(sim: &mut Simulation, spawn_pos: Vec3) -> Option { + profiling::scope!("spawn_bird"); + + log::info!("added bird at {}", spawn_pos); + + let id = sim.world.insert(BirdEnt { + trans: Transform::new(spawn_pos), + speed: Speed::default(), + }); + + Some(id) +} + +/// Update the movement of each bird in the world +pub fn bird_decision_system(world: &mut World, resources: &mut Resources) { + profiling::scope!("wildlife::bird_decision_system"); + let ra = &*resources.read::(); + let map = &*resources.read::(); + + let aabb = map.environment.bounds(); + + world.flocks.values().for_each(|flock| { + let flock_physics: Vec<(Transform, Speed)> = flock + .bird_ids + .iter() + .map(|bird_id| match world.birds.get_mut(*bird_id) { + Some(bird_ent) => (bird_ent.trans, bird_ent.speed.clone()), + None => unreachable!(), + }) + .collect(); + + let flock_center = center(&flock_physics); + let flock_avg_v = average_velocity(&flock_physics); + + flock + .bird_ids + .iter() + .for_each(|bird_id| match world.birds.get_mut(*bird_id) { + Some(bird_ent) => bird_decision( + ra, + &mut bird_ent.trans, + &mut bird_ent.speed, + flock_center, + flock_avg_v, + aabb, + &flock_physics, + ), + None => unreachable!(), + }) + }); +} + +/// Update the speed, position, and direction of a bird using the boids algorithm +pub fn bird_decision( + time: &GameTime, + trans: &mut Transform, + kin: &mut Speed, + flock_center: Vec3, + flock_avg_v: Vec3, + aabb: AABB, + flock_physics: &Vec<(Transform, Speed)>, +) { + // the initial velocity of the bird + let mut dv = trans.dir * kin.0; + + // fly towards the average position of all other birds + const CENTERING_FACTOR: f32 = 0.01; + let num_birds = flock_physics.len() as f32; + let perceived_center = (flock_center * num_birds - trans.position) / (num_birds - 1.0); + dv += (perceived_center - trans.position) * CENTERING_FACTOR; + + // match the flock's average velocity + const MATCHING_FACTOR: f32 = 0.01; + dv += (flock_avg_v - dv) * MATCHING_FACTOR; + + // avoid nearby birds + const AVOID_FACTOR: f32 = 0.01; + dv += separation_adjustment(trans, flock_physics) * AVOID_FACTOR; + + // avoid map boundaries + dv += bounds_adjustment(trans, aabb); + + // cap the speed of the bird + const SPEED_LIMIT: f32 = 10.0; + if dv.mag() > SPEED_LIMIT { + dv = dv.normalize_to(SPEED_LIMIT); + } + + // update the bird's speed, position, and direction + const ANG_VEL: f32 = 1.0; + trans.dir = angle_lerpxy(trans.dir, dv, ANG_VEL * time.realdelta).normalize(); + kin.0 = dv.mag(); + trans.position += dv * time.realdelta; +} + +/// Calculate the center of the flock (the average position of the flock) +fn center(flock_physics: &Vec<(Transform, Speed)>) -> Vec3 { + flock_physics + .iter() + .map(|(t, _)| t.position) + // TODO: use .sum() ? + .reduce(|a, b| a + b).unwrap() + / flock_physics.len() as f32 +} + +/// Calculate the average velocity of the flock +fn average_velocity(flock_physics: &Vec<(Transform, Speed)>) -> Vec3 { + flock_physics + .iter() + .map(|(t, s)| t.dir.normalize() * s.0) + // TODO: use .sum() ? + .reduce(|a, b| a + b).unwrap() + / flock_physics.len() as f32 +} + +/// Get an adjustment vector to move the bird away from other birds +fn separation_adjustment(trans: &Transform, flock_physics: &Vec<(Transform, Speed)>) -> Vec3 { + const MIN_DISTANCE: f32 = 5.0; + flock_physics + .iter() + .filter(|(other, _)| other.position.distance(trans.position) < MIN_DISTANCE) + .map(|(other, _)| trans.position - other.position) + // TODO: use .sum() ? + .reduce(|a, b| a + b) + .unwrap() +} + +/// Get an adjustment vector to move the bird away from the map bounds +fn bounds_adjustment(trans: &Transform, aabb: AABB) -> Vec3 { + const MARGIN: f32 = 2.0; + const MAX_Z: f32 = 200.0; + const TURN_AMOUNT: f32 = 1.0; + let mut v = Vec3::new(0.0, 0.0, 0.0); + // TODO: the ground might not be at z: 0 + if trans.position.z < MARGIN { + v.z += TURN_AMOUNT; + } + if trans.position.z > MAX_Z - MARGIN { + v.z -= TURN_AMOUNT; + } + if trans.position.x < aabb.ll.x + MARGIN { + v.x += TURN_AMOUNT; + } + if trans.position.x > aabb.ur.x - MARGIN { + v.x -= TURN_AMOUNT; + } + if trans.position.y < aabb.ll.y + MARGIN { + v.y += TURN_AMOUNT; + } + if trans.position.y > aabb.ur.y - MARGIN { + v.y -= TURN_AMOUNT; + } + v +} diff --git a/simulation/src/wildlife/mod.rs b/simulation/src/wildlife/mod.rs new file mode 100644 index 00000000..67725f34 --- /dev/null +++ b/simulation/src/wildlife/mod.rs @@ -0,0 +1,66 @@ +use geom::{Vec3, AABB}; + +use crate::utils::rand_provider::RandProvider; +use crate::wildlife::bird::spawn_bird; +use crate::{BirdID, Flock, Simulation}; + +pub mod bird; + +const MIN_SPAWN_HEIGHT: f32 = 2.0; +const SPAWN_HEIGHT_RANGE: f32 = 10.0; + +/// Get a random position within the bounding box +pub fn get_random_spawn_pos(aabb: AABB, r1: f32, r2: f32, r3: f32) -> Vec3 { + let AABB { ll, ur } = aabb; + Vec3 { + x: ll.x + (ur.x - ll.x) * r1, + y: ll.y + (ur.y - ll.y) * r2, + z: MIN_SPAWN_HEIGHT + SPAWN_HEIGHT_RANGE * r3, + } +} + +/// Get a random position within the ball with the given center and radius +pub fn get_random_pos_from_center(center: Vec3, radius: f32, r1: f32, r2: f32, r3: f32) -> Vec3 { + Vec3 { + x: center.x + radius * (r1 - 0.5), + y: center.y + radius * (r2 - 0.5), + z: MIN_SPAWN_HEIGHT + SPAWN_HEIGHT_RANGE * r3, + } +} + +const NUM_FLOCKS: u32 = 20; +const BIRDS_PER_FLOCK: u32 = 50; +const SPAWN_RANGE: f32 = 5.0; // how spread out birds in the flock should be initially + +/// spawns birds in random clusters around the map +pub(crate) fn add_flocks_randomly(sim: &mut Simulation) { + profiling::scope!("wildlife::add_flocks_randomly"); + + let num_flocks = sim.world().flocks.len(); + if num_flocks >= NUM_FLOCKS as usize { + return; + } + + let mut rng = RandProvider::new(num_flocks as u64); + + let aabb = sim.map().environment.bounds(); + let center_pos = get_random_spawn_pos(aabb, rng.next_f32(), rng.next_f32(), rng.next_f32()); + + let mut ids: Vec = Vec::new(); + + for _ in 0..BIRDS_PER_FLOCK { + let bird_pos = get_random_pos_from_center( + center_pos, + SPAWN_RANGE, + rng.next_f32(), + rng.next_f32(), + rng.next_f32(), + ); + match spawn_bird(sim, bird_pos) { + Some(id) => ids.push(id), + None => (), + } + } + + sim.world.insert(Flock { bird_ids: ids }); +} diff --git a/simulation/src/world.rs b/simulation/src/world.rs index d7661ecf..337a548d 100644 --- a/simulation/src/world.rs +++ b/simulation/src/world.rs @@ -27,6 +27,8 @@ new_key_type! { pub struct WagonID; pub struct FreightStationID; pub struct CompanyID; + pub struct BirdID; + pub struct FlockID; } impl_entity!(VehicleID, VehicleEnt, vehicles); @@ -35,6 +37,8 @@ impl_entity!(TrainID, TrainEnt, trains); impl_entity!(WagonID, WagonEnt, wagons); impl_entity!(FreightStationID, FreightStationEnt, freight_stations); impl_entity!(CompanyID, CompanyEnt, companies); +impl_entity!(BirdID, BirdEnt, birds); +impl_entity!(FlockID, Flock, flocks); impl_trans!(HumanID); impl_trans!(VehicleID); @@ -42,6 +46,7 @@ impl_trans!(TrainID); impl_trans!(WagonID); impl_trans!(FreightStationID); impl_trans!(CompanyID); +impl_trans!(BirdID); #[derive(PartialEq, Eq, Copy, Clone, Debug, From, TryInto)] pub enum AnyEntity { @@ -51,6 +56,7 @@ pub enum AnyEntity { FreightStationID(FreightStationID), CompanyID(CompanyID), HumanID(HumanID), + BirdID(BirdID), } #[derive(Inspect, Serialize, Deserialize)] @@ -177,6 +183,21 @@ impl SimDrop for CompanyEnt { } } +#[derive(Inspect, Serialize, Deserialize)] +pub struct BirdEnt { + pub trans: Transform, + pub speed: Speed, +} + +impl SimDrop for BirdEnt { + fn sim_drop(self, _: BirdID, _: &mut Resources) {} +} + +#[derive(Serialize, Deserialize)] +pub struct Flock { + pub bird_ids: Vec, +} + #[derive(Default, Serialize, Deserialize)] pub struct World { pub vehicles: HopSlotMap, @@ -185,6 +206,8 @@ pub struct World { pub wagons: HopSlotMap, pub freight_stations: HopSlotMap, pub companies: HopSlotMap, + pub birds: HopSlotMap, + pub flocks: HopSlotMap, } impl World { @@ -212,6 +235,7 @@ impl World { AnyEntity::FreightStationID(id) => self.storage_id(id).contains_key(id), AnyEntity::CompanyID(id) => self.storage_id(id).contains_key(id), AnyEntity::HumanID(id) => self.storage_id(id).contains_key(id), + AnyEntity::BirdID(id) => self.storage_id(id).contains_key(id), } } @@ -221,6 +245,7 @@ impl World { AnyEntity::TrainID(x) => self.pos(x), AnyEntity::WagonID(x) => self.pos(x), AnyEntity::HumanID(x) => self.pos(x), + AnyEntity::BirdID(x) => self.pos(x), _ => None, } } @@ -258,6 +283,7 @@ impl World { self.vehicles.iter().map(|(id, x)| (AnyEntity::VehicleID(id), x.trans.position.xy())), self.trains .iter().map(|(id, x)| (AnyEntity::TrainID(id), x.trans.position.xy())), self.wagons .iter().map(|(id, x)| (AnyEntity::WagonID(id), x.trans.position.xy())), + self.birds .iter().map(|(id, x)| (AnyEntity::BirdID(id), x.trans.position.xy())), )) } @@ -433,6 +459,7 @@ impl Display for AnyEntity { AnyEntity::WagonID(id) => write!(f, "{:?}", id), AnyEntity::FreightStationID(id) => write!(f, "{:?}", id), AnyEntity::CompanyID(id) => write!(f, "{:?}", id), + AnyEntity::BirdID(id) => write!(f, "{:?}", id), } } }