godot-bevy

repository·main·Indexed 19 days ago

https://github.com/bytemeadow/godot-bevy

An integration plugin that brings Bevy's high-performance Entity Component System (ECS) to the Godot engine. It enables developers to write game logic in Rust using Bevy patterns while utilizing Godot's editor and rendering capabilities. The plugin includes a scaffolding tool for project setup, a BevyAppSingleton for lifecycle management, and various feature plugins for asset loading, transform synchronization, collisions, audio, and input bridging.

Tokens
92.9K
Snippets
249
Records
349
Agent score
67%

What's inside godot-bevy

  1. What is godot-bevy?

    main

    godot-bevy is a Rust library designed for Godot developers who want to leverage Bevy's high-performance Entity Component System (ECS) and Rust's safety within their Godot projects.

    It is not a plugin for Bevy users to use Godot; rather, it is a tool for Godot users to use Bevy's ECS architecture for game logic while retaining Godot's visual editor, node system, and asset pipeline.

  2. Understand the Transform Sync Performance Benchmark

    main

    The perf-test example is a benchmark designed to measure the overhead of godot-bevy transform synchronization at scale. It compares a pure Godot (GDScript) implementation against a godot-bevy (Rust + ECS) implementation using a particle rain simulation.

    Purpose

    The benchmark isolates the pure overhead of the integration by using a simple algorithm (particle rain) that maximizes transform updates while minimizing computational complexity. This helps developers understand:

    • Transform Sync Cost: The performance cost of the Transform2DTransform sync → Godot scene tree pipeline.
    • ECS Overhead: Whether the ECS adds measurable overhead for simple operations.
    • Complexity Threshold: When the computational benefits of Rust/ECS outweigh the synchronization costs compared to GDScript.
  3. How the opt-in plugin system works

    main

    Starting from v0.8, godot-bevy uses an opt-in plugin system. By default, only the core features (Scene tree and assets) are included. To keep binaries small and performance high, you must explicitly add plugins for specific functionalities like transform synchronization, audio, or input handling.

    Available plugin patterns:

    • Minimal: Only core features (Scene tree/assets).
    • Feature-specific: Add only what you need (e.g., GodotTransformSyncPlugin, GodotAudioPlugin, BevyInputBridgePlugin).
    • Full (Legacy style): Use GodotDefaultPlugins to include everything at once.
    // Minimal setup - only core features
    #[bevy_app]
    fn build_app(app: &mut App) {
        app.add_systems(Update, my_systems);
    }
    
    // Add specific features as needed
    #[bevy_app]
    fn build_app(app: &mut App) {
        app.add_plugins(GodotTransformSyncPlugin::default())
            .add_plugins(GodotAudioPlugin)
            .add_plugins(BevyInputBridgePlugin);
    }
    
    // Or everything at once (like v0.7.x)
    #[bevy_app]
    fn build_app(app: &mut App) {
        app.add_plugins(GodotDefaultPlugins);
    }
  4. Use ButtonInput in FixedUpdate

    main

    In v0.12, ButtonInput<KeyCode>::just_pressed and just_released are now visible in FixedUpdate. This is because PreUpdate (which populates the resource) now runs before the fixed steps in the new schedule ordering.

    Caveats:

    • High Physics Rate: If you run more physics steps than display frames (N steps/frame), just_pressed will be true for all N FixedUpdate calls in that frame.
    • Low Physics Rate: If a frame runs zero physics steps, an input edge landing on that frame will not be seen in FixedUpdate (it will be cleared by the next frame's PreUpdate).

    For more reliable fixed-rate gameplay input, use GodotActions, which uses Godot's per-tick edge state and is not subject to these frame-rate caveats.

    app.add_systems(FixedUpdate, |keys: Res<ButtonInput<KeyCode>>| {
        if keys.just_pressed(KeyCode::Space) {
            // This is now visible in FixedUpdate
        }
    });
  5. Handle SceneTree.paused behavior in Update systems

    main

    In v0.12, BevyApp uses process_mode = ALWAYS. This means Update systems will continue to run even when SceneTree.paused is true. This allows for Bevy-authored pause menus to function.

    Note on FixedUpdate: FixedUpdate is unaffected; it freezes under pause because it keys on Time<Virtual>.

    How to stop Update systems on pause: If you have Update work that must stop when the game is paused, use one of these two methods:

    1. Scale the logic by Time<Virtual>::delta() (which is 0 while paused).
    2. Use a run condition: system.run_if(not(bevy_time::common_conditions::paused)).
  6. Use #[main_thread_system] for Godot API calls

    main

    v0.8.0 enables Bevy's multithreaded task executor by default. Because Godot's APIs are not thread-safe, any system that directly interacts with Godot resources or calls non-thread-safe Godot functions must be marked with the #[main_thread_system] attribute to ensure it runs on the main thread.

    When to use #[main_thread_system]

    • When using SceneTreeRef or other Godot resources.
    • When calling any Godot API functions that are not thread-safe.

    To maximize performance, separate your pure ECS logic (which can run in parallel on any thread) from your Godot API calls (which must run on the main thread) using Bevy events.

    1. Multi-threaded System: Perform heavy calculations or game logic and emit a Bevy Event.
    2. Main-thread System: Use #[main_thread_system] to read those events and perform the actual Godot API calls (e.g., playing audio, spawning nodes).
    // Multi-threaded: Process game logic on any thread
    fn enemy_ai_system(
        mut attack_events: EventWriter<AttackEvent>,
        enemy_query: Query<&Transform, With<Enemy>>,
    ) {
        // Send events instead of directly calling Godot APIs
        attack_events.send(AttackEvent { ... });
    }
    
    // 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>,
    ) {
        for event in attack_events.read() {
            audio_player.play();
        }
    }
  7. How the godot-bevy plugin system works

    main

    godot-bevy uses an opt-in plugin architecture inspired by Bevy. Instead of including all features by default, you explicitly add plugins to your App. This allows for smaller binaries, better performance, and clear dependency management.

    By default, using the #[bevy_app] macro automatically includes GodotPlugin, which provides GodotCorePlugins (minimal scene tree and asset management). To access more advanced features like physics, audio, or input, you must add the corresponding plugins.

    #[bevy_app]
    fn build_app(app: &mut App) {
        // GodotCorePlugins is already added by the macro
        app.add_systems(Update, my_game_system);
    }
  8. Use `SceneTreeRef` for main-thread scheduling

    main
    The SceneTreeRef SystemParam is also a NonSend type. If your system already requires SceneTreeRef, it is already pinned to the main thread. You do not need to add GodotAccess to the system signature if SceneTreeRef is already present, even if you intend to call Godot APIs.
  9. Use GodotChildOf and GodotChildren for scene tree relationships

    main

    To avoid conflicts with other plugins (like physics or AI) that use Bevy's built-in hierarchy, godot-bevy no longer uses Bevy's ChildOf and Children components to mirror the Godot scene tree. Instead, it uses custom ECS relationships: GodotChildOf and GodotChildren.

    When querying for parent or child entities in the Godot scene tree, you must use these specific components.

    // To find a parent
    fn parent_of(entity: Entity, query: Query<&GodotChildOf>) -> Option<Entity> {
        query.get(entity).ok().map(|parent| parent.get())
    }
    
    // To find children
    fn children_of(entity: Entity, query: Query<&GodotChildren>) -> Vec<Entity> {
        query
            .get(entity)
            .map(|children| children.iter().copied().collect())
            .unwrap_or_default()
    }
  10. Choose a movement approach in godot-bevy

    main

    godot-bevy provides three ways to handle position, rotation, and scale synchronization between Bevy ECS and Godot nodes. Choosing the right one depends on whether your movement logic lives in Bevy systems or relies on Godot's physics engine.

    1. ECS Transform Components (Default)

    Update standard Bevy Transform components in your systems. The plugin automatically syncs these changes to Godot nodes at the end of each frame.

    • Best for: Pure ECS games, simple movement, and clean separation of logic.
    • Pros: Cleanest API; uses standard Bevy patterns.
    • Cons: Small overhead from synchronization.

    2. Direct Godot Physics

    Use GodotNodeHandle and GodotAccess to call Godot's physics methods (like move_and_slide()) directly. This bypasses the ECS transform sync entirely.

    • Best for: Physics-heavy games, platformers, and using CharacterBody2D/3D or RigidBody2D/3D.
    • Pros: Zero transform sync overhead; full access to Godot's collision features.
    • Cons: Logic is tied to Godot's API rather than Bevy's ECS.

    3. Hybrid Approach

    Allows modifications from both the Godot side and the ECS side.

    • Best for: Migrating existing GDScript projects to godot-bevy or when some systems require ECS transforms while others require Godot physics.
    // Example 1: ECS Transform Components
    use godot_bevy::prelude::*;
    
    fn move_entity(mut query: Query<&mut Transform>) {
        for mut transform in query.iter_mut() {
            transform.translation.x += 1.0;
        }
    }
    
    // Example 2: Direct Godot Physics
    fn move_character(query: Query<&GodotNodeHandle>, mut godot: GodotAccess) {
        for handle in query.iter() {
            let mut body = godot.get::<CharacterBody2D>(*handle);
            body.set_velocity(Vector2::new(100.0, 0.0));
            body.move_and_slide();
        }
    }
  11. Access scene children with `NodeTreeView`

    main

    The #[derive(NodeTreeView)] macro provides a typed, ergonomic way to access specific nodes within a spawned Godot scene using node paths. This avoids manual and fragile GodotNodeHandle lookups.

    Defining a View

    Define a struct and derive NodeTreeView. Use the #[node("<path>")] attribute to map fields to specific nodes. Field types should be GodotNodeHandle or Option<GodotNodeHandle>.

    #[derive(NodeTreeView)]
    pub struct CharacterNodes {
        #[node("AnimatedSprite2D")]
        pub animated_sprite: GodotNodeHandle,
    
        #[node("VisibleOnScreenNotifier2D")]
        pub visibility_notifier: GodotNodeHandle,
    }

    Using the View

    To use the view, obtain a handle to the root node of the scene (e.g., via GodotAccess) and call CharacterNodes::from_node(root_handle).

    fn new_character_initialize(
        entities: Query<&GodotNodeHandle, Added<Character>>,
        mut godot: GodotAccess,
    ) {
        for handle in &entities {
            let character = godot.get::<RigidBody2D>(*handle);
            let character_nodes = CharacterNodes::from_node(character).unwrap();
            // character_nodes.animated_sprite is now accessible
        }
    }

    Path Patterns

    Node paths in the #[node] attribute support wildcards:

    • /root/*/HUD/CurrentLevel: Matches any single node name where * appears.
    • /root/Level*/HUD/CurrentLevel: Matches node names starting with "Level".
    • */HUD/CurrentLevel: Matches relative to the base node.

    Generated Path Constants

    The macro automatically generates public string constants for each field in the format <UPPERCASE_FIELD_NAME>_PATH inside the struct's impl block. For example, CharacterNodes::ANIMATED_SPRITE_PATH will equal "AnimatedSprite2D".

    #[derive(NodeTreeView)]
    pub struct CharacterNodes {
        #[node("AnimatedSprite2D")]
        pub animated_sprite: GodotNodeHandle,
    
        #[node("VisibleOnScreenNotifier2D")]
        pub visibility_notifier: GodotNodeHandle,
    }
    
    // Generated impl:
    impl CharacterNodes {
        pub const ANIMATED_SPRITE_PATH: &'static str = "AnimatedSprite2D";
        pub const VISIBILITY_NOTIFIER_PATH: &'static str = "VisibleOnScreenNotifier2D";
    }
  12. How custom transform sync works

    main

    Custom transform synchronization provides several architectural benefits for performance-critical applications:

    • Compile-time Optimization: Each sync system targets specific entities via queries, avoiding unnecessary iteration over the entire scene.
    • Automatic Change Detection: Systems use TransformSyncMetadata internally to prevent infinite synchronization loops between Bevy and Godot.
    • Schedule Alignment:
      • bevy_to_godot runs in FixedLast (matching Godot's physics rate).
      • godot_to_bevy runs in PreUpdate.
      • Bidirectional sync runs in both.
    • 2D/3D Unification: The macro automatically handles both 2D and 3D nodes using AnyOf<(&Node2DMarker, &Node3DMarker)>, performing the correct transform conversion at runtime.
    • Interpolation Handling: Custom sync resets physics interpolation on an entity's first write, preventing 'sliding' when nodes are freshly spawned.