Introduction
Welcome to godot-bevy, a Rust library that brings Bevy's powerful Entity Component System (ECS) to the versatile Godot Game Engine.
What is godot-bevy?
godot-bevy enables you to write high-performance game logic using Bevy's ergonomic ECS within your Godot projects. This is not a Godot plugin for Bevy users, but rather a library for Godot developers who want to leverage Rust and ECS for their game logic while keeping Godot's excellent editor and engine features.
Why godot-bevy?
The Best of Both Worlds
- Godot's Strengths: Visual scene editor, node system, asset pipeline, cross-platform deployment
- Bevy's Strengths: High-performance ECS, Rust's safety and speed, data-oriented architecture
- godot-bevy: Seamless integration between the two, letting you use each tool where it shines
Key Benefits
- Performance: Bevy's ECS provides cache-friendly data layouts and parallel system execution
- Safety: Rust's type system catches bugs at compile time
- Modularity: ECS encourages clean, decoupled code architecture
- Flexibility: Mix and match Godot nodes with ECS components as needed
Core Features
- Deep ECS Integration: True Bevy systems controlling Godot nodes
- Transform Synchronization: Automatic syncing between Bevy and Godot transforms
- Signal Handling: React to Godot signals in your ECS systems
- Collision Events: Handle physics collisions through the ECS
- Resource Management: Load Godot assets through Bevy's asset system
- Smart Scheduling: Separate physics and rendering update rates
Who Should Use godot-bevy?
This library is ideal for:
- Godot developers wanting to use Rust for game logic
- Teams looking for better code organization through ECS
- Projects requiring high-performance game systems
- Developers familiar with data-oriented design patterns
Getting Help
- Discord: Join our community Discord
- Documentation: Check the API docs
- Examples: Browse the example projects
- Issues: Report bugs on GitHub
Ready to Get Started?
Head to the Installation chapter to begin your godot-bevy journey!
Installation
This guide will walk you through setting up godot-bevy in a Godot project.
Prerequisites
Before you begin, ensure you have:
- Rust 1.87.0 or later - Install Rust
- Godot 4.3 - Download Godot
- Basic familiarity with both Rust and Godot
Create a New Project
1. Set Up Godot Project
First, create a new Godot project through the Godot editor:
- Open Godot and click "New Project"
- Choose a project name and location
- Select "Compatibility" renderer for maximum platform support
- Click "Create & Edit"
2. Set Up Rust Project
In your Godot project directory, create a new Rust library:
cd /path/to/your/godot/project
cargo init --lib rust
cd rust
3. Configure Cargo.toml
Edit rust/Cargo.toml
:
[package]
name = "your_game_name"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
godot-bevy = "0.7.0"
bevy = { version = "0.16", default-features = false }
godot = "0.3"
Configure Godot Integration
1. Create Extension File
Create rust.gdextension
in your Godot project root:
[configuration]
entry_symbol = "gdext_rust_init"
compatibility_minimum = 4.3
reloadable = true
[libraries]
macos.debug = "res://rust/target/debug/libyour_game_name.dylib"
macos.release = "res://rust/target/release/libyour_game_name.dylib"
windows.debug.x86_32 = "res://rust/target/debug/your_game_name.dll"
windows.release.x86_32 = "res://rust/target/release/your_game_name.dll"
windows.debug.x86_64 = "res://rust/target/debug/your_game_name.dll"
windows.release.x86_64 = "res://rust/target/release/your_game_name.dll"
linux.debug.x86_64 = "res://rust/target/debug/libyour_game_name.so"
linux.release.x86_64 = "res://rust/target/release/libyour_game_name.so"
linux.debug.arm64 = "res://rust/target/debug/libyour_game_name.so"
linux.release.arm64 = "res://rust/target/release/libyour_game_name.so"
linux.debug.rv64 = "res://rust/target/debug/libyour_game_name.so"
linux.release.rv64 = "res://rust/target/release/libyour_game_name.so"
Replace your_game_name
with your actual crate name from Cargo.toml
.
2. Create BevyApp Autoload
- In Godot, create a new scene
- Add a
BevyApp
node as the root - Save it as
bevy_app_singleton.tscn
- Go to Project → Project Settings → Autoload
- Add the scene with name "BevyAppSingleton"
Write Your First Code
Edit rust/src/lib.rs
:
#![allow(unused)] fn main() { use bevy::prelude::*; use godot_bevy::prelude::*; #[bevy_app] fn build_app(app: &mut App) { app.add_systems(Startup, hello_world); } fn hello_world() { godot::prelude::godot_print!("Hello from godot-bevy!"); } }
Build and Run
1. Build the Rust Library
cd rust
cargo build
2. Run in Godot
- Return to the Godot editor
- Press F5 or click the play button
- You should see "Hello from godot-bevy!" in the output console
Troubleshooting
Common Issues
"Can't open dynamic library"
- Ensure the paths in
rust.gdextension
match your library output - Check that you've built the Rust project
- On macOS, you may need to allow the library in System Preferences
"BevyApp not found"
- Make sure godot-bevy is properly added to your dependencies
- Rebuild the Rust project
- Restart the Godot editor
Build errors
- Verify your Rust version:
rustc --version
- Ensure all dependencies are compatible
- Check for typos in the crate name
Next Steps
Congratulations! You've successfully set up godot-bevy. Continue to Your First Project to build something more substantial.
Basic Concepts
Before diving into godot-bevy development, it's important to understand the key concepts that make this integration work.
The Hybrid Architecture
godot-bevy creates a bridge between two powerful systems:
Godot Side
- Scene tree with nodes
- Visual editor for level design
- Asset pipeline for resources
- Rendering engine
- Physics engine
Bevy Side
- Entity Component System (ECS)
- Systems for game logic
- Components for data
- Resources for shared state
- Schedules for execution order
The Bridge
godot-bevy seamlessly connects these worlds:
- Godot nodes ↔ ECS entities
- Node properties ↔ Components
- Signals → Events
- Resources ↔ Assets
Core Components
Entities
In godot-bevy, Godot nodes are automatically registered as ECS entities:
#![allow(unused)] fn main() { // When a node is added to the scene tree, // it becomes queryable as an entity fn find_player( query: Query<&Name, With<GodotNodeHandle>>, ) { for name in query.iter() { if name.as_str() == "Player" { // Found the player node! } } } }
Components
Components store data on entities. godot-bevy provides several built-in components:
GodotNodeHandle
- Reference to the Godot nodeTransform2D/3D
- Position, rotation, scaleName
- Node nameCollisions
- Collision eventsGroups
- Godot node groups
Systems
Systems contain your game logic and run on a schedule:
#![allow(unused)] fn main() { fn movement_system( time: Res<Time>, mut query: Query<&mut Transform2D, With<Player>>, ) { for mut transform in query.iter_mut() { transform.as_bevy_mut().translation.x += 100.0 * time.delta_seconds(); } } }
The #[bevy_app] Macro
The entry point for godot-bevy is the #[bevy_app]
macro:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { // Configure your Bevy app here app.add_systems(Update, my_system); } }
This macro:
- Creates the GDExtension entry point
- Sets up the Bevy app
- Integrates with Godot's lifecycle
- Handles all the bridging magic
Data Flow
Understanding how data flows between Godot and Bevy is crucial:
Godot → Bevy
- Node added to scene tree
- Entity created with components
- Signals converted to events
- Input forwarded to systems
Bevy → Godot
- Transform components sync to nodes
- Commands can modify scene tree
- Resources can be loaded
- Audio can be played
Key Principles
1. Godot for Content, Bevy for Logic
- Design levels in Godot's editor
- Write game logic in Bevy systems
- Let each tool do what it does best
2. Components as the Source of Truth
- Store game state in components
- Use Godot nodes for presentation
- Sync only what's necessary
3. Systems for Everything
- Movement? System.
- Combat? System.
- UI updates? System.
- This promotes modularity and reusability
4. Leverage Both Ecosystems
- Use Godot's assets and tools
- Use Bevy's plugins and crates
- Don't reinvent what already exists
Common Patterns
Finding Nodes by Name
#![allow(unused)] fn main() { fn setup( mut query: Query<(&Name, Entity)>, ) { let player = query.iter() .find_entity_by_name("Player") .expect("Player node must exist"); } }
Reacting to Signals
#![allow(unused)] fn main() { fn handle_button_press( mut events: EventReader<GodotSignal>, ) { for signal in events.read() { if signal.name == "pressed" { // Button was pressed! } } } }
Spawning Godot Scenes
#![allow(unused)] fn main() { fn spawn_enemy( mut commands: Commands, enemy_scene: Res<EnemyScene>, ) { commands.spawn(( GodotScene::from_handle(enemy_scene.0.clone()), Enemy { health: 100 }, Transform2D::default(), )); } }
Next Steps
Now that you understand the basic concepts:
- Try the examples
- Read about specific systems in detail
- Start building your game!
Remember: godot-bevy is about using the right tool for the right job. Embrace both Godot and Bevy's strengths!
Examples
Many additional godot-bevy examples are available in the examples directory. Examples are set up as executable binaries. An example can then be executed using the following cargo command line in the root of the godot-bevy repository:
cargo run --bin platformer_2d
The following additional examples are currently available if you want to check them out:
Example | Description |
---|---|
Dodge the Creeps | Ported example from Godot's tutorial on making a 2D game. |
Input Event Demo | Showcases the different ways in which you can get input either via Bevy's input API or using Godot's. |
Platformer 2D | A more complete example showing how to tag Godot nodes for an editor heavy. |
Simple Node2D Movement | A minimal example with basic movement. |
Timing Test | Internal test to measure frames. |
Scene Tree Initialization and Timing
The godot-bevy library automatically parses the Godot scene tree and creates corresponding Bevy entities before your game logic runs. This means you can safely query for scene entities in your Startup
systems:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { app.add_systems(Startup, find_player); } fn find_player(query: Query<&PlayerBundle>) { // Your player entity will be here! ✨ for player in &query { println!("Found the player!"); } } }
How It Works
The scene tree initialization happens in the PreStartup
schedule, ensuring entities are ready before any Startup
systems run. This process has two parallel systems:
initialize_scene_tree
- Traverses the entire Godot scene tree and creates Bevy entities with components likeGodotNodeHandle
,Name
, transforms, and moreconnect_scene_tree
- Sets up event listeners for runtime scene changes (nodes being added, removed, or renamed)
Both systems run in parallel during PreStartup
, and both complete before your Startup
systems run. This means you can safely query for Godot scene entities in Startup
!
Runtime Scene Updates
After the initial parse, the library continues to listen for scene tree changes during runtime. This is handled by two systems that run in the First
schedule:
write_scene_tree_events
- Receives events from Godot (via an mpsc channel) and writes them to Bevy's event systemread_scene_tree_events
- Processes those events to create/update/remove entities
This separation allows other systems to also react to SceneTreeEvent
s if needed.
What Components Are Available?
When the scene tree is parsed, each Godot node becomes a Bevy entity with these components:
GodotNodeHandle
- Reference to the Godot nodeName
- The node's name from GodotTransform2D
orTransform3D
- For Node2D and Node3D types respectivelyGroups
- The node's group membershipsCollisions
- If the node has collision signals- Node type markers - Components like
ButtonMarker
,Sprite2DMarker
, etc. - Custom bundles - Components from
#[derive(BevyBundle)]
are automatically added
BevyBundle Component Timing
If you've defined custom Godot node types with #[derive(BevyBundle)]
, their components are added immediately during scene tree processing. This happens:
- During
PreStartup
for nodes that exist when the scene is first loaded - During
First
for nodes added dynamically at runtime
This means BevyBundle components are available in Startup
systems for initial scene nodes, and immediately available for dynamically added nodes.
#![allow(unused)] fn main() { #[derive(GodotClass, BevyBundle)] #[class(base=Node2D)] #[bevy_bundle((Health), (Velocity))] pub struct Player { base: Base<Node2D>, } // This will work in Startup - the Health and Velocity components // are automatically added during PreStartup for existing nodes fn setup_player(mut query: Query<(Entity, &Health, &Velocity)>) { for (entity, health, velocity) in &mut query { // Player components are guaranteed to be here! } } }
Best Practices
- Use
Startup
for initialization - Scene entities are guaranteed to be ready - Use
Update
for gameplay logic - This is where most of your game code should live - Custom
PreStartup
systems - If you add systems toPreStartup
, be aware they run before scene parsing unless you explicitly order them with.after()
Understanding the Event Flow
Here's what happens when a node is added to the scene tree during runtime:
- Godot emits a
node_added
signal - The
SceneTreeWatcher
(on the Godot side) receives the signal - It sends a
SceneTreeEvent
through an mpsc channel write_scene_tree_events
(inFirst
schedule) reads from the channel and writes to Bevy's event systemread_scene_tree_events
(also inFirst
schedule) processes the event and creates/updates entities
This architecture allows for flexible event handling while maintaining a clean separation between Godot and Bevy.
Querying with Node Type Markers
When godot-bevy discovers nodes in your Godot scene tree, it automatically creates ECS entities with GodotNodeHandle
components to represent them. To enable efficient, type-safe querying, the library also adds marker components that indicate what type of Godot node each entity represents.
Overview
Every entity that represents a Godot node gets marker components automatically:
#![allow(unused)] fn main() { use godot_bevy::prelude::*; // Query all Sprite2D entities - no runtime type checking needed! fn update_sprites(mut sprites: Query<&mut GodotNodeHandle, With<Sprite2DMarker>>) { for mut handle in sprites.iter_mut() { // We know this is a Sprite2D, so .get() is safe let sprite = handle.get::<Sprite2D>(); // Work with the sprite... } } }
Available Marker Components
Base Node Types
NodeMarker
- All nodes (every entity gets this)Node2DMarker
- All 2D nodesNode3DMarker
- All 3D nodesControlMarker
- UI control nodesCanvasItemMarker
- Canvas items
Visual Nodes
Sprite2DMarker
/Sprite3DMarker
AnimatedSprite2DMarker
/AnimatedSprite3DMarker
MeshInstance2DMarker
/MeshInstance3DMarker
Physics Bodies
RigidBody2DMarker
/RigidBody3DMarker
CharacterBody2DMarker
/CharacterBody3DMarker
StaticBody2DMarker
/StaticBody3DMarker
Areas and Collision
Area2DMarker
/Area3DMarker
CollisionShape2DMarker
/CollisionShape3DMarker
CollisionPolygon2DMarker
/CollisionPolygon3DMarker
Audio Players
AudioStreamPlayerMarker
AudioStreamPlayer2DMarker
AudioStreamPlayer3DMarker
UI Elements
LabelMarker
ButtonMarker
LineEditMarker
TextEditMarker
PanelMarker
Cameras and Lighting
Camera2DMarker
/Camera3DMarker
DirectionalLight3DMarker
SpotLight3DMarker
Animation and Timing
AnimationPlayerMarker
AnimationTreeMarker
TimerMarker
Path Nodes
Path2DMarker
/Path3DMarker
PathFollow2DMarker
/PathFollow3DMarker
Hierarchical Markers
Node type markers follow Godot's inheritance hierarchy. For example, a CharacterBody2D
entity will have:
NodeMarker
(all nodes inherit from Node)Node2DMarker
(CharacterBody2D inherits from Node2D)CharacterBody2DMarker
(the specific type)
This lets you query at any level of specificity:
#![allow(unused)] fn main() { // Query ALL nodes fn system1(nodes: Query<&GodotNodeHandle, With<NodeMarker>>) { /* ... */ } // Query all 2D nodes fn system2(nodes_2d: Query<&GodotNodeHandle, With<Node2DMarker>>) { /* ... */ } // Query only CharacterBody2D nodes fn system3(characters: Query<&GodotNodeHandle, With<CharacterBody2DMarker>>) { /* ... */ } }
Advanced Query Patterns
Combining Markers
#![allow(unused)] fn main() { // Entities that have BOTH a Sprite2D AND a RigidBody2D fn physics_sprites( query: Query<&mut GodotNodeHandle, (With<Sprite2DMarker>, With<RigidBody2DMarker>)> ) { for mut handle in query.iter_mut() { let sprite = handle.get::<Sprite2D>(); let body = handle.get::<RigidBody2D>(); // Work with both components... } } }
Excluding Node Types
#![allow(unused)] fn main() { // All sprites EXCEPT character bodies (e.g., environmental sprites) fn environment_sprites( query: Query<&mut GodotNodeHandle, (With<Sprite2DMarker>, Without<CharacterBody2DMarker>)> ) { for mut handle in query.iter_mut() { // These are sprites but not character bodies let sprite = handle.get::<Sprite2D>(); // Work with environmental sprites... } } }
Multiple Specific Types
#![allow(unused)] fn main() { // Handle different audio player types efficiently fn update_audio_system( players_1d: Query<&mut GodotNodeHandle, With<AudioStreamPlayerMarker>>, players_2d: Query<&mut GodotNodeHandle, With<AudioStreamPlayer2DMarker>>, players_3d: Query<&mut GodotNodeHandle, With<AudioStreamPlayer3DMarker>>, ) { // Process each type separately - no runtime type checking! for mut handle in players_1d.iter_mut() { let player = handle.get::<AudioStreamPlayer>(); // Handle 1D audio... } for mut handle in players_2d.iter_mut() { let player = handle.get::<AudioStreamPlayer2D>(); // Handle 2D spatial audio... } for mut handle in players_3d.iter_mut() { let player = handle.get::<AudioStreamPlayer3D>(); // Handle 3D spatial audio... } } }
Performance Benefits
Node type markers provide significant performance improvements:
- Reduced Iteration: Only process entities you care about
- No Runtime Type Checking: Skip
try_get()
calls - Better ECS Optimization: Bevy can optimize queries with markers
- Cache Efficiency: Process similar entities together
Automatic Application
You don't need to add marker components manually. The library automatically:
- Detects the Godot node type during scene tree traversal
- Adds the appropriate marker component(s) to the entity
- Includes all parent type markers in the inheritance hierarchy
- Ensures every entity gets the base
NodeMarker
This happens transparently when nodes are discovered in your scene tree, making the markers immediately available for your systems to use.
Best Practices
- Use specific markers when you know the exact node type:
With<Sprite2DMarker>
- Use hierarchy markers for broader categories:
With<Node2DMarker>
for all 2D nodes - Combine markers to find entities with multiple components
- Prefer
.get()
over.try_get()
when using markers - it's both faster and safer
For migration information from pre-0.7.0 versions, see the Migration Guide.
Custom Node Markers
This chapter explains how to work with custom Godot nodes in godot-bevy and the important distinction between automatic markers for built-in Godot types versus custom nodes.
Automatic Markers vs Custom Nodes
Built-in Godot Types
godot-bevy automatically creates marker components for all built-in Godot node types:
#![allow(unused)] fn main() { // These markers are created automatically: // Sprite2DMarker, CharacterBody2DMarker, Area2DMarker, etc. fn update_sprites(sprites: Query<&GodotNodeHandle, With<Sprite2DMarker>>) { // Works automatically for any Sprite2D in your scene } }
Custom Godot Nodes
Custom nodes defined in Rust or GDScript do NOT receive automatic markers for their custom type, though they DO inherit markers from their base class (e.g., Node2DMarker
if they extend Node2D). This is by design - custom nodes should use the BevyBundle
macro for explicit component control.
#![allow(unused)] fn main() { // ❌ PlayerMarker is NOT automatically created fn update_players(players: Query<&GodotNodeHandle, With<PlayerMarker>>) { // PlayerMarker doesn't exist unless you create it } // ✅ But you CAN use the base class marker fn update_player_base(players: Query<&GodotNodeHandle, With<CharacterBody2DMarker>>) { // This works but includes ALL CharacterBody2D nodes, not just Players } // ✅ Use BevyBundle for custom components #[derive(GodotClass, BevyBundle)] #[class(base=CharacterBody2D)] #[bevy_bundle((Player), (Health), (Speed))] pub struct PlayerNode { base: Base<CharacterBody2D>, } }
Creating Markers for Custom Nodes
The recommended approach is to use meaningful components instead of generic markers:
#![allow(unused)] fn main() { #[derive(Component)] struct Player; #[derive(Component)] struct Health(f32); #[derive(Component)] struct Speed(f32); #[derive(GodotClass, BevyBundle)] #[class(base=CharacterBody2D)] #[bevy_bundle((Player), (Health: max_health), (Speed: speed))] pub struct PlayerNode { base: Base<CharacterBody2D>, #[export] max_health: f32, #[export] speed: f32, } // Now query using your custom components fn update_players( players: Query<(&Health, &Speed), With<Player>> ) { for (health, speed) in players.iter() { // Process player entities } } }
You can also leverage the automatic markers from the base class:
#![allow(unused)] fn main() { #[derive(Component)] struct Player; #[derive(GodotClass, BevyBundle)] #[class(base=CharacterBody2D)] #[bevy_bundle((Player))] pub struct PlayerNode { base: Base<CharacterBody2D>, } // Query using both the base class marker and your component fn update_player_bodies( players: Query<&GodotNodeHandle, (With<CharacterBody2DMarker>, With<Player>)> ) { for handle in players.iter() { let mut body = handle.get::<CharacterBody2D>(); body.move_and_slide(); } } }
You can also map properties from Godot Node exported vars to component values:
#![allow(unused)] fn main() { // ✅ Good: Meaningful components with property mapping #[derive(GodotClass, BevyBundle)] #[class(base=Node2D)] #[bevy_bundle((Enemy), (Health: max_health), (AttackDamage: damage))] pub struct Goblin { base: Base<Node2D>, #[export] max_health: f32, // This value initializes Health component #[export] damage: f32, // This value initializes AttackDamage component } }
Summary
- Built-in Godot types get automatic markers (e.g.,
Sprite2DMarker
) - Custom nodes do NOT get automatic markers for their type, but DO inherit base class markers
- Use
BevyBundle
to define components for custom nodes - Prefer semantic components over generic markers
- Combine base class markers with custom components for powerful queries
This design gives you full control over your ECS architecture while maintaining performance and clarity.
Transform System Overview
The transform system is one of the most important aspects of godot-bevy, handling position, rotation, and scale synchronization between Bevy ECS and Godot nodes.
Three Approaches to Movement
godot-bevy supports three distinct approaches for handling transforms and movement:
1. ECS Transform Components
Use Transform2D
/Transform3D
components with automatic syncing between ECS and Godot. This is the default approach. You update transforms in ECS, and we take care of syncing the transforms to the Godot side at the end of each frame.
#![allow(unused)] fn main() { use godot_bevy::prelude::*; fn move_entity(mut query: Query<&mut Transform2D>) { for mut transform in query.iter_mut() { transform.as_bevy_mut().translation.x += 1.0; } } }
2. Direct Godot Physics
Use GodotNodeHandle
to directly control Godot physics nodes. Perfect for physics-heavy games. This usually means you're calling Godot's move methods to have it handle physics for you.
#![allow(unused)] fn main() { fn move_character(mut query: Query<&mut GodotNodeHandle>) { for mut handle in query.iter_mut() { let mut body = handle.get::<CharacterBody2D>(); body.set_velocity(Vector2::new(100.0, 0.0)); body.move_and_slide(); } } }
3. Hybrid Approach
Allows for modifying transforms both from Godot's side and from ECS side. Useful during migration from a GDScript project to godot-bevy or when you're using Godot's physics methods but still want transforms to be updated for reading on the ECS side.
Default Behavior
By default, godot-bevy operates in one-way sync mode:
- ✅ Writing enabled: Changes to ECS transform components update Godot nodes
- ❌ Reading disabled: Changes to Godot nodes don't update ECS components
This is optimal for pure ECS applications where all movement logic lives in Bevy systems.
When to Use Each Approach
Use ECS Transforms When:
- Building a pure ECS game
- Movement logic is simple (no complex physics)
- You want clean separation between logic and presentation
- Performance of transform sync is acceptable
Use Direct Godot Physics When:
- Building platformers or physics-heavy games
- You need Godot's collision detection features
- Using CharacterBody2D/3D or RigidBody2D/3D
- You want zero transform sync overhead
Use Hybrid Approach When:
- Migrating an existing Godot project to ECS
- Some systems need ECS transforms, others need physics
- Gradually transitioning from GDScript to Rust
Key Concepts
Transform Components
godot-bevy provides two transform components that maintain both Bevy and Godot representations:
Transform2D
- For 2D gamesTransform3D
- For 3D games
These components automatically keep Bevy and Godot transforms in sync based on your configuration.
Sync Modes
The transform system supports three synchronization modes:
- Disabled - No syncing, no transform components created
- OneWay - ECS → Godot only (default)
- TwoWay - ECS ↔ Godot bidirectional sync
Performance Considerations
Each approach has different performance characteristics:
- ECS Transforms: Small overhead from syncing
- Direct Physics: Zero sync overhead
- Hybrid: Depends on usage pattern
Next Steps
- Learn about Sync Modes in detail
Transform Sync Modes
godot-bevy provides three transform synchronization modes to fit different use cases. Understanding these modes is crucial for optimal performance and correct behavior.
Available Modes
TransformSyncMode::Disabled
No transform syncing occurs and no transform components are created.
Characteristics:
- ✅ Zero performance overhead
- ✅ No memory usage for transform components
- ✅ Best for physics-heavy games
- ❌ Cannot use Transform2D/Transform3D components
Use when:
- Building platformers with CharacterBody2D
- Using RigidBody physics exclusively
- You need maximum performance
TransformSyncMode::OneWay
(Default)
Synchronizes transforms from ECS to Godot only.
Characteristics:
- ✅ ECS components control Godot node positions
- ✅ Good performance (minimal overhead)
- ✅ Clean ECS architecture
- ❌ Godot changes don't reflect in ECS
Use when:
- Building pure ECS games
- All movement logic is in Bevy systems
- You don't need to read Godot transforms
TransformSyncMode::TwoWay
Full bidirectional synchronization between ECS and Godot.
Characteristics:
- ✅ Changes in either system are reflected
- ✅ Works with Godot animations
- ✅ Supports hybrid architectures
- ❌ Higher performance overhead
Use when:
- Migrating from GDScript to ECS
- Using Godot's AnimationPlayer
- Mixing ECS and GDScript logic
Configuration
Configure the sync mode in your #[bevy_app]
function:
Disabled Mode
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { app.insert_resource(GodotTransformConfig::disabled()); // Use direct physics instead app.add_systems(Update, physics_movement); } }
One-Way Mode (Default)
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { // One-way is the default, no configuration needed // Or explicitly: app.insert_resource(GodotTransformConfig::one_way()); app.add_systems(Update, ecs_movement); } }
Two-Way Mode
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { app.insert_resource(GodotTransformConfig::two_way()); app.add_systems(Update, hybrid_movement); } }
Performance Impact
Disabled Mode Performance
Transform Components: Not created
Sync Systems: Not running
Memory Usage: None
CPU Usage: None
One-Way Mode Performance
Transform Components: Created
Write Systems: Running (Last schedule)
Read Systems: Not running
Memory Usage: ~48 bytes per entity
CPU Usage: O(changed entities)
Two-Way Mode Performance
Transform Components: Created
Write Systems: Running (Last schedule)
Read Systems: Running (PreUpdate schedule)
Memory Usage: ~48 bytes per entity
CPU Usage: O(all entities with transforms)
Implementation Details
System Execution Order
Write Systems (ECS → Godot)
- Schedule:
Last
- Only processes changed transforms
- Runs for both OneWay and TwoWay modes
Read Systems (Godot → ECS)
- Schedule:
PreUpdate
- Checks all transforms for external changes
- Only runs in TwoWay mode
Change Detection
The system uses Bevy's change detection to optimize writes:
#![allow(unused)] fn main() { fn post_update_transforms( mut query: Query< (&Transform2D, &mut GodotNodeHandle), Or<(Added<Transform2D>, Changed<Transform2D>)> > ) { // Only processes entities with new or changed transforms } }
Common Patterns
Switching Modes at Runtime
While not common, you can change modes during runtime:
#![allow(unused)] fn main() { fn switch_to_physics_mode( mut commands: Commands, ) { commands.insert_resource(GodotTransformConfig::disabled()); } }
Note: Existing transform components remain but stop syncing.
Checking Current Mode
#![allow(unused)] fn main() { fn check_sync_mode( config: Res<GodotTransformConfig>, ) { match config.sync_mode { TransformSyncMode::Disabled => { println!("Using direct physics"); } TransformSyncMode::OneWay => { println!("ECS drives transforms"); } TransformSyncMode::TwoWay => { println!("Bidirectional sync active"); } } } }
Best Practices
- Choose mode early - Switching modes mid-project can be complex
- Default to OneWay - Unless you specifically need other modes
- Benchmark your game - Measure actual performance impact
- Document your choice - Help team members understand the architecture
Troubleshooting
"Transform changes not visible"
- Check you're not in Disabled mode
- Ensure transform components exist on entities
- Verify systems are running in correct schedules
"Performance degradation with many entities"
- Consider switching from TwoWay to OneWay
- Use Disabled mode for physics entities
- Profile to identify bottlenecks
"Godot animations not affecting ECS"
- Enable TwoWay mode for animated entities
- Ensure transforms aren't being overwritten by ECS systems
- Check system execution order
Bevy vs Godot Input
godot-bevy offers two distinct approaches to handling input: Bevy's built-in input system and godot-bevy's bridged Godot input system. Understanding when to use each is crucial for building the right game experience.
Two Input Systems
Bevy's Built-in Input
Use Bevy's standard input resources for simple, direct input handling:
#![allow(unused)] fn main() { fn movement_system( keys: Res<ButtonInput<KeyCode>>, mut query: Query<&mut Transform2D, With<Player>>, ) { for mut transform in query.iter_mut() { if keys.pressed(KeyCode::ArrowLeft) { transform.as_bevy_mut().translation.x -= 200.0; } if keys.pressed(KeyCode::ArrowRight) { transform.as_bevy_mut().translation.x += 200.0; } } } }
godot-bevy's Bridged Input
Use godot-bevy's event-based system for more advanced input handling:
#![allow(unused)] fn main() { fn movement_system( mut events: EventReader<ActionInput>, mut query: Query<&mut Transform2D, With<Player>>, ) { for event in events.read() { if event.pressed { match event.action.as_str() { "move_left" => { // Handle left movement } "move_right" => { // Handle right movement } _ => {} } } } } }
When to Use Each System
🚀 Use Bevy Input For:
Simple desktop games and rapid prototyping
✅ Advantages:
- Zero setup - works immediately
- State-based queries - easy "is key held?" checks
- Rich API -
just_pressed()
,pressed()
,just_released()
- Direct and fast - no event processing overhead
- Familiar - standard Bevy patterns
❌ Limitations:
- Desktop-focused - limited mobile/console support
- Hardcoded keys - players can't remap controls
- No Godot integration - can't use input maps
Example use cases:
- Game jams and prototypes
- Desktop-only games
- Simple control schemes
- Internal tools
🎮 Use godot-bevy Input For:
Production games and cross-platform releases
✅ Advantages:
- Cross-platform - desktop, mobile, console support
- User remappable - integrates with Godot's input maps
- Touch support - native mobile input handling
- Action-based - semantic controls ("jump" vs "spacebar")
- Flexible - supports complex input schemes
❌ Trade-offs:
- Event-based - requires more complex state tracking
- Setup required - need to define input maps in Godot
- More complex - steeper learning curve
Example use cases:
- Commercial releases
- Mobile games
- Console ports
- Games with complex controls
Input Event Processing
godot-bevy processes Godot's dual input system intelligently to prevent duplicate events:
- Normal Input Events: Generate
ActionInput
events for mapped keys/buttons - Unhandled Input Events: Generate raw
KeyboardInput
,MouseButtonInput
, etc. for unmapped inputs
This ensures:
- ✅ No duplicate events - each physical input generates exactly one event
- ✅ Proper input flow - mapped inputs become actions, unmapped inputs become raw events
- ✅ Clean event streams - predictable, non-redundant event processing
#![allow(unused)] fn main() { // For a key mapped to "jump" action in Godot's Input Map: // ✅ Generates ONE ActionInput { action: "jump", pressed: true } // ❌ Does NOT generate duplicate KeyboardInput events // For an unmapped key (e.g., 'Q' with no action mapping): // ✅ Generates ONE KeyboardInput { keycode: Q, pressed: true } // ❌ Does NOT generate ActionInput events }
Available Input Events
godot-bevy provides several input event types:
ActionInput
The most important event type - maps to Godot's input actions:
#![allow(unused)] fn main() { fn handle_actions(mut events: EventReader<ActionInput>) { for event in events.read() { println!("Action: {}, Pressed: {}, Strength: {}", event.action, event.pressed, event.strength); } } }
KeyboardInput
Direct keyboard events:
#![allow(unused)] fn main() { fn handle_keyboard(mut events: EventReader<KeyboardInput>) { for event in events.read() { if event.pressed && event.keycode == Key::SPACE { println!("Space pressed!"); } } } }
MouseButtonInput
Mouse button events:
#![allow(unused)] fn main() { fn handle_mouse(mut events: EventReader<MouseButtonInput>) { for event in events.read() { println!("Mouse button: {:?} at {:?}", event.button_index, event.position); } } }
MouseMotion
Mouse movement events:
#![allow(unused)] fn main() { fn handle_mouse_motion(mut events: EventReader<MouseMotion>) { for event in events.read() { println!("Mouse moved by: {:?}", event.relative); } } }
Quick Reference
Feature | Bevy Input | godot-bevy Input |
---|---|---|
Setup complexity | None | Moderate |
Cross-platform | Limited | Full |
User remapping | No | Yes |
Touch support | No | Yes |
State queries | Easy | Manual tracking |
Performance | Fastest | Fast |
Godot integration | None | Full |
Choosing Your Approach
Start with Bevy Input if:
- Building a prototype or game jam entry
- Targeting desktop only
- Using simple controls
- Want immediate results
Use godot-bevy Input if:
- Building for release
- Need cross-platform support
- Want user-configurable controls
- Using complex input schemes
- Targeting mobile/console
Mixing Both Systems
You can use both systems in the same project:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { app.add_systems(Update, ( // Debug controls with Bevy input debug_controls, // Game controls with godot-bevy input game_controls, )); } fn debug_controls(keys: Res<ButtonInput<KeyCode>>) { if keys.just_pressed(KeyCode::F1) { // Toggle debug overlay } } fn game_controls(mut events: EventReader<ActionInput>) { for event in events.read() { // Handle game actions } } }
This gives you the best of both worlds: simple debug controls and flexible game controls.
Troubleshooting
Duplicate Events (Fixed in v0.7.0+)
If you're seeing duplicate ActionInput
events for the same key press, you may be using an older version of godot-bevy. This was fixed in version 0.7.0 through improved input event processing.
Symptoms:
#![allow(unused)] fn main() { // Old behavior (before v0.7.0): 🎮 Action: 'jump' pressed // First event 🎮 Action: 'jump' pressed // Duplicate event (unwanted) }
Solution: Update to godot-bevy v0.7.0 or later where input processing was improved to eliminate duplicates.
Mouse Events Only on Movement
MouseMotion
events are only generated when the mouse actually moves. If you need continuous mouse position tracking, consider using Godot's Input.get_global_mouse_position()
in a system that runs every frame.
Frame Execution Model
Understanding how godot-bevy integrates with Godot's frame timing is crucial for building performant games. This chapter explains the execution model and how different schedules interact.
Two Types of Frames
Visual Frames (_process
)
Visual frames run at your display's refresh rate and handle the main Bevy update cycle.
What runs: The complete app.update()
cycle
First
PreUpdate
Update
FixedUpdate
PostUpdate
Last
Frequency: Matches Godot's visual framerate (typically 60-144 FPS)
Use for:
- Game logic
- UI updates
- Rendering-related systems
- Most gameplay code
Physics Frames (_physics_process
)
Physics frames run at Godot's fixed physics tick rate.
What runs: Only the PhysicsUpdate
schedule
Frequency: Godot's physics tick rate (default 60 Hz)
Use for:
- Physics calculations
- Movement that needs to sync with Godot physics
- Collision detection
- Anything that must run at a fixed rate
Schedule Execution Order
Within Visual Frames
Visual Frame Start
├── First
├── PreUpdate (reads Godot → ECS transforms)
├── Update (your game logic)
├── FixedUpdate (0, 1, or multiple times)
├── PostUpdate
└── Last (writes ECS → Godot transforms)
Visual Frame End
Independent Physics Frames
Physics Frame Start
└── PhysicsUpdate (your physics logic)
Physics Frame End
⚠️ Important: Physics frames run independently and can execute:
- Before a visual frame starts
- Between any visual frame schedules
- After a visual frame completes
- Multiple times between visual frames
Frame Rate Relationships
Different parts of your game run at different rates:
Schedule | Rate | Use Case |
---|---|---|
Visual schedules | Display refresh (60-144 Hz) | Rendering, UI, general logic |
PhysicsUpdate | Physics tick (60 Hz) | Godot physics integration |
FixedUpdate | Bevy's rate (64 Hz default) | Consistent gameplay simulation |
Practical Example
Here's how different systems should be scheduled:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { // Visual frame systems app.add_systems(Update, ( ui_system, camera_follow, animation_system, )); // Fixed timestep for consistent simulation app.add_systems(FixedUpdate, ( ai_behavior, cooldown_timers, )); // Godot physics integration app.add_systems(PhysicsUpdate, ( character_movement, collision_response, )); } }
Delta Time Usage
Different schedules require different delta time sources:
In Update Systems
#![allow(unused)] fn main() { fn movement_system( time: Res<Time>, mut query: Query<&mut Transform2D>, ) { let delta = time.delta_seconds(); // Use Bevy's time for visual frame systems } }
In PhysicsUpdate Systems
#![allow(unused)] fn main() { fn physics_movement( physics_delta: Res<PhysicsDelta>, mut query: Query<&mut Transform2D>, ) { let delta = physics_delta.delta_seconds; // Use Godot's physics delta for physics systems } }
Common Pitfalls
❌ Don't modify the same data in multiple schedules
#![allow(unused)] fn main() { // BAD: Conflicting modifications app.add_systems(Update, move_player); app.add_systems(PhysicsUpdate, also_move_player); // Conflicts! }
❌ Don't expect immediate cross-schedule visibility
#![allow(unused)] fn main() { // BAD: Expecting immediate updates fn physics_system() { // Set position in PhysicsUpdate } fn visual_system() { // Won't see physics changes until next frame! } }
✅ Do use appropriate schedules for each task
#![allow(unused)] fn main() { // GOOD: Clear separation of concerns app.add_systems(Update, render_effects); app.add_systems(PhysicsUpdate, apply_physics); app.add_systems(FixedUpdate, update_ai); }
Performance Considerations
- Visual frames can vary widely (30-144+ FPS)
- PhysicsUpdate provides consistent timing for physics
- FixedUpdate may run multiple times per visual frame to catch up
- Transform syncing happens at schedule boundaries
Note: Scene tree entities are initialized during
PreStartup
, before anyStartup
systems run. This means you can safely query Godot scene entities in yourStartup
systems! See Scene Tree Initialization and Timing for details.
Next Steps
- Learn about the PhysicsUpdate Schedule in detail
- Understand Transform Syncing timing
- Explore Performance Optimization techniques
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.