elevator-core
A tick-based elevator simulation engine for Rust. Model anything from a 3-story office building to an orbital space elevator. Pluggable dispatch strategies, realistic trapezoidal motion profiles, O(1) population tracking per stop, and an extension system let you build exactly the simulation you need.
Table of Contents
- Getting Started
- Examples
- Architecture
- Dispatch Strategies
- Configuration
- Bevy Integration
- Testing
- Feature Flags
- License
Getting Started
Add elevator-core to your project:
From there, the typical workflow is:
- Configure stops -- define the building layout with named stops at arbitrary positions.
- Build the simulation --
SimulationBuildervalidates the config and returns a ready-to-runSimulation. - Spawn riders -- place riders at origin stops with a destination and weight.
- Step the loop -- each call to
sim.step()advances one tick through all eight phases. - Read metrics -- query aggregate wait times, ride times, and throughput at any point.
Examples
Basic
Create a simulation, spawn a rider, and run until delivery.
use *;
use ElevatorConfig;
use StopConfig;
let mut sim = new
.stops
.elevator
.build
.unwrap;
// Spawn a 75 kg rider going from Ground to Floor 3.
sim.spawn_rider_by_stop_id.unwrap;
// Run for 1000 ticks.
for _ in 0..1000
println!;
ElevatorConfig implements Default with sensible physics (max speed 2.0, acceleration 1.5, deceleration 2.0, 800 kg capacity), so .elevator(ElevatorConfig { starting_stop: StopId(0), ..Default::default() }) is all you need when the physics aren't the point.
Custom Dispatch
Swap in a different dispatch algorithm and react to simulation events.
use *;
use ElevatorConfig;
use EtdDispatch;
use StopConfig;
let mut sim = new
.stops
.elevator
.dispatch
.build
.unwrap;
sim.spawn_rider_by_stop_id.unwrap;
for _ in 0..2000
Extensions and Hooks
Attach custom data to entities and inject logic into the tick loop.
use *;
use ElevatorConfig;
use StopConfig;
use ;
let mut sim = new
.stops
.elevator
.
.after
.build
.unwrap;
// Spawn a rider and tag them as VIP.
let rider_id = sim.spawn_rider_by_stop_id.unwrap;
sim.world_mut.insert_ext;
// Later, read back the extension data.
if let Some = sim.world.
Architecture
Each call to sim.step() executes eight phases:
┌───────────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ Advance │──▶│ Dispatch │──▶│ Reposition │──▶│ Advance │
│ Transient │ │ │ │ │ │ Queue │
└───────────────┘ └──────────┘ └──────────────┘ └──────────────┘
│
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Metrics │◀──│ Loading │◀──│ Doors │◀──│ Movement │
│ │ │ │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
| Phase | Description |
|---|---|
| Advance Transient | Promotes one-tick states forward (Boarding to Riding, Exiting to Resident/Arrived). |
| Dispatch | Assigns idle elevators to stops via the pluggable DispatchStrategy. |
| Reposition | Moves idle elevators toward strategic positions to reduce future wait times. |
| Advance Queue | Reconciles each elevator's phase/target with its DestinationQueue front (honors imperative push_destination / push_destination_front / clear_destinations calls). |
| Movement | Updates elevator position and velocity using trapezoidal acceleration profiles. |
| Doors | Ticks door open/close finite-state machines at each stop. |
| Loading | Boards waiting riders onto elevators with open doors and exits riders at their destination. |
| Metrics | Aggregates wait times, ride times, and throughput from the current tick's events. |
Internally, elevator-core uses an ECS-style struct-of-arrays World with typed
component accessors, per-entity extension storage for game-specific data, and a
query builder for filtering and iterating entities by component composition.
Lifecycle hooks let you inject custom logic before or after any phase.
For per-phase semantics (events emitted, edge cases, design rationale), see
ARCHITECTURE.md — the single
canonical reference.
Dispatch Strategies
Four built-in strategies ship with the crate. All implement the DispatchStrategy trait.
| Strategy | Type | Description |
|---|---|---|
| SCAN | ScanDispatch |
Classic elevator algorithm. Sweeps end-to-end before reversing direction. |
| LOOK | LookDispatch |
Like SCAN, but reverses at the last pending request instead of the shaft end. |
| Nearest Car | NearestCarDispatch |
Assigns each hall call to the closest idle elevator. Coordinates across multi-elevator groups to avoid duplicate responses. |
| ETD | EtdDispatch |
Industry-standard Estimated Time to Destination. Evaluates every elevator for each call and minimizes total cost. |
To implement a custom strategy, implement the DispatchStrategy trait and pass
it to the builder:
use *;
use ElevatorConfig;
use StopConfig;
let sim = new
.stops
.elevator
.dispatch_for_group
.build
.unwrap;
Configuration
Simulations can be configured programmatically via SimulationBuilder or loaded
from RON files. The workspace includes example configs in assets/config/.
SimConfig(
building: BuildingConfig(
name: "Demo Tower",
stops: [
StopConfig(id: StopId(0), name: "Ground", position: 0.0),
StopConfig(id: StopId(1), name: "Floor 2", position: 4.0),
StopConfig(id: StopId(2), name: "Floor 3", position: 7.5),
StopConfig(id: StopId(3), name: "Floor 4", position: 11.0),
StopConfig(id: StopId(4), name: "Roof", position: 15.0),
],
),
elevators: [
ElevatorConfig(
id: 0,
name: "Main",
max_speed: 2.0,
acceleration: 1.5,
deceleration: 2.0,
weight_capacity: 800.0,
starting_stop: StopId(0),
door_open_ticks: 60,
door_transition_ticks: 15,
),
],
simulation: SimulationParams(
ticks_per_second: 60.0,
),
passenger_spawning: PassengerSpawnConfig(
mean_interval_ticks: 120,
weight_range: (50.0, 100.0),
),
)
To load a RON config at runtime:
use *;
let config: SimConfig = from_str?;
let sim = from_config.build?;
Bevy Integration
The elevator-bevy crate wraps the core simulation as a
Bevy 0.18 plugin. ElevatorSimPlugin reads a RON config file, constructs a
Simulation, and inserts it as a Bevy Resource. It bridges simulation events
into the Bevy message system, renders the building and riders with 2D meshes,
and supports configurable simulation speed via keyboard input.
Testing
- Unit, integration, and doc tests run under
cargo test -p elevator-core. - Criterion benchmarks cover dispatch, scaling, multi-line, query, and
tick-loop throughput (
cargo bench -p elevator-core). - Deterministic replay is guarded by an end-to-end diff test
(
tests/deterministic_replay.rs) that runs two identical scenarios and compares the full event stream byte-for-byte. - Mutation testing via
cargo mutantson the tick-loop hot path (src/systems/,src/dispatch/,door.rs,movement.rs,rider_index.rs). Reproduce with:
The headless example below is the shortest path to consuming the simulation from a non-Bevy context:
Feature Flags
| Flag | Default | Description |
|---|---|---|
traffic |
yes | Enables traffic pattern generation (adds rand dependency) |
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.