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 entities
  • godot_to_bevy only: Skips writing to Godot, useful for reading physics results
  • Both directions (no prefix): Full synchronization when needed

Real Example: High-Volume Transform Sync

From the perf-test example, which benchmarks transform sync with tens of thousands of entities:

#![allow(unused)]
fn main() {
use godot_bevy::{add_transform_sync_systems, prelude::*};

#[derive(Component)]
struct Particle;

#[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 Particle entities only
    // Only sync Bevy -> Godot since particles are driven by ECS movement systems
    add_transform_sync_systems! {
        app,
        Particle = bevy_to_godot: With<Particle>
    }

    // ... 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

Custom sync registers the same shared sync systems as auto sync, just restricted to your query filter, so the schedules match exactly:

  • bevy_to_godot (Bevy → Godot write) runs in FixedLast — the physics rate, which is what Godot's physics interpolation expects
  • godot_to_bevy (Godot → Bevy read) runs in PreUpdate
  • Bidirectional sync (no prefix) runs in both schedules

Custom sync resets physics interpolation on an entity's first write, so freshly spawned nodes don't slide from their old position.

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>,
}
}