use glam::Vec3;
const STOPPED: f32 = 0.3;
const AIR_DRAG: f32 = 0.3;
#[derive(Debug, Clone, PartialEq)]
pub struct Vehicle {
pub top_speed: f32,
pub reverse_speed: f32,
pub engine_force: f32,
pub brake_force: f32,
pub grip: f32,
pub rolling_resistance: f32,
pub engine_braking: f32,
pub turn_radius: f32,
pub max_turn_rate: f32,
pub gears: u32,
pub idle_rpm: f32,
pub max_rpm: f32,
pub load: f32,
pub max_load: f32,
pub fuel_capacity: Option<f32>,
pub consumption: f32,
pub engine_sound: Option<String>,
pub horn_sound: Option<String>,
pub start_sound: Option<String>,
pub shift_sound: Option<String>,
pub crash_sound: Option<String>,
pub throttle: f32,
pub brake: f32,
pub steer: f32,
pub handbrake: bool,
speed: f32,
gear: i32,
rpm: f32,
engine_on: bool,
fuel: f32,
distance: f32,
hold: (bool, bool),
}
impl Default for Vehicle {
fn default() -> Self {
Self::car()
}
}
#[derive(Debug, Default)]
pub(crate) struct StepReport {
pub gear_changed: Option<i32>,
}
impl Vehicle {
pub fn car() -> Self {
Self {
top_speed: 180.0 / 3.6,
reverse_speed: 25.0 / 3.6,
engine_force: 4500.0,
brake_force: 11_000.0,
grip: 9.0,
rolling_resistance: 0.15,
engine_braking: 0.8,
turn_radius: 5.0,
max_turn_rate: 90.0,
gears: 5,
idle_rpm: 800.0,
max_rpm: 6500.0,
load: 0.0,
max_load: f32::INFINITY,
fuel_capacity: None,
consumption: 12.0,
engine_sound: None,
horn_sound: None,
start_sound: None,
shift_sound: None,
crash_sound: None,
throttle: 0.0,
brake: 0.0,
steer: 0.0,
handbrake: false,
speed: 0.0,
gear: 1,
rpm: 800.0,
engine_on: true,
fuel: 0.0,
distance: 0.0,
hold: (false, false),
}
}
pub fn truck() -> Self {
Self {
top_speed: 90.0 / 3.6,
reverse_speed: 10.0 / 3.6,
engine_force: 20_000.0,
brake_force: 110_000.0,
grip: 7.0,
rolling_resistance: 0.1,
engine_braking: 0.5,
turn_radius: 11.0,
max_turn_rate: 35.0,
gears: 12,
idle_rpm: 600.0,
max_rpm: 2200.0,
max_load: 25_000.0,
consumption: 35.0,
gear: 1,
rpm: 600.0,
..Self::car()
}
}
pub fn with_top_speed_kmh(mut self, kmh: f32) -> Self {
self.top_speed = (kmh / 3.6).max(0.1);
self
}
pub fn with_reverse_speed_kmh(mut self, kmh: f32) -> Self {
self.reverse_speed = (kmh / 3.6).max(0.0);
self
}
pub fn with_engine_force(mut self, newtons: f32) -> Self {
self.engine_force = newtons.max(0.0);
self
}
pub fn with_brake_force(mut self, newtons: f32) -> Self {
self.brake_force = newtons.max(0.0);
self
}
pub fn with_grip(mut self, grip: f32) -> Self {
self.grip = grip.max(0.01);
self
}
pub fn with_rolling_resistance(mut self, resistance: f32) -> Self {
self.rolling_resistance = resistance.max(0.0);
self
}
pub fn with_engine_braking(mut self, braking: f32) -> Self {
self.engine_braking = braking.max(0.0);
self
}
pub fn with_turn_radius(mut self, meters: f32) -> Self {
self.turn_radius = meters.max(0.1);
self
}
pub fn with_max_turn_rate(mut self, degrees_per_second: f32) -> Self {
self.max_turn_rate = degrees_per_second.max(0.0);
self
}
pub fn with_gears(mut self, gears: u32, idle_rpm: f32, max_rpm: f32) -> Self {
self.gears = gears.max(1);
self.idle_rpm = idle_rpm.max(0.0);
self.max_rpm = max_rpm.max(self.idle_rpm + 1.0);
self.rpm = self.idle_rpm;
self
}
pub fn with_load(mut self, kg: f32) -> Self {
self.set_load(kg);
self
}
pub fn with_max_load(mut self, kg: f32) -> Self {
self.max_load = kg.max(0.0);
self.load = self.load.min(self.max_load);
self
}
pub fn with_fuel(mut self, capacity: f32, liters_per_100km: f32) -> Self {
self.fuel_capacity = Some(capacity.max(0.0));
self.fuel = capacity.max(0.0);
self.consumption = liters_per_100km.max(0.0);
self
}
pub fn with_engine_sound(mut self, name: impl Into<String>) -> Self {
self.engine_sound = Some(name.into());
self
}
pub fn with_horn_sound(mut self, name: impl Into<String>) -> Self {
self.horn_sound = Some(name.into());
self
}
pub fn with_start_sound(mut self, name: impl Into<String>) -> Self {
self.start_sound = Some(name.into());
self
}
pub fn with_shift_sound(mut self, name: impl Into<String>) -> Self {
self.shift_sound = Some(name.into());
self
}
pub fn with_crash_sound(mut self, name: impl Into<String>) -> Self {
self.crash_sound = Some(name.into());
self
}
pub fn engine_off(mut self) -> Self {
self.engine_on = false;
self.rpm = 0.0;
self
}
pub fn speed(&self) -> f32 {
self.speed
}
pub fn speed_kmh(&self) -> f32 {
self.speed * 3.6
}
pub fn set_speed(&mut self, speed: f32) {
self.speed = speed.clamp(-self.reverse_speed, self.top_speed);
}
pub fn describe_speed(&self) -> String {
let kmh = self.speed_kmh().abs().round() as i64;
let unit = if kmh == 1 { "kilometer" } else { "kilometers" };
if self.is_reversing() {
format!("reversing, {kmh} {unit} per hour")
} else {
format!("{kmh} {unit} per hour")
}
}
pub fn is_reversing(&self) -> bool {
self.speed < -0.05
}
pub fn gear(&self) -> i32 {
self.gear
}
pub fn gear_name(&self) -> String {
match self.gear {
-1 => "reverse".to_string(),
0 => "neutral".to_string(),
g => g.to_string(),
}
}
pub fn rpm(&self) -> f32 {
self.rpm
}
pub fn rpm_fraction(&self) -> f32 {
((self.rpm - self.idle_rpm) / (self.max_rpm - self.idle_rpm)).clamp(0.0, 1.0)
}
pub fn engine_pitch(&self) -> f32 {
0.8 + 1.2 * self.rpm_fraction()
}
pub fn is_engine_on(&self) -> bool {
self.engine_on
}
pub fn start_engine(&mut self) -> bool {
if self.is_out_of_fuel() {
return false;
}
self.engine_on = true;
if self.gear == 0 {
self.gear = 1;
}
self.rpm = self.rpm.max(self.idle_rpm);
true
}
pub fn stop_engine(&mut self) {
self.engine_on = false;
self.gear = 0;
self.rpm = 0.0;
}
pub fn fuel(&self) -> Option<f32> {
self.fuel_capacity.map(|_| self.fuel)
}
pub fn fuel_fraction(&self) -> f32 {
match self.fuel_capacity {
Some(capacity) if capacity > 0.0 => self.fuel / capacity,
Some(_) => 0.0,
None => 1.0,
}
}
pub fn is_out_of_fuel(&self) -> bool {
self.fuel_capacity.is_some() && self.fuel <= 0.0
}
pub fn refuel(&mut self, liters: f32) -> f32 {
let Some(capacity) = self.fuel_capacity else {
return 0.0;
};
let added = liters.max(0.0).min(capacity - self.fuel);
self.fuel += added;
added
}
pub fn set_load(&mut self, kg: f32) -> f32 {
self.load = kg.clamp(0.0, self.max_load);
self.load
}
pub fn distance(&self) -> f32 {
self.distance
}
pub(crate) fn simulate(&mut self, mass: f32, dt: f32) -> (f32, StepReport) {
let weight = (mass + self.load).max(1.0);
let throttle = self.throttle.clamp(0.0, 1.0);
let brake = self.brake.clamp(0.0, 1.0);
let v = self.speed;
if brake == 0.0 {
self.hold.0 = false;
}
if throttle == 0.0 {
self.hold.1 = false;
}
let (drive, pedal_brake) = if v > STOPPED {
self.hold.0 |= brake > 0.0;
(throttle, brake)
} else if v < -STOPPED {
self.hold.1 |= throttle > 0.0;
(-brake, throttle)
} else {
let (brake_held, gas_held) = self.hold;
let gas = if gas_held { 0.0 } else { throttle };
let reverse = if brake_held { 0.0 } else { brake };
let holding = if brake_held { brake } else { 0.0 }.max(if gas_held { throttle } else { 0.0 });
(gas - reverse, holding)
};
let drive = if self.engine_on && !self.handbrake { drive } else { 0.0 };
let mut accel = 0.0;
if drive > 0.0 {
let falloff = 1.0 - (v.max(0.0) / self.top_speed).powi(2);
accel = self.engine_force * drive * falloff.max(0.0) / weight;
} else if drive < 0.0 {
let falloff = 1.0 - ((-v).max(0.0) / self.reverse_speed.max(0.01)).powi(2);
accel = -self.engine_force * -drive * falloff.max(0.0) / weight;
}
accel = accel.clamp(-self.grip, self.grip);
let mut slow = self.brake_force * pedal_brake / weight;
if self.handbrake {
slow = self.grip;
}
slow = slow.min(self.grip) + self.rolling_resistance;
if drive == 0.0 && pedal_brake == 0.0 && self.engine_on {
slow += self.engine_braking;
}
if drive == 0.0 {
let top = if v >= 0.0 { self.top_speed } else { self.reverse_speed.max(0.01) };
slow += AIR_DRAG * self.engine_force / weight * (v / top).powi(2);
}
let mut speed = v + accel * dt;
let drop = slow * dt;
if speed > 0.0 {
speed = (speed - drop).max(0.0);
} else if speed < 0.0 {
speed = (speed + drop).min(0.0);
}
self.speed = speed.clamp(-self.reverse_speed, self.top_speed);
let rate = (self.speed.abs() / self.turn_radius).to_degrees().min(self.max_turn_rate);
let turn = rate * self.steer.clamp(-1.0, 1.0) * self.speed.signum() * dt;
if self.fuel_capacity.is_some() && self.engine_on {
let load_factor = weight / mass.max(1.0);
let per_meter = self.consumption / 100_000.0 * (0.3 + 0.7 * throttle.max(brake)) * load_factor;
self.fuel = (self.fuel - per_meter * self.speed.abs() * dt).max(0.0);
if self.fuel <= 0.0 {
self.stop_engine();
}
}
let report = StepReport { gear_changed: self.update_gearbox(drive) };
(turn, report)
}
fn update_gearbox(&mut self, drive: f32) -> Option<i32> {
let before = self.gear;
if !self.engine_on {
self.gear = 0;
self.rpm = 0.0;
return (before != 0).then_some(0);
}
let gears = self.gears.max(1) as i32;
let band = |g: i32| self.top_speed * g as f32 / gears as f32;
let fraction = if self.speed < -0.05 || (self.speed.abs() <= 0.05 && drive < 0.0) {
self.gear = -1;
-self.speed / self.reverse_speed.max(0.01)
} else {
if self.gear < 1 {
self.gear = 1;
}
while self.gear < gears && self.speed > band(self.gear) * 0.95 {
self.gear += 1;
}
while self.gear > 1 && self.speed < band(self.gear - 1) * 0.7 {
self.gear -= 1;
}
self.speed / band(self.gear)
};
let revving = drive.abs() * 0.3;
let fraction = fraction.clamp(0.0, 1.0).max(revving);
self.rpm = self.idle_rpm + (self.max_rpm - self.idle_rpm) * fraction;
(self.gear != before).then_some(self.gear)
}
pub(crate) fn blocked(&mut self, facing: f32, normal: Vec3) -> f32 {
let velocity = velocity(facing, self.speed);
let into = velocity.dot(normal);
if into >= 0.0 {
return 0.0;
}
let slide = velocity - normal * into;
self.speed = slide.dot(crate::math::forward(facing)).clamp(-self.reverse_speed, self.top_speed);
-into
}
pub(crate) fn add_distance(&mut self, meters: f32) {
self.distance += meters.abs();
}
}
pub(crate) fn velocity(facing: f32, speed: f32) -> Vec3 {
crate::math::forward(facing) * speed
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
const DT: f32 = 1.0 / 60.0;
fn run(world: &mut World, seconds: f32) {
for _ in 0..(seconds / DT).round() as usize {
world.step(DT);
}
}
fn time_to(world: &mut World, id: BodyId, kmh: f32) -> f32 {
world.vehicle_mut(id).unwrap().throttle = 1.0;
let mut t = 0.0;
while world.vehicle(id).unwrap().speed_kmh() < kmh && t < 300.0 {
world.step(DT);
t += DT;
}
t
}
#[test]
fn car_accelerates_and_stops() {
let mut world = World::new();
let car = world.add(Body::car(Vec3::ZERO));
let t = time_to(&mut world, car, 100.0);
assert!((7.0..14.0).contains(&t), "0-100 km/h took {t} s");
assert!(world.position(car).y > 50.0, "drives north, the default facing");
assert!(world.vehicle(car).unwrap().gear() > 1, "shifted up");
assert!((world.speed(car) * 3.6 - 100.0).abs() < 2.0);
let v = world.vehicle_mut(car).unwrap();
v.throttle = 0.0;
v.brake = 1.0;
run(&mut world, 5.0);
assert!(world.vehicle(car).unwrap().speed().abs() < 0.01, "stopped, not reversing yet");
world.vehicle_mut(car).unwrap().brake = 0.0;
world.step(DT);
world.vehicle_mut(car).unwrap().brake = 1.0;
run(&mut world, 1.0);
assert!(world.vehicle(car).unwrap().is_reversing(), "pressed again: reverses");
}
#[test]
fn top_speed_is_respected() {
let mut world = World::new();
let car = world.add(Body::car(Vec3::ZERO).with_vehicle(Vehicle::car().with_top_speed_kmh(50.0)));
world.vehicle_mut(car).unwrap().throttle = 1.0;
run(&mut world, 30.0);
assert!(world.vehicle(car).unwrap().speed_kmh() <= 50.0 + 1e-3);
}
#[test]
fn load_makes_a_truck_slower_and_brake_longer() {
let mut world = World::new();
let empty = world.add(Body::truck(Vec3::ZERO));
let full = world.add(Body::truck(vec3(100.0, 0.0, 0.0)));
world.vehicle_mut(full).unwrap().set_load(25_000.0);
assert_eq!(world.weight(full), 37_000.0);
let t_empty = time_to(&mut world, empty, 60.0);
world.vehicle_mut(empty).unwrap().throttle = 0.0;
let t_full = time_to(&mut world, full, 60.0);
assert!(t_full > t_empty * 2.0, "empty {t_empty} s, full {t_full} s");
for id in [empty, full] {
let v = world.vehicle_mut(id).unwrap();
v.set_speed(60.0 / 3.6);
v.throttle = 0.0;
v.brake = 1.0;
}
let (y_empty, y_full) = (world.position(empty).y, world.position(full).y);
run(&mut world, 10.0);
let stop_empty = world.position(empty).y - y_empty;
let stop_full = world.position(full).y - y_full;
assert!(stop_full > stop_empty * 2.0, "braking: empty {stop_empty} m, full {stop_full} m");
}
#[test]
fn brake_reverses_when_stopped() {
let mut world = World::new();
let car = world.add(Body::car(Vec3::ZERO));
world.vehicle_mut(car).unwrap().brake = 1.0;
run(&mut world, 3.0);
let v = world.vehicle(car).unwrap();
assert!(v.is_reversing());
assert_eq!(v.gear(), -1);
assert!(v.speed_kmh() >= -25.0 - 1e-3);
assert!(world.position(car).y < 0.0);
assert!(v.describe_speed().starts_with("reversing"));
}
#[test]
fn steering_needs_speed() {
let mut world = World::new();
let car = world.add(Body::car(Vec3::ZERO));
world.vehicle_mut(car).unwrap().steer = 1.0;
run(&mut world, 1.0);
assert_eq!(world.facing(car), 0.0, "a parked car does not turn");
world.vehicle_mut(car).unwrap().set_speed(10.0);
run(&mut world, 0.5);
let facing = world.facing(car);
assert!(facing > 20.0 && facing < 90.0, "turned right to {facing}");
}
#[test]
fn crash_into_wall() {
let mut world = World::new();
let wall = world.add(Body::fixed(vec3(0.0, 20.0, 0.0)).with_box(10.0, 1.0, 3.0));
let car = world.add(Body::car(Vec3::ZERO));
world.vehicle_mut(car).unwrap().set_speed(15.0);
let mut crash = None;
for _ in 0..200 {
world.clear_events();
world.step(DT);
if let Some(c) = world.crashed(car) {
crash = Some(c);
break;
}
}
let (other, speed) = crash.expect("crashed");
assert_eq!(other, wall);
assert!(speed > 10.0);
assert!(world.vehicle(car).unwrap().speed().abs() < 1.0, "lost its speed");
assert!(world.position(car).y < 19.0, "did not pass the wall");
}
#[test]
fn fuel_runs_out() {
let mut world = World::new();
let car = world.add(Body::car(Vec3::ZERO).with_vehicle(Vehicle::car().with_fuel(0.05, 10.0)));
world.vehicle_mut(car).unwrap().throttle = 1.0;
let mut t = 0.0;
while world.vehicle(car).unwrap().is_engine_on() && t < 120.0 {
world.step(DT);
t += DT;
}
let v = world.vehicle(car).unwrap();
assert!(v.is_out_of_fuel());
assert!(!v.is_engine_on());
assert_eq!(v.gear(), 0);
let driven = v.distance();
assert!((400.0..600.0).contains(&driven), "0.05 l at 10 l/100 km is 500 m, drove {driven}");
let v = world.vehicle_mut(car).unwrap();
assert!(v.refuel(10.0) > 0.0);
assert!(v.start_engine());
}
#[test]
fn context_shortcuts() {
let mut runner = Engine::new("t").headless().build_fn(|_| {});
runner.frame();
let ctx = &mut runner.ctx;
let car = ctx.physics.add(Body::car(Vec3::ZERO).with_vehicle(Vehicle::car().engine_off()));
assert!(!ctx.physics.vehicle(car).unwrap().is_engine_on());
assert!(ctx.start_engine(car));
assert!(!ctx.start_engine(car), "already running");
runner.key_down(Key::Up, false);
for _ in 0..30 {
runner.ctx.drive(car);
std::thread::sleep(std::time::Duration::from_millis(5));
runner.frame();
}
assert!(runner.ctx.physics.vehicle(car).unwrap().speed() > 0.0);
runner.ctx.stop_engine(car);
assert_eq!(runner.ctx.physics.vehicle(car).unwrap().gear(), 0);
}
}