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

  1. Performance: Bevy's ECS provides cache-friendly data layouts and parallel system execution
  2. Safety: Rust's type system catches bugs at compile time
  3. Modularity: ECS encourages clean, decoupled code architecture
  4. 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

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:

Create a New Project

1. Set Up Godot Project

First, create a new Godot project through the Godot editor:

  1. Open Godot and click "New Project"
  2. Choose a project name and location
  3. Select "Compatibility" renderer for maximum platform support
  4. 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

  1. In Godot, create a new scene
  2. Add a BevyApp node as the root
  3. Save it as bevy_app_singleton.tscn
  4. Go to Project → Project Settings → Autoload
  5. 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

  1. Return to the Godot editor
  2. Press F5 or click the play button
  3. 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 node
  • Transform2D/3D - Position, rotation, scale
  • Name - Node name
  • Collisions - Collision events
  • Groups - 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:

  1. Creates the GDExtension entry point
  2. Sets up the Bevy app
  3. Integrates with Godot's lifecycle
  4. Handles all the bridging magic

Data Flow

Understanding how data flows between Godot and Bevy is crucial:

Godot → Bevy

  1. Node added to scene tree
  2. Entity created with components
  3. Signals converted to events
  4. Input forwarded to systems

Bevy → Godot

  1. Transform components sync to nodes
  2. Commands can modify scene tree
  3. Resources can be loaded
  4. 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:

ExampleDescription
Dodge the CreepsPorted example from Godot's tutorial on making a 2D game.
Input Event DemoShowcases the different ways in which you can get input either via Bevy's input API or using Godot's.
Platformer 2DA more complete example showing how to tag Godot nodes for an editor heavy.
Simple Node2D MovementA minimal example with basic movement.
Timing TestInternal 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:

  1. initialize_scene_tree - Traverses the entire Godot scene tree and creates Bevy entities with components like GodotNodeHandle, Name, transforms, and more
  2. connect_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 system
  • read_scene_tree_events - Processes those events to create/update/remove entities

This separation allows other systems to also react to SceneTreeEvents 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 node
  • Name - The node's name from Godot
  • Transform2D or Transform3D - For Node2D and Node3D types respectively
  • Groups - The node's group memberships
  • Collisions - 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

  1. Use Startup for initialization - Scene entities are guaranteed to be ready
  2. Use Update for gameplay logic - This is where most of your game code should live
  3. Custom PreStartup systems - If you add systems to PreStartup, 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:

  1. Godot emits a node_added signal
  2. The SceneTreeWatcher (on the Godot side) receives the signal
  3. It sends a SceneTreeEvent through an mpsc channel
  4. write_scene_tree_events (in First schedule) reads from the channel and writes to Bevy's event system
  5. read_scene_tree_events (also in First 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 nodes
  • Node3DMarker - All 3D nodes
  • ControlMarker - UI control nodes
  • CanvasItemMarker - 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:

  1. Reduced Iteration: Only process entities you care about
  2. No Runtime Type Checking: Skip try_get() calls
  3. Better ECS Optimization: Bevy can optimize queries with markers
  4. Cache Efficiency: Process similar entities together

Automatic Application

You don't need to add marker components manually. The library automatically:

  1. Detects the Godot node type during scene tree traversal
  2. Adds the appropriate marker component(s) to the entity
  3. Includes all parent type markers in the inheritance hierarchy
  4. 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 games
  • Transform3D - 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:

  1. Disabled - No syncing, no transform components created
  2. OneWay - ECS → Godot only (default)
  3. 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

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

  1. Choose mode early - Switching modes mid-project can be complex
  2. Default to OneWay - Unless you specifically need other modes
  3. Benchmark your game - Measure actual performance impact
  4. 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

FeatureBevy Inputgodot-bevy Input
Setup complexityNoneModerate
Cross-platformLimitedFull
User remappingNoYes
Touch supportNoYes
State queriesEasyManual tracking
PerformanceFastestFast
Godot integrationNoneFull

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:

ScheduleRateUse Case
Visual schedulesDisplay refresh (60-144 Hz)Rendering, UI, general logic
PhysicsUpdatePhysics tick (60 Hz)Godot physics integration
FixedUpdateBevy'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

  1. Visual frames can vary widely (30-144+ FPS)
  2. PhysicsUpdate provides consistent timing for physics
  3. FixedUpdate may run multiple times per visual frame to catch up
  4. Transform syncing happens at schedule boundaries

Note: Scene tree entities are initialized during PreStartup, before any Startup systems run. This means you can safely query Godot scene entities in your Startup systems! See Scene Tree Initialization and Timing for details.

Next Steps

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)

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();
        }
    }
}
}
#![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

  1. Performance: Only iterate over entities you care about
  2. Safety: No more Option<> handling or potential panics
  3. Clarity: Query signatures clearly show what node types you expect
  4. 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:

  1. Rare usage: The system runs infrequently and performance isn't critical
  2. Dynamic typing: You genuinely need to handle unknown node types at runtime
  3. 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:

  1. Wrong marker: Make sure you're using the right marker for your query
  2. Node freed: The Godot node was freed but the entity still exists
  3. 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:

  1. Check node types: Verify your scene has the expected node types
  2. Check marker names: Ensure you're using the correct marker component
  3. 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:

  1. Replace broad Query<&mut GodotNodeHandle> with specific marker queries
  2. Replace try_get() calls with get() when using markers
  3. 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

  1. Remove autosync=true: This parameter no longer exists and will cause compilation errors
  2. Remove manual sync systems: If you were manually adding bundle sync systems, remove them
  3. Timing change: Bundle components are now available in Startup systems (was previously only available in Update)

Benefits of This Change

  1. Simplified API: No need to remember to set autosync=true
  2. Better timing: Bundle components are available earlier in the frame lifecycle
  3. Unified behavior: Both initial scene loading and dynamic node addition work the same way
  4. No missed registrations: Impossible to forget to register a bundle system

Migration Checklist

  • Remove autosync=true and autosync=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:

  1. New TransformSyncMode system: Transform syncing is now configurable via GodotTransformConfig
  2. Default changed from two-way to one-way: Previously, transforms were synced bidirectionally by default. Now the default is one-way (ECS → Godot only)
  3. 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

  1. TransformSyncMode::OneWay (NEW DEFAULT)

    • ECS transform changes update Godot nodes
    • Godot transform changes are NOT reflected in ECS
    • Best for pure ECS architectures
  2. TransformSyncMode::TwoWay (v0.6.x default behavior)

    • Full bidirectional sync between ECS and Godot
    • Required for Godot animations affecting ECS
    • Higher performance overhead
  3. 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.