Live property sync

Add sync = two_way or sync = to_godot to a mapped field to keep its Godot property and Bevy value connected after creation. Both GodotNode and BevyComponents support it. Fields without sync keep their creation-only behavior; no extra plugin is needed beyond the scene-tree support in GodotCorePlugins.

ModeAt creationDuring play
No syncCopy Godot into BevyNo further synchronization
sync = to_godotCopy Godot into BevyPublish changed Bevy values; never import later Godot edits
sync = two_wayCopy Godot into BevyImport changed Godot values and publish changed Bevy values; Bevy wins conflicts on the same field

Publish health to a Label

#![allow(unused)]
fn main() {
use bevy::prelude::{Component, Query};
use godot_bevy::prelude::GodotNode;

#[derive(Component, Default, GodotNode)]
#[gdbevy(class_name = HealthView)]
pub struct Health {
    #[gdbevy(export, default = 100, sync = to_godot)]
    pub current: i32,
}

pub fn take_damage(mut health: Query<&mut Health>) {
    for mut health in &mut health {
        health.current -= 1;
    }
}
}

Run take_damage in Update. A Label script can read the generated property:

extends Label
@export var source: HealthView

func _process(_delta: float) -> void:
    text = str(source.current)

Configure the Label to process after BevyApp if it must display the current publication. A Label that runs earlier sees the previous value. This mapping publishes HealthView.current; it does not synchronize the Label's native text property.

Tune a field from Godot

Use two_way for values authored by GDScript, the remote Inspector, or animation:

#![allow(unused)]
fn main() {
    use bevy::prelude::Component;
    use godot::prelude::*;
    use godot_bevy::prelude::BevyComponents;

    #[derive(Component, Default)]
    pub struct Movement {
        pub speed: f32,
        pub acceleration: f32,
    }

    #[derive(GodotClass, BevyComponents)]
    #[class(base = CharacterBody2D, init)]
    #[gdbevy(require(Movement { speed: speed, acceleration: acceleration }))]
    pub struct PlayerTuning {
        #[export]
        #[init(val = 250.0)]
        #[gdbevy(sync = two_way)]
        pub speed: f32,
        #[export]
        #[init(val = 800.0)]
        pub acceleration: f32,
    }
}

Godot edits to speed reach Movement.speed at the next read. acceleration is copied only at creation. A Bevy system can change either field without replacing the component or losing its other runtime data.

Convert in both directions

with = path takes an owned Godot value and returns the Bevy field value. into = path takes an owned Bevy field value and returns the Godot value. A live mapping with with must also provide into, including to_godot, because creation still uses the inbound conversion.

#![allow(unused)]
fn main() {
    use bevy::prelude::Component;
    use godot_bevy::prelude::GodotNode;

    #[derive(Component, Default, GodotNode)]
    #[gdbevy(class_name = ChargeView)]
    pub struct Charge {
        #[gdbevy(export, as = f32, sync = two_way,
                 with = from_percent, into = to_percent)]
        pub fraction: f32,
    }

    fn from_percent(value: f32) -> f32 {
        value / 100.0
    }

    fn to_percent(value: f32) -> f32 {
        value * 100.0
    }
}

An AnimationPlayer property track can animate ChargeView.fraction from 0 to 100. Bevy receives a fraction from 0 to 1. A Bevy edit publishes the percentage. The bridge remembers the raw Godot value it last read or wrote, so a lossy conversion does not feed its own output back into Bevy on the following frame.

For strings, keep the Bevy field as String and convert the Godot property:

#![allow(unused)]
fn main() {
    use bevy::prelude::Component;
    use godot::prelude::*;
    use godot_bevy::prelude::BevyComponents;

    #[derive(Component)]
    pub struct Caption(pub String);

    #[derive(GodotClass, BevyComponents)]
    #[class(base = Node, init)]
    pub struct CaptionView {
        #[export]
        #[gdbevy(component = Caption, sync = two_way,
                 with = String::from, into = to_godot)]
        pub caption: GString,
    }

    fn to_godot(value: String) -> GString {
        GString::from(&value)
    }
}

Read and write boundaries

Each render frame has one read in PreUpdate, in PropertySyncSet::Read, and one write in PostUpdate, in PropertySyncSet::Write. They run on frames with zero or several physics ticks, and while Bevy virtual time is paused, provided BevyApp still receives its Godot callbacks. These boundaries are separate from transform synchronization, which runs at physics rate.

FixedUpdate and Update consumers see the result of that frame's property read. Order a consumer in PreUpdate after Read; order an author in PostUpdate before Write:

#![allow(unused)]
fn main() {
use bevy::prelude::{App, IntoScheduleConfigs, PostUpdate, PreUpdate};
use godot_bevy::prelude::PropertySyncSet;

pub fn configure_property_consumers(app: &mut App) {
    app.add_systems(PreUpdate, read_health.after(PropertySyncSet::Read));
    app.add_systems(PostUpdate, take_damage.before(PropertySyncSet::Write));
}

fn read_health(health: Query<&Health>) {
    for health in &health {
        println!("Health: {}", health.current);
    }
}
}

Before importing each field, the bridge checks whether Bevy changed it since its last snapshot. A pending Bevy edit prevents an import and is published at Write. This includes edits in Startup, First, signal observers, and the ECS inspector, as well as gameplay systems. Inspector requests drain in First, after SceneTreeSet::Apply and TimeSystems, before the property read. An edit later in FixedUpdate or Update also publishes at Write. Normal Bevy system ordering decides the final value when several Bevy systems write the same field.

Conflicts are per field. If Godot changes speed and Bevy changes health on the same component, both changes survive. A Godot-only import does not cause an outbound echo. A pending Bevy edit does publish even when its converted output equals the previous Godot snapshot, so it can overwrite a competing late Godot edit.

Godot edits after Read arrive at the next Read, unless a Bevy edit overwrites them at Write. Edits after Write belong to the next cycle. Godot process priority and an AnimationPlayer's callback mode determine which side of these boundaries an author runs on. Intermediate writes that change back before the next sample cannot be observed.

Supported values and mappings

Live fields must be independently accessible values. The plain-value path requires Clone + PartialEq + Send + Sync + 'static on both the Bevy value and the Godot value. The whole Bevy component does not need Clone or Reflect. GString and StringName Godot properties have built-in owned String snapshots; they do not need to satisfy the plain-value snapshot bounds. For f32 and f64, comparisons use bits, so unchanged NaNs stay unchanged and positive and negative zero are distinct.

Snapshot cloning and comparison cost grows with the number and size of live values. Two-way fields are polled each render frame. to_godot fields have no recurring Godot reads. Arrays, dictionaries, Variant, and objects are unsupported on either side, even with converters. Expose a separate plain stored property for the value you want to synchronize.

See the mapping grammar for supported placements and restrictions on overlapping mappings and fan-out.

Godot-first live sources must be plain stored #[export] or #[var] fields. Custom getters and setters are unsupported. Immutable Bevy components support to_godot only. Native Godot properties such as Node2D.position and Label.text are outside this feature. Attachable components remain creation-only because their carrier nodes are consumed.

Component and node lifetimes

Creation seeds the snapshots from the components that were constructed. An unchanged new entity does not publish needlessly, and a Startup edit is recognized before the first read. Pure Bevy entities have no property-sync state; spawning a component alone does not create or bind a Godot node.

Removing a mapped component skips only its fields and does not recreate it. Reinserting it makes its current Bevy values authoritative for the first publication; subsequent Godot edits can import again. This also applies to a remove/reinsert between the read and write passes.

A reparent within the mirrored tree preserves the entity and its sync state. Changing the handle to another node of the mapped class reseeds from current Bevy values and publishes them to the new node. Missing, freed, queued-for-deletion, or wrong-class nodes are skipped. Excluded subtrees never acquire a binding. Ordinary detach/despawn and protected detach follow the scene-tree lifecycle.