use bevy::prelude::*;
use bevy_orca::Crowd;
use bevy_orca::{CrowdMember, CrowdParams};
use crate::behavior::Destination;
use crate::core::require_core;
use crate::{PersonIndex, SocietyPlane, SocietySettings, SocietySystems};
const NEIGHBOUR_RANGE: f32 = 3.0;
const TIME_HORIZON: f32 = 2.0;
const MAX_NEIGHBOURS: usize = 10;
const NUDGE: f32 = 0.3;
const FACE_MIN_SPEED: f32 = 0.05;
const WALL_RANGE: f32 = 2.0;
#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq)]
#[reflect(Component)]
pub struct Obstacle {
pub radius: f32,
}
impl Default for Obstacle {
fn default() -> Self {
Self { radius: 1.0 }
}
}
#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq)]
#[reflect(Component)]
#[require(SocialVelocity)]
pub struct SocialMover {
pub speed: f32,
pub radius: f32,
pub face_motion: bool,
}
impl Default for SocialMover {
fn default() -> Self {
Self {
speed: 1.4,
radius: 0.35,
face_motion: true,
}
}
}
#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq, Default)]
#[reflect(Component)]
pub struct SocialVelocity(pub Vec2);
pub struct SteeringPlugin;
impl Plugin for SteeringPlugin {
fn build(&self, app: &mut App) {
app.register_type::<SocialMover>()
.register_type::<SocialVelocity>()
.register_type::<Obstacle>()
.add_systems(
FixedUpdate,
(steer_movers, push_out_of_obstacles)
.chain()
.after(SocietySystems::Rules),
);
}
fn finish(&self, app: &mut App) {
require_core(app, "SteeringPlugin");
}
}
fn push_out_of_obstacles(
settings: Res<SocietySettings>,
obstacles: Query<(&Obstacle, &Transform), Without<SocialMover>>,
mut movers: Query<(&SocialMover, &mut Transform)>,
) {
let plane = &settings.plane;
let solids: Vec<(Vec2, f32)> = obstacles
.iter()
.map(|(obstacle, transform)| (to_plane(transform.translation, plane), obstacle.radius))
.collect();
if solids.is_empty() {
return;
}
for (mover, mut transform) in &mut movers {
let mut at = to_plane(transform.translation, plane);
let mut moved = false;
for (centre, radius) in &solids {
let reach = radius + mover.radius;
let away = at - *centre;
let distance = away.length();
if distance >= reach {
continue;
}
let direction = if distance > f32::EPSILON {
away / distance
} else {
Vec2::X
};
at = *centre + direction * reach;
moved = true;
}
if moved {
let height = transform.translation.y;
transform.translation = match plane {
SocietyPlane::XZ => Vec3::new(at.x, height, at.y),
SocietyPlane::XY => Vec3::new(at.x, at.y, height),
};
}
}
}
#[allow(clippy::too_many_arguments)]
fn steer_movers(
index: Res<PersonIndex>,
settings: Res<SocietySettings>,
time: Res<Time<Fixed>>,
obstacles: Query<(&Obstacle, &Transform), Without<SocialMover>>,
mut movers: Query<(
&SocialMover,
&mut SocialVelocity,
&mut Transform,
Option<&Destination>,
)>,
mut crowd: Local<Crowd>,
mut members: Local<Vec<CrowdMember>>,
) {
let dt = time.delta_secs();
let plane = &settings.plane;
let stop_radius = 0.5 * settings.interaction_radius;
let solids: Vec<(Vec2, f32)> = obstacles
.iter()
.map(|(obstacle, transform)| (to_plane(transform.translation, plane), obstacle.radius))
.collect();
members.clear();
members.reserve(index.sorted.len());
for &(id, entity) in &index.sorted {
let Ok((mover, velocity, transform, dest)) = movers.get(entity) else {
continue;
};
let pos = to_plane(transform.translation, plane);
let wish = dest.map(|d| {
let to_dest = d.0 - pos;
if to_dest.length() <= stop_radius {
Vec2::ZERO
} else {
to_dest.normalize_or_zero() * mover.speed
}
});
members.push(CrowdMember {
key: u64::from(id.0),
pos,
vel: if wish.is_some() {
velocity.0
} else {
Vec2::ZERO
},
radius: mover.radius,
max_speed: mover.speed,
pref_vel: wish,
});
}
if members.is_empty() {
return;
}
let params = CrowdParams {
time_horizon: TIME_HORIZON,
neighbour_range: NEIGHBOUR_RANGE,
max_neighbours: MAX_NEIGHBOURS,
nudge: NUDGE,
};
let solved = crowd.solve(
&members,
¶ms,
dt,
|_, _| true,
|i, walls| {
walls.clear();
let Some(me) = members.get(i) else {
return;
};
for (centre, radius) in &solids {
let to_solid = *centre - me.pos;
let gap = to_solid.length() - radius - me.radius;
if gap > WALL_RANGE {
continue;
}
walls.push((
to_solid.normalize_or_zero(),
if gap <= 0.0 { 0.0 } else { gap / dt },
));
}
},
);
let velocities: Vec<Option<Vec2>> = solved.to_vec();
for (&(_, entity), velocity) in index.sorted.iter().zip(velocities) {
let Ok((mover, mut stored, mut transform, dest)) = movers.get_mut(entity) else {
continue;
};
let velocity = velocity.filter(|v| v.is_finite()).unwrap_or(Vec2::ZERO);
if dest.is_none() {
stored.0 = Vec2::ZERO;
continue;
}
stored.0 = velocity;
step_on_plane(&mut transform, velocity * dt, plane);
if mover.face_motion && velocity.length() > FACE_MIN_SPEED {
transform.rotation = facing(velocity, plane);
}
}
}
fn to_plane(p: Vec3, plane: &SocietyPlane) -> Vec2 {
match plane {
SocietyPlane::XZ => Vec2::new(p.x, p.z),
SocietyPlane::XY => Vec2::new(p.x, p.y),
}
}
fn step_on_plane(transform: &mut Transform, delta: Vec2, plane: &SocietyPlane) {
match plane {
SocietyPlane::XZ => {
transform.translation.x += delta.x;
transform.translation.z += delta.y;
}
SocietyPlane::XY => {
transform.translation.x += delta.x;
transform.translation.y += delta.y;
}
}
}
fn facing(velocity: Vec2, plane: &SocietyPlane) -> Quat {
match plane {
SocietyPlane::XZ => Quat::from_rotation_y((-velocity.x).atan2(-velocity.y)),
SocietyPlane::XY => Quat::from_rotation_z(velocity.y.atan2(velocity.x)),
}
}