Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Birds #108

Closed
wants to merge 14 commits into from
3 changes: 3 additions & 0 deletions assets/models/bird.glb
Git LFS file not shown
7 changes: 5 additions & 2 deletions native_app/src/gui/inspect/inspect_debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -49,6 +49,9 @@ impl InspectRenderer {
AnyEntity::HumanID(x) => {
<HumanEnt as Inspect<HumanEnt>>::render(sim.get(x).unwrap(), "", ui, &args)
}
AnyEntity::BirdID(x) => {
<BirdEnt as Inspect<BirdEnt>>::render(sim.get(x).unwrap(), "", ui, &args)
}
}

if let AnyEntity::VehicleID(id) = entity {
Expand Down
2 changes: 2 additions & 0 deletions native_app/src/gui/selectable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down
14 changes: 14 additions & 0 deletions native_app/src/rendering/entity_render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub struct InstancedRender {
pub wagons_freight: InstancedMeshBuilder<true>,
pub trucks: InstancedMeshBuilder<true>,
pub pedestrians: InstancedMeshBuilder<true>,
pub birds: InstancedMeshBuilder<true>,
}

impl InstancedRender {
Expand All @@ -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()),
}
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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));
}
Expand Down
4 changes: 4 additions & 0 deletions simulation/src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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);
Expand All @@ -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::<GoodsCompanyRegistry>();
register_resource_noserialize::<ItemRegistry>();
Expand Down
1 change: 1 addition & 0 deletions simulation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub mod souls;
mod tests;
pub mod transportation;
pub mod utils;
pub mod wildlife;
mod world;
pub mod world_command;

Expand Down
166 changes: 166 additions & 0 deletions simulation/src/wildlife/bird.rs
Original file line number Diff line number Diff line change
@@ -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};

reeceyang marked this conversation as resolved.
Show resolved Hide resolved
/// spawns a bird in the world
pub fn spawn_bird(sim: &mut Simulation, spawn_pos: Vec3) -> Option<BirdID> {
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::<GameTime>();
let map = &*resources.read::<Map>();

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
}
66 changes: 66 additions & 0 deletions simulation/src/wildlife/mod.rs
Original file line number Diff line number Diff line change
@@ -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<BirdID> = 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 });
}
Loading
Loading