Property Mapping — #[gdbevy(...)] Grammar

This page was previously "Property Mapping with BevyBundle". The BevyBundle macro is gone; the unified #[gdbevy(...)] attribute replaces it on both bridging derives.

Both GodotNode (component-first) and BevyComponents (Godot-first) accept #[gdbevy(...)] annotations. The keys available depend on which derive you are using.

Component-first (GodotNode)

Companion component — newtype

Generate one exported property and wrap its value in a newtype component:

#![allow(unused)]
fn main() {
#[derive(Component, GodotNode, Default)]
#[gdbevy(base = CharacterBody2D, class_name = Player2D)]
#[gdbevy(require(speed: Speed, as = f32, default = 250.0))]
pub struct Player;
}
  • speed becomes a #[export] speed: f32 on the generated Player2D class (default 250.0).
  • When the node enters the tree, Speed(speed_value) is inserted.
  • as = f32 is required here — the macro cannot see Speed's inner type.

Companion component — struct

Generate multiple exported properties for a multi-field component:

#![allow(unused)]
fn main() {
#[derive(Component, GodotNode, Default)]
#[gdbevy(base = CharacterBody2D, class_name = Enemy2D)]
#[gdbevy(require(stats: Stats {
    health(as = f32, default = 100.0),
    mana(as = f32, default = 50.0),
}))]
pub struct Enemy;
}

Each inner field(as = T, …) follows the same as/default/with grammar as the newtype form. The name before : (e.g. stats) is required by the grammar but ignored — the generated export properties use the inner field names (health, mana).

Inspector metadata for generated exports

Metadata can be placed on a named or tuple primary field with #[gdbevy(export, ...)], on require(prop: Comp, ...), or on each field inside require(group: Comp { field(as = T, ...), ... }). Use one attribute per primary field.

Export types must implement gdext's Export trait. Rust String does not, so export a GString and convert it when constructing the component:

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

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

#[derive(Component, GodotNode, Default)]
#[gdbevy(class_name = WeaponNode)]
#[gdbevy(require(
    kind: WeaponKind,
    as = GString,
    with = from_godot_string,
    default = GString::from("Hands"),
    description = "Weapon selected by the designer",
    hint = ENUM,
    hint_string = "Hands,Knife"
))]
pub struct Weapon;

fn from_godot_string(value: GString) -> String {
    value.to_string()
}
}

description takes a string literal. Primary field /// docs are copied before an explicit description. hint names a bare Godot PropertyHint variant, and hint_string takes an expression. A hint string requires a hint.

Enable the register-docs feature on your godot-bevy dependency to register property descriptions in Godot's editor:

godot-bevy = { version = "0.12", features = ["register-docs"] }

This feature is off by default and requires Godot API 4.3 or later. With it off, hints still work and descriptions remain Rust docs. API 4.2 consumers must leave it disabled.

Marker companion

Insert a component via Default with no exported property:

#![allow(unused)]
fn main() {
#[gdbevy(require(Stunned))]
}

Primary field with conversion

Fields on the component struct itself can declare a type conversion:

#![allow(unused)]
fn main() {
#[derive(Component, GodotNode, Default)]
#[gdbevy(base = Node2D, class_name = Slider2D)]
pub struct Slider {
    /// Editor shows 0–100; component gets 0.0–1.0
    #[gdbevy(export, as = f32, with = percentage_to_fraction)]
    pub value: f32,
}

fn percentage_to_fraction(v: f32) -> f32 { v / 100.0 }
}

as = T is optional when the field type already implements gdext's Export trait; add it only when the export type differs from the Rust field type.

Tuple fields

Tuple structs are supported for component-first nodes. Exported fields are named value0, value1, and so on in the generated Godot class.

#![allow(unused)]
fn main() {
#[derive(Component, GodotNode, Default)]
pub struct Velocity(
    #[gdbevy(export)] f32,
    #[gdbevy(export)] f32,
);
}

All component-first keys

PlacementKeyRequired?Meaning
structbase = GodotBaseno (default: Node)Godot class to extend
structclass_name = Nameno (default: <Struct>BevyComponent)Generated class name
structrequire(…)noCompanion component (see forms above)
fieldexportyesMarks the field as a generated Godot export
fieldas = TnoGodot export type
fielddefault = exprnoEditor default (via #[init(val = …)]); a pure-Bevy spawn(T) uses the struct's own Default — make them agree if you rely on spawn(T).
fieldwith = fnnoGodot-value → field-value conversion
require(prop: Comp, …)as = TyesExport type for the generated property
require(prop: Comp, …)default = exprnoExport default
require(prop: Comp, …)with = fnnoConversion before constructing the component
generated primary or companion fieldsync = two_way or sync = to_godotnoOpt into live synchronization
generated primary or companion fieldinto = fnwith live withBevy-value → Godot-value conversion; requires sync
generated primary or companion fielddescription = "..."noProperty docs, registered with register-docs
generated primary or companion fieldhint = NAMEnoBare Godot PropertyHint variant
generated primary or companion fieldhint_string = exprnoHint string, requires hint

Godot-first (BevyComponents)

Field binding

Map a single #[export] property to a newtype component:

#![allow(unused)]
fn main() {
#[derive(GodotClass, BevyComponents)]
#[class(base = Node2D, init)]
pub struct EnemyNode {
    base: Base<Node2D>,

    #[gdbevy(component = Health)]
    #[export]
    max_health: f32,

    #[gdbevy(component = Speed, with = to_speed)]
    #[export]
    #[init(val = 100.0)]
    speed: f32,
}
}

component = Comp is required for a newtype binding. with = fn is optional. as and default are not allowed — gdext's #[init(val = …)] owns defaults on this path.

Marker at the struct level

#![allow(unused)]
fn main() {
#[derive(GodotClass, BevyComponents)]
#[class(base = CharacterBody2D, init)]
#[gdbevy(require(Player))]
pub struct PlayerNode {
    base: Base<CharacterBody2D>,
    // ...
}
}

N→1 binding

Build a multi-field component from several existing #[export] properties:

#![allow(unused)]
fn main() {
#[derive(GodotClass, BevyComponents)]
#[class(base = CharacterBody2D, init)]
#[gdbevy(require(Stats { health: max_health, mana: max_mana }))]
pub struct PlayerNode {
    base: Base<CharacterBody2D>,
    #[export] max_health: f32,
    #[export] max_mana: f32,
}
}

Add #[gdbevy(with = converter)] to a source field to convert its value before assigning it to each N→1 component field that reads it. No component key is needed on that field; for example, exported GString paths can populate String fields:

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

#[derive(Component, Default)]
pub struct ChallengeSelect {
    pub back_scene_path: String,
    pub accept_scene_path: String,
}

#[derive(GodotClass, BevyComponents)]
#[class(init, base = CanvasLayer)]
#[gdbevy(require(ChallengeSelect {
    back_scene_path: back_scene_path,
    accept_scene_path: accept_scene_path,
}))]
pub struct ChallengeSelectUI {
    #[export(file = "*.tscn")]
    #[gdbevy(with = String::from)]
    pub back_scene_path: GString,
    #[export(file = "*.tscn")]
    #[gdbevy(with = String::from)]
    pub accept_scene_path: GString,
}
}

All Godot-first keys

PlacementKeyRequired?Meaning
structrequire(Marker)noInsert Marker::default()
structrequire(Comp { bevy_field: godot_field, … })noBuild struct component from existing exports
fieldcomponent = Compfor newtype bindingsBevy component type (Comp(value))
fieldwith = fnnoGodot-value → component-value conversion
fieldsync = two_way or sync = to_godotnoOpt into live synchronization
fieldinto = fnwith live withBevy-value → Godot-value conversion; requires sync

Godot-first classes own their gdext declaration, so use gdext's native #[var(hint = ..., hint_string = ...)] and Rust documentation attributes for Inspector metadata on those fields.

Native property descriptions also require godot-bevy/register-docs and Godot API 4.3 or later. Keep metadata on the native attributes, outside gdbevy.

Live mapping grammar

Mappings are creation-only unless they opt into sync. The same directives work on these placements:

PlacementExample
GodotNode named or tuple field#[gdbevy(export, sync = two_way)]
Generated newtype companionrequire(speed: Speed, as = f32, sync = two_way)
Generated struct companion fieldrequire(stats: Stats { health(as = i32, sync = to_godot) })
BevyComponents newtype source#[gdbevy(component = Speed, sync = two_way)]
BevyComponents N→1 source#[gdbevy(sync = two_way)] on the field named by require(...)

Both converter values are Rust paths, not strings or closures. A live mapping with with requires into. Without converters the value types must agree. into may also be supplied without with when no inbound conversion is needed.

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

    #[derive(Component)]
    pub struct Speed(pub f32);

    #[derive(Component, Default)]
    pub struct Stats {
        pub health: i32,
        pub mana: i32,
    }

    #[derive(Component, Default, GodotNode)]
    #[gdbevy(class_name = ActorView)]
    #[gdbevy(require(speed: Speed, as = f32, default = 250.0, sync = two_way))]
    #[gdbevy(require(stats: Stats {
        health(as = i32, default = 100, sync = to_godot),
        mana(as = i32, default = 50, sync = two_way),
    }))]
    pub struct Actor;

    #[derive(Component, Default, GodotNode)]
    #[gdbevy(class_name = LevelView)]
    pub struct Level(#[gdbevy(export, sync = two_way)] pub i32);
}

Here LevelView.value0 maps to Level.0. The ActorView properties speed, health, and mana map to their companion fields independently.

See the Godot-first tuning example for an N→1 binding with live and creation-only fields.

Each live source must feed exactly one destination. Two live mappings cannot write the same Bevy field; a live target cannot overlap a creation-only target either. Creation-only sources can still populate several destinations. Marker companions, unmapped fields, and AttachableComponent carriers cannot use sync or into.

Godot-first live sources require a plain stored #[export] or #[var] with no custom getter or setter. Arrays, dictionaries, Variant, and objects are unsupported on either side, even with converters. Immutable components accept only to_godot. See live property sync for value bounds, conversion examples, conflict rules, and lifecycle behavior.