Migration Guide: v0.6 to v0.7
This guide covers breaking changes and new features when upgrading from godot-bevy 0.6.x to 0.7.0.
Table of Contents
- Node Type Markers (New Feature)
- BevyBundle Autosync Simplification
- Transform Sync Modes (Breaking Change)
Node Type Markers (New Feature)
What Changed
Starting in v0.7.0, all entities representing Godot nodes automatically receive marker components that indicate their node type. This enables type-safe, efficient ECS queries without runtime type checking.
Migration Path
This change is backwards compatible - your existing code will continue to work. However, you can improve performance and safety by migrating to marker-based queries.
Before (v0.6.x approach - still works)
#![allow(unused)] fn main() { use godot_bevy::prelude::*; fn update_sprites(mut all_nodes: Query<&mut GodotNodeHandle>) { for mut handle in all_nodes.iter_mut() { // Runtime type checking - works but inefficient if let Some(sprite) = handle.try_get::<Sprite2D>() { sprite.set_modulate(Color::RED); } } } fn update_character_bodies(mut all_nodes: Query<&mut GodotNodeHandle>) { for mut handle in all_nodes.iter_mut() { // Check every single entity in your scene if let Some(mut body) = handle.try_get::<CharacterBody2D>() { body.move_and_slide(); } } } }
After (v0.7.0 recommended approach)
#![allow(unused)] fn main() { use godot_bevy::prelude::*; fn update_sprites(mut sprites: Query<&mut GodotNodeHandle, With<Sprite2DMarker>>) { for mut handle in sprites.iter_mut() { // ECS pre-filters to only Sprite2D entities - much faster! let sprite = handle.get::<Sprite2D>(); // No Option<> - guaranteed to work sprite.set_modulate(Color::RED); } } fn update_character_bodies(mut bodies: Query<&mut GodotNodeHandle, With<CharacterBody2DMarker>>) { for mut handle in bodies.iter_mut() { // Only iterates over CharacterBody2D entities let mut body = handle.get::<CharacterBody2D>(); body.move_and_slide(); } } }
Benefits of Migration
- Performance: Only iterate over entities you care about
- Safety: No more
Option<>
handling or potential panics - Clarity: Query signatures clearly show what node types you expect
- Optimization: Better ECS query optimization and caching
Common Migration Patterns
Pattern 1: Single Node Type
Before:
#![allow(unused)] fn main() { fn system(mut all_nodes: Query<&mut GodotNodeHandle>) { for mut handle in all_nodes.iter_mut() { if let Some(mut timer) = handle.try_get::<Timer>() { if timer.is_stopped() { timer.start(); } } } } }
After:
#![allow(unused)] fn main() { fn system(mut timers: Query<&mut GodotNodeHandle, With<TimerMarker>>) { for mut handle in timers.iter_mut() { let mut timer = handle.get::<Timer>(); if timer.is_stopped() { timer.start(); } } } }
Pattern 2: Multiple Node Types
Before:
#![allow(unused)] fn main() { fn audio_system(mut all_nodes: Query<&mut GodotNodeHandle>) { for mut handle in all_nodes.iter_mut() { if let Some(mut player) = handle.try_get::<AudioStreamPlayer>() { player.set_volume_db(-10.0); } else if let Some(mut player_2d) = handle.try_get::<AudioStreamPlayer2D>() { player_2d.set_volume_db(-10.0); } else if let Some(mut player_3d) = handle.try_get::<AudioStreamPlayer3D>() { player_3d.set_volume_db(-10.0); } } } }
After:
#![allow(unused)] fn main() { fn audio_system( mut players_1d: Query<&mut GodotNodeHandle, With<AudioStreamPlayerMarker>>, mut players_2d: Query<&mut GodotNodeHandle, With<AudioStreamPlayer2DMarker>>, mut players_3d: Query<&mut GodotNodeHandle, With<AudioStreamPlayer3DMarker>>, ) { // Process each type separately - much more efficient! for mut handle in players_1d.iter_mut() { let mut player = handle.get::<AudioStreamPlayer>(); player.set_volume_db(-10.0); } for mut handle in players_2d.iter_mut() { let mut player = handle.get::<AudioStreamPlayer2D>(); player.set_volume_db(-10.0); } for mut handle in players_3d.iter_mut() { let mut player = handle.get::<AudioStreamPlayer3D>(); player.set_volume_db(-10.0); } } }
Pattern 3: Complex Conditions
Before:
#![allow(unused)] fn main() { fn physics_sprites(mut all_nodes: Query<&mut GodotNodeHandle>) { for mut handle in all_nodes.iter_mut() { if let Some(sprite) = handle.try_get::<Sprite2D>() { if let Some(body) = handle.try_get::<RigidBody2D>() { // Entity has both Sprite2D and RigidBody2D handle_physics_sprite(sprite, body); } } } } }
After:
#![allow(unused)] fn main() { fn physics_sprites( mut entities: Query<&mut GodotNodeHandle, (With<Sprite2DMarker>, With<RigidBody2DMarker>)> ) { for mut handle in entities.iter_mut() { // ECS guarantees both components exist let sprite = handle.get::<Sprite2D>(); let body = handle.get::<RigidBody2D>(); handle_physics_sprite(sprite, body); } } }
Available Marker Components
All marker components are available in the prelude:
#![allow(unused)] fn main() { use godot_bevy::prelude::*; // Examples of available markers: // Sprite2DMarker, CharacterBody2DMarker, Area2DMarker, // AudioStreamPlayerMarker, LabelMarker, ButtonMarker, // Camera2DMarker, RigidBody2DMarker, etc. }
See the complete list of markers in the querying documentation.
Performance Impact
Marker-based queries provide several performance advantages:
- Reduced iteration: Only process entities that match your node type, rather than checking every entity in the scene
- Eliminated runtime type checking: Skip
try_get()
calls since the ECS guarantees type matches - Better cache locality: Process similar entities together rather than jumping between different node types
- ECS optimization: Bevy can better optimize queries when it knows the component filters upfront
The actual performance improvement will depend on your scene size and how many entities match your queries, but the benefits are most noticeable in systems that run frequently (like every frame) and in larger scenes.
When NOT to Migrate
You might want to keep the old approach if:
- Rare usage: The system runs infrequently and performance isn't critical
- Dynamic typing: You genuinely need to handle unknown node types at runtime
- Gradual migration: You're updating a large codebase incrementally
The old try_get()
patterns will continue to work indefinitely.
Troubleshooting
"Entity doesn't have expected component"
If you get panics when using .get()
instead of .try_get()
, it usually means:
- Wrong marker: Make sure you're using the right marker for your query
- Node freed: The Godot node was freed but the entity still exists
- Timing issue: The node was removed between query execution and access
Solution: Use marker-based queries to ensure type safety, or fall back to .try_get()
if needed.
"Query doesn't match any entities"
If your marker-based query returns no entities:
- Check node types: Verify your scene has the expected node types
- Check marker names: Ensure you're using the correct marker component
- Check timing: Make sure the scene tree has been processed
Solution: Use Query<&GodotNodeHandle, With<NodeMarker>>
to see all entities, then check what markers they have.
Summary
The node type markers feature in v0.7.0 provides a significant upgrade to querying performance and type safety. While migration is optional, it's highly recommended for any systems that process specific Godot node types frequently.
The migration path is straightforward:
- Replace broad
Query<&mut GodotNodeHandle>
with specific marker queries - Replace
try_get()
calls withget()
when using markers - Handle multiple node types with separate queries rather than runtime checks
This results in cleaner, faster, and safer code while maintaining the flexibility of the ECS architecture.
BevyBundle Autosync Simplification
What Changed
In v0.7.0, the autosync
parameter has been removed from #[derive(BevyBundle)]
. All BevyBundle derives now automatically register their bundles and apply them during scene tree processing.
Migration Path
This change requires minimal code changes but may affect your app architecture if you were manually managing bundle systems.
Before (v0.6.x)
#![allow(unused)] fn main() { // Manual autosync control #[derive(GodotClass, BevyBundle)] #[class(base=Node2D)] #[bevy_bundle((Health), (Velocity), autosync=true)] // ← autosync parameter pub struct Player { base: Base<Node2D>, } // Alternative: manually registering the system #[derive(GodotClass, BevyBundle)] #[class(base=Node2D)] #[bevy_bundle((Health), (Velocity))] // ← autosync=false (default) pub struct Enemy { base: Base<Node2D>, } #[bevy_app] fn build_app(app: &mut App) { // Had to manually add the sync system app.add_systems(Update, EnemyAutoSyncPlugin); } }
After (v0.7.0)
#![allow(unused)] fn main() { // Automatic registration - much simpler! #[derive(GodotClass, BevyBundle)] #[class(base=Node2D)] #[bevy_bundle((Health), (Velocity))] // ← No autosync parameter needed pub struct Player { base: Base<Node2D>, } #[derive(GodotClass, BevyBundle)] #[class(base=Node2D)] #[bevy_bundle((Health), (Velocity))] // ← Always automatic now pub struct Enemy { base: Base<Node2D>, } #[bevy_app] fn build_app(app: &mut App) { // No manual system registration needed! // Bundles are automatically applied during scene tree processing } }
Breaking Changes
- Remove
autosync=true
: This parameter no longer exists and will cause compilation errors - Remove manual sync systems: If you were manually adding bundle sync systems, remove them
- Timing change: Bundle components are now available in
Startup
systems (was previously only available inUpdate
)
Benefits of This Change
- Simplified API: No need to remember to set
autosync=true
- Better timing: Bundle components are available earlier in the frame lifecycle
- Unified behavior: Both initial scene loading and dynamic node addition work the same way
- No missed registrations: Impossible to forget to register a bundle system
Migration Checklist
-
Remove
autosync=true
andautosync=false
from all#[bevy_bundle()]
attributes - Remove any manually registered bundle sync systems from your app
-
Test that bundle components are available in
Startup
systems (they now are!) - Update any documentation or comments that reference the old autosync behavior
Example Migration
Before (v0.6.x):
#![allow(unused)] fn main() { #[derive(GodotClass, BevyBundle)] #[class(base=CharacterBody2D)] #[bevy_bundle((Speed: speed), (Health: max_health), autosync=true)] pub struct Player { base: Base<CharacterBody2D>, #[export] speed: f32, #[export] max_health: f32, } #[bevy_app] fn build_app(app: &mut App) { app.add_systems(Startup, setup_game) .add_systems(Update, player_movement); } fn setup_game(players: Query<&Health>) { // This would be empty in v0.6.x because bundles // weren't applied until the first Update println!("Found {} players", players.iter().count()); } }
After (v0.7.0):
#![allow(unused)] fn main() { #[derive(GodotClass, BevyBundle)] #[class(base=CharacterBody2D)] #[bevy_bundle((Speed: speed), (Health: max_health))] // ← Removed autosync pub struct Player { base: Base<CharacterBody2D>, #[export] speed: f32, #[export] max_health: f32, } #[bevy_app] fn build_app(app: &mut App) { app.add_systems(Startup, setup_game) .add_systems(Update, player_movement); } fn setup_game(players: Query<&Health>) { // This now works in Startup! Bundle components are available immediately println!("Found {} players", players.iter().count()); } }
This change makes BevyBundle usage more intuitive and eliminates a common source of timing-related bugs.
Transform Sync Modes (Breaking Change)
What Changed
In v0.7.0, transform synchronization behavior has changed significantly:
- New
TransformSyncMode
system: Transform syncing is now configurable viaGodotTransformConfig
- Default changed from two-way to one-way: Previously, transforms were synced bidirectionally by default. Now the default is one-way (ECS → Godot only)
- Explicit configuration required: You must now explicitly choose your sync mode
Migration Path
If your v0.6.x code relied on the implicit two-way transform sync, you need to explicitly enable it in v0.7.0.
Before (v0.6.x - implicit two-way sync)
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { // Transform syncing was always bidirectional app.add_systems(Update, movement_system); } fn movement_system( mut query: Query<&mut Transform2D>, ) { // Could read Godot transform changes automatically } }
After (v0.7.0 - explicit configuration)
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { // Restore v0.6.x behavior with explicit two-way sync app.insert_resource(GodotTransformConfig::two_way()); app.add_systems(Update, movement_system); } }
Available Sync Modes
-
TransformSyncMode::OneWay
(NEW DEFAULT)- ECS transform changes update Godot nodes
- Godot transform changes are NOT reflected in ECS
- Best for pure ECS architectures
-
TransformSyncMode::TwoWay
(v0.6.x default behavior)- Full bidirectional sync between ECS and Godot
- Required for Godot animations affecting ECS
- Higher performance overhead
-
TransformSyncMode::Disabled
(NEW)- No transform components created
- Zero sync overhead
- Perfect for physics-only games
Common Migration Scenarios
Scenario 1: Using Godot's AnimationPlayer
If you use Godot's AnimationPlayer to move entities:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { // Must use two-way sync for animations app.insert_resource(GodotTransformConfig::two_way()); } }
Scenario 2: Pure ECS Movement
If all movement is handled by Bevy systems:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { // One-way is the default, but you can be explicit app.insert_resource(GodotTransformConfig::one_way()); } }
Scenario 3: Physics-Only Game
If using CharacterBody2D or RigidBody2D exclusively:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { // Disable transform syncing entirely app.insert_resource(GodotTransformConfig::disabled()); } }
Breaking Changes Checklist
- Default behavior changed: If you relied on reading Godot transform changes in ECS, you must enable two-way sync
- Performance may improve: One-way sync has less overhead than the old default
- New optimization opportunity: Consider disabling transforms for physics entities
Troubleshooting
"Transform changes in Godot not visible in ECS"
This is the most common issue when migrating. The solution is to enable two-way sync:
#![allow(unused)] fn main() { app.insert_resource(GodotTransformConfig::two_way()); }
"Transform components missing"
If you disabled sync mode but still need transforms:
#![allow(unused)] fn main() { // Either switch to one-way or two-way mode app.insert_resource(GodotTransformConfig::one_way()); }
Performance Comparison
v0.6.x (implicit two-way):
- Read systems: Always running (PreUpdate)
- Write systems: Always running (Last)
- Overhead: O(all entities) every frame
v0.7.0 one-way (new default):
- Read systems: Not running
- Write systems: Running (Last)
- Overhead: O(changed entities) only
v0.7.0 disabled:
- No systems running
- Zero overhead
Summary
The transform sync system in v0.7.0 gives you explicit control over performance and behavior. While this is a breaking change for projects that relied on implicit two-way sync, it provides better defaults and more optimization opportunities. Simply add app.insert_resource(GodotTransformConfig::two_way())
to restore v0.6.x behavior.