Why #[State] is useful

#[State] keeps behavior and data separate. Behavior methods stay shareable on script type, while runtime stores per-node instance data. Because state access is scoped with closures, runtime borrows end predictably and cross-system calls stay safe.

Behavior: methods on &self reused across many node instances.

Data: runtime-owned #[State] instance per node/script binding.

Safety: with_state!/with_state_mut! scope borrows and prevent long-lived mutable access.

API links

State struct example

use perro_api::prelude::*; #[derive(Clone, Copy, Variant)]struct AimPoint {    target: Vector2,} #[State]struct PlayerState {    #[default = 100]    health: i32,     #[default = 220.0]    speed: f32,     #[default = AimPoint { target: Vector2::ZERO }]    aim: AimPoint,}

Runtime signatures

with_state!(ctx.run, StateType, node_id, |state| -> V { ... }) -> Vwith_state_mut!(ctx.run, StateType, node_id, |state| -> V { ... }) -> Option<V>get_var!(ctx.run, node_id, member) -> Variantset_var!(ctx.run, node_id, member, value) -> ()call_method!(ctx.run, node_id, method, params) -> Variant

with_state_mut! does mutation and can return closure data through Option<V>.

let hp_after = with_state_mut!(ctx.run, PlayerState, ctx.id, |state| {    state.health -= 5;    state.health}); match hp_after {    Some(hp) => { /* use hp */ }    None => { /* state missing / type mismatch */ }}

Scene script_vars mapping

script_vars inject per-instance script data from scene authoring. Shared behavior reads/writes those values at runtime without hardcoding per-node constants.

[player][Node2D]script = "res://scripts/player.rs"script_vars = {    health = 140,    speed = 280.0,    aim = { target = (32.0, -16.0) }}[/Node2D][/player]