Files
game_engine/crates/voltex_ai/src/obstacle.rs
2026-03-26 07:15:40 +09:00

177 lines
5.4 KiB
Rust

use voltex_math::Vec3;
/// A dynamic obstacle represented as a position and radius.
#[derive(Debug, Clone)]
pub struct DynamicObstacle {
pub position: Vec3,
pub radius: f32,
}
/// Compute avoidance steering force using velocity obstacle approach.
///
/// Projects the agent's velocity forward by `look_ahead` distance and checks
/// for circle intersections with obstacles. Returns a steering force perpendicular
/// to the approach direction to avoid the nearest threatening obstacle.
pub fn avoid_obstacles(
agent_pos: Vec3,
agent_vel: Vec3,
agent_radius: f32,
obstacles: &[DynamicObstacle],
look_ahead: f32,
) -> Vec3 {
let speed = agent_vel.length();
if speed < f32::EPSILON {
return Vec3::ZERO;
}
let forward = agent_vel * (1.0 / speed);
let mut nearest_t = f32::INFINITY;
let mut avoidance = Vec3::ZERO;
for obs in obstacles {
let to_obs = obs.position - agent_pos;
let combined_radius = agent_radius + obs.radius;
// Project obstacle center onto the velocity ray
let proj = to_obs.dot(forward);
// Obstacle is behind or too far ahead
if proj < 0.0 || proj > look_ahead {
continue;
}
// Lateral distance from the velocity ray to obstacle center (XZ only for ground agents)
let closest_on_ray = agent_pos + forward * proj;
let diff = obs.position - closest_on_ray;
let lateral_dist_sq = diff.x * diff.x + diff.z * diff.z;
let combined_sq = combined_radius * combined_radius;
if lateral_dist_sq >= combined_sq {
continue; // No collision
}
// This obstacle threatens the agent — check if it's the nearest
if proj < nearest_t {
nearest_t = proj;
// Avoidance direction: perpendicular to approach, away from obstacle
// Use XZ plane lateral vector
let lateral = Vec3::new(diff.x, 0.0, diff.z);
let lat_len = lateral.length();
if lat_len > f32::EPSILON {
// Steer away from obstacle (opposite direction of lateral offset)
let steer_dir = lateral * (-1.0 / lat_len);
// Strength inversely proportional to distance (closer = stronger)
let strength = 1.0 - (proj / look_ahead);
avoidance = steer_dir * strength * speed;
} else {
// Agent heading straight at obstacle center — pick perpendicular
// Use cross product with Y to get a lateral direction
let perp = Vec3::new(-forward.z, 0.0, forward.x);
avoidance = perp * speed;
}
}
}
avoidance
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_obstacle_zero_force() {
let force = avoid_obstacles(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
0.5,
&[],
5.0,
);
assert!(force.length() < 1e-6, "no obstacles should give zero force");
}
#[test]
fn test_obstacle_behind_zero_force() {
let obs = DynamicObstacle {
position: Vec3::new(-3.0, 0.0, 0.0),
radius: 1.0,
};
let force = avoid_obstacles(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
0.5,
&[obs],
5.0,
);
assert!(force.length() < 1e-6, "obstacle behind should give zero force");
}
#[test]
fn test_obstacle_ahead_lateral_force() {
let obs = DynamicObstacle {
position: Vec3::new(3.0, 0.0, 0.5), // slightly to the right
radius: 1.0,
};
let force = avoid_obstacles(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(2.0, 0.0, 0.0), // moving in +X
0.5,
&[obs],
5.0,
);
assert!(force.length() > 0.1, "obstacle ahead should give non-zero force");
// Force should push away from obstacle (obstacle is at +Z, force should be -Z)
assert!(force.z < 0.0, "force should push away from obstacle (negative Z)");
}
#[test]
fn test_obstacle_far_away_zero_force() {
let obs = DynamicObstacle {
position: Vec3::new(3.0, 0.0, 10.0), // far to the side
radius: 1.0,
};
let force = avoid_obstacles(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
0.5,
&[obs],
5.0,
);
assert!(force.length() < 1e-6, "distant obstacle should give zero force");
}
#[test]
fn test_obstacle_beyond_lookahead_zero_force() {
let obs = DynamicObstacle {
position: Vec3::new(10.0, 0.0, 0.0),
radius: 1.0,
};
let force = avoid_obstacles(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
0.5,
&[obs],
5.0, // look_ahead is only 5
);
assert!(force.length() < 1e-6, "obstacle beyond lookahead should give zero force");
}
#[test]
fn test_zero_velocity_zero_force() {
let obs = DynamicObstacle {
position: Vec3::new(3.0, 0.0, 0.0),
radius: 1.0,
};
let force = avoid_obstacles(
Vec3::new(0.0, 0.0, 0.0),
Vec3::ZERO,
0.5,
&[obs],
5.0,
);
assert!(force.length() < 1e-6, "zero velocity should give zero force");
}
}