Perro offers four communication paths. Pick from what the caller knows.
typed state = shared compile-time knowledge
method = command/question to one target
signal = fact announced to zero/many listeners
dynamic var = runtime-selected member bridge
| Caller knows | Use | Result |
|---|---|---|
| concrete Rust state type | with_state! / with_state_mut! |
typed read or write |
| target node + behavior name | call_method! |
targeted call + Variant reply |
| event name | signal_emit! |
zero or many listeners |
| target node + runtime member name | get_var! / set_var! |
dynamic Variant access |
Every dynamic path β call_method!, signal handlers, get_var! / set_var! β reaches only members the target declares pub. Non-pub members stay internal (with_state!, self.method(ctx)) and get no dispatch glue in the compiled binary. See state visibility and method visibility.
Use methods for commands sent to one known target. Methods accept params and may return a value.
let result = call_method!(
ctx.run,
door_id,
method!("set_open"),
params![true]
);
let opened = result.as_bool().unwrap_or(false);The receiver declares the dynamic API in methods! β pub marks it callable from other scripts:
methods!({
pub fn set_open(&self, ctx: &mut ScriptContext<'_, API>, open: bool) -> bool {
with_state_mut!(ctx.run, DoorState, ctx.id, |state| {
state.open = open;
}).is_some()
}
});Inside the same script, call self.set_open(ctx, true) directly; that works with or without pub.
Use signals for facts that happened. The emitter does not choose or know the listeners.
signal_emit!(
ctx.run,
signal!("player_health_changed"),
params![health]
);Each listener connects one of its own methods once. Signal dispatch runs through the same generated call glue as call_method!, so the handler must be a pub fn:
lifecycle!({
fn on_all_init(&self, ctx: &mut ScriptContext<'_, API>) {
signal_connect!(
ctx.run,
ctx.id,
signal!("player_health_changed"),
func!("on_health_changed")
);
}
});
methods!({
pub fn on_health_changed(&self, ctx: &mut ScriptContext<'_, API>, health: i32) {
let _ = (ctx.id, health);
}
});Prefer a signal when many systems may react, the receiver may live in another scene, or the emitter must stay independent from UI/audio/analytics code.
Use dynamic vars for generic tools and behavior where the member name or state
type is selected at runtime. The target field must be pub in its state struct.
let old = get_var!(ctx.run, target_id, var!("health"))
.as_i32()
.unwrap_or(0);
set_var!(ctx.run, target_id, var!("health"), variant!(old + 10));Do not use get_var! / set_var! as the default way to reach a known state
type. Dynamic access loses compile-time field checking and returns no typed
borrow.
get_var! and call_method! return Variant. Decode at the call site with an
as_* accessor, parse::<T>(), or into_parse::<T>().
Dynamic calls can miss a target/member or fail to decode params/replies. Signal emission does not guarantee a listener or reply. Typed access gives the strongest compile-time checks. Use it freely for a script's own state. Cross-script typed access requires the target state to be public and intentionally importable from its generated script module; prefer a method when that module/type coupling is not part of the design. Keep behavior invariants behind target methods; do not use dynamic vars to bypass target validation.
Signals are runtime-global channels keyed by SignalID, not by emitter. When
more than one source may emit the same event, include ctx.id in params or use
distinct channel names so listeners can identify the source.