Migration Guide: v0.11 to v0.12
This guide covers breaking changes when upgrading from godot-bevy 0.11.x to 0.12.0.
Table of Contents
- Breaking: Detached nodes remain alive
- Behavior: Shutdown runs on destruction
- Dependencies: Bevy 0.19 and godot 0.5
- New: Event bridge
- Breaking: Integration tests live in the game crate
- New: GodotNode on tuple structs
- New: Inspector metadata on generated exports
- New: AttachableComponent
- Known limitations: Web builds
- Macro redesign --
#[gdbevy(...)]unified grammar (breaking change) - Native schedule ordering
- ButtonInput now works in FixedUpdate
- TwoWay transform sync -- now per-axis co-authorship
- Reparenting preserves ECS component state
- CollisionEnded now fires when a colliding node is freed
- CollisionStarted now fires for Area spawn-into-overlap
- Warning for contact_monitor-less RigidBodies
- GodotActions: Update readers need .after(GodotInputSet)
- RunFixedMainLoop removed from MainScheduleOrder
- PhysicsUpdate and related APIs removed
- FixedUpdate now runs at Godot's physics rate
- Delta time: use Res<Time> everywhere
- Engine.time_scale honored automatically
- SceneTree.paused now runs Update systems (process_mode = ALWAYS)
- Physics interpolation support
- Dependencies: gdext bumped to 0.5.5
Breaking: Detached nodes remain alive
A node removed with remove_child is no longer freed by godot-bevy (#263). Its mirrored entity still despawns when the removal is processed. Re-adding the surviving node creates a fresh entity. This differs from an in-tree reparent, which preserves the existing entity and its components.
This is the change most likely to affect code that relied on the old behavior. If detaching meant deleting in your game, call queue_free() explicitly. If you keep nodes for reuse, keep their handles and restore any runtime ECS state after re-entry.
ProtectedNodeEntity keeps the entity and its gameplay components. Cleanup removes its Godot components, index entry, and scene-tree relationships. To reuse that entity, insert the surviving node's GodotNodeHandle before re-entry. Re-entry alone creates a fresh entity.
Behavior: Shutdown runs on destruction
A started BevyApp writes AppExit::Success and runs one final Last pass on real destruction or an explicit teardown() (#262). Detaching or reparenting the app node no longer tears down its world. teardown() is idempotent, so a later call or destruction does not run shutdown again.
If your cleanup reads AppExit, handle it in Last. If you previously detached the app node to dispose of the world, call teardown() between frames instead. On destruction, Godot children may already be gone, so shutdown systems must not assume their nodes are still valid.
Dependencies: Bevy 0.19 and godot 0.5
All projects upgrading from 0.11 need to move Bevy from 0.18 to 0.19 and godot from 0.4 to 0.5 (#225, #238). These are breaking dependency upgrades. Update your manifest:
godot-bevy = "0.12"
bevy = { version = "0.19", default-features = false }
godot = "0.5"
Follow Bevy's 0.18 to 0.19 migration guide and gdext's migration to 0.5 for changes in your own systems and Godot classes. The minimum gdext patch is 0.5.5, as described below.
The Godot API selectors api-4-2, api-4-3, api-4-4, api-4-5, and api-4-6 all build (#252, #264). Projects targeting an older Godot runtime can select its API level. Enable only one selector and keep the GDExtension descriptor's compatibility_minimum consistent with it. Without a selector, the default API is 4.6.
New: Event bridge
The event bridge delivers typed Bevy events from GDScript and Rust (#242). Register a GDScript name and payload decoder with app.add_godot_event::<T>(...), then call BevyAppSingleton.send_event("name", payload) from GDScript. Rust callers can use send_event or clone GodotEventSender for channel sends, including from other threads. Events are queued and delivered to On<T> observers at the next First drain, once per render frame.
If you used GodotMailboxPlugin, replace its field polling with event registration and explicit sends. Replace MessageReader<T> consumers with observers, or accumulate events in a resource when you need fixed-step batches. See The Event Bridge for the migration table and callback timing restrictions.
Breaking: Integration tests live in the game crate
The documented test setup uses one crate with an optional godot-bevy-test dependency behind an itest feature (#260). The old multi-crate layout is no longer documented or supported when it links the game as an rlib, because that creates duplicate GDExtension entry symbols.
Move integration tests into a feature-gated module in the game crate. Keep its existing cdylib and #[bevy_app] entry point, and call godot_bevy_test::declare_test_runner!() there under the same feature. The usual test signature is #[itest] async fn test(ctx: TestContext). The context is owned so it can outlive the spawned task; the explicit #[itest(async)] form remains available.
Configure the game runner and use cargo run --features itest. Direct Godot launches must set GODOT_BEVY_ITEST=1 so the game autoload waits for the test runner. See Testing for dependencies, runner setup, and frame control.
New: GodotNode on tuple structs
GodotNode now supports tuple structs (#261). Exported fields use value0, value1, and so on, based on their tuple index. This lets component-first newtypes and tuples use the same #[gdbevy(export, ...)] attributes as named fields. Existing named structs need no changes. If you convert one to a tuple, update saved property names and any scripts that access those exports.
New: Inspector metadata on generated exports
Generated exports accept description, hint, and hint_string metadata (#262). These work on primary fields and required-component exports. description is a string literal, hint names a Godot PropertyHint variant, and hint_string requires a hint. Existing exports need no changes; add metadata where designers need help text or a specific Inspector control.
Enable the opt-in register-docs feature to show descriptions in Godot's editor help. It requires Godot API 4.3 or later. On API 4.2, leave it disabled. Hints work without it, and descriptions remain Rust docs. See Inspector metadata for the attribute syntax.
New: AttachableComponent
AttachableComponent turns an editor-authored child node into a component on its parent's Bevy entity (#265). Use it for configuration authored as child nodes. The carrier is consumed after successful attachment and never gets its own entity, so do not keep references to it or expect live Inspector synchronization. Existing components need no migration. See Attachable Components for conversion, placement, and lifetime rules.
Known limitations: Web builds
The web/wasm example does not build on current nightly (#268). The library's web features compile, but web execution remains unverified for 0.12. If you ship to browsers, keep this upgrade blocked on your own end-to-end validation and follow #268 for the build failure.
Macro Redesign -- #[gdbevy(...)] Unified Grammar (Breaking Change)
The four old bridging macros (BevyBundle, #[bevy_bundle(...)], #[godot_node(...)],
#[godot_export(...)], #[export_fields(...)]) are removed. They are replaced by two derives
that share one #[gdbevy(...)] grammar:
GodotNode-- component-first. Derive alongsideComponent; the macro generates the Godot class.BevyComponents-- Godot-first. Derive alongsideGodotClass; you own the class.
There are no deprecation aliases -- the old names are gone and will fail to compile.
Godot-first: BevyBundle → BevyComponents
Before:
#![allow(unused)] fn main() { #[derive(GodotClass, BevyBundle)] #[class(base = Node2D)] #[bevy_bundle((Player), (Speed: speed))] struct PlayerNode { base: Base<Node2D>, #[export] #[bevy_bundle(transform_with = "to_speed")] speed: f32, } #[godot_api] impl INode2D for PlayerNode { fn init(base: Base<Node2D>) -> Self { Self { base, speed: 250.0 } } } }
After:
#![allow(unused)] fn main() { #[derive(GodotClass, BevyComponents)] #[class(base = Node2D, init)] #[gdbevy(require(Player))] struct PlayerNode { base: Base<Node2D>, #[gdbevy(component = Speed, with = to_speed)] #[export] #[init(val = 250.0)] speed: f32, } }
Changes:
- Replace
BevyBundlederive withBevyComponents. - Replace
#[bevy_bundle((Marker), (Comp: field))]with#[gdbevy(require(Marker))]at the struct level and#[gdbevy(component = Comp)]at the field level. - Replace
#[bevy_bundle(transform_with = "fn")]with#[gdbevy(with = fn)](unquoted path, not a string). - Replace the hand-written
init()with gdext's#[class(…, init)]+#[init(val = …)]field defaults.
Struct-level #[bevy_bundle((Comp { field: godot_field, … }))] (N→1 binding) becomes:
#![allow(unused)] fn main() { #[gdbevy(require(Comp { bevy_field: godot_field, … }))] }
Component-first: bundle mode → GodotNode
The old #[derive(Bundle, GodotNode)] + #[export_fields(...)] form is removed. Use the
component-first GodotNode path with require(...) companions instead.
Before:
#![allow(unused)] fn main() { #[derive(Bundle, GodotNode)] #[godot_node(base(CharacterBody2D), class_name(Player2D))] pub struct PlayerBundle { pub player: Player, #[export_fields(value(export_type(f32), default(250.0)))] pub speed: Speed, #[export_fields(value(export_type(f32), default(-400.0)))] pub jump_velocity: JumpVelocity, #[export_fields(value(export_type(f32), default(godot::classes::ProjectSettings::singleton() .get_setting("physics/2d/default_gravity") .try_to::<f32>() .unwrap_or(980.0))))] pub gravity: Gravity, } }
After:
#![allow(unused)] fn main() { #[derive(Component, GodotNode, Default)] #[gdbevy(base = CharacterBody2D, class_name = Player2D)] #[gdbevy( require(speed: Speed, as = f32, default = 250.0), require(jump_velocity: JumpVelocity, as = f32, default = -400.0), require(gravity: Gravity, as = f32, default = 980.0), )] pub struct Player; }
Changes:
- Replace
#[derive(Bundle, GodotNode)]with#[derive(Component, GodotNode, Default)]. - Replace
#[godot_node(base(…), class_name(…))]with#[gdbevy(base = …, class_name = …)]. - Replace
#[export_fields(value(export_type(T), default(expr)))]on bundle fields withrequire(prop: Comp, as = T, default = expr)at the struct level. - Gravity default: The old default called
ProjectSettings::singleton(), which works on the Godot main thread. The newdefault = expralso runs inside a required-component constructor (called during pure Bevyspawn), where Godot may not be running. Use a static value:default = 980.0instead of theProjectSettingslookup.
The component-first #[godot_export(default(…))] on primary struct fields becomes
#[gdbevy(export, default = …)]:
Before:
#![allow(unused)] fn main() { #[derive(Component, GodotNode, Default)] #[godot_node(base(Area2D), class_name(Door2D))] pub struct Door { #[godot_export(default(LevelId::Level1))] pub level_id: LevelId, } }
After:
#![allow(unused)] fn main() { #[derive(Component, GodotNode, Default)] #[gdbevy(base = Area2D, class_name = Door2D)] pub struct Door { #[gdbevy(export, default = LevelId::Level1)] pub level_id: LevelId, } }
Reserved keys
into and sync are reserved for the upcoming component-sync feature. Using them now produces a
compile error. Remove any speculative use of these keys.
Migration checklist
-
Replace
BevyBundlederives withBevyComponents. -
Replace
#[bevy_bundle(...)]struct attributes with#[gdbevy(require(...))]. -
Replace
#[bevy_bundle(transform_with = "fn")]with#[gdbevy(with = fn)](no quotes). -
Replace bundle-mode
#[derive(Bundle, GodotNode)]+#[export_fields]with component-first#[derive(Component, GodotNode, Default)]+require(...). -
Replace
#[godot_node(base(…), class_name(…))]with#[gdbevy(base = …, class_name = …)]. -
Replace
#[godot_export(default(…))]with#[gdbevy(export, default = …)]. -
Replace any
ProjectSettings-dependent defaults with static values. -
Remove any
into/synckeys.
Behavior: Native schedule ordering
The main schedule now runs in stock Bevy order:
First → PreUpdate → StateTransition → [FixedMain ×N] → Update → PostUpdate → Last
The prefix (First through StateTransition) runs once per render frame. It runs in the first _physics_process, or in _process if the frame has no physics step. Further physics steps run only FixedMain. The suffix (Update through Last) runs in _process. clear_trackers fires once at the end of _process, covering the full render frame.
What changed from v0.11: In v0.11 the prefix ran in _process after the fixed steps. That meant PreUpdate and StateTransition came after the fixed loop instead of before it. The new ordering matches what you'd see in a plain Bevy desktop app.
Practical effects:
Changed<T>,Added<T>, andRemovedComponentsfromPreUpdatemutations are visible inFixedUpdatewithin the same render frame.OnEnter/OnExitstate transitions (fired byStateTransition) are visible toFixedUpdatein the same frame.- Systems that do clock-sensitive Godot FFI (e.g. reading physics state) and were previously safe in
First/PreUpdatemay now run under the physics clock. Move such work toUpdateor later if it must run after Godot's physics integration for the current frame.
Secondary Bevy SubApps are rejected when the app is built. The driver runs Main directly and cannot extract or update a secondary app. If a plugin creates one, remove that plugin or move its logic into the main app.
Behavior: ButtonInput now works in FixedUpdate
ButtonInput<KeyCode>::just_pressed and just_released are now visible in FixedUpdate. Previously they were always false there because PreUpdate (where keyboard_input_system populates the resource) ran after the fixed steps. With native ordering, PreUpdate runs before the fixed steps -- so when a physics step runs, edges are already set by the time FixedUpdate runs.
#![allow(unused)] fn main() { // Now visible in FixedUpdate (when a physics step runs on the edge's frame) app.add_systems(FixedUpdate, |keys: Res<ButtonInput<KeyCode>>| { if keys.just_pressed(KeyCode::Space) { /* see caveats below */ } }); }
Caveat: just_pressed is a one-render-frame edge (cleared each frame in PreUpdate), so the render frame's physics-step count matters:
- More physics than display (N steps/frame):
just_pressedistruein all NFixedUpdatecalls that frame. - More display than physics: a frame can run zero physics steps; an edge landing on a step-less frame is never seen in
FixedUpdate(cleared by the next frame). Same caveat as stock Bevy.
GodotActions uses Godot's per-tick edge state and has neither issue -- prefer it for fixed-rate gameplay input.
No code change is required -- this is a fix. If you were working around the limitation (e.g. by reading input only in Update and storing state in a resource), the workaround is no longer necessary.
Behavior: TwoWay transform sync -- now per-axis co-authorship
TwoWay transform sync is improved. The Godot→Bevy read runs in FixedFirst on every physics step. On frames with zero physics steps, a PreUpdate fallback reads transforms unless virtual time is paused. FixedUpdate systems see the current step's Godot-authored transform, and Update sees the last read for that frame. If you read synced transforms in PreUpdate, move that work to FixedUpdate or Update.
The echo guard is a per-entity value shadow, so a node can be co-authored per axis: Godot drives some translation/scale components while Bevy drives others, on the same node, every frame, and neither clobbers the other. Rotation is whole (quaternion components aren't independently meaningful); if both sides change the same axis in one frame, Bevy wins.
No changes to the co-authorship setup are required.
Breaking: GodotActions Update readers need .after(GodotInputSet)
GodotActionsPlugin previously polled the process snapshot in First. It now polls in Update (to stay in the suffix, where the process clock is correct). If you have Update systems that read Res<GodotActions>, they must run after the poll:
#![allow(unused)] fn main() { // Before: no ordering needed (poll was in First, readers ran later by default) app.add_systems(Update, my_system); // After: explicit ordering required app.add_systems(Update, my_system.after(GodotInputSet)); }
FixedUpdate systems are unaffected -- the fixed-schedule driver polls the physics snapshot automatically.
If you added .after(GodotInputSet) already (as the GodotActions docs recommended), no change is needed.
Breaking: RunFixedMainLoop removed from MainScheduleOrder
RunFixedMainLoop is no longer a member of MainScheduleOrder.labels. Its slot is held by a private marker so app.update() does not drive the fixed loop (godot-bevy drives it from _physics_process instead).
Affected: plugins that insert top-level schedules by calling insert_after(RunFixedMainLoop, MySchedule).
#![allow(unused)] fn main() { // This panics -- RunFixedMainLoop is not in the label list order.insert_after(RunFixedMainLoop, MySchedule); }
Fix: insert relative to a schedule that is still in the list, or use the public anchor sets:
#![allow(unused)] fn main() { // Prefix (before fixed steps): target First, PreUpdate, or StateTransition order.insert_after(PreUpdate, MyPrefixSchedule); // Suffix (after fixed steps): target Update, PostUpdate, or Last order.insert_after(Update, MySuffixSchedule); // Adjacent to the fixed loop: use the anchor sets on RunFixedMainLoop itself app.add_systems( RunFixedMainLoop, my_system.in_set(RunFixedMainLoopSystems::BeforeFixedMainLoop), ); }
Breaking: PhysicsUpdate and related APIs removed
The custom PhysicsUpdate, PrePhysicsUpdate, PhysicsDelta, and SystemDeltaTimer types are removed. godot-bevy now drives Bevy's standard FixedMain schedule directly from Godot's _physics_process.
Move systems to standard Fixed schedules
Before:
app.add_systems(PhysicsUpdate, character_movement);
app.add_systems(PrePhysicsUpdate, read_input);
After:
app.add_systems(FixedUpdate, character_movement);
app.add_systems(FixedPreUpdate, read_input);
The full FixedMain schedule order is:
| Old | New equivalent |
|---|---|
PrePhysicsUpdate | FixedFirst or FixedPreUpdate |
PhysicsUpdate | FixedUpdate |
| (none) | FixedPostUpdate |
| (none) | FixedLast |
Replace PhysicsDelta with Res<Time>
Before:
fn move_player(
physics_delta: Res<PhysicsDelta>,
mut q: Query<&mut Transform, With<Player>>,
) {
let delta = physics_delta.delta_seconds;
for mut t in &mut q {
t.translation.x += 100.0 * delta;
}
}
After:
fn move_player(
time: Res<Time>,
mut q: Query<&mut Transform, With<Player>>,
) {
let delta = time.delta_secs();
for mut t in &mut q {
t.translation.x += 100.0 * delta;
}
}
Res<Time> in a FixedUpdate system automatically reports the fixed physics delta -- no special type needed.
SystemDeltaTimer removed
SystemDeltaTimer is removed. For cooldowns and accumulators inside fixed systems, use Bevy's standard timer types (Timer, Stopwatch) or accumulate time.delta_secs() directly.
Breaking: FixedUpdate now runs at Godot's physics rate
Previously, FixedUpdate ran on Bevy's own 64 Hz clock (independent of Godot). Now it runs on Godot's authoritative physics clock.
Default rate: 60 Hz (controlled by Project Settings → Physics → Common → Physics Ticks Per Second).
Do not hardcode the timestep:
// BAD: breaks if you change physics rate in Project Settings
let delta = 1.0 / 60.0;
// GOOD: always correct
fn my_system(time: Res<Time>) {
let delta = time.delta_secs();
}
New: Engine.time_scale honored automatically
Engine.time_scale (slow-motion, fast-forward, freeze) now scales both schedules. Godot's physics delta already incorporates the scale, so FixedUpdate systems reading Res<Time> slow down for free. The Update schedule scales too: godot-bevy mirrors time_scale onto Time<Virtual>, so timers, tweens, and camera lerps in Update honor it as well. Time<Real> stays truthful -- read it for anything that must ignore time scaling (diagnostics, real-time UI).
Breaking: SceneTree.paused now runs Update systems (process_mode = ALWAYS)
BevyApp now sets its process_mode to ALWAYS, so _process and _physics_process keep firing while SceneTree.paused is true. Previously an INHERIT/PAUSABLE BevyApp froze both callbacks, so a tree-pause stopped the entire ECS -- including any Bevy-authored pause menu. Now the ECS keeps ticking through a pause, which is what makes a pause menu in Bevy possible.
The trade-off: an Update system that does not scale by delta now runs under SceneTree.paused, where the frozen callback used to stop it. FixedUpdate is unaffected -- godot-bevy freezes the fixed schedule under pause (it keys on Time<Virtual>, and a tree-pause drives that), so physics-rate gameplay still halts.
If you have Update work that must stop on pause, either:
- scale it by
Time<Virtual>::delta()(it is 0 while paused), or - gate it:
system.run_if(not(bevy_time::common_conditions::paused)).
There is no opt-out from ALWAYS -- it is the single behavior. Pausing the tree, pausing Time<Virtual> directly, and setting Engine.time_scale all still work; only the "the whole app freezes" side effect is gone.
New: Physics interpolation support
godot-bevy now writes transforms at physics rate (FixedLast). To get smooth rendering between physics ticks, enable Godot's built-in physics interpolation.
In Project Settings: Physics → Common → Physics Interpolation = true
In project.godot:
[physics]
common/physics_interpolation=true
When a node teleports (respawn, warp), call reset_physics_interpolation() on it immediately after moving so Godot doesn't interpolate across the jump.
Bevy-side interpolation plugins and rollback netcode are not supported in godot-bevy's transform sync path -- use Godot's built-in interpolation instead.
Dependencies: gdext bumped to 0.5.5
godot-bevy now requires godot >= 0.5.5. Three effects:
- Editor hot-reload is safer. Custom signal callables connected through godot-bevy's signal bridge are now auto-removed when the extension reloads (gdext #1612), so rebuilding the Rust dylib while a game runs in the editor with connected signals no longer risks an editor crash. (This is a development-time property; exported builds never hot-reload.)
- Reference counting no longer touches gdext internals. 0.5.5 removed the internal reference-count operations godot-bevy used, so the floor moves with it (#271).
- No more libclang/LLVM to build from source. gdext 0.5.3+ generates its bindings from JSON and no longer depends on
bindgen, so building godot-bevy no longer requires a system libclang/LLVM.
You only need to act if you pin godot to an exact version below 0.5.5 (e.g. godot = "=0.5.1") and also depend on godot-bevy -- bump your pin to 0.5.5. A normal caret dependency (godot = "0.5") resolves to 0.5.5 automatically.
Behavior: Reparenting preserves ECS component state
Moving a mirrored node to a new parent no longer re-runs component initializers or autosync bundle creators, so Transform and your #[derive(GodotNode)] / BevyComponents components keep the values your systems set. Previously a reparent silently reset them to the node's exported values. Node group membership (Groups) is likewise now a spawn-time snapshot rather than being re-read on reparent. If you relied on a reparent refreshing any of these, refresh the component explicitly in a system.
No action required unless you depended on the old reset-on-reparent behavior.
Behavior: CollisionEnded now fires when a colliding node is freed
Freeing a node while it overlaps another now emits CollisionEnded for each pair the
node was in, and clears those pairs from Collisions. Previously the pair leaked: the
freed node lost its entity mapping before the exit signal resolved, so the channel
dropped the Ended and Collisions::contains reported the dead pair forever. This is
the "bullet dies on hit" case.
The freed side of CollisionEnded references a despawned entity -- the same contract
Avian uses; observers already tolerate a target that may be gone.
The free-driven end surfaces on MessageReader<CollisionEnded> and On<CollisionEnded>
observers, not on CollisionState::ended(). The observer fires during the despawn
flush in First, while ended_this_frame is cleared at the top of the collision step
in FixedFirst (which runs after First), so a First-timed push would be wiped before
any reader saw it. Read the message or observer tier for free-driven ends.
No action required unless you filtered these events assuming both entities are always alive.
Behavior: CollisionStarted now fires for Area spawn-into-overlap
An Area2D/Area3D spawned already overlapping something now emits CollisionStarted
for that overlap. Previously the overlap was silently missed: the Area's real enter
signal fired before godot-bevy connected to it, so nothing recorded the pair. At connect
the plugin now seeds the Area's current overlaps (get_overlapping_bodies /
get_overlapping_areas) as synthetic starts. The seed is deduplicated against the real
signal, so a normal (not-pre-overlapping) spawn is unaffected.
The seed is Area-only. A RigidBody2D/RigidBody3D spawned into an existing overlap
still misses its CollisionStarted even with contact_monitor enabled (see the residual
limitation below).
No action required.
Behavior: Warning for contact_monitor-less RigidBodies
Connecting a RigidBody2D/RigidBody3D whose contact_monitor is disabled now logs a
one-time warning at connect. With contact_monitor off, Godot never fires the body's
body_entered/body_exited signals, so godot-bevy would silently report no collisions
for that body -- an easy trap to hit. godot-bevy does not auto-enable
contact_monitor (it changes physics cost and max_contacts_reported has no
library-correct default); set it yourself if you want the events:
# On the RigidBody, in the editor or in code
contact_monitor = true
max_contacts_reported = 4 # > 0, sized to your needs
Residual limitation: enabling contact_monitor makes the signals fire, but the plugin
still does not seed RigidBody overlaps (the seed is Area-only). So a contact_monitor-
enabled RigidBody spawned into an existing overlap still misses that first
CollisionStarted -- its enter fired before connect and there is no seed backstop. Enter
overlaps after spawn work normally. If you need spawn-into-overlap detection, use an
Area2D/Area3D.
Migration Checklist
-
Replace
PhysicsUpdatewithFixedUpdate. -
Replace
PrePhysicsUpdatewithFixedFirstorFixedPreUpdate. -
Replace
Res<PhysicsDelta>+physics_delta.delta_secondswithRes<Time>+time.delta_secs(). -
Remove any
SystemDeltaTimerusage; use standard BevyTimer/Stopwatchor accumulatetime.delta_secs(). -
Remove hardcoded
1.0 / 60.0timestep constants; readtime.delta_secs()instead. -
Enable physics interpolation in
project.godotif your game has moving objects. -
Call
reset_physics_interpolation()on nodes that teleport. -
If using
avianphysics, passFixedUpdateinstead ofPhysicsUpdatetoPhysicsPlugins::new(...).