Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Movement and Physics

Elevator movement uses a trapezoidal velocity profile – accelerate, cruise at max speed, decelerate to stop precisely at the target. This chapter covers the physics model, per-elevator parameters, position interpolation for rendering, ETA queries, and passing-floor detection.

Trapezoidal velocity profile

Each tick, tick_movement() advances an elevator’s position and velocity through three regions:

graph LR
    A[Accelerate] --> B[Cruise]
    B --> C[Decelerate]
    C --> D[Arrived]
  • Accelerate – speed increases by acceleration * dt per tick until reaching max_speed.
  • Cruise – speed holds at max_speed while the remaining distance exceeds the braking distance.
  • Decelerate – speed decreases by deceleration * dt per tick, arriving at the target with velocity near zero.

For short trips where the elevator cannot reach max speed, the profile becomes triangular: accelerate then immediately decelerate. The system handles this automatically – no special configuration needed.

Per-elevator physics parameters

Physics are configured per-elevator on ElevatorConfig:

ParameterTypeDefaultDescription
max_speedSpeed2.0Maximum speed magnitude
accelerationAccel1.5Rate of acceleration (positive)
decelerationAccel2.0Rate of deceleration (positive)

All three are stored on the Elevator component at runtime, so different elevators (express vs. local, freight vs. passenger) can have different physics in the same simulation.

Braking distance

The braking_distance() function computes the distance required to stop from a given velocity:

#![allow(unused)]
fn main() {
use elevator_core::movement::braking_distance;

let distance = braking_distance(2.5, 1.0);  // v=2.5, decel=1.0
// distance = v^2 / (2 * a) = 3.125
}

This is useful for custom dispatch strategies that want to consider opportunistic stops – whether an elevator passing a floor can brake in time. The Simulation also exposes sim.braking_distance(elev) to get the current braking distance for a specific elevator without reimplementing the physics.

Position interpolation

Game renderers typically run at a higher framerate than the simulation tick rate. To produce smooth motion between ticks, use position_at() with an interpolation alpha:

#![allow(unused)]
fn main() {
use elevator_core::prelude::*;
use elevator_core::__doctest_prelude::*;
let sim: Simulation = todo!();
let elev: EntityId = todo!();
// alpha = 0.0 is the position at tick start, 1.0 at tick end.
let p_start = sim.position_at(elev, 0.0).unwrap();
let p_mid   = sim.position_at(elev, 0.5).unwrap();
let p_end   = sim.position_at(elev, 1.0).unwrap();
}

A typical render loop at 4x the sim rate samples at alpha = 0.0, 0.25, 0.5, 0.75:

#![allow(unused)]
fn main() {
use elevator_core::prelude::*;
use elevator_core::__doctest_prelude::*;
let mut sim: Simulation = todo!();
let elev: EntityId = todo!();
sim.step();

// Render 4 sub-frames per tick.
for i in 0..4 {
    let alpha = i as f64 / 4.0;
    let y = sim.position_at(elev, alpha).unwrap();
    // Set camera or sprite position to y.
}
}

The current velocity is available via sim.velocity(elev), which returns Option<f64> – the signed value (positive = upward), or None if the entity isn’t an elevator.

ETA queries

Two methods estimate arrival times for UI countdown displays and dispatch logic:

sim.eta(elevator, stop) – returns a Duration estimating how long until elevator reaches stop, based on its current queue and trapezoidal physics. The estimate assumes no mid-trip changes (door commands, new riders, dispatch reassignment).

sim.best_eta(stop, direction) – returns the (EntityId, Duration) of the elevator with the shortest ETA to a stop in the given direction, or None if no eligible car has that stop queued.

#![allow(unused)]
fn main() {
use elevator_core::prelude::*;
use elevator_core::__doctest_prelude::*;
let sim: Simulation = todo!();
let elev: ElevatorId = todo!();
let lobby: EntityId = todo!();
// How long until this specific elevator reaches the lobby?
match sim.eta(elev, lobby) {
    Ok(duration) => println!("ETA: {duration:.1?}"),
    Err(e) => println!("Cannot estimate: {e}"),
}

// Which elevator will reach the lobby soonest (going down)?
if let Some((car, eta)) = sim.best_eta(lobby, Direction::Down) {
    println!("Car {car:?} arriving in {eta:.1?}");
}
}

ETA queries can fail with EtaError – the elevator might not be headed to that stop, or it might be in an excluded service mode. See Error Handling for details.

PassingFloor events

When an elevator passes a stop without stopping, a PassingFloor event fires:

#![allow(unused)]
fn main() {
use elevator_core::prelude::*;
use elevator_core::__doctest_prelude::*;
fn handle(event: Event) {
if let Event::PassingFloor { elevator, stop, moving_up, tick } = event {
    let _ = (elevator, stop, moving_up, tick);
}
}
}

This is useful for:

  • Playing a “ding” or updating a floor indicator in your game UI
  • Custom dispatch strategies that react to passing traffic
  • Analytics on which floors see the most pass-throughs

SortedStops resource

Passing-floor detection uses the SortedStops resource internally – a sorted list of (position, EntityId) pairs that enables O(log n) binary search to find which stops an elevator crossed during a tick. This is an implementation detail; you do not interact with SortedStops directly.

Next steps