Frame-Rate Independent Entity Movement in Bevy: A Practical ECS Guide
Learn how to move Bevy entities using a custom Velocity component, Transform mutations, and Time::delta_seconds() so speed stays consistent across hardware.
18 Jan 2026, 20:41 UTC

The problem: movement speed tied to frame rate
If you add a fixed offset to an entity's position every frame, the entity moves faster on a 144 Hz monitor than on a 60 Hz laptop. The useful takeaway is simple: multiply velocity by the elapsed frame time so motion is measured in units per second, not units per frame. In Bevy, that means a custom Velocity component, a system that mutates Transform, and the Time resource's delta_seconds() value.
Desired outcome and prerequisites
You want entities to travel a predictable distance per second regardless of how often the frame loop runs. This guide assumes a recent stable Bevy release; because Bevy APIs evolve, confirm exact type and method names against the version pinned in your Cargo.toml or Cargo.lock.
- A working Rust toolchain installed.
- A Bevy project created with
cargo new my_bevy_appand thebevydependency added. - Basic familiarity with components, systems, and the
Appbuilder.
Define the Velocity component
Components in Bevy are plain structs. Store direction and speed together in a Vec3 so the movement system stays generic.
use bevy::prelude::*;
#[derive(Component)]
struct Velocity(Vec3);Write the movement system
The system queries every entity that has both a mutable Transform and a Velocity. It reads the frame delta from the Time resource and advances the translation vector.
fn movement_system(
mut query: Query<(&mut Transform, &Velocity)>,
time: Res<Time>,
) {
for (mut transform, velocity) in query.iter_mut() {
transform.translation += velocity.0 * time.delta_seconds();
}
}Note the mutable borrow on Transform. Without &mut, the compiler rejects the assignment because the system cannot mutate the component.
Register the system and spawn entities
Add the system to the Update schedule so it runs every frame. DefaultPlugins initializes the Time resource and the internal Transform propagation systems.
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Update, movement_system)
.run();Entities become eligible for movement only when they carry both components. A typical startup spawn looks like this:
commands.spawn((
Transform::from_xyz(0.0, 0.0, 0.0),
Velocity(Vec3::new(2.0, 0.0, 0.0)),
));Run cargo run from the project root. No elevated permissions are required. The first build downloads and compiles the full Bevy dependency tree, which can take several minutes.
Expected checks
Verify behavior with these concrete checks:
- Motion occurs. After
cargo run, the spawned entity should drift along the positive X axis. If you added a logging system or an inspector, confirm theTransformtranslation changes over time. - Frame-rate independence. Limit the application frame rate using your OS tools or Bevy's window settings, then compare the distance traveled over a fixed wall-clock interval. The distance should remain roughly constant.
- Selective movement. Spawn a second entity with only
Transformand noVelocity. It must remain stationary. - Clean build. Run
cargo checkand confirm the project compiles without warnings related to unused imports or unreachable code.
Recovery options
Common failures map directly to the cautions in this pattern:
| Symptom | Likely cause | Fix |
|---|---|---|
Panic when accessing Time | DefaultPlugins missing | Add .add_plugins(DefaultPlugins) before systems that read Time |
| Speed changes with frame rate | delta_seconds() omitted | Multiply the velocity vector by time.delta_seconds() |
| Compile error on mutation | Query uses &Transform | Change the query to &mut Transform |
| Entity moves once, then stops | System added to Startup | Move .add_systems(Update, movement_system) |
Limitations and next steps
This pattern produces constant linear motion. It does not handle acceleration, friction, collision response, or rotation. If the frame hitches, delta_seconds() can spike and produce a large jump; for simulation-heavy logic, investigate Bevy's fixed timestep utilities rather than raw delta time. For networked or replayed scenarios, raw Transform mutation is insufficient without deterministic scheduling and input handling.
To confirm the result in your own project, add a temporary system that logs transform.translation each second, or attach a camera and watch the entity cross the viewport. Remove the logging before shipping.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.