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.87.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-bevy
folder 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.9.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
.gdextension
file 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.9.0"
bevy = { version = "0.16", default-features = false }
godot = "0.3"
Configure Godot Integration
1. Create Extension File
Create rust.gdextension
in your Godot project root:
[configuration]
entry_symbol = "gdext_rust_init"
compatibility_minimum = 4.3
reloadable = true
[libraries]
macos.debug = "res://rust/target/debug/libyour_game_name.dylib"
macos.release = "res://rust/target/release/libyour_game_name.dylib"
windows.debug.x86_32 = "res://rust/target/debug/your_game_name.dll"
windows.release.x86_32 = "res://rust/target/release/your_game_name.dll"
windows.debug.x86_64 = "res://rust/target/debug/your_game_name.dll"
windows.release.x86_64 = "res://rust/target/release/your_game_name.dll"
linux.debug.x86_64 = "res://rust/target/debug/libyour_game_name.so"
linux.release.x86_64 = "res://rust/target/release/libyour_game_name.so"
linux.debug.arm64 = "res://rust/target/debug/libyour_game_name.so"
linux.release.arm64 = "res://rust/target/release/libyour_game_name.so"
linux.debug.rv64 = "res://rust/target/debug/libyour_game_name.so"
linux.release.rv64 = "res://rust/target/release/libyour_game_name.so"
Replace your_game_name
with your actual crate name from Cargo.toml
.
2. Create BevyApp Autoload
- In Godot, create a new scene
- Add a
BevyApp
node as the root - Save it as
bevy_app_singleton.tscn
- Go to Project → Project Settings → 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-bevy
folder 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
cargo
installed 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.gdextension
match your library output - Check that you've built the Rust project
- On macOS, you may need to allow the library in System Preferences
"BevyApp not found"
- Make sure godot-bevy is properly added to your dependencies
- Rebuild the Rust project
- Restart the Godot editor
Build errors
- Verify your Rust version:
rustc --version
- Ensure all dependencies are compatible
- Check for typos in the crate name
Next Steps
Congratulations! You've successfully set up godot-bevy 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 nameCollisions
- Collision eventsGroups
- Godot node groups
Systems
Systems contain your game logic and run on a schedule:
#![allow(unused)] fn main() { fn movement_system( time: Res<Time>, mut query: Query<&mut 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
Data Flow
Understanding how data flows between Godot and Bevy is crucial:
Godot → Bevy
- Node added to scene tree
- Entity created with components
- Signals converted to events
- Input forwarded to systems
Bevy → Godot
- Transform components sync to nodes
- Commands can modify scene tree
- Resources can be loaded
- Audio can be played
Key Principles
1. Godot for Content, Bevy for Logic
- Design levels in Godot's editor
- Write game logic in Bevy systems
- Let each tool do what it does best
2. Components as the Source of Truth
- Store game state in components
- Use Godot nodes for presentation
- Sync only what's necessary
3. Systems for Everything
- Movement? System.
- Combat? System.
- UI updates? System.
- This promotes modularity and reusability
4. Leverage Both Ecosystems
- Use Godot's assets and tools
- Use Bevy's plugins and crates
- Don't reinvent what already exists
Common Patterns
Finding Nodes by Name
#![allow(unused)] fn main() { fn setup( mut query: Query<(&Name, Entity)>, ) { let player = query.iter() .find_entity_by_name("Player") .expect("Player node must exist"); } }
Reacting to Signals
#![allow(unused)] fn main() { fn handle_button_press( mut events: EventReader<GodotSignal>, ) { for signal in events.read() { if signal.name == "pressed" { // Button was pressed! } } } }
Spawning Godot Scenes
#![allow(unused)] fn main() { 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().clone()), 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 detectionGodotSignalsPlugin
: Signal to event bridgeBevyInputBridgePlugin
: 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:
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
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
Collisions
component with entered/exited tracking - Converts Godot collision signals to queryable data
-
GodotSignalsPlugin
: Signal event bridge- Converts Godot signals to Bevy events
- Use
EventReader<GodotSignal>
to handle 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.,
T
forTRACE
,D
forDEBUG
,I
forINFO
,W
forWARN
,E
forERROR
- Log messages are suffixed with a shortened path and line number location, e.g.,
@ loading_state/systems.rs:186
- Log level filtering is
INFO
and 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(GodotSignalsPlugin) // 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(GodotSignalsPlugin) // 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
→ includesGodotInputEventPlugin
GodotPlugin
→ 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
GodotSignalsPlugin
- Do I want to detect collisions? → Add
GodotCollisionsPlugin
- Do I want to handle input? → Add
BevyInputBridgePlugin
orGodotInputEventPlugin
- 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
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 SceneTreeEvent
s if needed.
What Components Are Available?
When the scene tree is parsed, each Godot node becomes a Bevy entity with these components:
GodotNodeHandle
- Reference to the Godot nodeName
- The node's name from GodotGroups
- The node's group membershipsCollisions
- If the node has collision signals- Node type markers - Components like
ButtonMarker
,Sprite2DMarker
, etc. - Custom bundles - Components from
#[derive(BevyBundle)]
are automatically added
BevyBundle Component Timing
If you've defined custom Godot node types with #[derive(BevyBundle)]
, their components are added immediately during scene tree processing. This happens:
- During
PreStartup
for nodes that exist when the scene is first loaded - During
First
for nodes added dynamically at runtime
This means BevyBundle components are available in Startup
systems for initial scene nodes, and immediately available for dynamically added nodes.
#![allow(unused)] fn main() { #[derive(GodotClass, BevyBundle)] #[class(base=Node2D)] #[bevy_bundle((Health), (Velocity))] pub struct Player { base: Base<Node2D>, } // This will work in Startup - the Health and Velocity components // are automatically added during PreStartup for existing nodes fn setup_player(mut query: Query<(Entity, &Health, &Velocity)>) { for (entity, health, velocity) in &mut query { // Player components are guaranteed to be here! } } }
Best Practices
- Use
Startup
for initialization - Scene entities are guaranteed to be ready - Use
Update
for gameplay logic - This is where most of your game code should live - Custom
PreStartup
systems - If you add systems toPreStartup
, be aware they run before scene parsing unless you explicitly order them with.after()
Understanding the Event Flow
Here's what happens when a node is added to the scene tree during runtime:
- Godot emits a
node_added
signal - The
SceneTreeWatcher
(on the Godot side) receives the signal - It sends a
SceneTreeEvent
through an mpsc channel write_scene_tree_events
(inFirst
schedule) reads from the channel and writes to Bevy's event systemread_scene_tree_events
(also inFirst
schedule) processes the event and creates/updates entities
This architecture allows for flexible event handling while maintaining a clean separation between Godot and Bevy.
Querying with Node Type Markers
When godot-bevy discovers nodes in your Godot scene tree, it automatically creates ECS entities with GodotNodeHandle
components to represent them. To enable efficient, type-safe querying, the library also adds marker components that indicate what type of Godot node each entity represents.
Overview
Every entity that represents a Godot node gets marker components automatically:
#![allow(unused)] fn main() { use godot_bevy::prelude::*; // Query all Sprite2D entities - no runtime type checking needed! fn update_sprites(mut sprites: Query<&mut GodotNodeHandle, With<Sprite2DMarker>>) { for mut handle in sprites.iter_mut() { // We know this is a Sprite2D, so .get() is safe let sprite = handle.get::<Sprite2D>(); // Work with the sprite... } } }
Available Marker Components
Base Node Types
NodeMarker
- All nodes (every entity gets this)Node2DMarker
- All 2D nodesNode3DMarker
- All 3D nodesControlMarker
- UI control nodesCanvasItemMarker
- Canvas items
Visual Nodes
Sprite2DMarker
/Sprite3DMarker
AnimatedSprite2DMarker
/AnimatedSprite3DMarker
MeshInstance2DMarker
/MeshInstance3DMarker
Physics Bodies
RigidBody2DMarker
/RigidBody3DMarker
CharacterBody2DMarker
/CharacterBody3DMarker
StaticBody2DMarker
/StaticBody3DMarker
Areas and Collision
Area2DMarker
/Area3DMarker
CollisionShape2DMarker
/CollisionShape3DMarker
CollisionPolygon2DMarker
/CollisionPolygon3DMarker
Audio Players
AudioStreamPlayerMarker
AudioStreamPlayer2DMarker
AudioStreamPlayer3DMarker
UI Elements
LabelMarker
ButtonMarker
LineEditMarker
TextEditMarker
PanelMarker
Cameras and Lighting
Camera2DMarker
/Camera3DMarker
DirectionalLight3DMarker
SpotLight3DMarker
Animation and Timing
AnimationPlayerMarker
AnimationTreeMarker
TimerMarker
Path Nodes
Path2DMarker
/Path3DMarker
PathFollow2DMarker
/PathFollow3DMarker
Hierarchical Markers
Node type markers follow Godot's inheritance hierarchy. For example, a CharacterBody2D
entity will have:
NodeMarker
(all nodes inherit from Node)Node2DMarker
(CharacterBody2D inherits from Node2D)CharacterBody2DMarker
(the specific type)
This lets you query at any level of specificity:
#![allow(unused)] fn main() { // Query ALL nodes fn system1(nodes: Query<&GodotNodeHandle, With<NodeMarker>>) { /* ... */ } // Query all 2D nodes fn system2(nodes_2d: Query<&GodotNodeHandle, With<Node2DMarker>>) { /* ... */ } // Query only CharacterBody2D nodes fn system3(characters: Query<&GodotNodeHandle, With<CharacterBody2DMarker>>) { /* ... */ } }
Advanced Query Patterns
Combining Markers
#![allow(unused)] fn main() { // Entities that have BOTH a Sprite2D AND a RigidBody2D fn physics_sprites( query: Query<&mut GodotNodeHandle, (With<Sprite2DMarker>, With<RigidBody2DMarker>)> ) { for mut handle in query.iter_mut() { let sprite = handle.get::<Sprite2D>(); let body = handle.get::<RigidBody2D>(); // Work with both components... } } }
Excluding Node Types
#![allow(unused)] fn main() { // All sprites EXCEPT character bodies (e.g., environmental sprites) fn environment_sprites( query: Query<&mut GodotNodeHandle, (With<Sprite2DMarker>, Without<CharacterBody2DMarker>)> ) { for mut handle in query.iter_mut() { // These are sprites but not character bodies let sprite = handle.get::<Sprite2D>(); // Work with environmental sprites... } } }
Multiple Specific Types
#![allow(unused)] fn main() { // Handle different audio player types efficiently fn update_audio_system( players_1d: Query<&mut GodotNodeHandle, With<AudioStreamPlayerMarker>>, players_2d: Query<&mut GodotNodeHandle, With<AudioStreamPlayer2DMarker>>, players_3d: Query<&mut GodotNodeHandle, With<AudioStreamPlayer3DMarker>>, ) { // Process each type separately - no runtime type checking! for mut handle in players_1d.iter_mut() { let player = handle.get::<AudioStreamPlayer>(); // Handle 1D audio... } for mut handle in players_2d.iter_mut() { let player = handle.get::<AudioStreamPlayer2D>(); // Handle 2D spatial audio... } for mut handle in players_3d.iter_mut() { let player = handle.get::<AudioStreamPlayer3D>(); // Handle 3D spatial audio... } } }
Performance Benefits
Node type markers provide significant performance improvements:
- Reduced Iteration: Only process entities you care about
- No Runtime Type Checking: Skip
try_get()
calls - Better ECS Optimization: Bevy can optimize queries with markers
- Cache Efficiency: Process similar entities together
Automatic Application
You don't need to add marker components manually. The library automatically:
- Detects the Godot node type during scene tree traversal
- Adds the appropriate marker component(s) to the entity
- Includes all parent type markers in the inheritance hierarchy
- Ensures every entity gets the base
NodeMarker
This happens transparently when nodes are discovered in your scene tree, making the markers immediately available for your systems to use.
Best Practices
- Use specific markers when you know the exact node type:
With<Sprite2DMarker>
- Use hierarchy markers for broader categories:
With<Node2DMarker>
for all 2D nodes - Combine markers to find entities with multiple components
- Prefer
.get()
over.try_get()
when using markers - it's both faster and safer
For migration information from pre-0.7.0 versions, see the Migration Guide.
Custom 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
BevyBundle
to define components for custom nodes - Prefer semantic components over generic markers
- Combine base class markers with custom components for powerful queries
This design gives you full control over your ECS architecture while maintaining performance and clarity.
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>)> ) { for handle in players.iter() { let mut body = handle.get::<CharacterBody2D>(); 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.
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
to directly control Godot physics nodes. Perfect for physics-heavy games. This usually means you're calling Godot's move methods to have it handle physics for you.
#![allow(unused)] fn main() { fn move_character(mut query: Query<&mut GodotNodeHandle>) { for mut handle in query.iter_mut() { let mut body = handle.get::<CharacterBody2D>(); body.set_velocity(Vector2::new(100.0, 0.0)); body.move_and_slide(); } } }
3. Hybrid Approach
Allows for modifying transforms both from Godot's side and from ECS side. Useful during migration from a GDScript project to godot-bevy or when you're using Godot's physics methods but still want transforms to be updated for reading on the ECS side.
Default Behavior
By default, godot-bevy operates in one-way sync mode:
- ✅ Writing enabled: Changes to ECS transform components update Godot nodes
- ❌ Reading disabled: Changes to Godot nodes don't update ECS components
This is optimal for pure ECS applications where all movement logic lives in Bevy systems.
When to Use Each Approach
Use ECS Transforms When:
- Building a pure ECS game
- Movement logic is simple (no complex physics)
- You want clean separation between logic and presentation
- Performance of transform sync is acceptable
Use Direct Godot Physics When:
- Building platformers or physics-heavy games
- You need Godot's collision detection features
- Using CharacterBody2D/3D or RigidBody2D/3D
- You want zero transform sync overhead
Use Hybrid Approach When:
- Migrating an existing Godot project to ECS
- Some systems need ECS transforms, others need physics
- Gradually transitioning from GDScript to Rust
Key Concepts
Transform Components
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_godot
only: Skips reading Godot transforms, ideal for UI elements and ECS-driven entitiesgodot_to_bevy
only: 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_godot
systems run in theLast
schedulegodot_to_bevy
systems run in thePreUpdate
schedule- 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: EventReader<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
ActionInput
events for mapped keys/buttons - Unhandled Input Events: Generate raw
KeyboardInput
,MouseButtonInput
, etc. for unmapped inputs
This ensures:
- ✅ No duplicate events - each physical input generates exactly one event
- ✅ Proper input flow - mapped inputs become actions, unmapped inputs become raw events
- ✅ Clean event streams - predictable, non-redundant event processing
#![allow(unused)] fn main() { // For a key mapped to "jump" action in Godot's Input Map: // ✅ Generates ONE ActionInput { action: "jump", pressed: true } // ❌ Does NOT generate duplicate KeyboardInput events // For an unmapped key (e.g., 'Q' with no action mapping): // ✅ Generates ONE KeyboardInput { keycode: Q, pressed: true } // ❌ Does NOT generate ActionInput events }
Available Input Events
godot-bevy provides several input event types:
ActionInput
The most important event type - maps to Godot's input actions:
#![allow(unused)] fn main() { fn handle_actions(mut events: EventReader<ActionInput>) { for event in events.read() { println!("Action: {}, Pressed: {}, Strength: {}", event.action, event.pressed, event.strength); } } }
KeyboardInput
Direct keyboard events:
#![allow(unused)] fn main() { fn handle_keyboard(mut events: EventReader<KeyboardInput>) { for event in events.read() { if event.pressed && event.keycode == Key::SPACE { println!("Space pressed!"); } } } }
MouseButtonInput
Mouse button events:
#![allow(unused)] fn main() { fn handle_mouse(mut events: EventReader<MouseButtonInput>) { for event in events.read() { println!("Mouse button: {:?} at {:?}", event.button_index, event.position); } } }
MouseMotion
Mouse movement events:
#![allow(unused)] fn main() { fn handle_mouse_motion(mut events: EventReader<MouseMotion>) { for event in events.read() { println!("Mouse moved by: {:?}", event.relative); } } }
Quick Reference
Feature | Bevy Input | godot-bevy Input |
---|---|---|
Setup complexity | None | Moderate |
Cross-platform | Limited | Full |
User remapping | No | Yes |
Touch support | No | Yes |
State queries | Easy | Manual tracking |
Performance | Fastest | Fast |
Godot integration | None | Full |
Choosing Your Approach
Start with Bevy Input if:
- Building a prototype or game jam entry
- Targeting desktop only
- Using simple controls
- Want immediate results
Use godot-bevy Input if:
- Building for release
- Need cross-platform support
- Want user-configurable controls
- Using complex input schemes
- Targeting mobile/console
Mixing Both Systems
You can use both systems in the same project:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { app.add_systems(Update, ( // Debug controls with Bevy input debug_controls, // Game controls with godot-bevy input game_controls, )); } fn debug_controls(keys: Res<ButtonInput<KeyCode>>) { if keys.just_pressed(KeyCode::F1) { // Toggle debug overlay } } fn game_controls(mut events: EventReader<ActionInput>) { for event in events.read() { // Handle game actions } } }
This gives you the best of both worlds: simple debug controls and flexible game controls.
Troubleshooting
Duplicate Events (Fixed in v0.7.0+)
If you're seeing duplicate ActionInput
events for the same key press, you may be using an older version of godot-bevy. This was fixed in version 0.7.0 through improved input event processing.
Symptoms:
#![allow(unused)] fn main() { // Old behavior (before v0.7.0): 🎮 Action: 'jump' pressed // First event 🎮 Action: 'jump' pressed // Duplicate event (unwanted) }
Solution: Update to godot-bevy v0.7.0 or later where input processing was improved to eliminate duplicates.
Mouse Events Only on Movement
MouseMotion
events are only generated when the mouse actually moves. If you need continuous mouse position tracking, consider using Godot's Input.get_global_mouse_position()
in a system that runs every frame.
Signal Handling
Godot signals are a core communication mechanism in the Godot engine, allowing nodes to notify other parts of the game when events occur. godot-bevy bridges Godot signals into Bevy's event system, enabling ECS systems to respond to UI interactions, collision events, and other Godot-specific events.
How Signal Bridging Works
When you connect a Godot signal through godot-bevy, the signal is automatically converted into a GodotSignal
event that can be read by Bevy systems using EventReader<GodotSignal>
. This includes support for signals with arguments - the signal arguments are preserved and passed along with the event.
Basic Signal Connection
To connect to a Godot signal, use the GodotSignals
resource to connect to any node's signal:
#![allow(unused)] fn main() { use bevy::prelude::*; use godot_bevy::prelude::*; fn connect_signals( mut scene_tree: SceneTreeRef, signals: GodotSignals, ) { 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()); signals.connect(&mut handle, "pressed"); } } } }
Reading Signal Events
Once connected, signals become GodotSignal
events that you can read in any Bevy system:
#![allow(unused)] fn main() { fn handle_signals(mut signal_events: EventReader<GodotSignal>) { for signal in signal_events.read() { match signal.name.as_str() { "pressed" => { println!("Button was pressed!"); } "toggled" => { println!("Toggle button changed state"); } _ => {} } } } }
Signals with Arguments
Many Godot signals carry arguments that provide additional context about the event. godot-bevy preserves these arguments and makes them available through the arguments
field:
#![allow(unused)] fn main() { fn handle_input_signals(mut signal_events: EventReader<GodotSignal>) { for signal in signal_events.read() { if signal.name == "input_event" { println!("Received input_event signal with {} arguments", signal.arguments.len()); // CollisionObject2D.input_event has 3 arguments: viewport, event, shape_idx if signal.arguments.len() >= 2 { // The second argument is the InputEvent let event_arg = &signal.arguments[1]; // Parse the event argument to determine event type if event_arg.value.contains("InputEventMouseButton") { println!("Mouse button event detected!"); if event_arg.value.contains("pressed=true") { if event_arg.value.contains("button_index=1") { println!("Left mouse button clicked!"); } else if event_arg.value.contains("button_index=2") { println!("Right mouse button clicked!"); } } } else if event_arg.value.contains("InputEventMouseMotion") { println!("Mouse motion over area"); } } } } } }
Signal Arguments Structure
Signal arguments are provided as a Vec<SignalArgument>
where each SignalArgument
has:
value
: AString
representation of the argument's value- Additional metadata about the argument type (implementation details may vary)
For complex signal arguments like InputEvent
, you'll typically need to parse the value
string to extract the information you need, as shown in the examples above.
Common Signal Patterns
UI Signals
#![allow(unused)] fn main() { // Button pressed if signal.name == "pressed" { println!("Button clicked!"); } // CheckBox toggled if signal.name == "toggled" && signal.arguments.len() > 0 { let pressed = signal.arguments[0].value.contains("true"); println!("Checkbox is now: {}", if pressed { "checked" } else { "unchecked" }); } // LineEdit text changed if signal.name == "text_changed" && signal.arguments.len() > 0 { println!("Text changed to: {}", signal.arguments[0].value); } }
Physics Signals
For physics-related events like collisions, godot-bevy provides dedicated resources that are more efficient than signals. Instead of connecting to physics signals, use the Collisions
resource:
#![allow(unused)] fn main() { // Instead of using signals for collision detection, use the Collisions resource fn check_player_death( mut player: Query<(&mut GodotNodeHandle, &Collisions), With<Player>>, mut next_state: ResMut<NextState<GameState>>, ) { if let Ok((mut player, collisions)) = player.single_mut() { if collisions.colliding().is_empty() { return; } player.get::<Node2D>().set_visible(false); next_state.set(GameState::GameOver); } } }
The Collisions
resource provides direct access to collision state without the overhead of signal processing, making it ideal for gameplay-critical physics events.
For non-gameplay physics events that need custom data, signals are still appropriate:
#![allow(unused)] fn main() { // Custom physics events that carry additional data if signal.name == "projectile_hit" && signal.arguments.len() > 0 { let damage = signal.arguments[0].value.parse::<f32>().unwrap_or(0.0); println!("Projectile hit for {} damage", damage); } }
Best Practices
1. One-time Connection Setup
Use a resource or local state to ensure signals are connected only once:
#![allow(unused)] fn main() { #[derive(Resource, Default)] struct SignalConnectionState { connected: bool, } fn setup_signals( mut state: ResMut<SignalConnectionState>, // ... other parameters ) { if !state.connected { // Connect signals state.connected = true; } } }
2. Signal Name Matching
Use string matching or consider creating an enum for frequently used signals:
#![allow(unused)] fn main() { #[derive(Debug, PartialEq)] enum GameSignal { ButtonPressed, PlayerHit, AreaEntered, Unknown(String), } impl From<&str> for GameSignal { fn from(name: &str) -> Self { match name { "pressed" => Self::ButtonPressed, "player_hit" => Self::PlayerHit, "body_entered" => Self::AreaEntered, other => Self::Unknown(other.to_string()), } } } }
Frame Execution Model
Understanding how godot-bevy integrates with Godot's frame timing is crucial for building performant games. This chapter explains the execution model and how different schedules interact.
Two Types of Frames
Visual Frames (_process
)
Visual frames run at your display's refresh rate and handle the main Bevy update cycle.
What runs: The complete app.update()
cycle
First
PreUpdate
Update
FixedUpdate
PostUpdate
Last
Frequency: Matches Godot's visual framerate (typically 60-144 FPS)
Use for:
- Game logic
- UI updates
- Rendering-related systems
- Most gameplay code
Physics Frames (_physics_process
)
Physics frames run at Godot's fixed physics tick rate.
What runs: Only the PhysicsUpdate
schedule
Frequency: Godot's physics tick rate (default 60 Hz)
Use for:
- Physics calculations
- Movement that needs to sync with Godot physics
- Collision detection
- Anything that must run at a fixed rate
Schedule Execution Order
Within Visual Frames
Visual Frame Start
├── First
├── PreUpdate (reads Godot → ECS transforms)
├── Update (your game logic)
├── FixedUpdate (0, 1, or multiple times)
├── PostUpdate
└── Last (writes ECS → Godot transforms)
Visual Frame End
Independent Physics Frames
Physics Frame Start
└── PhysicsUpdate (your physics logic)
Physics Frame End
⚠️ Important: Physics frames run independently and can execute:
- Before a visual frame starts
- Between any visual frame schedules
- After a visual frame completes
- Multiple times between visual frames
Frame Rate Relationships
Different parts of your game run at different rates:
Schedule | Rate | Use Case |
---|---|---|
Visual schedules | Display refresh (60-144 Hz) | Rendering, UI, general logic |
PhysicsUpdate | Physics tick (60 Hz) | Godot physics integration |
FixedUpdate | Bevy's rate (64 Hz default) | Consistent gameplay simulation |
Practical Example
Here's how different systems should be scheduled:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { // Visual frame systems app.add_systems(Update, ( ui_system, camera_follow, animation_system, )); // Fixed timestep for consistent simulation app.add_systems(FixedUpdate, ( ai_behavior, cooldown_timers, )); // Godot physics integration app.add_systems(PhysicsUpdate, ( character_movement, collision_response, )); } }
Delta Time Usage
Different schedules require different delta time sources:
In Update Systems
#![allow(unused)] fn main() { fn movement_system( time: Res<Time>, mut query: Query<&mut 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 anyStartup
systems run. This means you can safely query Godot scene entities in yourStartup
systems! See Scene Tree Initialization and Timing for details.
Thread Safety and Godot APIs
Some Godot APIs are not thread-safe and 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
Input
singleton - File I/O operations through Godot's resource system
The #[main_thread_system]
Macro
The #[main_thread_system]
attribute macro provides a clean way to mark systems that require main thread execution:
#![allow(unused)] fn main() { use godot_bevy::prelude::*; #[main_thread_system] fn update_ui_labels( mut query: Query<&mut GodotNodeHandle, With<PlayerStats>>, stats: Res<GameStats>, ) { for mut handle in query.iter_mut() { if let Some(mut label) = handle.try_get::<Label>() { label.set_text(&format!("Score: {}", stats.score)); } } } }
The macro automatically adds a NonSend<MainThreadMarker>
parameter to the system, which forces Bevy to schedule it on the main thread. This approach requires no imports and keeps the function signature clean.
Best Practices: Minimize Systems That Call Godot APIs
While the #[main_thread_system]
macro makes Godot API access convenient, systems assigned to the main thread 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
, underdependencies
add 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
, underfeatures
add 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 theConnect
button 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
If you see the following warning:
#![allow(unused)] fn main() { warning: unexpected `cfg` condition value: `trace_tracy` }
after ugprading to Godot Bevy 0.9
, add the following at the top of the problematic file (wherever you use the bevy_app
macro):
#![allow(unused)] #![allow(unexpected_cfgs)] // silence potential `tracy_trace` feature config warning brought in by `bevy_app` macro fn main() { }
Migration Guides
This section contains migration guides for various versions.
Migration Guide: v0.6 to v0.7
This guide covers breaking changes and new features when upgrading from godot-bevy 0.6.x to 0.7.0.
Table of Contents
- Node Type Markers (New Feature)
- BevyBundle Autosync Simplification
- Transform Sync Modes (Breaking Change)
Node Type Markers (New Feature)
What Changed
Starting in v0.7.0, all entities representing Godot nodes automatically receive marker components that indicate their node type. This enables type-safe, efficient ECS queries without runtime type checking.
Migration Path
This change is backwards compatible - your existing code will continue to work. However, you can improve performance and safety by migrating to marker-based queries.
Before (v0.6.x approach - still works)
#![allow(unused)] fn main() { use godot_bevy::prelude::*; fn update_sprites(mut all_nodes: Query<&mut GodotNodeHandle>) { for mut handle in all_nodes.iter_mut() { // Runtime type checking - works but inefficient if let Some(sprite) = handle.try_get::<Sprite2D>() { sprite.set_modulate(Color::RED); } } } fn update_character_bodies(mut all_nodes: Query<&mut GodotNodeHandle>) { for mut handle in all_nodes.iter_mut() { // Check every single entity in your scene if let Some(mut body) = handle.try_get::<CharacterBody2D>() { body.move_and_slide(); } } } }
After (v0.7.0 recommended approach)
#![allow(unused)] fn main() { use godot_bevy::prelude::*; fn update_sprites(mut sprites: Query<&mut GodotNodeHandle, With<Sprite2DMarker>>) { for mut handle in sprites.iter_mut() { // ECS pre-filters to only Sprite2D entities - much faster! let sprite = handle.get::<Sprite2D>(); // No Option<> - guaranteed to work sprite.set_modulate(Color::RED); } } fn update_character_bodies(mut bodies: Query<&mut GodotNodeHandle, With<CharacterBody2DMarker>>) { for mut handle in bodies.iter_mut() { // Only iterates over CharacterBody2D entities let mut body = handle.get::<CharacterBody2D>(); body.move_and_slide(); } } }
Benefits of Migration
- Performance: Only iterate over entities you care about
- Safety: No more
Option<>
handling or potential panics - Clarity: Query signatures clearly show what node types you expect
- Optimization: Better ECS query optimization and caching
Common Migration Patterns
Pattern 1: Single Node Type
Before:
#![allow(unused)] fn main() { fn system(mut all_nodes: Query<&mut GodotNodeHandle>) { for mut handle in all_nodes.iter_mut() { if let Some(mut timer) = handle.try_get::<Timer>() { if timer.is_stopped() { timer.start(); } } } } }
After:
#![allow(unused)] fn main() { fn system(mut timers: Query<&mut GodotNodeHandle, With<TimerMarker>>) { for mut handle in timers.iter_mut() { let mut timer = handle.get::<Timer>(); if timer.is_stopped() { timer.start(); } } } }
Pattern 2: Multiple Node Types
Before:
#![allow(unused)] fn main() { fn audio_system(mut all_nodes: Query<&mut GodotNodeHandle>) { for mut handle in all_nodes.iter_mut() { if let Some(mut player) = handle.try_get::<AudioStreamPlayer>() { player.set_volume_db(-10.0); } else if let Some(mut player_2d) = handle.try_get::<AudioStreamPlayer2D>() { player_2d.set_volume_db(-10.0); } else if let Some(mut player_3d) = handle.try_get::<AudioStreamPlayer3D>() { player_3d.set_volume_db(-10.0); } } } }
After:
#![allow(unused)] fn main() { fn audio_system( mut players_1d: Query<&mut GodotNodeHandle, With<AudioStreamPlayerMarker>>, mut players_2d: Query<&mut GodotNodeHandle, With<AudioStreamPlayer2DMarker>>, mut players_3d: Query<&mut GodotNodeHandle, With<AudioStreamPlayer3DMarker>>, ) { // Process each type separately - much more efficient! for mut handle in players_1d.iter_mut() { let mut player = handle.get::<AudioStreamPlayer>(); player.set_volume_db(-10.0); } for mut handle in players_2d.iter_mut() { let mut player = handle.get::<AudioStreamPlayer2D>(); player.set_volume_db(-10.0); } for mut handle in players_3d.iter_mut() { let mut player = handle.get::<AudioStreamPlayer3D>(); player.set_volume_db(-10.0); } } }
Pattern 3: Complex Conditions
Before:
#![allow(unused)] fn main() { fn physics_sprites(mut all_nodes: Query<&mut GodotNodeHandle>) { for mut handle in all_nodes.iter_mut() { if let Some(sprite) = handle.try_get::<Sprite2D>() { if let Some(body) = handle.try_get::<RigidBody2D>() { // Entity has both Sprite2D and RigidBody2D handle_physics_sprite(sprite, body); } } } } }
After:
#![allow(unused)] fn main() { fn physics_sprites( mut entities: Query<&mut GodotNodeHandle, (With<Sprite2DMarker>, With<RigidBody2DMarker>)> ) { for mut handle in entities.iter_mut() { // ECS guarantees both components exist let sprite = handle.get::<Sprite2D>(); let body = handle.get::<RigidBody2D>(); handle_physics_sprite(sprite, body); } } }
Available Marker Components
All marker components are available in the prelude:
#![allow(unused)] fn main() { use godot_bevy::prelude::*; // Examples of available markers: // Sprite2DMarker, CharacterBody2DMarker, Area2DMarker, // AudioStreamPlayerMarker, LabelMarker, ButtonMarker, // Camera2DMarker, RigidBody2DMarker, etc. }
See the complete list of markers in the querying documentation.
Performance Impact
Marker-based queries provide several performance advantages:
- Reduced iteration: Only process entities that match your node type, rather than checking every entity in the scene
- Eliminated runtime type checking: Skip
try_get()
calls since the ECS guarantees type matches - Better cache locality: Process similar entities together rather than jumping between different node types
- ECS optimization: Bevy can better optimize queries when it knows the component filters upfront
The actual performance improvement will depend on your scene size and how many entities match your queries, but the benefits are most noticeable in systems that run frequently (like every frame) and in larger scenes.
When NOT to Migrate
You might want to keep the old approach if:
- Rare usage: The system runs infrequently and performance isn't critical
- Dynamic typing: You genuinely need to handle unknown node types at runtime
- Gradual migration: You're updating a large codebase incrementally
The old try_get()
patterns will continue to work indefinitely.
Troubleshooting
"Entity doesn't have expected component"
If you get panics when using .get()
instead of .try_get()
, it usually means:
- Wrong marker: Make sure you're using the right marker for your query
- Node freed: The Godot node was freed but the entity still exists
- Timing issue: The node was removed between query execution and access
Solution: Use marker-based queries to ensure type safety, or fall back to .try_get()
if needed.
"Query doesn't match any entities"
If your marker-based query returns no entities:
- Check node types: Verify your scene has the expected node types
- Check marker names: Ensure you're using the correct marker component
- Check timing: Make sure the scene tree has been processed
Solution: Use Query<&GodotNodeHandle, With<NodeMarker>>
to see all entities, then check what markers they have.
Summary
The node type markers feature in v0.7.0 provides a significant upgrade to querying performance and type safety. While migration is optional, it's highly recommended for any systems that process specific Godot node types frequently.
The migration path is straightforward:
- Replace broad
Query<&mut GodotNodeHandle>
with specific marker queries - Replace
try_get()
calls withget()
when using markers - Handle multiple node types with separate queries rather than runtime checks
This results in cleaner, faster, and safer code while maintaining the flexibility of the ECS architecture.
BevyBundle Autosync Simplification
What Changed
In v0.7.0, the autosync
parameter has been removed from #[derive(BevyBundle)]
. All BevyBundle derives now automatically register their bundles and apply them during scene tree processing.
Migration Path
This change requires minimal code changes but may affect your app architecture if you were manually managing bundle systems.
Before (v0.6.x)
#![allow(unused)] fn main() { // Manual autosync control #[derive(GodotClass, BevyBundle)] #[class(base=Node2D)] #[bevy_bundle((Health), (Velocity), autosync=true)] // ← autosync parameter pub struct Player { base: Base<Node2D>, } // Alternative: manually registering the system #[derive(GodotClass, BevyBundle)] #[class(base=Node2D)] #[bevy_bundle((Health), (Velocity))] // ← autosync=false (default) pub struct Enemy { base: Base<Node2D>, } #[bevy_app] fn build_app(app: &mut App) { // Had to manually add the sync system app.add_systems(Update, EnemyAutoSyncPlugin); } }
After (v0.7.0)
#![allow(unused)] fn main() { // Automatic registration - much simpler! #[derive(GodotClass, BevyBundle)] #[class(base=Node2D)] #[bevy_bundle((Health), (Velocity))] // ← No autosync parameter needed pub struct Player { base: Base<Node2D>, } #[derive(GodotClass, BevyBundle)] #[class(base=Node2D)] #[bevy_bundle((Health), (Velocity))] // ← Always automatic now pub struct Enemy { base: Base<Node2D>, } #[bevy_app] fn build_app(app: &mut App) { // No manual system registration needed! // Bundles are automatically applied during scene tree processing } }
Breaking Changes
- Remove
autosync=true
: This parameter no longer exists and will cause compilation errors - Remove manual sync systems: If you were manually adding bundle sync systems, remove them
- Timing change: Bundle components are now available in
Startup
systems (was previously only available inUpdate
)
Benefits of This Change
- Simplified API: No need to remember to set
autosync=true
- Better timing: Bundle components are available earlier in the frame lifecycle
- Unified behavior: Both initial scene loading and dynamic node addition work the same way
- No missed registrations: Impossible to forget to register a bundle system
Migration Checklist
-
Remove
autosync=true
andautosync=false
from all#[bevy_bundle()]
attributes - Remove any manually registered bundle sync systems from your app
-
Test that bundle components are available in
Startup
systems (they now are!) - Update any documentation or comments that reference the old autosync behavior
Example Migration
Before (v0.6.x):
#![allow(unused)] fn main() { #[derive(GodotClass, BevyBundle)] #[class(base=CharacterBody2D)] #[bevy_bundle((Speed: speed), (Health: max_health), autosync=true)] pub struct Player { base: Base<CharacterBody2D>, #[export] speed: f32, #[export] max_health: f32, } #[bevy_app] fn build_app(app: &mut App) { app.add_systems(Startup, setup_game) .add_systems(Update, player_movement); } fn setup_game(players: Query<&Health>) { // This would be empty in v0.6.x because bundles // weren't applied until the first Update println!("Found {} players", players.iter().count()); } }
After (v0.7.0):
#![allow(unused)] fn main() { #[derive(GodotClass, BevyBundle)] #[class(base=CharacterBody2D)] #[bevy_bundle((Speed: speed), (Health: max_health))] // ← Removed autosync pub struct Player { base: Base<CharacterBody2D>, #[export] speed: f32, #[export] max_health: f32, } #[bevy_app] fn build_app(app: &mut App) { app.add_systems(Startup, setup_game) .add_systems(Update, player_movement); } fn setup_game(players: Query<&Health>) { // This now works in Startup! Bundle components are available immediately println!("Found {} players", players.iter().count()); } }
This change makes BevyBundle usage more intuitive and eliminates a common source of timing-related bugs.
Transform Sync Modes (Breaking Change)
What Changed
In v0.7.0, transform synchronization behavior has changed significantly:
- New
TransformSyncMode
system: Transform syncing is now configurable viaGodotTransformConfig
- Default changed from two-way to one-way: Previously, transforms were synced bidirectionally by default. Now the default is one-way (ECS → Godot only)
- Explicit configuration required: You must now explicitly choose your sync mode
Migration Path
If your v0.6.x code relied on the implicit two-way transform sync, you need to explicitly enable it in v0.7.0.
Before (v0.6.x - implicit two-way sync)
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { // Transform syncing was always bidirectional app.add_systems(Update, movement_system); } fn movement_system( mut query: Query<&mut Transform2D>, ) { // Could read Godot transform changes automatically } }
After (v0.7.0 - explicit configuration)
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { // Restore v0.6.x behavior with explicit two-way sync app.insert_resource(GodotTransformConfig::two_way()); app.add_systems(Update, movement_system); } }
Available Sync Modes
-
TransformSyncMode::OneWay
(NEW DEFAULT)- ECS transform changes update Godot nodes
- Godot transform changes are NOT reflected in ECS
- Best for pure ECS architectures
-
TransformSyncMode::TwoWay
(v0.6.x default behavior)- Full bidirectional sync between ECS and Godot
- Required for Godot animations affecting ECS
- Higher performance overhead
-
TransformSyncMode::Disabled
(NEW)- No transform components created
- Zero sync overhead
- Perfect for physics-only games
Common Migration Scenarios
Scenario 1: Using Godot's AnimationPlayer
If you use Godot's AnimationPlayer to move entities:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { // Must use two-way sync for animations app.insert_resource(GodotTransformConfig::two_way()); } }
Scenario 2: Pure ECS Movement
If all movement is handled by Bevy systems:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { // One-way is the default, but you can be explicit app.insert_resource(GodotTransformConfig::one_way()); } }
Scenario 3: Physics-Only Game
If using CharacterBody2D or RigidBody2D exclusively:
#![allow(unused)] fn main() { #[bevy_app] fn build_app(app: &mut App) { // Disable transform syncing entirely app.insert_resource(GodotTransformConfig::disabled()); } }
Breaking Changes Checklist
- Default behavior changed: If you relied on reading Godot transform changes in ECS, you must enable two-way sync
- Performance may improve: One-way sync has less overhead than the old default
- New optimization opportunity: Consider disabling transforms for physics entities
Troubleshooting
"Transform changes in Godot not visible in ECS"
This is the most common issue when migrating. The solution is to enable two-way sync:
#![allow(unused)] fn main() { app.insert_resource(GodotTransformConfig::two_way()); }
"Transform components missing"
If you disabled sync mode but still need transforms:
#![allow(unused)] fn main() { // Either switch to one-way or two-way mode app.insert_resource(GodotTransformConfig::one_way()); }
Performance Comparison
v0.6.x (implicit two-way):
- Read systems: Always running (PreUpdate)
- Write systems: Always running (Last)
- Overhead: O(all entities) every frame
v0.7.0 one-way (new default):
- Read systems: Not running
- Write systems: Running (Last)
- Overhead: O(changed entities) only
v0.7.0 disabled:
- No systems running
- Zero overhead
Summary
The transform sync system in v0.7.0 gives you explicit control over performance and behavior. While this is a breaking change for projects that relied on implicit two-way sync, it provides better defaults and more optimization opportunities. Simply add app.insert_resource(GodotTransformConfig::two_way())
to restore v0.6.x behavior.
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:
GodotSceneTreeMirroringPlugin
automatically includesGodotSceneTreeEventsPlugin
BevyInputBridgePlugin
automatically 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_app
function -
Optimization: Replace
GodotDefaultPlugins
with 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_signal
no longer requiresSceneTreeRef
parameter - New SystemParam required: Add
GodotSignals
parameter to systems that connect signals - Recommended API change: Use
signals.connect()
instead of directconnect_godot_signal()
calls
Migration Checklist
-
Add
GodotSignals
parameter to systems that connect signals -
Replace
connect_godot_signal(&mut handle, signal_name, &mut scene_tree)
withsignals.connect(&mut handle, signal_name)
-
Remove unused
SceneTreeRef
parameters 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
GodotSignals
parameter to systems that connect signals - Replace
connect_godot_signal(&mut handle, signal, &mut scene_tree)
withsignals.connect(&mut handle, signal)
- Remove unused
SceneTreeRef
parameters
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
SceneTreeRef
or 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.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 Transform
s, 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::Transform2D
andgodot_bevy::prelude::Transform3D
were removed
Migration Checklist
-
Transform components changed: Replaced
godot_bevy::prelude::Transform2D
andgodot_bevy::prelude::Transform3D
withbevy::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
GodotCorePlugins
directly (withoutGodotDefaultPlugins
) - You load Godot resources using
Handle<GodotResource>
orAssetServer
- You use
GodotAudioPlugin
orGodotPackedScenePlugin
(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
GodotCorePlugins
no longer includesGodotAssetsPlugin
GodotAssetsPlugin
is now inGodotDefaultPlugins
GodotAudioPlugin
andGodotPackedScenePlugin
requireGodotAssetsPlugin
to function
Migration Checklist
- Using GodotDefaultPlugins: No action needed
-
Using GodotCorePlugins + asset loading: Add
app.add_plugins(GodotAssetsPlugin)
-
Using GodotAudioPlugin: Ensure
GodotAssetsPlugin
is included -
Using GodotPackedScenePlugin: Ensure
GodotAssetsPlugin
is 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>
) - ❌
GilrsPlugin
functionality - ❌ 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()); }