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!
Getting Started
Installation
This guide will walk you through setting up godot-bevy in a Godot project.
Prerequisites
Before you begin, ensure you have:
- Rust 1.88.0 or later - Install Rust
- Godot 4.3 - Download Godot
- Basic familiarity with both Rust and Godot
Installation Methods
There are two ways to set up godot-bevy in your project:
- Plugin Installation (Recommended) - Use the godot-bevy editor plugin for automatic setup
- Manual Installation - Set up the project manually
Plugin Installation
The easiest way to get started is using the godot-bevy editor plugin, which automatically generates the Rust project and configures the BevyApp singleton.
1. Install the Plugin
- Download the
addons/godot-bevyfolder from the godot-bevy repository - Copy it to your Godot project's
addons/directory - In Godot, go to Project > Project Settings > Plugins
- Enable the "Godot-Bevy Integration" plugin
2. Create Your Project
- Go to Project > Tools > Setup godot-bevy Project
- Configure your project settings:
- Project name: Used for the Rust crate name
- godot-bevy version: Library version (default: 0.10.0)
- Release build: Whether to build in release mode initially
- Click "Create Project"
The plugin will automatically:
- Create a
rust/directory with Cargo.toml and lib.rs - Generate the
.gdextensionfile with correct platform paths - Create and register the BevyApp singleton scene
- Build the Rust project
- Restart the editor to apply changes
3. Run Your Project
After the editor restarts:
- Press F5 or click the play button
- You should see "Hello from Bevy ECS!" in the output console every second
The generated rust/src/lib.rs includes a complete example.
4. Plugin Features
The plugin provides additional useful features:
- Add BevyApp Singleton Only: If you already have a Rust project, use Project > Tools > Add BevyApp Singleton to just create and register the singleton
- Build Rust Project: Use Project > Tools > Build Rust Project to rebuild without restarting the editor
- Bulk Transform Optimization: The generated singleton includes optimized bulk transform methods that godot-bevy automatically detects and uses for better performance
Manual Installation
If you prefer to set up everything manually, follow these steps:
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 = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
godot-bevy = "0.10.0"
bevy = { version = "0.17", default-features = false }
godot = "0.4"
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
BevyAppnode as the root - Save it as
bevy_app_singleton.tscn - Go to Project → Project Settings → Globals → Autoload
- Add the scene with name "BevyAppSingleton"
Write Your First Code
Edit rust/src/lib.rs:
#![allow(unused)] fn main() { use godot::prelude::*; 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
Plugin Installation Issues
"Plugin not found" or "Plugin failed to load"
- Ensure the
addons/godot-bevyfolder is in the correct location - Check that all plugin files are present (plugin.cfg, plugin.gd, etc.)
- Restart the Godot editor after copying the plugin files
"Setup godot-bevy Project" menu item missing
- Verify the plugin is enabled in Project Settings > Plugins
- Check the Godot console for plugin error messages
- Try disabling and re-enabling the plugin
Plugin setup fails or hangs
- Ensure you have
cargoinstalled and available in your system PATH - Check that you have write permissions in the project directory
- Look for error messages in the Godot output console
Manual Installation Issues
"Can't open dynamic library"
- Ensure the paths in
rust.gdextensionmatch 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 using either the plugin or manual installation method.
The plugin installation automatically includes the new bulk transform API optimizations, while manual installations can add these by updating their BevyApp singleton scene.
Continue to Basic Concepts to learn more about godot-bevy's architecture and capabilities.
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 nodeName- Node nameGroups- Godot node groups
For collision detection, use the Collisions system param (see Plugins).
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 Transform, With<Player>>, ) { for mut transform in query.iter_mut() { transform.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
Configuring Core Behavior
The #[bevy_app] macro accepts configuration attributes to customize godot-bevy's core behavior:
Scene Tree Relationships
godot-bevy mirrors Godot's scene tree with a custom ECS relationship:
GodotChildOf / GodotChildren. This avoids conflicts with Bevy's built-in
ChildOf / Children relationship (used by many plugins for their own hierarchies).
By default, despawning a parent entity will also despawn its Godot children. You can
disable that behavior with the scene_tree_auto_despawn_children attribute:
#![allow(unused)] fn main() { #[bevy_app(scene_tree_auto_despawn_children = false)] fn build_app(app: &mut App) { // Children can outlive their parents (useful for pooling or custom lifetimes) app.add_plugins(PhysicsPlugins::new(PhysicsUpdate)); } }
When to use this:
- ✅ When you want entities to outlive their Godot nodes
- ✅ When you manage lifetimes manually (pooling, reuse, gameplay-driven despawns)
- ❌ When you rely on automatic child cleanup on parent despawn
Default behavior (when not specified): scene_tree_auto_despawn_children = true
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
5. The Godot Boundary (Main Thread Only)
- Any call into Godot (via
GodotAccess,Gd<T>,Input::singleton, etc.) must run on the main thread - Systems that include
GodotAccessare forced onto the main thread and run sequentially, so keep them small and push heavy work to parallel systems - Treat
GodotNodeHandleas an ID; resolve toGd<T>only viaGodotAccess - See Thread Safety and Godot APIs for details and patterns
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() { #[derive(Message, Debug, Clone)] struct ButtonPressed; fn handle_button_press( mut events: MessageReader<ButtonPressed>, ) { for _ in events.read() { // Button was pressed! } } }
Spawning Godot Scenes
#![allow(unused)] fn main() { use bevy::app::{App, Plugin, Startup, Update}; use bevy::asset::{AssetServer, Handle}; use bevy::prelude::{Commands, Component, Res, Resource, Single, Transform, With}; use godot_bevy::bridge::GodotNodeHandle; use godot_bevy::prelude::{GodotResource, GodotScene}; struct EnemyPlugin; impl Plugin for EnemyPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, load_assets); app.add_systems(Update, spawn_enemy); } } #[derive(Resource, Debug)] struct EnemyScene(Handle<GodotResource>); #[derive(Component, Debug)] struct Enemy { health: i32, } #[derive(Component, Debug)] struct EnemySpawner; fn load_assets(mut commands: Commands, server: Res<AssetServer>) { let handle: Handle<GodotResource> = server.load("scenes/enemy.tscn"); commands.insert_resource(EnemyScene(handle)); } fn spawn_enemy( mut commands: Commands, enemy_scene: Res<EnemyScene>, enemy_spawner: Single<&GodotNodeHandle, With<EnemySpawner>>, ) { commands.spawn(( GodotScene::from_handle(enemy_scene.0.clone()) .with_parent(enemy_spawner.into_inner().id()), Enemy { health: 100 }, Transform::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!
Plugin System
godot-bevy follows Bevy's philosophy of opt-in plugins, giving you granular control over which features are included in your build. This results in smaller binaries, better performance, and clearer dependencies.
Default Behavior
By default, GodotPlugin (automatically included by the #[bevy_app] macro) only provides minimal core functionality through GodotCorePlugins:
- Scene tree management (automatic entity mirroring)
- Asset loading system
- Basic Bevy setup
All other features must be explicitly added as plugins.
Plugin Groups
-
GodotCorePlugins: Minimal required functionality- Automatically included by
#[bevy_app]macro viaGodotPlugin - Includes:
GodotBaseCorePlugin: Bevy MinimalPlugins, logging, diagnostics, schedulesGodotSceneTreePlugin: Scene tree entity mirroring and management
- Automatically included by
-
GodotDefaultPlugins: Contains all plugins typically necessary for building a game- Includes:
GodotAssetsPlugin: Godot resource loading through Bevy's asset systemGodotTransformSyncPlugin: Transform synchronizationGodotCollisionsPlugin: Collision detectionBevyInputBridgePlugin: Bevy input API supportGodotAudioPlugin: Audio systemGodotPackedScenePlugin: Runtime scene spawningGodotBevyLogPlugin: Unify/improve bevy and godot logging such thatinfo!,debug!, etc log messages are visible in the Godot Editor
- Includes:
Typed signals are opt-in per message type using GodotTypedSignalsPlugin::<T>.
Available Plugins
Core Infrastructure (Included by Default)
-
GodotBaseCorePlugin: Foundation setup- Bevy MinimalPlugins (without ScheduleRunnerPlugin)
- Asset system with Godot resource reader
- Logging and diagnostics
- Physics update schedule
- Main thread marker resource
-
GodotSceneTreePlugin: Scene tree management- Automatic entity creation for scene nodes
- Scene tree change monitoring
- Transform component addition (configurable)
- AutoSync bundle registration
- Groups component for Godot groups
NodeEntityIndexresource for O(1) lookup from GodotInstanceIdto BevyEntity
Additional Plugins
-
GodotAssetsPlugin: Asset loading- Load Godot resources through Bevy's AssetServer
- Supports .tscn, .tres, textures, sounds, etc.
- Development and export path handling
-
GodotTransformSyncPlugin: Transform synchronization- Configure sync mode:
Disabled,OneWay(default), orTwoWay - Synchronizes Bevy Transform components with Godot node transforms
- Required for moving/positioning nodes from Bevy
- Configure sync mode:
-
GodotCollisionsPlugin: Collision detection- Monitors Area2D/3D and RigidBody2D/3D collision signals
- Provides
Collisionssystem param for querying collision state - Provides
CollisionStarted/CollisionEndedevents (messages + observers)
-
GodotTypedSignalsPlugin<T>: Typed signal bridge- Add one plugin per message type you want to emit
- Use
TypedGodotSignals<T>to connect signals - Essential for UI interactions (button clicks, etc.)
-
GodotInputEventPlugin: Raw input events- Provides Godot input as Bevy events
- Keyboard, mouse, touch, gamepad, and action events
- Lower-level alternative to
BevyInputBridgePlugin
-
BevyInputBridgePlugin: Bevy input API- Use Bevy's standard
ButtonInput<KeyCode>, mouse events, etc. - Automatically includes
GodotInputEventPlugin - Higher-level, more ergonomic than raw events
- Use Bevy's standard
-
GodotAudioPlugin: Audio system- Channel-based audio API
- Spatial audio support
- Audio tweening and easing
- Integrates with Godot's audio engine
-
GodotPackedScenePlugin: Scene spawning- Spawn/instantiate scenes at runtime
- Support for both asset handles and paths
- Automatic transform application
-
GodotBevyLogPlugin: Improved logging by default- Log message components are color-coded for readability by default. Color coding can be disabled entirely. NOTE: There is a performance penalty for color-coding, so if your application is very performance sensitive, consider disabling this feature
- Log messages are prefixed with a short timestamp, e.g.,
12:00:36.196. Timestamps can be customized or entirely disabled - Log messages are prefixed with a short log level, e.g.,
TforTRACE,DforDEBUG,IforINFO,WforWARN,EforERROR - Log messages are suffixed with a shortened path and line number location, e.g.,
@ loading_state/systems.rs:186 - Log level filtering is
INFOand higher severity by default, this can be customized directly in your code or set at runtime usingRUST_LOG, e.g.,RUST_LOG=trace cargo run
Usage Examples
Minimal Setup (Default)
The #[bevy_app] macro automatically provides core functionality:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { // GodotCorePlugins is already added // You have scene tree, assets, and basic setup app.add_systems(Update, my_game_system); } }
Adding Specific Features
Add only the plugins you need:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { app.add_plugins(GodotTransformSyncPlugin::default()) // Move nodes .add_plugins(GodotAudioPlugin) // Play sounds .add_plugins(BevyInputBridgePlugin); // Handle input app.add_systems(Update, my_game_systems); } }
Everything Enabled
For all features or easy migration from older versions:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { app.add_plugins(GodotDefaultPlugins); // All optional features app.add_systems(Update, my_game_systems); } }
Game-Specific Configurations
Pure ECS Game:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { app.add_plugins(GodotTransformSyncPlugin::default()) // Move entities .add_plugins(GodotAudioPlugin) // Play sounds .add_plugins(BevyInputBridgePlugin); // Input handling // Core plugins handle entity creation } }
Physics Platformer:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { app.add_plugins(GodotTransformSyncPlugin { sync_mode: TransformSyncMode::Disabled, // Use Godot physics }) .add_plugins(GodotCollisionsPlugin) // Detect collisions .add_plugins(GodotTypedSignalsPlugin::<UiSignal>::default()) // Handle signals .add_plugins(GodotAudioPlugin); // Play sounds } }
UI-Heavy Game:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { app.add_plugins(GodotTypedSignalsPlugin::<UiSignal>::default()) // Button clicks, etc. .add_plugins(BevyInputBridgePlugin) // Keyboard shortcuts .add_plugins(GodotAudioPlugin); // UI sounds // Don't need transform sync for UI } }
Plugin Configuration
Transform Sync Modes
#![allow(unused)] fn main() { // Default: One-way sync (Bevy → Godot) app.add_plugins(GodotTransformSyncPlugin::default()); // Two-way sync (Bevy ↔ Godot) app.add_plugins(GodotTransformSyncPlugin { sync_mode: TransformSyncMode::TwoWay, }); // Disabled (use Godot physics directly) app.add_plugins(GodotTransformSyncPlugin { sync_mode: TransformSyncMode::Disabled, }); }
Scene Tree Configuration
#![allow(unused)] fn main() { // Configure transform component creation app.add_plugins(GodotSceneTreePlugin::default()); }
Note: This is already included in GodotCorePlugins, so you'd need to disable the default GodotPlugin and build your own plugin setup to customize this.
Plugin Dependencies
Some plugins automatically include their dependencies:
BevyInputBridgePlugin→ includesGodotInputEventPluginGodotPlugin→ includesGodotCorePlugins
Choosing the Right Plugins
Ask Yourself:
- Do I want to load Godot resources through Bevy's asset system? → Add
GodotAssetsPlugin - Do I want to move/position nodes from Bevy? → Add
GodotTransformSyncPlugin - Do I want to play sounds and music? → Add
GodotAudioPlugin - Do I want to respond to UI signals? → Add
GodotTypedSignalsPlugin::<YourMessage> - Do I want to detect collisions? → Add
GodotCollisionsPlugin - Do I want to handle input? → Add
BevyInputBridgePluginorGodotInputEventPlugin - Do I want to spawn scenes at runtime? → Add
GodotPackedScenePlugin
When in Doubt:
Start with GodotDefaultPlugins and optimize later by removing unused plugins.
Benefits
Smaller Binaries
Only compile the features you actually use.
Better Performance
Skip unused systems and resources.
Clear Dependencies
Your plugin list shows exactly what features you're using.
Future-Proof
New optional features can be added without breaking existing code.
Migration Note
If upgrading from an older version where all features were included by default, simply add:
#![allow(unused)] fn main() { app.add_plugins(GodotDefaultPlugins); }
This restores the old behavior with all features enabled.
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_example
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
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 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 nodeName- The node's name from GodotGroups- The node's group memberships- Node type markers - Components like
ButtonMarker,Sprite2DMarker, etc. - Custom bundles - Components from
#[derive(BevyBundle)]are automatically added
For collision detection, use the Collisions system param and CollisionStarted/CollisionEnded events (requires GodotCollisionsPlugin).
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
PreStartupfor nodes that exist when the scene is first loaded - During
Firstfor 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
Startupfor initialization - Scene entities are guaranteed to be ready - Use
Updatefor gameplay logic - This is where most of your game code should live - Custom
PreStartupsystems - 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_addedsignal - The
SceneTreeWatcher(on the Godot side) receives the signal - It sends a
SceneTreeEventthrough an mpsc channel write_scene_tree_events(inFirstschedule) reads from the channel and writes to Bevy's event systemread_scene_tree_events(also inFirstschedule) 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(sprites: Query<&GodotNodeHandle, With<Sprite2DMarker>>, mut godot: GodotAccess) { for handle in sprites.iter() { // We know this is a Sprite2D, so .get() is safe let sprite = godot.get::<Sprite2D>(*handle); // 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/Sprite3DMarkerAnimatedSprite2DMarker/AnimatedSprite3DMarkerMeshInstance2DMarker/MeshInstance3DMarker
Physics Bodies
RigidBody2DMarker/RigidBody3DMarkerCharacterBody2DMarker/CharacterBody3DMarkerStaticBody2DMarker/StaticBody3DMarker
Areas and Collision
Area2DMarker/Area3DMarkerCollisionShape2DMarker/CollisionShape3DMarkerCollisionPolygon2DMarker/CollisionPolygon3DMarker
Audio Players
AudioStreamPlayerMarkerAudioStreamPlayer2DMarkerAudioStreamPlayer3DMarker
UI Elements
LabelMarkerButtonMarkerLineEditMarkerTextEditMarkerPanelMarker
Cameras and Lighting
Camera2DMarker/Camera3DMarkerDirectionalLight3DMarkerSpotLight3DMarker
Animation and Timing
AnimationPlayerMarkerAnimationTreeMarkerTimerMarker
Path Nodes
Path2DMarker/Path3DMarkerPathFollow2DMarker/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<&GodotNodeHandle, (With<Sprite2DMarker>, With<RigidBody2DMarker>)>, mut godot: GodotAccess, ) { for handle in query.iter() { let sprite = godot.get::<Sprite2D>(*handle); let body = godot.get::<RigidBody2D>(*handle); // 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<&GodotNodeHandle, (With<Sprite2DMarker>, Without<CharacterBody2DMarker>)>, mut godot: GodotAccess, ) { for handle in query.iter() { // These are sprites but not character bodies let sprite = godot.get::<Sprite2D>(*handle); // Work with environmental sprites... } } }
Multiple Specific Types
#![allow(unused)] fn main() { // Handle different audio player types efficiently fn update_audio_system( players_1d: Query<&GodotNodeHandle, With<AudioStreamPlayerMarker>>, players_2d: Query<&GodotNodeHandle, With<AudioStreamPlayer2DMarker>>, players_3d: Query<&GodotNodeHandle, With<AudioStreamPlayer3DMarker>>, mut godot: GodotAccess, ) { // Process each type separately - no runtime type checking! for handle in players_1d.iter() { let player = godot.get::<AudioStreamPlayer>(*handle); // Handle 1D audio... } for handle in players_2d.iter() { let player = godot.get::<AudioStreamPlayer2D>(*handle); // Handle 2D spatial audio... } for handle in players_3d.iter() { let player = godot.get::<AudioStreamPlayer3D>(*handle); // 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 Nodes
This section 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.
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
BevyBundleto 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.
Automatic Markers
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>, } }
Property Mapping from Godot to Bevy
The BevyBundle macro allows you to attach Bevy Components to Custom Godot nodes.
It supports several ways to map Godot node properties to Bevy components:
Default Component Creation
The simplest form creates a component with its default value:
#![allow(unused)] fn main() { #[derive(GodotClass, BevyBundle)] #[class(base=Node2D)] #[bevy_bundle((Player))] pub struct PlayerNode { base: Base<Node2D>, } }
Single Field Mapping
Map a single Godot property to initialize a component:
#![allow(unused)] fn main() { #[derive(Component)] struct Health(f32); #[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 } }
Struct Component Mapping
Map multiple Godot properties to fields in a struct component:
#![allow(unused)] fn main() { #[derive(Component)] struct Stats { health: f32, mana: f32, stamina: f32, } #[derive(GodotClass, BevyBundle)] #[class(base=CharacterBody2D)] #[bevy_bundle((Player), (Stats { health: max_health, mana: max_mana, stamina: max_stamina }))] pub struct PlayerCharacter { base: Base<CharacterBody2D>, #[export] max_health: f32, #[export] max_mana: f32, #[export] max_stamina: f32, } }
Transform Function
Sometimes a Godot property's type isn't convertable to a Bevy/Rust compatible type,
or maybe you want to process the value from Godot before it's assigned to a component.
To solve this, you can use transform_with to apply a transformation function to
convert Godot values before they're assigned to components:
#![allow(unused)] fn main() { fn percentage_to_fraction(value: f32) -> f32 { value / 100.0 } #[derive(GodotClass, BevyBundle)] #[class(base=Node2D)] #[bevy_bundle((Enemy), (Health: health_percentage))] pub struct Enemy { base: Base<Node2D>, #[export] #[bevy_bundle(transform_with = "percentage_to_fraction")] health_percentage: f32, // Editor shows 0-100, component gets 0.0-1.0 } }
Recommended approach
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>)>, mut godot: GodotAccess, ) { for handle in players.iter() { let mut body = godot.get::<CharacterBody2D>(*handle); body.move_and_slide(); } } }
Complete Example
#![allow(unused)] fn main() { #[derive(Component)] struct Velocity(Vec2); #[derive(Component)] struct Combat { damage: f32, attack_speed: f32, range: f32, } fn degrees_to_radians(degrees: f32) -> f32 { degrees.to_radians() } #[derive(GodotClass, BevyBundle)] #[class(base=CharacterBody2D)] #[bevy_bundle( (Player), (Health: max_health), (Velocity: movement_speed), (Combat { damage: attack_damage, attack_speed: attack_rate, range: attack_range }) )] pub struct PlayerNode { base: Base<CharacterBody2D>, #[export] max_health: f32, #[export] movement_speed: Vec2, #[export] attack_damage: f32, #[export] attack_rate: f32, #[export] attack_range: f32, #[export] #[bevy_bundle(transform_with = "degrees_to_radians")] rotation_degrees: f32, // Can be transformed even if not used in components } }
Nodes from Components and Bundles
Often, we want to make Godot nodes from Rust ECS types. The GodotNode derive macro supports two types:
- Component:
#[derive(Component, GodotNode)] - Bundle:
#[derive(Bundle, GodotNode)]
Both generate a Godot class you can place in the editor and auto‑insert the corresponding ECS data when the scene is scanned.
See the GodotNode Rust docs for full syntax and options:
https://docs.rs/godot-bevy/latest/godot_bevy/prelude/derive.GodotNode.html.
Configuring the Node
You can configure the Godot node's base type and class name with the godot_node struct-level attribute:
#![allow(unused)] fn main() { #[derive(GodotNode, ...)] #[godot_node(base(Area2D), class_name(Gem2D))] pub struct Gem; }
Component + GodotNode → Node
Use the following method to create a Godot node from a single component. Use when you want to expose a single component to the editor.
Gem marker component:
#![allow(unused)] fn main() { #[derive(Component, GodotNode, Default, Debug, Clone)] #[godot_node(base(Area2D), class_name(Gem2D))] pub struct Gem; }
Door with an exported property:
#![allow(unused)] fn main() { #[derive(Component, GodotNode, Default, Debug, Clone)] #[godot_node(base(Area2D), class_name(Door2D))] pub struct Door { #[godot_export(default(LevelId::Level1))] pub level_id: LevelId, } }
Each derive generates a corresponding Godot class (e.g., Gem2D, Door2D) and inserts the component when the node is discovered. Fields marked with #[godot_export] become Godot editor properties.
Bundle + GodotNode → Node
Sometimes a single component isn’t the right abstraction for your editor node. When you want one node to represent an entity with multiple components, derive on a Bevy Bundle:
#![allow(unused)] fn main() { #[derive(Bundle, GodotNode)] #[godot_node(base(CharacterBody2D), class_name(Player2D))] pub struct PlayerBundle { // Inserted as Default::default(), no Godot properties pub player: Player, // Tuple/newtype → property name is the bundle field name #[export_fields(value(export_type(f32), default(250.0)))] pub speed: Speed, #[export_fields(value(export_type(f32), default(-400.0)))] pub jump_velocity: JumpVelocity, // Custom default pulled from ProjectSettings #[export_fields(value(export_type(f32), default(godot::classes::ProjectSettings::singleton() .get_setting("physics/2d/default_gravity") .try_to::<f32>() .unwrap_or(980.0))))] pub gravity: Gravity, } }
What #[export_fields] does:
- Selects which component data is exported to the Godot editor
- Sets the Godot property type with
export_type(Type) - Optionally provides a default with
default(expr) - Optionally converts Godot → Bevy with
transform_with(path::to::fn)when building the bundle
Property naming rules:
- Struct field entries export using the Bevy field name
- Tuple/newtype entry (value(...)) exports using the bundle field name
- Renaming is not supported; duplicate property names across the bundle are a compile error
Construction rules:
- Components without
#[export_fields]are constructed withDefault::default() - For struct components, only the exported fields are set; the rest come from
..Default::default() - Nested bundles are allowed and will be flattened by Bevy on insertion; only top‑level fields can export properties
This derive generates a Godot class (Player2D above) and an autosync registration so the bundle is inserted automatically for matching nodes.
Spawning Godot scenes with NodeTreeView
GodotScene is a Bevy Component that lets us attach and instantiate Godot scene files (.tscn) to Bevy entities.
When we add a GodotScene to an entity, it spawns that scene in Godot's scene tree and links it to our Bevy entity,
letting us combine Godot's visual editor with Bevy's ECS architecture.
When we spawn scenes, we almost always need to reach into that scene’s node tree to:
- grab child nodes like sprites, notifiers, or UI controls
- connect signals
- drive animations or physics bodies
Doing this manually with raw GodotNodeHandle lookups quickly becomes repetitive and fragile.
The #[derive(NodeTreeView)] macro gives us a typed, ergonomic view of a scene’s node tree, driven by node paths.
This page explains how to:
- Define a
NodeTreeViewfor a scene - Spawn the scene via
GodotScene - Use the generated view to access nodes and connect signals
1. Spawn Godot scenes with GodotScene
To spawn a Godot scene from Bevy, insert GodotScene into our entity:
use bevy::prelude::*;
use godot_bevy::prelude::{GodotResource, GodotScene};
use bevy::state::app::StatesPlugin;
use bevy_asset_loader::asset_collection::AssetCollection;
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash, States)]
enum GameState {
#[default]
Loading,
InGame,
}
fn plugin(app: &mut App) {
app.add_plugins(StatesPlugin)
.init_state::<GameState>()
.add_loading_state(
LoadingState::new(GameState::Loading).continue_to_state(GameState::InGame),
);
app.configure_loading_state(
LoadingStateConfig::new(GameState::Loading).load_collection::<PickupAssets>(),
);
app.add_message::<PickupBodyEntered>();
app.add_systems(Update, pickup_system);
}
/// This example uses `bevy_asset_loader` to load the
/// scene file as a packed scene at startup.
#[derive(AssetCollection, Resource)]
pub struct CharacterAssets {
#[asset(path = "scenes/character.tscn")]
pub character_scene: Handle<GodotResource>,
}
fn spawn_character(mut commands: Commands, assets: Res<CharacterAssets>) {
commands
.spawn_empty()
// Add additional Bevy components here (e.g. position, gameplay data, etc.)
.insert(Transform::default())
.insert(
// Attach the Godot scene to this Bevy entity
GodotScene::from_handle(assets.character_scene.clone())
// Optionally, connect signals here with the
// `with_signal_connection` builder method discussed below.
);
}
At this point, the Bevy entity is linked to the Godot scene instance.
Now we would like to access nodes inside that scene.
2. Access scene children with NodeTreeView
Let's assume we have a Godot scene with the following node structure:
Node2D(the “character”)AnimatedSprite2DVisibleOnScreenNotifier2D
We can describe the nodes of the scene we want to access by their path like so:
use godot_bevy::interop::GodotNodeHandle;
use godot_bevy::prelude::NodeTreeView;
#[derive(NodeTreeView)]
pub struct CharacterNodes {
#[node("AnimatedSprite2D")]
pub animated_sprite: GodotNodeHandle,
#[node("VisibleOnScreenNotifier2D")]
pub visibility_notifier: GodotNodeHandle,
}
The NodeTreeView field types can be GodotNodeHandle or Option<GodotNodeHandle>.
The #[node("<node_path>")] attribute supports wildcards (*).
See below or the NodeTreeView docs for more details.
Then we can access the tree view in our system like this:
fn new_character_initialize(
entities: Query<&GodotNodeHandle, Added<Character>>,
mut godot: GodotAccess,
) {
for handle in &entities {
let character = godot.get::<RigidBody2D>(*handle);
let character_nodes = CharacterNodes::from_node(character).unwrap();
}
}
Path patterns
Node paths support simple patterns to avoid hard-coding full names:
/root/*/HUD/CurrentLevel- matches any single node name where * appears/root/Level*/HUD/CurrentLevel- matches node names starting with "Level"*/HUD/CurrentLevel- matches relative to the base node
Generated path constants
For each #[node("<node_path>")] field, NodeTreeView generates a public string constant named
<UPPERCASE_FIELD_NAME>_PATH inside our struct’s impl.
Given the CharacterNodes example above, the macro generates an impl like:
impl CharacterNodes {
pub const ANIMATED_SPRITE_PATH: &'static str = "AnimatedSprite2D";
pub const VISIBILITY_NOTIFIER_PATH: &'static str = "VisibleOnScreenNotifier2D";
}
These constants are very convenient when we need to refer to the same path in multiple places, especially when connecting signals from a spawned scene (covered below).
3. Connect signals to scene children using GodotScene::with_signal_connection
When spawning scenes, we often want to connect signals to child nodes.
There are three useful resources when connecting signals:
GodotScene'swith_signal_connectionbuilder method.NodeTreeView's generated path constants.godot_bevy::interop::<GODOT_NODE_TYPE>Signalstypes which contain string constants for all signals of a given Godot node type.
Here is an example using the CharacterNodes NodeTreeView from above and
the VisibleOnScreenNotifier2DSignals::SCREEN_EXITED string constant to
connect the VisibleOnScreenNotifier2D's screen_exited signal to a Bevy message.
use godot_bevy::interop::VisibleOnScreenNotifier2DSignals;
use godot_bevy::prelude::GodotScene;
use bevy::ecs::entity::Entity;
use bevy::prelude::Message;
#[derive(Message, Debug, Clone, Copy)]
pub struct CharacterScreenExited {
pub entity: Entity,
}
fn spawn_character_with_signals(mut commands: Commands, assets: Res<CharacterAssets>) {
commands
.spawn_empty()
.insert(Transform::default())
.insert(
GodotScene::from_handle(assets.character_scene.clone())
.with_signal_connection(
// Use the NodeTreeView-generated path constant:
CharacterNodes::VISIBILITY_NOTIFIER_PATH,
// The Godot signal we want to connect:
VisibleOnScreenNotifier2DSignals::SCREEN_EXITED,
// Closure to turn a Godot signal into a Bevy message:
|_args, _node_handle, entity| {
Some(CharacterScreenExited {
entity: entity.expect("entity was provided"),
})
},
),
);
}
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 standard bevy Transform components with automatic syncing from ECS to 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. You can also configure bi-directional synchronization, or disable all synchronization.
#![allow(unused)] fn main() { use godot_bevy::prelude::*; fn move_entity(mut query: Query<&mut Transform>) { for mut transform in query.iter_mut() { transform.translation.x += 1.0; } } }
2. Direct Godot Physics
Use GodotNodeHandle with GodotAccess 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(query: Query<&GodotNodeHandle>, mut godot: GodotAccess) { for handle in query.iter() { let mut body = godot.get::<CharacterBody2D>(*handle); 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
Use standard bevy Transform components. 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. You can also configure bi-directional synchronization, or disable all synchronization.
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
- ✅ Best for when your ECS systems rarely read/write Godot Node transforms, or you wish to explicitly control when synchronization occurs
- ❌ Godot Transform changes aren't automatically reflected in ECS
- ❌ ECS Transform changes aren't automatically reflected in Godot
Use when:
- Building platformers with CharacterBody2D
- 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
- Physics is controlled in ECS, i.e., you've disabled all Godot Physics engines and use something like Avian physics
TransformSyncMode::TwoWay
Full bidirectional synchronization between ECS and Godot.
Characteristics:
- ✅ Changes in either system are reflected
- ✅ Works with Godot animations
- ✅ Supports hybrid architectures
- ❌ Highest performance cost
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< (&Transform, &mut GodotNodeHandle), Or<(Added<Transform>, Changed<Transform>)> > ) { // 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
Custom Transform Sync
For performance-critical applications, you can create custom transform sync systems that only synchronize specific entities. This uses compile-time queries for maximum performance and automatically handles both 2D and 3D nodes.
When to Use Custom Sync
Use custom transform sync when:
- You have many entities but only some need synchronization
- Performance is critical and you want to minimize overhead
- You need fine-grained control over which entities sync
- Different entity types need different sync directions
Basic Usage
1. Disable Auto Sync
Option A: When Adding the Plugin Manually
Use the .without_auto_sync() method to disable automatic transform syncing while keeping the Transform and TransformSyncMetadata components:
#![allow(unused)] fn main() { use godot_bevy::prelude::*; #[bevy_app] fn build_app(app: &mut App) { // Disable auto sync but keep transform components app.add_plugins( GodotTransformSyncPlugin::default() .without_auto_sync() ); } }
Option B: When Using GodotDefaultPlugins
If you're using GodotDefaultPlugins, you need to disable the included GodotTransformSyncPlugin and add your own configured version:
#![allow(unused)] fn main() { use godot_bevy::prelude::*; #[bevy_app] fn build_app(app: &mut App) { // Remove the default transform sync plugin and add a custom one app.add_plugins( GodotDefaultPlugins .build() .disable::<GodotTransformSyncPlugin>() ); // Add your custom-configured transform sync plugin app.add_plugins( GodotTransformSyncPlugin::default() .without_auto_sync() ); } }
2. Define Custom Systems
Use the add_transform_sync_systems! macro to define which entities should sync:
#![allow(unused)] fn main() { use godot_bevy::add_transform_sync_systems; use godot_bevy::interop::node_markers::*; use bevy::ecs::query::{Or, With}; #[bevy_app] fn build_app(app: &mut App) { // Disable auto sync app.add_plugins( GodotTransformSyncPlugin::default() .without_auto_sync() ); // Sync all physics bodies (both 2D and 3D automatically) add_transform_sync_systems! { app, PhysicsEntities = Or<( With<RigidBody2DMarker>, With<CharacterBody2DMarker>, With<StaticBody2DMarker>, With<RigidBody3DMarker>, With<CharacterBody3DMarker>, With<StaticBody3DMarker>, )> } } }
Advanced Usage
Directional Sync Control
You can specify which direction of synchronization you need for optimal performance:
#![allow(unused)] fn main() { add_transform_sync_systems! { app, // Only ECS → Godot (one-way sync) UIElements = bevy_to_godot: With<UIElement>, // Only Godot → ECS (useful for reading physics results) PhysicsResults = godot_to_bevy: With<PhysicsActor>, // Full bidirectional sync Player = With<Player>, } }
This provides significant performance benefits:
bevy_to_godotonly: Skips reading Godot transforms, ideal for UI elements and ECS-driven entitiesgodot_to_bevyonly: Skips writing to Godot, useful for reading physics results- Both directions (no prefix): Full synchronization when needed
Real Example: Boids Performance Optimization
From the boids performance test example:
#![allow(unused)] fn main() { use godot_bevy::{add_transform_sync_systems, prelude::*}; #[derive(Component)] struct Boid { velocity: Vec2, // ... other fields } #[bevy_app] fn build_app(app: &mut App) { // Disable auto sync since we want custom sync for performance app.add_plugins( GodotTransformSyncPlugin::default() .without_auto_sync() ); // Add custom transform sync systems for Boid entities only // Only sync Bevy -> Godot since boids are driven by ECS movement systems add_transform_sync_systems! { app, Boid = bevy_to_godot: With<Boid> } // ... movement systems, etc. } }
Multiple Sync Systems in One Call
You can define multiple sync systems with different directions in a single macro call:
#![allow(unused)] fn main() { add_transform_sync_systems! { app, // All physics bodies (bidirectional) - both 2D and 3D PhysicsBodies = Or<( With<RigidBody2DMarker>, With<CharacterBody2DMarker>, With<StaticBody2DMarker>, With<RigidBody3DMarker>, With<CharacterBody3DMarker>, With<StaticBody3DMarker>, )>, // UI elements (ECS-driven only) - both 2D and 3D UIElements = bevy_to_godot: Or<( With<ButtonMarker>, With<LabelMarker>, With<Sprite3DMarker>, )>, // Physics result readers (Godot-driven only) - both 2D and 3D PhysicsReaders = godot_to_bevy: With<PhysicsListener>, } }
Custom Marker Components
For maximum control, create custom marker components:
#![allow(unused)] fn main() { use bevy::prelude::*; #[derive(Component)] struct NeedsTransformSync; #[derive(Component)] struct HighPrioritySync; #[derive(Component)] struct ReadOnlyTransform; // Opt-in sync systems add_transform_sync_systems! { app, // Only entities explicitly marked for sync OptInEntities = With<NeedsTransformSync>, // High priority entities (bidirectional) HighPriorityEntities = With<HighPrioritySync>, // Read-only from Godot ReadOnlyEntities = godot_to_bevy: With<ReadOnlyTransform>, } // In your spawning systems fn spawn_entity(mut commands: Commands) { commands.spawn(( RigidBody3DMarker, NeedsTransformSync, // Only entities with this will sync // ... other components )); } }
Key Features
Built-in Change Detection
The custom sync systems automatically use TransformSyncMetadata to prevent infinite loops:
#![allow(unused)] fn main() { // The generated systems automatically include change detection // No need to manually handle sync loops - it's built in! add_transform_sync_systems! { app, Player = With<Player>, // Safe bidirectional sync } }
Compile-time Optimization
Each sync system targets only specific entities, avoiding unnecessary iteration:
#![allow(unused)] fn main() { // This creates separate optimized systems for each query add_transform_sync_systems! { app, FastEntities = With<Player>, // Only checks Player entities SlowEntities = With<DebugMarker>, // Only checks DebugMarker entities PhysicsEntities = With<RigidBody2DMarker>, // Only checks physics entities } }
Automatic System Registration
The macro automatically registers systems in the appropriate schedules:
bevy_to_godotsystems run in theLastschedulegodot_to_bevysystems run in thePreUpdateschedule- Bidirectional sync (no prefix) runs in both schedules
2D and 3D Support
The macro automatically handles both 2D and 3D nodes in the same system:
- Uses
AnyOf<(&Node2DMarker, &Node3DMarker)>to query both types - Runtime type detection chooses the appropriate transform conversion
- Single system per query instead of separate 2D/3D systems
Common Use Cases
UI Elements (ECS → Godot only)
UI elements are typically driven by ECS systems and don't need to be read back:
#![allow(unused)] fn main() { #[derive(Component)] struct HealthBar; #[derive(Component)] struct MenuItem; add_transform_sync_systems! { app, UIElements = bevy_to_godot: Or<( With<HealthBar>, With<MenuItem>, With<LabelMarker>, )> } }
Physics Results (Godot → ECS only)
When using Godot physics, you often only need to read the results:
#![allow(unused)] fn main() { #[derive(Component)] struct PhysicsActor; add_transform_sync_systems! { app, PhysicsActors = godot_to_bevy: Or<( With<RigidBody2DMarker>, With<CharacterBody2DMarker>, With<RigidBody3DMarker>, With<CharacterBody3DMarker>, With<PhysicsActor>, )> } }
Interactive Elements (Bidirectional)
Player characters and interactive objects often need both directions:
#![allow(unused)] fn main() { #[derive(Component)] struct Player; #[derive(Component)] struct NPC; add_transform_sync_systems! { app, Interactive = Or<(With<Player>, With<NPC>)>, } }
Best Practices
1. Start Simple
Begin with a single, broad filter and optimize as needed:
#![allow(unused)] fn main() { #[derive(Component)] struct GameEntity; add_transform_sync_systems! { app, GameEntities = With<GameEntity> } }
2. Use Descriptive Names
Choose clear names for your sync systems:
#![allow(unused)] fn main() { add_transform_sync_systems! { app, MovingEntities = Or<(With<Player>, With<Enemy>)>, StaticUI = bevy_to_godot: With<StaticUIElement>, } }
3. Avoid Over-Optimization
Don't create too many specialized systems unless profiling shows it's necessary:
#![allow(unused)] fn main() { // Good: Logical groups add_transform_sync_systems! { app, GameEntities = Or<(With<Player>, With<Enemy>, With<Pickup>)>, UiElements = bevy_to_godot: Or<(With<ButtonMarker>, With<LabelMarker>)>, } // Avoid: Too many micro-optimizations add_transform_sync_systems! { app, Players = With<Player>, Enemies = With<Enemy>, Pickups = With<Pickup>, Buttons = bevy_to_godot: With<ButtonMarker>, Labels = bevy_to_godot: With<LabelMarker>, // ... too granular } }
4. Profile Performance
Use Bevy's diagnostic tools to measure the impact of your custom sync systems:
#![allow(unused)] fn main() { use bevy::diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}; #[bevy_app] fn build_app(app: &mut App) { app.add_plugins(( FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin::default(), )); // Your custom sync systems add_transform_sync_systems! { app, OptimizedEntities = With<Player>, } } }
Syntax Reference
The macro supports three sync directions:
#![allow(unused)] fn main() { add_transform_sync_systems! { app, // Bidirectional sync (default) EntityName = With<Component>, // One-way: ECS → Godot only EntityName = bevy_to_godot: With<Component>, // One-way: Godot → ECS only EntityName = godot_to_bevy: With<Component>, } }
You can mix multiple directions in a single macro call, and use any Bevy query filter:
#![allow(unused)] fn main() { add_transform_sync_systems! { app, PhysicsBodies = Or<(With<CharacterBody2DMarker>, With<RigidBody2DMarker>)>, UIElements = bevy_to_godot: (With<UIElement>, Without<Disabled>), PlayerInputs = godot_to_bevy: With<PlayerInput>, } }
Input Handling
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 Transform, With<Player>>, ) { for mut transform in query.iter_mut() { if keys.pressed(KeyCode::ArrowLeft) { transform.translation.x -= 200.0; } if keys.pressed(KeyCode::ArrowRight) { transform.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: MessageReader<ActionInput>, mut query: Query<&mut Transform, 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
ActionInputevents 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: MessageReader<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: MessageReader<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: MessageReader<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: MessageReader<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: MessageReader<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.
Signal Handling
Godot signals are a core communication mechanism in the Godot engine. godot-bevy bridges those signals into Bevy observers so your ECS systems can react to UI, gameplay, and scene-tree events in a type-safe, reactive way.
Outline
- Quick Start
- Multiple Signal Events
- Passing Context (Node, Entity, Arguments)
- Deferred Connections
- Attaching signals to Godot scenes
Quick Start
- Define a Bevy event for your signal:
#![allow(unused)] fn main() { use bevy::prelude::*; use godot_bevy::prelude::*; #[derive(Event, Debug, Clone)] struct StartGameRequested; }
- Register the signals plugin for your event type:
#![allow(unused)] fn main() { fn build_app(app: &mut App) { app.add_plugins(GodotSignalsPlugin::<StartGameRequested>::default()); } }
- Connect a Godot signal and map it to your event:
#![allow(unused)] fn main() { fn connect_button( buttons: Query<&GodotNodeHandle, With<Button>>, signals: GodotSignals<StartGameRequested>, ) { for handle in &buttons { signals.connect( *handle, "pressed", None, |_args, _node_handle, _ent| Some(StartGameRequested), ); } } }
- React to the event with an observer:
#![allow(unused)] fn main() { fn setup(app: &mut App) { app.add_observer(on_start_game); } fn on_start_game( _trigger: On<StartGameRequested>, mut next_state: ResMut<NextState<GameState>>, ) { next_state.set(GameState::Playing); } }
Observers fire immediately when the signal is received, giving you reactive, push-based event handling rather than polling each frame.
Multiple Signal Events
Use one plugin per event type. You can map the same Godot signal to multiple typed events if you like:
#![allow(unused)] fn main() { #[derive(Event, Debug, Clone)] struct ToggleFullscreen; #[derive(Event, Debug, Clone)] struct QuitRequested { source: GodotNodeHandle } fn setup(app: &mut App) { app.add_plugins(GodotSignalsPlugin::<ToggleFullscreen>::default()) .add_plugins(GodotSignalsPlugin::<QuitRequested>::default()) .add_observer(on_toggle_fullscreen) .add_observer(on_quit); } fn connect_menu( menu: Query<(&GodotNodeHandle, &MenuTag)>, toggle: GodotSignals<ToggleFullscreen>, quit: GodotSignals<QuitRequested>, ) { for (button, tag) in &menu { match tag { MenuTag::Fullscreen => { toggle.connect( *button, "pressed", None, |_a, _node_handle, _e| Some(ToggleFullscreen), ); } MenuTag::Quit => { quit.connect( *button, "pressed", None, |_a, node_handle, _e| Some(QuitRequested { source: node_handle }), ); } } } } fn on_toggle_fullscreen(_trigger: On<ToggleFullscreen>, mut godot: GodotAccess) { // Toggle fullscreen } fn on_quit(_trigger: On<QuitRequested>) { // Quit the game } }
Passing Context (Node, Entity, Arguments)
The mapper closure receives:
args: &[Variant]: raw Godot arguments (clone if you need detailed parsing)node_handle: GodotNodeHandle: emitting node handle (use it later withGodotAccess)entity: Option<Entity>: Bevy entity if you passedSome(entity)toconnect
Important: the mapper runs inside the Godot signal callback. Do not call Godot APIs in the mapper; resolve the node_handle in an observer or system with GodotAccess on the main thread. Connections are queued and applied on the main thread; connections made during a frame take effect on the next frame. If you need same-frame connection, use connect_immediate with a GodotAccess parameter. See Thread Safety and Godot APIs.
Example including the entity in the event:
#![allow(unused)] fn main() { #[derive(Event, Debug, Clone, Copy)] struct AreaExited { entity: Entity } fn connect_area( q: Query<(Entity, &GodotNodeHandle), With<Area2D>>, signals: GodotSignals<AreaExited>, ) { for (entity, area) in &q { signals.connect( *area, "body_exited", Some(entity), |_a, _node_handle, e| Some(AreaExited { entity: e.unwrap() }), ); } } fn on_area_exited(trigger: On<AreaExited>, mut commands: Commands) { let entity = trigger.event().entity; commands.entity(entity).despawn(); } }
Deferred Connections
When spawning entities before their GodotNodeHandle is ready, you can defer connections. Add DeferredSignalConnections<T> with a signal-to-event mapper; the GodotTypedSignalsPlugin<T> wires it once the handle appears.
#![allow(unused)] fn main() { #[derive(Component)] struct MyArea; #[derive(Event, Debug, Clone, Copy)] struct BodyEntered { entity: Entity } fn setup(app: &mut App) { app.add_plugins(GodotSignalsPlugin::<BodyEntered>::default()) .add_observer(on_body_entered); } fn spawn_area(mut commands: Commands) { commands.spawn(( MyArea, // Defer until GodotNodeHandle is available on this entity DeferredSignalConnections::<BodyEntered>::with_connection( "body_entered", |_a, _node_handle, e| Some(BodyEntered { entity: e.unwrap() }), ), )); } fn on_body_entered(trigger: On<BodyEntered>) { println!("Body entered area on entity {:?}", trigger.event().entity); } }
Attaching signals to Godot scenes
When spawning an entity associated with a Godot scene, you can schedule
signals to be connected to children of the scene once the scene is spawned.
When inserting a GodotScene resource, use the with_signal_connection builder method to schedule connections.
The method arguments are similar to other typed signal constructors such as connect:
node_path- Path relative to the scene root (e.g., "VBox/MyButton" or "." for root node). Argument supports the same syntax as Node.get_node.signal_name- Name of the Godot signal to connect (e.g., "pressed").mapper- Closure that maps signal arguments to your typed event.- The closure receives three arguments:
args,node_handle, andentity:args: &[Variant]: raw Godot arguments (clone if you need detailed parsing).node_handle: GodotNodeHandle: emitting node handle.entity: Option<Entity>: Bevy entity the GodotScene component is attached to (Always Some).
- The closure returns an optional Bevy Event, or None to not send the event.
- The closure receives three arguments:
impl Command for SpawnPickup {
fn apply(self, world: &mut World) -> () {
let assets = world.get_resource::<PickupAssets>().cloned();
let mut pickup = world.spawn_empty();
pickup
.insert(Name::new("Pickup"))
.insert(Transform::from_xyz(200.0, 200.0, 0.0));
// Only insert GodotScene if Godot engine is running; useful when running tests without Godot.
if let Some(assets) = assets {
pickup.insert(
GodotScene::from_handle(assets.scene.clone())
// Schedule the "area_entered" signal on the Area2D child
// to be connected to PickupAreaEntered event
.with_signal_connection(
"Area2D",
"area_entered",
|_args, _node_handle, _entity| {
// Pickup "area_entered" signal mapped
Some(PickupAreaEntered)
},
),
);
}
}
}
For physics signals (collisions), use the collisions plugin/events instead of raw signals when possible.
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
FirstPreUpdateUpdateFixedUpdatePostUpdateLast
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 Transform>, ) { 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 Transform>, ) { 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 anyStartupsystems run. This means you can safely query Godot scene entities in yourStartupsystems! See Scene Tree Initialization and Timing for details.
Thread Safety and Godot APIs
Some Godot APIs are not thread-safe and must be called exclusively from the main thread. This creates an important constraint when working with Bevy's multi-threaded ECS, where systems typically run in parallel across multiple threads. For additional details, see Thread-safe APIs — Godot Engine.
The Main Thread Requirement
Any system that interacts with Godot APIs—such as calling methods on Node, accessing scene tree properties, or manipulating UI elements—must run on the main thread. This includes:
- Scene tree operations (
add_child,queue_free, etc.) - Transform modifications on Godot nodes
- UI updates (setting text, visibility, etc.)
- Audio playback controls
- Input handling via Godot's
Inputsingleton - File I/O operations through Godot's resource system
Main-thread access with GodotAccess
Use the GodotAccess SystemParam whenever you need to call Godot APIs. It carries a NonSend guard, so any system that includes it is scheduled on the main thread:
#![allow(unused)] fn main() { use godot_bevy::prelude::*; fn update_ui_labels( query: Query<&GodotNodeHandle, With<PlayerStats>>, stats: Res<GameStats>, mut godot: GodotAccess, ) { for handle in &query { if let Some(mut label) = godot.try_get::<Label>(*handle) { label.set_text(&format!("Score: {}", stats.score)); } } } }
SceneTreeRef is also a NonSend SystemParam. If a system already takes SceneTreeRef, it is pinned to the main thread and you do not need an extra GodotAccess parameter unless you actually call Godot APIs.
Best Practices: Minimize Systems That Call Godot APIs
While GodotAccess makes Godot API access explicit, systems that use it cannot execute in parallel with other main thread-assigned systems. This can become a performance bottleneck in complex applications, as all systems requiring Godot API access must wait their turn to execute sequentially on this single thread.
Recommended Architecture
The most efficient approach is to minimize main thread systems by using an event-driven architecture:
- Multi-threaded systems handle game logic and emit events
- Main thread systems consume events and update Godot APIs
Benefits of Event-Driven Architecture
- Better parallelization: Core game logic runs on multiple threads
- Cleaner separation: Business logic decoupled from presentation layer
- Easier testing: Game logic systems can be tested without Godot APIs
- Reduced main thread contention: Fewer systems competing for main thread time
Profiling
Godot-Bevy, together with Bevy native, supports several methods of profiling. In this article, we'll discuss using Tracy. We recommend you read Bevy's profiling doc first.
Instructions
- In your
Cargo.toml, underdependenciesadd necessary tracy dependencies, e.g.:
[dependencies]
tracing = "0.1"
tracing-tracy = { version = "0.11.4", default-features = false, features = [
"enable",
"manual-lifetime",
"ondemand",
"broadcast", # announce presence
], optional = true }
- In your
Cargo.toml, underfeaturesadd atrace_tracy(feel free to rename it):
[features]
trace_tracy = ["dep:tracing-tracy", "godot-bevy/trace_tracy"]
- Install Tracy, see
https://github.com/bevyengine/bevy/blob/main/docs/profiling.md for details on
picking the correct version to install. As of July 2025, you need Tracy
Profiler
0.12.2, which you can obtain from The official site. Alternatively, you can use the zig-built version, which makes it much easier to build c binaries across platforms, see https://github.com/allyourcodebase/tracy - Once built, run the Tracy Profiler (
tracy-profiler), and hit theConnectbutton so it's listening/ready to receive real time data from your game - Build your game. You can use either dev or release, both work, though we recommend release since you'll still get symbol resolution and your profiling numbers will reflect what you're actually shipping in addition to being much faster than a dev build.
- Run your game, you should see real time data streaming into the Tracy profiler GUI.
- For a complete example of this in action, see our Bevy Boids example
Notes
Note for version 0.9.3+: The
check-cfgworkaround is no longer needed. Tracy integration has been refactored to prevent dependency leaks.
Debugging
Godot-bevy includes a built-in entity inspector that displays your Bevy ECS state directly in the Godot editor. When running your game, you can see all entities, their components, and parent-child relationships in real time.
Entity Inspector
The inspector appears as a "Entities" tab next to the Scene tab in the editor's left dock. It shows:
- All Bevy entities with their names and appropriate icons
- Entity hierarchy (scene tree via
GodotChildOf/GodotChildren) - Components attached to each entity with type-specific icons
- Entities with Godot nodes show their node type icon (e.g., Node2D, Sprite2D)
Enabling the Inspector
The inspector is included in GodotDefaultPlugins:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { app.add_plugins(GodotDefaultPlugins); } }
Or add it individually:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { app.add_plugins(GodotDebuggerPlugin); } }
Configuration
Control the inspector through the DebuggerConfig resource:
#![allow(unused)] fn main() { fn configure_debugger(mut config: ResMut<DebuggerConfig>) { config.enabled = true; // Toggle on/off config.update_interval = 0.5; // Seconds between updates } }
Using the Inspector
- Open your project in Godot
- Look for the "Bevy" tab in the left dock (next to Scene/Import)
- Run your game
- The inspector populates with your ECS state
Entities display as a tree. Click to expand and see:
- Child entities (nested under parents)
- Components (shown in blue, with full type path on hover)
Entity icons indicate the Godot node type when a marker component is present (e.g., Node2DMarker shows the Node2D icon). Entities with a GodotNodeHandle but no specific marker show the Godot logo.
Debugging Hierarchy Issues
The inspector mirrors the Godot scene tree via GodotChildOf/GodotChildren, not Bevy's
built-in ChildOf/Children. If an entity appears at the wrong level:
- Verify the Godot node was in the scene tree when the entity was created
- If you reparent nodes, wait a frame for the hierarchy update to process
Performance Considerations
The inspector sends data every 0.5 seconds by default. For games with thousands of entities, you may want to increase the interval or disable it in release builds:
#![allow(unused)] fn main() { fn setup_debugger(mut config: ResMut<DebuggerConfig>) { #[cfg(debug_assertions)] { config.enabled = true; config.update_interval = 1.0; // Slower updates for large scenes } #[cfg(not(debug_assertions))] { config.enabled = false; } } }
Integration Testing
Testing game logic can be tricky. Unit tests work great for pure Rust functions, but godot-bevy code interacts deeply with both Godot's runtime and Bevy's ECS. That's where integration testing comes in.
The godot-bevy-test crate provides a framework for writing tests that run inside Godot with real frame progression. Your tests have full access to both Bevy's ECS and Godot's scene tree, letting you verify that your game logic actually works in the runtime environment.
Why Integration Tests?
Consider testing a player movement system:
#![allow(unused)] fn main() { fn player_movement( time: Res<Time>, mut query: Query<(&mut Transform, &GodotNodeHandle), With<Player>>, ) { for (mut transform, handle) in query.iter_mut() { transform.translation.x += 100.0 * time.delta_secs(); } } }
A unit test can't verify this works correctly because:
Timecomes from Bevy's runtimeGodotNodeHandlerequires a real Godot node- Transform sync needs Godot's scene tree
- Frame timing depends on Godot's main loop
Integration tests solve this by running your code in the actual Godot environment.
Setting Up Integration Tests
1. Create a Test Crate
Integration tests live in a separate crate that compiles to a GDExtension library:
# my-game-tests/Cargo.toml
[package]
name = "my-game-tests"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
godot = "0.4"
godot-bevy = "0.9"
godot-bevy-test = "0.9"
bevy = { version = "0.17", default-features = false }
# Import your game crate to test its systems
my-game = { path = "../my-game" }
2. Create the Test Entry Point
#![allow(unused)] fn main() { // my-game-tests/src/lib.rs use godot::init::{ExtensionLibrary, gdextension}; use godot_bevy_test::prelude::*; // This macro creates the Godot class that runs tests godot_bevy_test::declare_test_runner!(); // Include test modules mod movement_tests; mod combat_tests; #[gdextension(entry_symbol = my_game_tests)] unsafe impl ExtensionLibrary for IntegrationTests {} }
3. Set Up the Godot Project
Create a minimal Godot project to run the tests:
my-game-tests/
├── rust/
│ └── ... (your test crate)
└── godot/
├── project.godot
└── my-game-tests.gdextension
Your .gdextension file should point to your test library:
[configuration]
entry_symbol = "my_game_tests"
compatibility_minimum = 4.3
[libraries]
linux.debug.x86_64 = "res://../target/debug/libmy_game_tests.so"
macos.debug = "res://../target/debug/libmy_game_tests.dylib"
windows.debug.x86_64 = "res://../target/debug/my_game_tests.dll"
Writing Tests
Basic Async Test
Most tests are async because they need to wait for Godot frames:
#![allow(unused)] fn main() { use godot_bevy_test::prelude::*; use bevy::prelude::*; #[itest(async)] fn test_entity_spawns(ctx: &TestContext) -> godot::task::TaskHandle { godot::task::spawn(async move { // Create a test app with your plugins let mut app = TestApp::new(&ctx, |app| { app.add_plugins(MyGamePlugin); }).await; // Run one frame to initialize app.update().await; // Spawn an entity app.with_world_mut(|world| { world.spawn((Player::default(), Transform::default())); }); // Run another frame app.update().await; // Verify the entity exists let count = app.with_world(|world| { world.query::<&Player>().iter(world).count() }); assert_eq!(count, 1, "Player should exist"); }) } }
Using bevy_app_test! for Quick Tests
For simpler tests, the bevy_app_test! macro reduces boilerplate:
#![allow(unused)] fn main() { #[itest(async)] fn test_system_runs_each_frame(ctx: &TestContext) -> godot::task::TaskHandle { bevy_app_test!(ctx, counter, |app| { #[derive(Resource)] struct FrameCount(Counter); app.insert_resource(FrameCount(counter.clone())); app.add_systems(Update, |count: Res<FrameCount>| { count.0.increment(); }); }, async { await_frames(5).await; assert!(counter.get() >= 4, "System should run each frame"); }) } }
Skip and Focus
During development, you can skip or focus tests:
#![allow(unused)] fn main() { #[itest(skip)] // Skip this test fn test_not_ready_yet(ctx: &TestContext) { // Work in progress } #[itest(focus)] // Only run focused tests fn test_debugging_this(ctx: &TestContext) { // When any test has focus, only focused tests run } #[itest(async, skip)] // Combine attributes fn test_flaky(ctx: &TestContext) -> godot::task::TaskHandle { // ... } }
Running Tests
Build and run tests in headless mode:
cd my-game-tests
# Build the test library
cargo build
# Run tests (adjust path to your Godot binary)
godot4 --headless --path godot --quit-after 5000
You'll see output like:
Run godot-bevy async integration tests...
Found 5 async tests in 2 files.
test_entity_spawns ... ok
test_transform_syncs ... ok
test_system_runs_each_frame ... ok
test_not_ready_yet ... [SKIP]
test_player_movement ... ok
Test result:
4 passed, 1 skipped in 0.42s
All tests passed!
Test Script
Create a shell script for convenience:
#!/bin/bash
# run-tests.sh
set -e
cd "$(dirname "$0")"
cargo build
# Cross-platform temp file for exit code
EXIT_FILE="${TMPDIR:-/tmp}/godot_test_exit_code"
rm -f "$EXIT_FILE"
export GODOT_TEST_EXIT_CODE_PATH="$EXIT_FILE"
godot4 --headless --path godot --quit-after 5000
if [ -f "$EXIT_FILE" ]; then
exit $(cat "$EXIT_FILE")
else
exit 1
fi
Benchmarks
The framework also supports benchmarks:
#![allow(unused)] fn main() { use godot_bevy_test::bench; #[bench] fn benchmark_spawning() -> i32 { // Code to benchmark - must return a value to prevent optimization let mut count = 0; for _ in 0..1000 { count += 1; } count } #[bench(repeat = 50)] // Custom iteration count fn benchmark_expensive_op() -> i32 { expensive_operation(); 42 } }
Run benchmarks with a separate runner:
godot4 --headless --path godot -s addons/godot-bevy/test/BenchRunner.tscn --quit-after 30000
Best Practices
1. Test Real Behavior
Don't mock Godot - use real nodes and scene tree:
#![allow(unused)] fn main() { // Good: Real Godot node let mut node = Node2D::new_alloc(); ctx.scene_tree.clone().add_child(&node); // Bad: Trying to fake it // let fake_handle = GodotNodeHandle::fake(); // Don't do this }
2. Clean Up After Tests
Always clean up nodes you create:
#![allow(unused)] fn main() { #[itest(async)] fn test_with_cleanup(ctx: &TestContext) -> godot::task::TaskHandle { godot::task::spawn(async move { let mut node = Node2D::new_alloc(); ctx.scene_tree.clone().add_child(&node); let mut app = TestApp::new(&ctx, |_| {}).await; // ... test code ... // Clean up: BevyApp first, then nodes app.cleanup(); node.queue_free(); await_frame().await; }) } }
3. Wait for Frame Processing
Operations often need a frame to complete:
#![allow(unused)] fn main() { // Spawn entity app.with_world_mut(|world| { world.spawn(MyComponent); }); // Wait for systems to process app.update().await; // Now query the result }
4. Use TestApp::cleanup() Before Freeing Nodes
If your test creates Godot nodes that are tracked by Bevy, clean up the BevyApp first:
#![allow(unused)] fn main() { // Wrong order: may crash node.queue_free(); app.cleanup(); // Transform sync might access freed node! // Correct order app.cleanup(); // Stop Bevy systems first node.queue_free(); }
Platform Targets
Platform-specific setup guides for godot-bevy projects.
- Android - Android development setup
Android
This guide covers building godot-bevy projects for Android devices. Android development requires cross-compilation from your development machine to ARM64 architecture used by most modern Android devices.
Prerequisites
-
Android NDK - The Native Development Kit provides the compilers and tools needed to build native code for Android. Download from Android NDK Downloads
-
Rust target - Install the Android ARM64 compilation target for Rust:
rustup target add aarch64-linux-android
Step 1: Configure Build Environment
The Rust cc build system needs explicit paths to Android NDK compilers for cross-compilation. This tells Rust which Android-specific compilers to use instead of your system's default compilers.
Option A: Environment Variables
Set these once per terminal session:
export NDK_HOME="/path/to/your/android/ndk"
export CC="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang"
export CXX="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang++"
export AR="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
Option B: Inline Command
Self-contained approach that doesn't modify your shell environment:
CC="$NDK_HOME/..." CXX="$NDK_HOME/..." AR="$NDK_HOME/..." cargo build --target aarch64-linux-android
Step 2: Configure Cargo Linker
Create .cargo/config.toml in your rust/ directory. This tells Cargo how to link the compiled code into Android-compatible libraries:
[target.aarch64-linux-android]
linker = "/path/to/your/android/ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang"
ar = "/path/to/your/android/ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
Step 3: Modify Entry Point
Android requires a specific entry point function name. Change your Bevy app entry point (likely lib.rs) from:
#![allow(unused)] fn main() { #[bevy_app] fn build_bevy_app(app: &mut App) { // ... } }
To:
#![allow(unused)] fn main() { #[bevy_app] #[no_mangle] fn android_main(app: &mut App) { // ... } }
The #[no_mangle] attribute prevents Rust from changing the function name during compilation, ensuring Android can find the entry point.
Step 4: Build for Android
Build your Rust library for Android:
cargo build --target aarch64-linux-android
or use the self-contained command from above if you haven't setup the envs.
This creates a shared library file lib{your_app_name}.so in rust/target/aarch64-linux-android/debug/ that Android can load.
Step 5: Update Godot Configuration
Update rust.gdextension
Tell Godot where to find your Android library by adding these paths to your rust.gdextension file:
[libraries]
# ... existing entries ...
android.debug.arm64 = "res://rust/target/aarch64-linux-android/debug/lib{your_app_name}.so"
android.release.arm64 = "res://rust/target/aarch64-linux-android/release/lib{your_app_name}.so"
Configure Godot Export
- Go to Project → Export...
- Select Android preset
- Under Architectures, ensure arm64-v8a is selected
Note: The Rust target
aarch64-linux-androidcorresponds to Android'sarm64-v8aarchitecture
Export your app and deploy to your Android device.
Migration Guides
This section contains migration guides for various versions.
Migration Guide: v0.10 to v0.11
This guide covers breaking changes and new behavior when upgrading from godot-bevy 0.10.x to 0.11.0.
Table of Contents
Breaking changes:
- Typed signals now use observers
- Collisions API redesigned
- Main-thread access is now explicit via GodotAccess
- GodotNodeHandle is ID-only; GodotNodeId removed
- Legacy untyped signals removed
Breaking: Typed signals now use observers
The typed signals API has been redesigned to use Bevy's observer pattern instead of the Message/MessageReader pattern. This provides more reactive, push-based event handling where signal handlers fire immediately when signals are received.
Type changes
Signal types now derive Event instead of Message:
Before:
#[derive(Message, Debug, Clone)]
struct StartGameRequested;
After:
#[derive(Event, Debug, Clone)]
struct StartGameRequested;
SystemParam renamed
The TypedGodotSignals<T> SystemParam has been renamed to GodotSignals<T>:
Before:
fn connect_button(signals: TypedGodotSignals<StartGameRequested>) {
// ...
}
After:
fn connect_button(signals: GodotSignals<StartGameRequested>) {
// ...
}
Method renamed
The connect_map method has been renamed to connect:
Before:
signals.connect_map(*handle, "pressed", None, |_args, _node, _ent| {
Some(StartGameRequested)
});
After:
signals.connect(*handle, "pressed", None, |_args, _node, _ent| {
Some(StartGameRequested)
});
Handling signals with observers
Replace MessageReader<T> systems with observers using On<T>:
Before:
fn on_start_game(mut reader: MessageReader<StartGameRequested>) {
for _ in reader.read() {
// Start the game
}
}
app.add_systems(Update, on_start_game);
After:
fn on_start_game(
_trigger: On<StartGameRequested>,
mut next_state: ResMut<NextState<GameState>>,
) {
next_state.set(GameState::Playing);
}
app.add_observer(on_start_game);
Note: Observers are registered with app.add_observer(), not app.add_systems().
Deferred connections renamed
The TypedDeferredSignalConnections<T> component has been renamed to DeferredSignalConnections<T>:
Before:
commands.spawn((
MyArea,
TypedDeferredSignalConnections::<BodyEntered>::with_connection(
"body_entered",
|_a, _node, e| Some(BodyEntered(e.unwrap())),
),
));
After:
commands.spawn((
MyArea,
DeferredSignalConnections::<BodyEntered>::with_connection(
"body_entered",
|_a, _node, e| Some(BodyEntered { entity: e.unwrap() }),
),
));
Plugin renamed
The GodotTypedSignalsPlugin<T> has been renamed to GodotSignalsPlugin<T>:
Before:
app.add_plugins(GodotTypedSignalsPlugin::<ButtonPressed>::default());
After:
app.add_plugins(GodotSignalsPlugin::<ButtonPressed>::default());
Summary of changes
| Old API | New API |
|---|---|
#[derive(Message)] | #[derive(Event)] |
GodotTypedSignalsPlugin<T> | GodotSignalsPlugin<T> |
TypedGodotSignals<T> | GodotSignals<T> |
connect_map() | connect() |
connect_map_immediate() | connect_immediate() |
MessageReader<T> | On<T> (observer) |
app.add_systems(Update, handler) | app.add_observer(handler) |
TypedDeferredSignalConnections<T> | DeferredSignalConnections<T> |
Breaking: Collisions API redesigned
The collision API has been redesigned, providing a cleaner separation between querying current collision state and reacting to collision events.
Collisions component removed
The per-entity Collisions component has been replaced with a Collisions system parameter
for querying global collision state.
Before:
fn check_player_death(
player: Query<(&Player, &Collisions)>,
) {
for (player, collisions) in player.iter() {
for &other in collisions.colliding() {
// handle collision
}
for &other in collisions.recent_collisions() {
// handle new collision this frame
}
}
}
After:
fn check_player_death(
player: Query<(Entity, &Player)>,
collisions: Collisions,
) {
for (entity, player) in player.iter() {
// Query current collisions
for &other in collisions.colliding_with(entity) {
// handle collision
}
// Check specific pair
if collisions.contains(entity, enemy) {
// player is touching enemy
}
}
}
New collision events for change detection
To detect when collisions start or end, use the new CollisionStarted and CollisionEnded
events instead of querying per-frame.
As messages:
fn handle_hits(mut started: MessageReader<CollisionStarted>) {
for event in started.read() {
println!("{:?} hit {:?}", event.entity1, event.entity2);
}
}
As observers:
app.add_observer(|trigger: Trigger<CollisionStarted>| {
let event = trigger.event();
println!("{:?} hit {:?}", event.entity1, event.entity2);
});
CollisionMessage removed
The internal CollisionMessage type has been replaced with the public CollisionStarted
and CollisionEnded event types.
Summary of changes
| Old API | New API |
|---|---|
Query<&Collisions> | Collisions system param |
collisions.colliding() | collisions.colliding_with(entity) |
collisions.recent_collisions() | MessageReader<CollisionStarted> |
CollisionMessage | CollisionStarted / CollisionEnded |
Breaking: Main-thread access is now explicit via GodotAccess
The #[main_thread_system] macro has been removed. Systems that call Godot APIs must include
GodotAccess in their parameters. GodotAccess is a NonSend SystemParam that pins the system
to the main thread.
Before:
#[main_thread_system]
fn update_ui(mut q: Query<&mut GodotNodeHandle>) {
for mut handle in &mut q {
if let Some(mut label) = handle.try_get::<Label>() {
label.set_text("Hi");
}
}
}
After:
fn update_ui(q: Query<&GodotNodeHandle>, mut godot: GodotAccess) {
for handle in &q {
if let Some(mut label) = godot.try_get::<Label>(*handle) {
label.set_text("Hi");
}
}
}
Notes:
SceneTreeRefis also aNonSendSystemParam. If a system already takesSceneTreeRefand does not call Godot APIs, you do not need an extraGodotAccessparameter.- If you only have an
InstanceId, useGodotAccess::try_get_instance_idorget_instance_id.
Breaking: GodotNodeHandle is ID-only; GodotNodeId removed
GodotNodeId has been removed. GodotNodeHandle is now a lightweight, copyable wrapper around
an InstanceId.
Before:
struct QuitRequested { source: GodotNodeId }
After:
struct QuitRequested { source: GodotNodeHandle }
Constructors:
Before:
let handle = GodotNodeHandle::from_id(node_id);
After:
let handle = GodotNodeHandle::from_instance_id(node_id);
// or: let handle: GodotNodeHandle = node_id.into();
If you were using GodotNodeHandle::get or try_get directly, switch to
GodotAccess::get or GodotAccess::try_get instead.
Breaking: Legacy untyped signals removed
The untyped signal API has been removed:
GodotSignalsPluginGodotSignalmessageconnect_godot_signal
Use typed signals instead (GodotTypedSignalsPlugin::<T> + GodotSignals<T>).
Migration Checklist
-
Replace
#[derive(Message)]with#[derive(Event)]for signal types. -
Replace
GodotTypedSignalsPlugin<T>withGodotSignalsPlugin<T>. -
Replace
TypedGodotSignals<T>withGodotSignals<T>. -
Replace
connect_map()withconnect(). -
Replace
MessageReader<T>signal handlers with observers usingOn<T>. -
Replace
app.add_systems(Update, handler)withapp.add_observer(handler)for signal handlers. -
Replace
TypedDeferredSignalConnections<T>withDeferredSignalConnections<T>. -
Replace
Query<&Collisions>withCollisionssystem param. -
Replace
collisions.colliding()withcollisions.colliding_with(entity). -
Replace
collisions.recent_collisions()withMessageReader<CollisionStarted>or observer. -
Replace
#[main_thread_system]withGodotAccessparameters where you call Godot APIs. -
Replace
GodotNodeIdwithGodotNodeHandle. -
Replace
handle.getorhandle.try_getwithgodot.getorgodot.try_get. - Replace legacy untyped signal APIs with typed signals.
Migration Guide: v0.9 to v0.10
This guide covers breaking changes and new features when upgrading from godot-bevy 0.9.x to 0.10.0.
Table of Contents
Breaking changes:
- Upgrade to Bevy 0.17
- Custom scene tree relationships
- NodeTreeView::from_node now returns a Result type
connect_mapCallback Now ReturnsOption<T>
Breaking: Upgrade to Bevy 0.17
✨ Godot-Bevy is now up to date with Bevy 0.17! ✨
Here is the Bevy 0.16 to 0.17 migration guide for reference: https://bevy.org/learn/migration-guides/0-16-to-0-17/
The biggest difference is that Godot-Bevy now uses Message instead of Event.
Migration Path
Cargo.toml
🗑️ Before:
bevy = { version = "0.16", default-features = false, features = ["bevy_state"] }
bevy_asset_loader = "0.23.0"
🟢 After:
bevy = { version = "0.17", default-features = false, features = ["bevy_state"] }
bevy_asset_loader = "0.24.0-rc.1"
Event to Message
🗑️ Before:
#[derive(Event, Debug)]
pub enum SceneOperationEvent {
🟢 After:
#[derive(Message, Debug)]
pub enum SceneOperationMessage {
🗑️ Before:
app.add_event::<SceneOperationEvent>()
🟢 After:
app.add_message::<SceneOperationMessage>()
🗑️ Before:
mut operation_events: EventReader<SceneOperationEvent>,
🟢 After:
mut operation_events: MessageReader<SceneOperationMessage>,
Migration Checklist
-
Update Bevy to
0.17. -
Update Bevy Asset Loader to
0.24.0-rc.1. -
Replace usages of
EventwithMessage.
Breaking: Custom Scene Tree Relationships
Godot's scene tree is now represented by a custom ECS relationship:
GodotChildOf / GodotChildren. The built-in Bevy ChildOf / Children
relationship is no longer used for scene tree mirroring.
This avoids conflicts with other plugins that use Bevy's hierarchy for their own purposes (physics, AI, scene graphs, etc.).
Migration Path
Queries and Traversal
🗑️ Before:
fn parent_of(entity: Entity, query: Query<&ChildOf>) -> Option<Entity> {
query.get(entity).ok().map(|parent| parent.parent())
}
fn children_of(entity: Entity, query: Query<&Children>) -> Vec<Entity> {
query
.get(entity)
.map(|children| children.iter().copied().collect())
.unwrap_or_default()
}
🟢 After:
fn parent_of(entity: Entity, query: Query<&GodotChildOf>) -> Option<Entity> {
query.get(entity).ok().map(|parent| parent.get())
}
fn children_of(entity: Entity, query: Query<&GodotChildren>) -> Vec<Entity> {
query
.get(entity)
.map(|children| children.iter().copied().collect())
.unwrap_or_default()
}
Configuration
The scene_tree_add_child_relationship attribute and
GodotSceneTreePlugin { add_child_relationship: ... } have been removed.
If you want children to outlive their parents, use the new
scene_tree_auto_despawn_children attribute (or the plugin config).
🗑️ Before:
#[bevy_app(scene_tree_add_child_relationship = false)]
fn build_app(app: &mut App) {}
app.add_plugins(GodotSceneTreePlugin {
add_child_relationship: true,
});
🟢 After:
#[bevy_app(scene_tree_auto_despawn_children = false)]
fn build_app(app: &mut App) {}
app.add_plugins(GodotSceneTreePlugin {
auto_despawn_children: true,
});
Breaking changes
- Bevy
ChildOf/Childrenare no longer used for Godot scene tree hierarchy. scene_tree_add_child_relationshipwas removed.- New
scene_tree_auto_despawn_childrenconfiguration option.
Migration Checklist
-
Replace
ChildOf/Childrenqueries withGodotChildOf/GodotChildren. -
Remove
scene_tree_add_child_relationshipusage. -
Use
scene_tree_auto_despawn_childrenif you need to keep children alive on parent despawn.
Breaking: NodeTreeView::from_node now returns a Result type
NodeTreeView::from_node derive macro now returns a Result<Self, NodeTreeViewError> type to avoid surprise panic in
user code if a godot node is not found.
Migration Path
Use match, if let or unwrap to handle the Result type returned by NodeTreeView::from_node.
Example 1:
🗑️ Before:
let mut mob_nodes = MobNodes::from_node(mob);
🟢 After:
let mut mob_nodes = MobNodes::from_node(mob).unwrap();
Example 2:
🗑️ Before:
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| MenuUi::from_node(root))) {
Ok(menu_ui) => {
info!("MainMenu: Successfully found menu nodes");
}
Err(_) => {
debug!("MainMenu: Menu nodes not ready yet, will retry next frame");
}
}
🟢 After:
match MenuUi::from_node(root) {
Ok(menu_ui) => {
info!("MainMenu: Successfully found menu nodes");
}
Err(_) => {
debug!("MainMenu: Menu nodes not ready yet, will retry next frame");
}
}
Breaking changes
Return type of NodeTreeView::from_node derive macro changed from Self to Result<Self, NodeTreeViewError>.
Migration Checklist
-
Use
match,if letorunwrapto handle theResulttype returned byNodeTreeView::from_node.
Breaking: connect_map Callback Now Returns Option<T>
The callback provided to TypedGodotSignals::connect_map (and related APIs)
must now return Option<T> instead of T. When an event is emitted,
return Some(event) or return None to suppress event emission. This applies
to both immediate (connect_map) and deferred
(TypedDeferredSignalConnections) signal connections.
This enables advanced uses like filtering certain signal events, but requires
code update for all users of connect_map and deferred connection mappers.
Migration Path
🗑️ Before (old API: return event value directly):
typed.connect_map(&mut node, "signal", None, |_args, _node_id, _ent| MyEvent {});
// or for deferred:
TypedDeferredSignalConnections::<MyEvent>::with_connection("signal", |_a, _node_id, _e| MyEvent {});
🟢 After (new API: return event value inside Some, or None to suppress):
typed.connect_map(&mut node, "signal", None, |_args, _node_id, _ent| Some(MyEvent {}));
// or for deferred:
TypedDeferredSignalConnections::<MyEvent>::with_connection("signal", |_a, _node_id, _e| Some(MyEvent {}));
Migration Checklist
-
Update all usages of
TypedGodotSignals<...>::connect_mapto returnSome(event)(orNoneto suppress event). -
Update all
TypedDeferredSignalConnections::<...>::with_connectionmappers for typed deferred signals to returnOption<T>.
Migration Guide: v0.8 to v0.9
This guide covers breaking changes and new features when upgrading from godot-bevy 0.8.x to 0.9.0.
Table of Contents
- Godot Bevy now uses standard Bevy Transforms (Breaking Change)
- Assets Plugin Moved to Optional (Breaking Change)
- Gamepad Support Now Optional
- Scene Tree Plugin Configuration Simplified
Godot Bevy now uses standard Bevy Transforms (Breaking Change)
What Changed
In v.0.9.0, we've made significant changes to how we use bevy Transform components. We now operate directly on standard
Transform components and you can too, whereas before, we had wrapped the Transform component in higher level
Transform2D and Transform3D components and required you to use the wrappers. While wrapping provided important
benefits (change detection, dual-godot/bevy-API access with built-in multi-threaded safety) it came with some notable
drawbacks (incompatible with other bevy ecosystem plugins that operate directly on Transforms, extra memory overhead,
less ergonomic as it required extra API calls to access the underlying data).
Migration Path
The main change is switching all of your usages of godot_bevy::prelude::Transform2D or
godot_bevy::prelude::Transform3D to bevy::transform::components::Transform.
Before (v.0.9.0)
#![allow(unused)] fn main() { fn orbit_system( // The `transform` parameter is a Bevy `Query` that matches all `Transform2D` components. // `Transform2D` is a Godot-Bevy-provided component that matches all Node2Ds in the scene. // (https://docs.rs/godot-bevy/latest/godot_bevy/plugins/core/transforms/struct.Transform2D.html) mut transform: Query<(&mut Transform2D, &InitialPosition, &mut Orbiter)>, // This is equivalent to Godot's `_process` `delta: float` parameter. process_delta: Res<Time>, ) { // For single matches, you can use `single_mut()` instead: // `if let Ok(mut transform) = transform.single_mut() {` for (mut transform, initial_position, mut orbiter) in transform.iter_mut() { transform.as_godot_mut().origin = initial_position.pos + Vector2::from_angle(orbiter.angle) * 100.0; orbiter.angle += process_delta.as_ref().delta_secs(); orbiter.angle %= 2.0 * PI; } } }
After (v.0.9.0)
#![allow(unused)] fn main() { fn orbit_system( // The `transform` parameter is a Bevy `Query` that matches all `Transform` components. // `Transform` is a Godot-Bevy-provided component that matches all Node2Ds in the scene. // (https://docs.rs/godot-bevy/latest/godot_bevy/plugins/core/transforms/struct.Transform.html) mut transform: Query<(&mut Transform, &InitialPosition, &mut Orbiter)>, // This is equivalent to Godot's `_process` `delta: float` parameter. process_delta: Res<Time>, ) { // For single matches, you can use `single_mut()` instead: // `if let Ok(mut transform) = transform.single_mut() {` for (mut transform, initial_position, mut orbiter) in transform.iter_mut() { let position2d = initial_position.pos + Vector2::from_angle(orbiter.angle) * 100.0; transform.translation.x = position2d.x; transform.translation.y = position2d.y; orbiter.angle += process_delta.as_ref().delta_secs(); orbiter.angle %= 2.0 * PI; } } }
Breaking changes
godot_bevy::prelude::Transform2Dandgodot_bevy::prelude::Transform3Dwere removed
Migration Checklist
-
Transform components changed: Replaced
godot_bevy::prelude::Transform2Dandgodot_bevy::prelude::Transform3Dwithbevy::transform::components::Transform. The APIs from the former must be mapped to the latter:-
Remove the now extra
as_bevy()andas_bevy_mut()calls, since you're operating directly on bevy Transforms, e.g.,transform.as_bevy_mut().translation.x->transform.translation.x. These changes should be easy. -
Remap the
as_godot()andas_godot_mut()calls. These changes may be tricky, as there may not be direct replacements for all Godot APIs in native Bevy transforms. One important benefit of doing this work is that it promotes a clean separation where your bevy transform systems remain portable to other Bevy projects (with or without godot-bevy). You can always fall back on using GodotNodeHandle to get at the original Godot Node APIs, then replicate position, scale, and rotation back to the bevy Transform as necessary.
-
Remove the now extra
Assets Plugin Moved to Optional (Breaking Change)
What Changed
In v0.9.0, GodotAssetsPlugin has been moved from GodotCorePlugins (included by default) to GodotDefaultPlugins (optional). This change provides a cleaner architecture where core functionality is truly minimal and reduces runtime overhead for applications that don't need to load Godot resources through Bevy's asset system.
Who Is Affected
You are affected if:
- You use
GodotCorePluginsdirectly (withoutGodotDefaultPlugins) - You load Godot resources using
Handle<GodotResource>orAssetServer - You use
GodotAudioPluginorGodotPackedScenePlugin(they require assets)
Migration Path
If you use GodotDefaultPlugins
No changes needed - GodotAssetsPlugin is included in GodotDefaultPlugins.
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { app.add_plugins(GodotDefaultPlugins); // ✅ Assets included } }
If you use GodotCorePlugins and need asset loading
Add GodotAssetsPlugin explicitly:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { // GodotCorePlugins no longer includes assets app.add_plugins(GodotAssetsPlugin) // Add this line .add_plugins(GodotAudioPlugin) // Requires GodotAssetsPlugin .add_plugins(GodotPackedScenePlugin); // Requires GodotAssetsPlugin } }
If you don't need asset loading
No changes needed - enjoy reduced runtime overhead!
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { // Now truly minimal - no asset loading overhead app.add_plugins(GodotTransformSyncPlugin) .add_plugins(GodotSignalsPlugin) .add_plugins(BevyInputBridgePlugin); } }
Breaking Changes
GodotCorePluginsno longer includesGodotAssetsPluginGodotAssetsPluginis now inGodotDefaultPluginsGodotAudioPluginandGodotPackedScenePluginrequireGodotAssetsPluginto function
Migration Checklist
- Using GodotDefaultPlugins: No action needed
-
Using GodotCorePlugins + asset loading: Add
app.add_plugins(GodotAssetsPlugin) -
Using GodotAudioPlugin: Ensure
GodotAssetsPluginis included -
Using GodotPackedScenePlugin: Ensure
GodotAssetsPluginis included - Pure ECS without assets: Consider removing unused plugins for better runtime performance
Gamepad Support Now Optional
What Changed
In v0.9.0, gamepad support through Bevy's GilrsPlugin is now controlled by an optional feature flag bevy_gamepad. This feature is enabled by default but can be disabled to reduce compile time and dependencies for applications that don't use gamepads.
Migration Path
If you use gamepads with Bevy's input API
No changes needed if using GodotDefaultPlugins - GilrsPlugin is included automatically.
If using custom plugin setup: Add GilrsPlugin manually:
#![allow(unused)] fn main() { use godot_bevy::prelude::*; // Includes bevy_prelude::GilrsPlugin #[bevy_app] fn build_app(app: &mut App) { app.add_plugins(GilrsPlugin) // Available via bevy_prelude .add_plugins(BevyInputBridgePlugin); } }
If you only use Godot's gamepad input
No changes needed - Godot's gamepad support through GodotInputEventPlugin works regardless of the feature flag.
If you don't use gamepads at all
Optional: Disable the feature for faster compile times:
[dependencies]
godot-bevy = { version = "0.9", default-features = false, features = [...] }
What Still Works Without the Feature
- ✅ Godot's gamepad input via
GodotInputEventPlugin - ✅ Raw gamepad events in
EventReader<GamepadButtonInput>andEventReader<GamepadAxisInput> - ✅ All keyboard, mouse, and touch input
What Requires the Feature
- ❌ Bevy's standard gamepad API (
ButtonInput<GamepadButton>,Axis<GamepadAxis>) - ❌
GilrsPluginfunctionality - ❌ Cross-platform gamepad detection outside of Godot
Note: GilrsPlugin is included in GodotDefaultPlugins when the feature is enabled, but must be added manually if using a custom plugin configuration.
Scene Tree Plugin Configuration Simplified
What Changed
The add_transforms configuration option has been removed from GodotSceneTreePlugin. Transform components are now automatically added to scene tree entities when the GodotTransformSyncPlugin is included in your app.
Migration Path
If you were using the add_transforms configuration option, you can simply remove it. Transform components will be automatically added if you include the transform plugin.
Before (v0.8.x)
#![allow(unused)] fn main() { app.add_plugins(GodotSceneTreePlugin { add_transforms: true, add_child_relationship: true, }); }
After (v0.9.0)
#![allow(unused)] fn main() { // Transform components are automatically added when GodotTransformSyncPlugin is included app.add_plugins(GodotSceneTreePlugin { add_child_relationship: true, }); // Add the transform plugin to get automatic transform components app.add_plugins(GodotTransformSyncPlugin::default()); }
Migration Guide: v0.7 to v0.8
This guide covers breaking changes and new features when upgrading from godot-bevy 0.7.x to 0.8.0.
Table of Contents
- Opt-in Plugin System (Breaking Change)
- GodotSignals Resource (Breaking Change)
- Multithreaded Bevy and #[main_thread_system] (New Feature)
- BevyBundle Enhanced Property Mapping (New Feature)
Opt-in Plugin System (Breaking Change)
What Changed
In v0.8.0, godot-bevy has adopted Bevy's philosophy of opt-in plugins. This gives users granular control over which features are included in their build.
Breaking Change: GodotPlugin now only includes minimal core functionality by default (basic scene tree access and assets). Automatic entity creation and other features must be explicitly opted-in.
Migration Path
The quickest migration is to use GodotDefaultPlugins for the old behavior, but we recommend adding only the plugins you need.
Option 1: Quick Migration (Old Behavior)
Replace the #[bevy_app] macro usage with explicit plugin registration:
Before (v0.7.x):
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { // GodotPlugin automatically included all features: // - Automatic entity creation for scene tree nodes // - Transform synchronization // - Collision detection // - Signal handling // - Input events // - Audio system app.add_systems(Update, my_game_systems); } }
After (v0.8.0) - Quick Fix:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { // Add all features like before app.add_plugins(GodotDefaultPlugins); app.add_systems(Update, my_game_systems); } }
Option 2: Recommended - Add Only What You Need
Pure ECS game (transforms + basic features):
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { app.add_plugins(GodotSceneTreeMirroringPlugin { add_transforms: true, }) .add_plugins(GodotTransformSyncPlugin::default()) // OneWay sync .add_plugins(GodotAudioPlugin); // Audio system app.add_systems(Update, my_game_systems); } }
Platformer (no transform conflicts):
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { app.add_plugins(GodotSceneTreeMirroringPlugin { add_transforms: false, // Use Godot physics instead }) .add_plugins(GodotCollisionsPlugin) // Collision detection .add_plugins(GodotAudioPlugin) // Audio system .add_plugins(GodotSignalsPlugin); // UI signals app.add_systems(Update, my_game_systems); } }
Full-featured game:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { app.add_plugins(GodotDefaultPlugins); // Everything app.add_systems(Update, my_game_systems); } }
Available Plugins
Core (Always Included):
GodotCorePlugins- Basic scene tree access, assets, basic setup (automatically included by#[bevy_app])
Scene Tree Plugins:
GodotSceneTreeRefPlugin- Basic scene tree access (included inGodotCorePlugins)GodotSceneTreeEventsPlugin- Monitor scene tree changes without creating entitiesGodotSceneTreeMirroringPlugin- Auto-create entities for scene nodes (equivalent to v0.7.x behavior)
Optional Feature Plugins:
GodotTransformSyncPlugin- Add if you want to move/position nodes from Bevy systemsGodotAudioPlugin- Add if you want to play sounds and music from Bevy systemsGodotSignalsPlugin- Add if you want to respond to Godot signals (button clicks, etc.) in Bevy systemsGodotCollisionsPlugin- Add if you want to detect collisions and physics events in Bevy systemsGodotInputEventPlugin- Add if you want to handle input from Godot in Bevy systemsBevyInputBridgePlugin- Add if you prefer Bevy's input API (auto-includesGodotInputEventPlugin)GodotPackedScenePlugin- Add if you want to spawn scenes dynamically from Bevy systems
Convenience Bundles:
GodotDefaultPlugins- All plugins enabled (equivalent to old v0.7.x behavior)
Plugin Dependencies
Some plugins automatically include their dependencies:
GodotSceneTreeMirroringPluginautomatically includesGodotSceneTreeEventsPluginBevyInputBridgePluginautomatically includesGodotInputEventPlugin
Benefits of the New System
- Smaller binaries - Only compile what you use
- Better performance - Skip unused systems
- Clearer dependencies - Explicit about what your game needs
- Future-proof - Easy to add new optional features
Migration Checklist
-
Quick fix: Add
app.add_plugins(GodotDefaultPlugins)to yourbuild_appfunction -
Optimization: Replace
GodotDefaultPluginswith only the specific plugins you need - Test: Ensure all features work correctly with your plugin selection
- Consider: Whether you can disable some features (e.g., transform sync for physics games)
GodotSignals Resource (Breaking Change)
What Changed
In v0.8.0, the signal connection system has been significantly simplified and improved:
New GodotSignals SystemParam: Signal connections are now handled through a dedicated GodotSignals resource
Migration Path
The main change is switching from the standalone connect_godot_signal function to the new GodotSignals SystemParam.
Before (v0.7.x)
#![allow(unused)] fn main() { use godot_bevy::prelude::*; fn connect_signals( mut scene_tree: SceneTreeRef, ) { if let Some(root) = scene_tree.get().get_root() { if let Some(button) = root.try_get_node_as::<Button>("UI/MyButton") { let mut handle = GodotNodeHandle::from_instance_id(button.instance_id()); // Old function signature required SceneTreeRef parameter connect_godot_signal(&mut handle, "pressed", &mut scene_tree); } } } }
After (v0.8.0)
#![allow(unused)] fn main() { use godot_bevy::prelude::*; fn connect_signals( mut scene_tree: SceneTreeRef, signals: GodotSignals, // ← New SystemParam ) { if let Some(root) = scene_tree.get().get_root() { if let Some(button) = root.try_get_node_as::<Button>("UI/MyButton") { let mut handle = GodotNodeHandle::from_instance_id(button.instance_id()); // New simplified API signals.connect(&mut handle, "pressed"); } } } }
Breaking Changes
- Function signature changed:
connect_godot_signalno longer requiresSceneTreeRefparameter - New SystemParam required: Add
GodotSignalsparameter to systems that connect signals - Recommended API change: Use
signals.connect()instead of directconnect_godot_signal()calls
Migration Checklist
-
Add
GodotSignalsparameter to systems that connect signals -
Replace
connect_godot_signal(&mut handle, signal_name, &mut scene_tree)withsignals.connect(&mut handle, signal_name) -
Remove unused
SceneTreeRefparameters if they were only used for signal connections - Test that all signal connections work correctly with the new system
Summary
The v0.8.0 signal system simplifies signal connections while improving performance. The main migration step is:
- Add
GodotSignalsparameter to systems that connect signals - Replace
connect_godot_signal(&mut handle, signal, &mut scene_tree)withsignals.connect(&mut handle, signal) - Remove unused
SceneTreeRefparameters
The signal event handling (EventReader<GodotSignal>) remains unchanged, so only the connection setup needs to be updated.
Multithreaded Bevy and #[main_thread_system] (New Feature)
What's New
In v0.8.0, godot-bevy now enables Bevy's multithreaded task executor by default, allowing systems to run in parallel for better performance. However, since Godot's APIs are not thread-safe, we've introduced the #[main_thread_system] attribute to mark systems that must run on the main thread.
Key Changes:
- Multithreaded Bevy enabled: Systems can now run in parallel by default
- New
#[main_thread_system]attribute: Mark systems that use Godot APIs - Better performance: ECS systems can utilize multiple CPU cores
Migration Path
Most existing code will continue to work without changes, but you should add the #[main_thread_system] attribute to any system that directly calls Godot APIs.
When to Use #[main_thread_system]
Add this attribute to systems that:
- Use
SceneTreeRefor other Godot resources - Call any Godot API functions that are not thread-safe
Examples
Systems that need #[main_thread_system]:
#![allow(unused)] fn main() { use godot_bevy::prelude::*; // ✅ Using SceneTreeRef - needs main thread #[main_thread_system] fn spawn_enemy( mut commands: Commands, scene_tree: SceneTreeRef, enemy_spawner: Res<EnemySpawner>, ) { if let Some(scene) = scene_tree.get().get_root() { // Spawn enemy logic using Godot APIs } } // ✅ Calling non-thread-safe Godot APIs - needs main thread #[main_thread_system] fn play_sound_effects( mut audio_events: EventReader<AudioEvent>, audio_player: Res<AudioStreamPlayer>, ) { for event in audio_events.read() { // Direct Godot API calls are not thread-safe audio_player.play(); } } }
Benefits
- Better Performance: ECS systems can now utilize multiple CPU cores
- Explicit Threading: Clear distinction between main-thread and multi-thread systems
- Safety: Prevents accidental concurrent access to Godot APIs
- Scalability: Better performance on multi-core systems
Migration Checklist
- Review existing systems: Identify which systems use Godot APIs
-
Add
#[main_thread_system]: Mark systems that use SceneTreeRef or call non-thread-safe Godot APIs - Test performance: Verify that multithreading improves your game's performance
- Consider refactoring: Separate pure ECS logic from Godot API calls for better parallelization
Common Patterns
Pattern 1: Separate data processing from rendering
#![allow(unused)] fn main() { // Multi-threaded: Process game logic fn calculate_damage( mut health_query: Query<&mut Health>, damage_events: EventReader<DamageEvent>, ) { // Pure ECS logic - runs on any thread } // Main thread: Use SceneTreeRef for scene management #[main_thread_system] fn update_scene_structure( scene_tree: SceneTreeRef, spawn_events: EventReader<SpawnEvent>, ) { // SceneTreeRef access - runs on main thread } }
Pattern 2: Use events to bridge threads
#![allow(unused)] fn main() { // Multi-threaded: Game logic generates events fn enemy_ai_system( mut attack_events: EventWriter<AttackEvent>, enemy_query: Query<&Transform, With<Enemy>>, ) { // Send events instead of directly calling Godot APIs } // Main thread: Handle events with non-thread-safe Godot APIs #[main_thread_system] fn handle_attack_events( mut attack_events: EventReader<AttackEvent>, audio_player: Res<AudioStreamPlayer>, ) { // Process events using non-thread-safe Godot APIs for event in attack_events.read() { audio_player.play(); } } }
Summary
The multithreaded Bevy feature significantly improves performance by allowing systems to run in parallel. The main migration step is adding #[main_thread_system] to systems that use Godot APIs, ensuring thread safety while maximizing performance.
BevyBundle Enhanced Property Mapping (New Feature)
What's New
In v0.8.0, the BevyBundle macro has been significantly enhanced with more flexible property mapping options:
- Struct Component Mapping: Map multiple Godot properties to fields in a struct component
- Transform Functions: Apply transformation functions to convert values during mapping
- Improved Syntax: More intuitive syntax for single and multi-field mappings
New Mapping Options
Struct Component Mapping
You can now map multiple Godot properties to fields in a struct component:
#![allow(unused)] fn main() { #[derive(Component)] struct Stats { health: f32, mana: f32, stamina: f32, } #[derive(GodotClass, BevyBundle)] #[class(base=CharacterBody2D)] #[bevy_bundle((Player), (Stats { health: max_health, mana: max_mana, stamina: max_stamina }))] pub struct PlayerCharacter { base: Base<CharacterBody2D>, #[export] max_health: f32, #[export] max_mana: f32, #[export] max_stamina: f32, } }
Transform Functions
Apply transformation functions to convert Godot values before assigning to components:
#![allow(unused)] fn main() { fn percentage_to_fraction(value: f32) -> f32 { value / 100.0 } #[derive(GodotClass, BevyBundle)] #[class(base=Node2D)] #[bevy_bundle((Enemy), (Health: health_percentage))] pub struct Enemy { base: Base<Node2D>, #[export] #[bundle(transform_with = "percentage_to_fraction")] health_percentage: f32, // Editor shows 0-100, component gets 0.0-1.0 } }
Backwards Compatibility
All existing v0.7.x BevyBundle syntax remains fully supported:
#![allow(unused)] fn main() { // Still works in v0.8.0 #[bevy_bundle((Player), (Health: max_health))] }
Benefits
- Better Component Design: Create struct components that group related data
- Editor-Friendly Values: Use transform functions to convert between editor-friendly and system-friendly values
- Type Safety: All mappings are verified at compile time
- Flexibility: Mix and match different mapping styles as needed
For complete documentation on the new features, see the Custom Node Markers section.
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
Startupsystems (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=trueandautosync=falsefrom all#[bevy_bundle()]attributes - Remove any manually registered bundle sync systems from your app
-
Test that bundle components are available in
Startupsystems (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
TransformSyncModesystem: 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.