Call Model

Back to Scripts

Why this model exists

Perro separates behavior and data so script methods stay shareable while each node instance keeps isolated runtime state. This avoids mutable aliasing on script objects and enables cross-script calls without object ownership graphs.

Behavior path

methods! defines reusable behavior on shared script type. Use &self + ScriptContext.

Data path

#[State] stores per-instance data. Access via with_state!/with_state_mut! closures to scope borrows.

Scene vars bridge

script_vars in scene files map into runtime script vars and can be read/write through get_var!/set_var!.

Dispatch path

call_method! uses method ids + Variant params for deterministic runtime dispatch across scripts.

Borrow-scope rule

Keep each runtime data borrow in its own closure. End one state borrow before nested runtime calls. This pattern prevents borrow collisions while preserving clear intent.

Net effect: scripts can mutate other script instances from different systems and call sites safely, as long as each mutation lives in its own scoped runtime closure.

Real hazard is nested runtime window usage inside an already-active runtime closure; avoid that and borrow behavior stays predictable.

Dispatch 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, var!("name")) -> Variantset_var!(ctx.run, node_id, var!("name"), variant!(value)) -> ()call_method!(ctx.run, node_id, func!("method"), params![...]) -> Variant

Example

methods!({    fn apply_damage(        &self,        ctx: &mut ScriptContext<'_, RT, RS, IP>,        amount: i32,    ) {        with_state_mut!(ctx.run, EnemyState, ctx.id, |state| {            state.health -= amount;        });         let hp = with_state!(ctx.run, EnemyState, ctx.id, |state| state.health)            ;         signal_emit!(ctx.run, signal!("enemy_hp_changed"), params![ctx.id, hp]);    }});