Basic Concepts
Before diving into godot-bevy development, it's important to understand the key concepts that make this integration work.
The Hybrid Architecture
godot-bevy creates a bridge between two powerful systems:
Godot Side
- Scene tree with nodes
- Visual editor for level design
- Asset pipeline for resources
- Rendering engine
- Physics engine
Bevy Side
- Entity Component System (ECS)
- Systems for game logic
- Components for data
- Resources for shared state
- Schedules for execution order
The Bridge
godot-bevy seamlessly connects these worlds:
- Godot nodes ↔ ECS entities
- Node properties ↔ Components
- Signals → Events
- Resources ↔ Assets
Core Components
Entities
In godot-bevy, Godot nodes are automatically registered as ECS entities:
#![allow(unused)] fn main() { // When a node is added to the scene tree, // it becomes queryable as an entity fn find_player( query: Query<&Name, With<GodotNodeHandle>>, ) { for name in query.iter() { if name.as_str() == "Player" { // Found the player node! } } } }
Components
Components store data on entities. godot-bevy provides several built-in components:
GodotNodeHandle- Reference to the Godot nodeName- Node nameGroups- Godot node groups
For collision detection, use the Collisions system param (see Plugins).
Systems
Systems contain your game logic and run on a schedule:
#![allow(unused)] fn main() { fn movement_system( time: Res<Time>, mut query: Query<&mut Transform, With<Player>>, ) { for mut transform in query.iter_mut() { transform.translation.x += 100.0 * time.delta_secs(); } } }
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
Shutdown lifecycle
A BevyApp disposes of its world when the node is destroyed or when you call
teardown. Reinitialization, TestApp::cleanup, and TestApp Drop use the same
teardown path. Call explicit teardown or reinitialization between frames.
Repeated teardown calls do nothing.
After hosted Startup completes, a healthy app receives AppExit::Success and one
final Last pass before disposal. Ordinary Last systems also run every render
frame. Unstarted apps are dropped without running Startup or Last. Frame panics
and an existing unwind suppress the terminal pass.
Destruction includes free, queued node or ancestor deletion, scene replacement
for scene-local apps, and graceful engine quit. An autoload survives scene
replacement. Detaching or reparenting does not itself shut down a hosted world;
scene-mirrored entities still follow their normal membership rules. The caller
must eventually free a detached node. Use queue_free to delete a host or its
ancestor from a system. Synchronous destruction while its Rust instance is
borrowed is unsupported.
At destruction, the node's children are already freed. At engine quit, other
nodes may be gone too. Last systems and pending signal connections must tolerate
expired Godot handles. Call teardown before deletion if cleanup needs live nodes.
A terminal panic is caught and reported, and the app is still dropped. Resource
destructor panics are also caught and reported. Other cleanup systems may not
finish after a panic. Hard termination and aborting or double panics cannot
provide this cleanup guarantee. Bevy AppExit messages do not quit Godot.
The extension's on_stage_deinit hook is a separate process-level lifecycle event;
it shuts down extension-wide services such as profiling and is not a replacement for
per-BevyApp shutdown.
Configuring Core Behavior
The #[bevy_app] macro accepts configuration attributes to customize godot-bevy's core behavior:
Scene Tree Relationships
godot-bevy mirrors Godot's scene tree with a custom ECS relationship:
GodotChildOf / GodotChildren. This avoids conflicts with Bevy's built-in
ChildOf / Children relationship (used by many plugins for their own hierarchies).
By default, despawning a parent entity will also despawn its Godot children. You can
disable that behavior with the scene_tree_auto_despawn_children attribute:
#![allow(unused)] fn main() { #[bevy_app(scene_tree_auto_despawn_children = false)] fn build_app(app: &mut App) { // Children can outlive their parents (useful for pooling or custom lifetimes) app.add_plugins(PhysicsPlugins::new(FixedUpdate)); } }
When to use this:
- ✅ When you want entities to outlive their Godot nodes
- ✅ When you manage lifetimes manually (pooling, reuse, gameplay-driven despawns)
- ❌ When you rely on automatic child cleanup on parent despawn
Default behavior (when not specified): scene_tree_auto_despawn_children = true
Data Flow
Understanding how data flows between Godot and Bevy is crucial:
Godot → Bevy
- Node added to scene tree
- Entity created with components
- Signals converted to events
- Input forwarded to systems
Bevy → Godot
- Transform components sync to nodes
- Commands can modify scene tree
- Resources can be loaded
- Audio can be played
Key Principles
1. Godot for Content, Bevy for Logic
- Design levels in Godot's editor
- Write game logic in Bevy systems
- Let each tool do what it does best
2. Components as the Source of Truth
- Store game state in components
- Use Godot nodes for presentation
- Sync only what's necessary
3. Systems for Everything
- Movement? System.
- Combat? System.
- UI updates? System.
- This promotes modularity and reusability
4. Leverage Both Ecosystems
- Use Godot's assets and tools
- Use Bevy's plugins and crates
- Don't reinvent what already exists
5. The Godot Boundary (Main Thread Only)
- Any call into Godot (via
GodotAccess,Gd<T>,Input::singleton, etc.) must run on the main thread - Systems that include
GodotAccessare forced onto the main thread and run sequentially, so keep them small and push heavy work to parallel systems - Treat
GodotNodeHandleas an ID; resolve toGd<T>only viaGodotAccess - See Thread Safety and Godot APIs for details and patterns
Common Patterns
Finding Nodes by Name
#![allow(unused)] fn main() { fn setup( mut query: Query<(&Name, Entity)>, ) { let player = query.iter() .find_entity_by_name("Player") .expect("Player node must exist"); } }
Reacting to Signals
#![allow(unused)] fn main() { #[derive(Message, Debug, Clone)] struct ButtonPressed; fn handle_button_press( mut events: MessageReader<ButtonPressed>, ) { for _ in events.read() { // Button was pressed! } } }
Spawning Godot Scenes
#![allow(unused)] fn main() { use bevy::app::{App, Plugin, Startup, Update}; use bevy::asset::{AssetServer, Handle}; use bevy::prelude::{Commands, Component, Res, Resource, Single, Transform, With}; use godot_bevy::bridge::GodotNodeHandle; use godot_bevy::prelude::{GodotResource, GodotScene}; struct EnemyPlugin; impl Plugin for EnemyPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, load_assets); app.add_systems(Update, spawn_enemy); } } #[derive(Resource, Debug)] struct EnemyScene(Handle<GodotResource>); #[derive(Component, Debug)] struct Enemy { health: i32, } #[derive(Component, Debug)] struct EnemySpawner; fn load_assets(mut commands: Commands, server: Res<AssetServer>) { let handle: Handle<GodotResource> = server.load("scenes/enemy.tscn"); commands.insert_resource(EnemyScene(handle)); } fn spawn_enemy( mut commands: Commands, enemy_scene: Res<EnemyScene>, enemy_spawner: Single<&GodotNodeHandle, With<EnemySpawner>>, ) { commands.spawn(( GodotScene::from_handle(enemy_scene.0.clone()) .with_parent(enemy_spawner.into_inner().id()), Enemy { health: 100 }, Transform::default(), )); } }
Next Steps
Now that you understand the basic concepts:
- Try the examples
- Read about specific systems in detail
- Start building your game!
Remember: godot-bevy is about using the right tool for the right job. Embrace both Godot and Bevy's strengths!