State is per-script-instance memory. Put a value here when one callback writes it and a later callback must read it, or when a scene must choose it per instance.
scene/default -> #[State] instance -> lifecycle/method callbacks
Use #[State] for data that belongs to one script instance and must survive a
callback:
- mutable gameplay values such as health, velocity, or mode
- cached runtime values used by later callbacks
- fixed node dependencies as
NodeIDorOption<NodeID> - per-instance resources as typed IDs such as
TextureIDorMeshID
Keep constants as Rust constants. Keep one-callback results as local vars.
Do not use state as a global registry by default. Put scene-wide flow on its controller and shared immutable values in Rust modules/resources.
#[derive(Clone, Default, Variant)]
struct CharacterLook {
portrait: TextureID,
materials: Vec<MaterialID>,
}
#[State]
struct PlayerState {
#[default = 100]
#[expose]
pub health: i32,
#[expose]
#[node_ref(Camera3D)]
pub camera: Option<NodeID>,
#[expose]
pub look: CharacterLook,
velocity: Vector3,
}#[expose] controls editor inspector layout. pub is the injection gate:
only pub fields receive a scene script_vars value, and an exposed field
must also be pub for its editor-authored value to apply.
script_vars = {
health = 125,
camera = @MainCamera,
look = {
portrait = "res://textures/player.png",
materials = ["res://materials/body.pmat", "res://materials/trim.pmat"]
}
}
Scene strings coerce to TextureID, MaterialID, MeshID, AnimationID,
AnimationTreeID, NavMeshID, and SoundFontID before on_init.
Coercion recurses through options, lists, map values, tuples, and custom
#[derive(Variant)] values. Missing or invalid values keep the field default.
Resource load failure keeps each resource API's normal nil/failure result.
Runtime set_var! stays strict. It expects the field's runtime Variant type;
it does not load an asset from a path string.
Use Option<NodeID> when a scene ref may be absent. A referenced node may also
be removed later. Skip absent targets without a panic or required log.
let camera = with_state!(ctx.run, PlayerState, ctx.id, |state| state.camera).unwrap_or_default();
if let Some(camera) = camera {
with_node_mut!(ctx.run, Camera3D, camera, |node| {
node.fov = 70.0;
});
}Copy the ID out before node access. A non-nil ID may still refer to a node removed after injection, so typed access remains failure-tolerant.