Getting Started
In this chapter we will build a minimal elevator simulation from scratch: a 3-stop building with one elevator, a single rider, and a loop that runs until the rider arrives at their destination.
Add the dependency
cargo add elevator-core
Import the prelude
The prelude re-exports everything you need for typical usage:
#![allow(unused)]
fn main() {
use elevator_core::prelude::*;
}
This brings in, at a glance:
| Group | Items |
|---|---|
| Builder & sim | SimulationBuilder, Simulation, RiderBuilder |
| Components | Rider, RiderPhase, Elevator, ElevatorPhase, Stop, Line, Position, Velocity, FloorPosition, Route, Patience, Preferences, AccessControl, Orientation, ServiceMode |
| Config | SimConfig, GroupConfig, LineConfig |
| Dispatch traits | DispatchStrategy, RepositionStrategy |
| Reposition strategies | NearestIdle, ReturnToLobby, SpreadEvenly, DemandWeighted |
| Identity | EntityId, StopId, GroupId |
| Errors & events | SimError, RejectionReason, RejectionContext, Event, EventBus |
| Misc | Metrics, TimeAdapter |
Not in the prelude (import explicitly): the concrete built-in dispatch types (ScanDispatch, LookDispatch, NearestCarDispatch, EtdDispatch — see Dispatch Strategies), ElevatorConfig and StopConfig from elevator_core::config, the traffic module (feature-gated), the snapshot module, and the World type (needed as a parameter when implementing custom dispatch).
Feature flags
| Flag | Default? | Enables |
|---|---|---|
traffic | yes | traffic module: PoissonSource, TrafficPattern, TrafficSchedule. Pulls in rand. |
energy | no | Per-elevator EnergyProfile/EnergyMetrics components and snapshot fields. |
Turn off defaults with default-features = false if you want a leaner build and intend to write your own rider spawning.
Build a simulation
We will use SimulationBuilder to set up a 3-stop building with one elevator, SCAN dispatch (the builder’s default), and 60 ticks per second. ElevatorConfig implements Default with sensible physics (max speed 2.0, acceleration 1.5, deceleration 2.0, 800 kg capacity) so the elevator is one line; stops we spell out because positions are the whole point.
use elevator_core::prelude::*;
use elevator_core::config::ElevatorConfig;
use elevator_core::stop::StopId;
fn main() -> Result<(), SimError> {
let mut sim = SimulationBuilder::new()
.stop(StopId(0), "Lobby", 0.0)
.stop(StopId(1), "Floor 2", 4.0)
.stop(StopId(2), "Floor 3", 8.0)
.elevator(ElevatorConfig { starting_stop: StopId(0), ..Default::default() })
.building_name("Tutorial Tower")
.build()?;
Ok(())
}
Override any ElevatorConfig field with struct-update syntax (ElevatorConfig { max_speed: 4.0, ..Default::default() }) — the Configuration chapter covers every field.
Spawn a rider
A rider is anything that rides an elevator. To spawn one, you provide an origin stop, a destination stop, and a weight:
use elevator_core::prelude::*;
use elevator_core::config::ElevatorConfig;
use elevator_core::stop::StopId;
fn main() -> Result<(), SimError> {
let mut sim = SimulationBuilder::new()
.stop(StopId(0), "Lobby", 0.0)
.stop(StopId(1), "Floor 2", 4.0)
.stop(StopId(2), "Floor 3", 8.0)
.elevator(ElevatorConfig { starting_stop: StopId(0), ..Default::default() })
.build()?;
let rider_id = sim.spawn_rider_by_stop_id(
StopId(0), // origin: Lobby
StopId(2), // destination: Floor 3
75.0, // weight in kg
)?;
println!("Spawned rider: {:?}", rider_id);
Ok(())
}
spawn_rider_by_stop_id maps config-level StopId values to runtime EntityId values internally. It returns Result<EntityId, SimError> – it will fail if you pass a StopId that does not exist in your building.
Run the simulation loop
Each call to sim.step() advances the simulation by one tick, running all eight phases of the tick loop (advance transient, dispatch, reposition, advance queue, movement, doors, loading, metrics). After stepping, you can drain events to see what happened:
use elevator_core::prelude::*;
use elevator_core::config::ElevatorConfig;
use elevator_core::stop::StopId;
fn main() -> Result<(), SimError> {
let mut sim = SimulationBuilder::new()
.stop(StopId(0), "Lobby", 0.0)
.stop(StopId(1), "Floor 2", 4.0)
.stop(StopId(2), "Floor 3", 8.0)
.elevator(ElevatorConfig { starting_stop: StopId(0), ..Default::default() })
.build()?;
let rider_id = sim.spawn_rider_by_stop_id(StopId(0), StopId(2), 75.0)?;
let mut arrived = false;
while !arrived {
sim.step();
for event in sim.drain_events() {
match event {
Event::RiderBoarded { rider, elevator, tick } => {
println!("Tick {}: rider {:?} boarded elevator {:?}", tick, rider, elevator);
}
Event::ElevatorArrived { elevator, at_stop, tick } => {
println!("Tick {}: elevator {:?} arrived at stop {:?}", tick, elevator, at_stop);
}
Event::RiderExited { rider, stop, tick, .. } => {
println!("Tick {}: rider {:?} arrived at stop {:?}", tick, rider, stop);
if rider == rider_id {
arrived = true;
}
}
_ => {}
}
}
}
println!("Rider delivered!");
println!("Total ticks: {}", sim.current_tick());
println!("Avg wait time: {:.1} ticks", sim.metrics().avg_wait_time());
Ok(())
}
The complete program
Here is everything together as a single runnable file:
use elevator_core::prelude::*;
use elevator_core::config::ElevatorConfig;
use elevator_core::stop::StopId;
fn main() -> Result<(), SimError> {
// 1. Build a 3-stop building.
let mut sim = SimulationBuilder::new()
.stop(StopId(0), "Lobby", 0.0)
.stop(StopId(1), "Floor 2", 4.0)
.stop(StopId(2), "Floor 3", 8.0)
.elevator(ElevatorConfig { starting_stop: StopId(0), ..Default::default() })
.building_name("Tutorial Tower")
.build()?;
// 2. Spawn a rider going from the Lobby to Floor 3.
let rider_id = sim.spawn_rider_by_stop_id(StopId(0), StopId(2), 75.0)?;
// 3. Run until the rider arrives.
let mut arrived = false;
while !arrived {
sim.step();
for event in sim.drain_events() {
match event {
Event::RiderBoarded { rider, elevator, tick } => {
println!("Tick {tick}: rider {rider:?} boarded elevator {elevator:?}");
}
Event::ElevatorArrived { elevator, at_stop, tick } => {
println!("Tick {tick}: elevator {elevator:?} arrived at {at_stop:?}");
}
Event::RiderExited { rider, stop, tick, .. } => {
println!("Tick {tick}: rider {rider:?} exited at {stop:?}");
if rider == rider_id {
arrived = true;
}
}
_ => {}
}
}
}
// 4. Print summary metrics.
println!("\n--- Summary ---");
println!("Total ticks: {}", sim.current_tick());
// Metrics implements Display for a compact one-liner.
println!("{}", sim.metrics());
Ok(())
}
Run it with cargo run and you should see the rider move from the Lobby to Floor 3, with events printed along the way. The summary output looks like:
--- Summary ---
Total ticks: 482
1 delivered, avg wait 87.3t, 0% util
What just happened?
- The builder created a
Simulationcontaining aWorldwith three stop entities and one elevator entity, plus a SCAN dispatch strategy. spawn_rider_by_stop_idcreated a rider entity at the Lobby with a route to Floor 3.- Each
step()ran the seven-phase tick loop. The dispatch phase noticed a waiting rider and sent the elevator to the Lobby. The reposition phase was a no-op (no reposition strategy configured). The movement phase moved the elevator using a trapezoidal velocity profile. The doors phase opened and closed doors. The loading phase boarded and exited the rider. The metrics phase updated aggregate stats. - Events fired at each significant moment, and we pattern-matched on them to detect arrival.
Next up: Core Concepts dives deeper into the entity model, the tick loop phases, and the lifecycle of riders and elevators.