Actions are named, device-independent controls. Instead of checking a specific
key, you check "jump" or "fire", and the input map decides which keyboard,
mouse, gamepad, or Joy-Con bindings satisfy it. That indirection is what makes a
rebindable controls menu possible: the same gameplay code keeps calling
action_pressed!(ctx.ipt, "jump") while the player swaps the underlying binding
at runtime. Perro captures the new binding natively; your game owns saving it.
- One control, many devices: bind "jump" to Space, the A button, and a Joy-Con
button at once, and read it with
action_pressed!(ctx.ipt, "jump").
- Rebindable controls menu: start a live listener with
action_start_rebind!(ctx.ipt, "jump"), show a "press any key" prompt while
action_is_rebinding! is true, then persist action_rebind_result!.
- Coyote-time / jump buffering: latch the
action_pressed!(ctx.ipt, "jump")
edge into a short timer so a slightly-early press still triggers the jump.
- Held vs. edge intent: charge while
action_down!(ctx.ipt, "fire") and release
on action_released!(ctx.ipt, "fire").
- Hot-path reads: cache an action hash once and poll it with
pressed_hash / down_hash to skip re-hashing the name each frame.
The input map owns physical bindings; gameplay owns the meaning of an action. Use named actions for player intent that must survive rebinding and device changes. Use raw key, mouse, or gamepad reads for device-selection UI and diagnostics. Save completed rebind results in project/save data; do not make individual gameplay scripts own separate binding truth.
- Script context path:
ctx.ipt
- Module access:
ctx.ipt.Actions()
- Lifecycle examples stay inside
lifecycle! because script hooks get API from the macro expansion.
Perro provides native live input rebinding. Start a listener for an action, and
the next new keyboard, mouse, gamepad, or Joy-Con button press replaces that
action's bindings in the active input map.
The engine applies the rebind in memory. Game code owns persistence: read the
result, save it through the project's chosen storage format, and restore the
saved bindings on a later run. Perro does not write player settings by itself.
#[State]
struct ControlsState {
#[default = false]
waiting_for_key: bool,
}
methods!({
// Bound to a "Rebind Jump" button in the options menu.
fn on_rebind_jump_click(&self, ctx: &mut ScriptContext<'_, API>, _button: NodeID) {
action_start_rebind!(ctx.ipt, "jump");
with_state_mut!(ctx.run, ControlsState, ctx.id, |state| state.waiting_for_key = true);
}
});
lifecycle!({
fn on_update(&self, ctx: &mut ScriptContext<'_, API>) {
// Gameplay reads the abstract action; bindings can change under it.
if action_pressed!(ctx.ipt, "jump") {
// start jump
}
// Detect rebind completion and hand the binding to game-owned storage.
if let Some(result) = action_rebind_result!(ctx.ipt) {
// result.action, result.action_hash, result.binding
let _ = result;
with_state_mut!(ctx.run, ControlsState, ctx.id, |state| state.waiting_for_key = false);
}
}
});
| Field |
Detail |
| Access |
ctx.ipt.Actions() |
| Signature |
pub fn down(&self, name: &str) -> bool |
| Params |
&self, name: &str |
| Returns |
bool |
| Use when |
Any binding for the action is held (charge, hold-to-aim). |
| Edge behavior |
Hashes the name, then reads the input map; unknown actions return false. |
| Field |
Detail |
| Access |
ctx.ipt.Actions() |
| Signature |
pub fn pressed(&self, name: &str) -> bool |
| Params |
&self, name: &str |
| Returns |
bool |
| Use when |
Any binding for the action fires on the down edge this frame (jump, confirm). |
| Edge behavior |
true only on the frame a binding transitions to down. |
| Field |
Detail |
| Access |
ctx.ipt.Actions() |
| Signature |
pub fn released(&self, name: &str) -> bool |
| Params |
&self, name: &str |
| Returns |
bool |
| Use when |
Any binding for the action fires on release (release a charged shot). |
| Edge behavior |
true only on the frame a binding transitions to up. |
| Field |
Detail |
| Access |
ctx.ipt.Actions() |
| Signature |
pub fn down_hash(&self, name_hash: u64) -> bool |
| Params |
&self, name_hash: u64 |
| Returns |
bool |
| Use when |
Hot-path held check using a precomputed action hash. |
| Edge behavior |
Same as down without re-hashing the name. |
| Field |
Detail |
| Access |
ctx.ipt.Actions() |
| Signature |
pub fn pressed_hash(&self, name_hash: u64) -> bool |
| Params |
&self, name_hash: u64 |
| Returns |
bool |
| Use when |
Hot-path press-edge check using a precomputed action hash. |
| Edge behavior |
Same as pressed without re-hashing the name. |
| Field |
Detail |
| Access |
ctx.ipt.Actions() |
| Signature |
pub fn released_hash(&self, name_hash: u64) -> bool |
| Params |
&self, name_hash: u64 |
| Returns |
bool |
| Use when |
Hot-path release-edge check using a precomputed action hash. |
| Edge behavior |
Same as released without re-hashing the name. |
Start listening from a script callback:
ctx.ipt.Actions().start_rebind("jump");
The command starts at the next input frame. The next new button press becomes
the action's only binding. Held buttons and repeated key events do not count as
new presses.
Poll for completion and save the returned data:
let actions = ctx.ipt.Actions();
if let Some(result) = actions.rebind_result() {
// result.action: action name
// result.action_hash: stable action hash
// result.binding: captured InputBinding
// Save this data with the game's own settings/storage code.
}
Restore saved data while building the runtime input map:
input_map.set_bindings("jump", vec![saved_binding]);
input.set_input_map(input_map);
InputMap::set_bindings_hash provides the same load path for a saved action
hash. Both methods return false when the action does not exist.
| Field |
Detail |
| Access |
ctx.ipt.Actions() |
| Signature |
pub fn start_rebind(&self, name: &str) |
| Params |
Action name |
| Returns |
() |
| Use when |
Start native live rebind by action name. |
| Edge behavior |
Queued command; starts next input frame only when the action exists. |
| Field |
Detail |
| Access |
ctx.ipt.Actions() |
| Signature |
pub fn start_rebind_hash(&self, action_hash: u64) |
| Params |
Stable action hash |
| Returns |
() |
| Use when |
Start native live rebind with a cached action hash. |
| Edge behavior |
Queued command; starts next input frame only when the action exists. |
| Field |
Detail |
| Access |
ctx.ipt.Actions() |
| Signature |
pub fn cancel_rebind(&self) |
| Returns |
() |
| Use when |
Close a rebind prompt without changing bindings. |
| Edge behavior |
Queued command; input received before command application may still bind. |
| Field |
Detail |
| Access |
ctx.ipt.Actions() |
| Signature |
pub fn is_rebinding(&self) -> bool |
| Returns |
bool |
| Use when |
Show or hide a waiting-for-input prompt. |
| Edge behavior |
Turns false after capture or cancellation. |
| Field |
Detail |
| Access |
ctx.ipt.Actions() |
| Signature |
pub fn rebind_result(&self) -> Option<&RebindResult> |
| Returns |
Captured action name, action hash, and InputBinding. |
| Use when |
Detect completion and save developer-owned settings. |
| Edge behavior |
Remains available until another rebind starts. Perro does not save it automatically. |
Use macro forms when script code prefers the compact input API:
action_start_rebind!(ctx.ipt, "jump");
if action_is_rebinding!(ctx.ipt) {
// Show waiting prompt.
}
if let Some(result) = action_rebind_result!(ctx.ipt) {
// Save result.action + result.binding.
}
action_cancel_rebind!(ctx.ipt);
Literal action names use a compile-time action hash. Runtime string expressions
also work and hash at the call site.
Starting another rebind clears the prior result. Rebinding an unknown action
does not start a listener. Call cancel_rebind() to stop a pending listener.
| Macro |
Signature |
Returns |
action_down! |
action_down!(ctx.ipt, "jump") |
bool |
action_pressed! |
action_pressed!(ctx.ipt, "jump") |
bool |
action_released! |
action_released!(ctx.ipt, "jump") |
bool |