The action layer for registering assets, reading move and jump values from gameplay code, reacting to action transitions on the event bus, and building runtime prompt labels from binding data.
Summary
This page covers the action layer in detail: registering an InputActionAsset with Scylla’s action manager, looking actions up, reading their current values, subscribing to action transitions on the SEX (Scylla Event eXchange) bus, and inspecting bindings at runtime for UI prompts and configuration screens. Three wrapper types do the heavy lifting (ScyllaInputActionMap, ScyllaInputAction, ScyllaInputBinding), all owned by IInputActionManager and reached through ScyllaInput.Instance.ActionManager.
Unity’s Input System ships with InputActionAsset, InputActionMap, InputAction, and InputBinding types that handle parsing, devices, and the binding evaluation pipeline correctly. What it does not ship is a coherent answer to four day-to-day questions a real game asks: what is the single shape for “read a value or react to a transition”, how do action callbacks compose with the event bus that the rest of the game already subscribes to, how do modifier-key bindings (“Ctrl+Z”) suppress the bare-key bindings on the same trigger (“Z”) that would otherwise fire together, and what is the supported path for showing the player’s current binding as a UI prompt without reaching into Unity’s path strings. Scylla Input’s action layer answers all four.
The wrapper triad maps cleanly onto Unity’s types. A ScyllaInputActionMap wraps an InputActionMap and exposes enable/disable plus the contained actions. A ScyllaInputAction wraps an InputAction and adds the value-reading helpers, the per-frame state queries, and the suppression hook that drives modifier disambiguation. A ScyllaInputBinding wraps each entry in InputAction.bindings and exposes the override state, display string, group membership, and detected device type. The wrappers add behaviour without hiding what is underneath: every ScyllaInputAction exposes its UnityAction, every ScyllaInputBinding exposes its UnityBinding, so escape hatches into the Unity API are always available when something more specialised is needed.
The two read paths exist because game code has two shapes of “I want input”. Per-frame reads (movement, look, aim) live in Update and want the action’s current value; polling via ReadVector2 / ReadFloat / WasPerformedThisFrame is the right answer. One-shot transitions (jump, attack, interact, weapon swap) want to react when the action fires without polling; subscribing to InputActionPerformedEvent on the bus is the right answer for those. Both paths are first-class and can coexist on the same action.
Use this page for:
- Registering an
InputActionAssetand looking actions up by ID. - Reading continuous values (movement, look) from
UpdateviaReadVector2,ReadFloat,ReadValue<T>. - Reading one-shot transitions (jump, attack) via
WasPerformedThisFrame(polling) orInputActionPerformedEvent(events). - Configuring composite bindings with
ButtonWithOneModifier/ButtonWithTwoModifiersand relying on the framework’s automatic disambiguation against the bare-key action. - Building composite Vector2 bindings (WASD, IJKL) that share an action.
- Iterating bindings to render the player’s current key/button assignment as a UI prompt.
- Constructing action maps programmatically when bindings cannot live in an
InputActionAsset(test rigs, generated UI, runtime-authored gameplay modes).
Features
- Wrapper triad:
ScyllaInputActionMap,ScyllaInputAction,ScyllaInputBinding. Each wraps a Unity type, adds behaviour without hiding the underlying object, and is owned byIInputActionManager. The Unity-side type is always reachable through a public property (UnityMap,UnityAction,UnityBinding) when an escape hatch is needed. - Two registration paths.
RegisterActionAsset(InputActionAsset)wraps every map and action inside the asset automatically;RegisterActionMap(ScyllaInputActionMap)accepts a programmatically constructed map for tests, generated UI, or runtime-authored modes. Both feed the same lookup, value-read, and event surface. - Two read paths. Polling (
ActionManager.IsActionPressed("Jump"),ActionManager.GetActionVector2("Move"),action.WasPerformedThisFrame) for per-frame reads inUpdate; events (ScyllaEvents.Listen<InputActionPerformedEvent>(...)) for one-shot transitions reached from anywhere in the codebase. - Three value-read methods plus a safe variant.
ReadFloatandReadVector2dispatch on the action’s expected control type so a misconfigured action returns zero instead of throwing.ReadValue<T>is the raw passthrough;ReadValueSafe<T>catchesInvalidOperationExceptionfor callers that cannot guarantee the type match. - Modifier-aware disambiguation, automatic. When
Ctrl+ZandZare both bound, pressingCtrl+Zfires only the modifier-bound action; the bareZaction is silently suppressed for the duration. Computed at registration time by walking each action’s binding list; runtime cost is one boolean check on the suppression-flagged actions only. - Per-frame state queries.
IsPressed,WasStartedThisFrame,WasPerformedThisFrame,WasCanceledThisFrameavailable both asScyllaInputActionproperties (for cached references) and asIInputActionManagermethods that take anactionID(for one-off polling without caching). Mirrors Unity’sInputActionAPI surface exactly. - Typed action events on the bus.
InputActionPerformedEvent,InputActionStartedEvent,InputActionCanceledEventcarryActionID,MapID,Value,Vector2Value,Device, andTime. Subscribers filter byActionIDand get the payload without needing to query the action manager themselves. - Per-action / per-map / global enable control. Disable a single action via
action.Disable(), a whole map viaActionManager.DisableActionMap("Gameplay"), every map at once viaActionManager.DisableAllActionMaps(). The right granularity for everything from a temporary cooldown to a full modal pause. - Binding introspection for UI. Each
ScyllaInputBindingexposes the design-timePath, the runtimeEffectivePath(with overrides), a precomputedDisplayString(“Space”, “South Button”), the control schemeGroups, and aGetDeviceType()classifier. Build per-action prompt rows or per-control-scheme columns from this data.
Concepts
The vocabulary used throughout the rest of the page. Each row is a type or a term that shows up repeatedly in the recipes below.
| Concept | Type / shape | Notes |
|---|---|---|
InputActionAsset | Unity Input System asset | Editor-authored container of action maps. Registered with Scylla via ActionManager.RegisterActionAsset(asset); the framework calls asset.Enable() and wraps each contained map. |
ScyllaInputActionMap | wrapper around InputActionMap | A named group of actions (e.g. “Gameplay”, “UI”). Enable / disable as a unit. Constructed automatically from each InputActionMap inside a registered asset, or programmatically via new ScyllaInputActionMap(mapID, displayName). |
ScyllaInputAction | wrapper around Unity’s InputAction | The unit of “an action performed”. Exposes value-read helpers, per-frame state queries, three event hooks (OnPerformed, OnStarted, OnCanceled), and modifier-disambiguation metadata (ModifierDepth, NeedsModifierCheck). |
ScyllaInputBinding | snapshot wrapper around InputBinding | One entry in the action’s flat binding list. Holds the design-time Path, the override-aware EffectivePath, a precomputed DisplayString, the control scheme Groups, and the composite flags (IsComposite, IsPartOfComposite). |
| Action ID resolution | string lookup contract | Plain action names work when unique across all registered maps. When two actions share a name (e.g. “Cancel” in both “UI” and “Gameplay”), the second is reachable via the fully qualified form "MapID/ActionID". TryGetAction returns false on ambiguity instead of guessing. |
| Modifier depth | int in {0, 1, 2} per action | 0 for simple bindings, 1 for ButtonWithOneModifier, 2 for ButtonWithTwoModifiers. Computed at registration. Drives the suppression rule that lets Ctrl+Z coexist cleanly with Z. |
| Trigger control path | normalised string per action | The control path of the binding’s “button” part (e.g. <Keyboard>/z for both Z and Ctrl+Z). Actions sharing a trigger path compete during disambiguation; actions on different triggers (e.g. Space vs Z) are independent. |
InputActionPerformedEvent and friends | ScyllaEvent subtypes published by the manager | Fired for every action transition. Carries ActionID, MapID, Value, Vector2Value, Device, Time. Subscribers route on ActionID rather than subscribing to one event per action. |
Recipes
Each recipe here is self-contained. If you are new to the action layer, start with the first recipe – it sets up the cached ScyllaInputAction references that the others reuse. The rest can be read in any order, so jump to whichever matches what you are building.
Register an asset and cache action references
Problem. You have an InputActionAsset authored in the editor with a “Move” action and a “Jump” action, and you want gameplay code to hold stable references to them without doing a dictionary lookup every frame.
Solution. Register the asset once against ActionManager, then cache the resulting ScyllaInputAction objects in Start (inside a RunWhenReady callback). The references stay valid for the lifetime of the registration; you only need to look them up again after an UnregisterActionAsset / RegisterActionAsset round-trip.
using Scylla.Core;
using Scylla.Input;
using UnityEngine;
using UnityEngine.InputSystem;
public sealed class PlayerInputBootstrap : MonoBehaviour
{
[SerializeField] private InputActionAsset _playerActions;
/* Stable references cached at startup. The framework holds these for the
* lifetime of the asset registration; re-lookup is only needed after an
* UnregisterActionAsset / RegisterActionAsset round trip. */
private ScyllaInputAction _moveAction;
private ScyllaInputAction _jumpAction;
private ScyllaInputAction _inventoryAction;
private ScyllaInputAction _hotkeyAction;
private ScyllaInputAction _debugAction;
private void Start()
{
/* RunWhenReady fires immediately if the framework is already up, or
* queues until readiness if not - the safe entry point for any code
* that needs the Input module. */
ScyllaCore.Instance.RunWhenReady(OnFrameworkReady);
}
private void OnFrameworkReady()
{
var actions = ScyllaInput.Instance.ActionManager;
/* Registering the asset calls asset.Enable() on the Unity side and
* wraps every InputActionMap inside it into a ScyllaInputActionMap.
* Registering the same asset twice is a no-op (a warning is logged). */
actions.RegisterActionAsset(_playerActions);
/* GetAction resolves on the plain action name first. If the name has no
* slash and no exact match exists, every registered map is searched.
* Use the fully qualified "MapID/ActionID" form to pin the lookup to one
* specific map when two maps share an action name. */
_moveAction = actions.GetAction("Move");
_jumpAction = actions.GetAction("Jump");
_inventoryAction = actions.GetAction("Inventory");
_hotkeyAction = actions.GetAction("Gameplay/Hotkey");
/* TryGetAction is the no-throw overload - the right shape for optional
* bindings (a debug action that may or may not exist in the shipped asset). */
if (actions.TryGetAction("OptionalDebugAction", out var debugAction))
{
_debugAction = debugAction;
}
}
}
Read movement and react to jump from a player controller
Problem. You are building a platformer player controller. “Move” is a continuous Vector2 composite (WASD or left stick) that you need every frame to drive locomotion. “Jump” is a one-shot button that you want to fire exactly once per press – polling for it in Update works, but you also want the audio system and the animation controller to react without needing a reference to the PlayerController.
Solution. Use both read paths on the same actions. Read “Move” by polling in Update with ReadVector2. For “Jump”, subscribe to InputActionPerformedEvent on the Event System bus: any system in the codebase can react without knowing about the controller.
using Scylla.Core.Events;
using Scylla.Input;
using UnityEngine;
public sealed class PlayerController : MonoBehaviour
{
[SerializeField] private float _moveSpeed = 5f;
/* Cached from the registration recipe above. */
private ScyllaInputAction _moveAction;
private ScyllaInputAction _jumpAction;
/* Subscription token for the event-driven path. Field-store with lifetime
* matching this component; dispose in OnDestroy. */
private ScyllaEventSubscription _jumpSubscription;
private void Start()
{
ScyllaCore.Instance.RunWhenReady(() =>
{
var actions = ScyllaInput.Instance.ActionManager;
_moveAction = actions.GetAction("Move");
_jumpAction = actions.GetAction("Jump");
/* Event subscription for Jump. The handler fires once on the rising
* edge; no per-frame check needed. The bus delivers in priority order. */
_jumpSubscription = ScyllaEvents.Listen<InputActionPerformedEvent>(
subscriber: this,
handler: OnAnyActionPerformed,
priority: ScyllaEventPriority.Medium
);
});
}
private void OnDestroy()
{
/* Dispose the token. Missing this is the most common cause of "handler
* fired after the scene reloaded" bugs. */
_jumpSubscription?.Dispose();
}
private void Update()
{
if (_moveAction == null) return;
/* Continuous read: ReadVector2 returns the composite WASD value (or analog
* stick). Returns Vector2.zero when no binding is held. Safe on type mismatch:
* if Move were misconfigured as a Button action, ReadVector2 returns zero
* instead of throwing. */
Vector2 move = _moveAction.ReadVector2();
transform.Translate(move * _moveSpeed * UnityEngine.Time.deltaTime);
/* The polling alternative for Jump - valid if you want all input handling
* in one Update block rather than spread across subscriptions. */
if (_jumpAction != null && _jumpAction.WasPerformedThisFrame)
{
ExecuteJump();
}
}
private void OnAnyActionPerformed(InputActionPerformedEvent evt)
{
/* One handler dispatching on ActionID is faster and clearer than multiple
* subscriptions filtering through type checks. */
if (evt.ActionID == "Jump")
{
ExecuteJump();
}
}
private void ExecuteJump() { /* ... */ }
}
Disambiguate modifier combos (Shift+Key vs bare Key)
Problem. You want to bind “Toggle Console” to backquote and “Toggle Performance Monitor” to Shift+Backquote. With Unity’s Input System alone, pressing Shift+Backquote fires both actions because ButtonWithOneModifier composites do not suppress the bare-key binding on the same trigger. You get double-fire on every modifier combo.
Solution. Scylla solves this automatically at registration time. No configuration is required. When your asset contains the three bindings below, the framework walks each action’s binding list, computes the modifier depth (0 for a simple binding, 1 for ButtonWithOneModifier, 2 for ButtonWithTwoModifiers), and groups actions that share a trigger control path. At callback time, an action with lower modifier depth is suppressed when a sibling with higher depth is satisfiable given the currently held modifiers.
/* Three actions on the same trigger key (Backquote):
Action: "ToggleConsole" Binding: <Keyboard>/backquote -> depth 0
Action: "TogglePerfMonitor" Binding: Shift + <Keyboard>/backquote -> depth 1
Action: "CycleVisualStyle" Binding: Ctrl + Shift + <Keyboard>/backquote -> depth 2
Trigger control path for all three: "<Keyboard>/backquote". The framework
groups them at registration and flags the two lower-depth actions for the
runtime suppression check.
At runtime:
Press Backquote alone -> only ToggleConsole fires
Press Shift+Backquote -> only TogglePerfMonitor fires; ToggleConsole is suppressed
Press Ctrl+Shift+Backquote -> only CycleVisualStyle fires; the other two are suppressed
Actions on unrelated keys (Jump on Space, Move on WASD) are never affected
because they have no competing siblings on the same trigger path.
OnCanceled always fires regardless of suppression so subscribers that track
pressed/held state correctly reset themselves even when the corresponding
OnStarted was suppressed. */
The suppression decision flows through ScyllaInputAction.ShouldSuppress, an internal delegate injected by the action manager. The runtime cost is paid only by actions with the NeedsModifierCheck flag set, which the manager sets only on actions that have at least one sibling with a higher modifier depth on the same trigger path. Actions on a unique trigger – which is most actions – pay no runtime cost at all.
Build runtime button-prompt labels
Problem. You are building an in-world “Press [E] to interact” label and an options screen that shows the player’s current key assignments. You need the display string for an action’s active binding without digging into Unity’s InputControlPath strings yourself, and you need to know which device family the binding targets so you can pick the right prompt sprite atlas.
Solution. Each ScyllaInputBinding exposes a precomputed DisplayString for the most common display case and a GetDeviceType() classifier. GetBindingsForGroup filters down to the active control scheme so you get the right binding when the player is on a gamepad versus keyboard. For the custom-binding indicator (a “you changed this” asterisk in the options UI), HasOverride tells you whether EffectivePath differs from the design-time Path.
using Scylla.Input;
using UnityEngine;
using UnityEngine.InputSystem;
public sealed class PromptResolver
{
private readonly IInputActionManager _actions;
public PromptResolver(IInputActionManager actions)
{
_actions = actions;
}
/* Returns the first binding's display label for the named action, filtered to
* the requested control scheme group. Call this from UI build code:
* var label = resolver.GetPromptLabel("Interact", "Keyboard");
* var pad = resolver.GetPromptLabel("Interact", "Gamepad");
* Returns null when no matching binding exists. */
public string GetPromptLabel(string actionID, string group)
{
var action = _actions.GetAction(actionID);
if (action == null) return null;
/* GetBindingsForGroup does a case-insensitive substring match against each
* binding's semicolon-separated Groups string. Allocates a list per call;
* cache the result outside Update (see Best Practices). */
var bindings = action.GetBindingsForGroup(group);
if (bindings.Count == 0) return null;
/* DisplayString is precomputed with OmitDevice, so it reads as a short label
* ("E") rather than "Keyboard/E". Call binding.GetDisplayString(options) on
* demand for formatting with explicit options. */
return bindings[0].DisplayString;
}
/* Returns the device type that the first binding for an action targets.
* Use this to choose prompt sprite atlases (keyboard vs Xbox vs PlayStation). */
public InputDeviceType GetPromptDevice(string actionID)
{
var action = _actions.GetAction(actionID);
if (action == null || action.BindingCount == 0)
return InputDeviceType.Unknown;
return action.GetBinding(0).GetDeviceType();
}
/* HasOverride is the hook for a "you customised this binding" indicator.
* The design-time Path vs runtime EffectivePath difference drives the indicator. */
public bool HasPlayerOverride(string actionID)
{
var action = _actions.GetAction(actionID);
if (action == null) return false;
foreach (var binding in action.Bindings)
{
if (binding.HasOverride) return true;
}
return false;
}
}
Bind multiple keys or devices to the same action
Problem. You want “Jump” to fire on both the spacebar (keyboard players) and the South button (gamepad players) without writing separate handling for each device. You want both bindings to feed the same ScyllaInputAction and the same event on the bus.
Solution. Author both bindings in the same action in the InputActionAsset editor. The action wraps them both; whichever device the player uses, the same ScyllaInputAction.WasPerformedThisFrame and InputActionPerformedEvent fire. The Device field in the event payload tells you which physical device triggered it when you need to branch (for example, to show the correct prompt icon).
/* In the InputActionAsset editor, "Jump" has two bindings:
* Binding 0: <Keyboard>/space (Groups: "Keyboard")
* Binding 1: <Gamepad>/buttonSouth (Groups: "Gamepad")
*
* Both are wrapped into the same ScyllaInputAction; no code change is needed
* to support both. */
using Scylla.Core.Events;
using Scylla.Input;
using UnityEngine;
using UnityEngine.InputSystem;
public sealed class JumpHandler : MonoBehaviour
{
private ScyllaEventSubscription _jumpSub;
private void Start()
{
ScyllaCore.Instance.RunWhenReady(() =>
{
_jumpSub = ScyllaEvents.Listen<InputActionPerformedEvent>(
subscriber: this,
handler: OnActionPerformed,
priority: ScyllaEventPriority.Medium
);
});
}
private void OnDestroy()
{
_jumpSub?.Dispose();
}
private void OnActionPerformed(InputActionPerformedEvent evt)
{
if (evt.ActionID != "Jump") return;
/* evt.Device tells you what triggered it. Branch here only if the
* response actually differs per device (e.g. different haptic feedback). */
var isGamepad = evt.Device is Gamepad;
ExecuteJump(isGamepad);
}
private void ExecuteJump(bool fromGamepad) { /* ... */ }
}
Construct action maps in code for test rigs and runtime modes
Problem. You have a test fixture that needs to inject synthetic input without an InputActionAsset file, or a runtime gameplay mode that generates its bindings at startup from a data file. You want these bindings to participate in the same modifier-disambiguation rules and the same event bus as the editor-authored actions.
Solution. ScyllaInputActionMap exposes a string constructor that builds an empty, unattached map. Add actions one by one, register the map, and from that point it behaves identically to a map wrapped from an asset. Note that programmatic maps start disabled; call Enable() when the actions should start receiving input.
using Scylla.Input;
using UnityEngine.InputSystem;
public static class TestInputBuilder
{
/* Builds an action map programmatically for a test or a generated gameplay mode.
* The Unity InputAction is constructed directly; the ScyllaInputAction wrapper
* is created from it and added to the new map. Once registered, the map's actions
* behave identically to ones loaded from an asset. */
public static ScyllaInputActionMap BuildTestMap(IInputActionManager actions)
{
/* Empty map with an explicit ID and display name. */
var map = new ScyllaInputActionMap(mapID: "Test", displayName: "Test Map");
/* Construct a Unity InputAction with a button binding. The expectedControlType
* drives the value-read dispatch in the ScyllaInputAction wrapper, so set it
* deliberately to avoid ReadFloat / ReadVector2 type-mismatch fallbacks. */
var jumpUnityAction = new InputAction(
name: "Jump",
type: InputActionType.Button,
binding: "<Keyboard>/space",
expectedControlType: "Button"
);
/* Wrap and add. The ScyllaInputAction subscribes to Unity callbacks on
* construction; the map subscribes to the ScyllaInputAction events on AddAction. */
var jumpAction = new ScyllaInputAction(jumpUnityAction, mapID: "Test");
map.AddAction(jumpAction);
/* Register with the manager. Modifier-disambiguation metadata is recomputed
* across every registered action after the new map joins, so the new bindings
* participate in the same suppression rules as editor-authored ones. */
actions.RegisterActionMap(map);
/* Programmatic maps are not auto-enabled. Enable explicitly when the actions
* should start receiving input. */
map.Enable();
return map;
}
}
API Sketch
The grouping below mirrors how your code reaches into the action layer: manager facade, action wrapper, map wrapper, binding wrapper, then the event types published on the bus.
namespace Scylla.Input
{
public interface IInputActionManager
{
bool IsInitialized { get; }
int ActionMapCount { get; }
int ActionCount { get; }
/* Registration. */
void RegisterActionAsset (UnityEngine.InputSystem.InputActionAsset asset);
void UnregisterActionAsset(UnityEngine.InputSystem.InputActionAsset asset);
void RegisterActionMap (ScyllaInputActionMap map);
void UnregisterActionMap (string mapID);
bool HasActionMap (string mapID);
/* Lookup. */
ScyllaInputAction GetAction (string actionID);
bool TryGetAction (string actionID, out ScyllaInputAction action);
ScyllaInputActionMap GetActionMap (string mapID);
IReadOnlyList<ScyllaInputAction> GetAllActions ();
IReadOnlyList<ScyllaInputAction> GetActionsInMap(string mapID);
IReadOnlyList<ScyllaInputActionMap> GetAllActionMaps();
/* Map control. */
void EnableActionMap (string mapID);
void DisableActionMap (string mapID);
bool IsActionMapEnabled (string mapID);
void EnableAllActionMaps ();
void DisableAllActionMaps();
/* Value and state queries by ID (the no-cache convenience overloads). */
float GetActionValue (string actionID);
Vector2 GetActionVector2 (string actionID);
bool IsActionPressed (string actionID);
bool WasActionPerformedThisFrame (string actionID);
bool WasActionStartedThisFrame (string actionID);
bool WasActionCanceledThisFrame (string actionID);
}
public class ScyllaInputAction
{
public string ActionID { get; }
public string MapID { get; }
public string DisplayName { get; }
public UnityEngine.InputSystem.InputActionType ActionType { get; }
public System.Type ValueType { get; }
public string ExpectedControlType { get; }
public UnityEngine.InputSystem.InputAction UnityAction { get; }
public bool IsEnabled { get; }
public bool IsPressed { get; }
public bool WasStartedThisFrame { get; }
public bool WasPerformedThisFrame { get; }
public bool WasCanceledThisFrame { get; }
public IReadOnlyList<ScyllaInputBinding> Bindings { get; }
public int BindingCount { get; }
/* Most-recent activity tracking, updated on every Unity callback. */
public UnityEngine.InputSystem.InputDevice LastActiveDevice { get; }
public UnityEngine.InputSystem.InputControl LastActiveControl { get; }
public double LastPerformedTime { get; }
/* Modifier disambiguation metadata, computed at registration. */
public int ModifierDepth { get; }
public bool NeedsModifierCheck { get; }
/* Value reads. */
public float ReadFloat ();
public Vector2 ReadVector2 ();
public T ReadValue<T> () where T : struct; /* throws on type mismatch */
public T ReadValueSafe<T>() where T : struct; /* returns default on mismatch */
/* Binding access. */
public ScyllaInputBinding GetBinding (int index);
public string GetBindingDisplayString(int bindingIndex = 0,
InputControlPath.HumanReadableStringOptions options = InputControlPath.HumanReadableStringOptions.OmitDevice);
public IReadOnlyList<ScyllaInputBinding> GetBindingsForGroup(string group);
public void RefreshBindings();
/* Per-action enable / disable (use map-level enable for the common case). */
public void Enable ();
public void Disable();
/* Event hooks at the wrapper level. Same payload as the bus events. */
public event Action<ScyllaInputAction> OnPerformed;
public event Action<ScyllaInputAction> OnStarted;
public event Action<ScyllaInputAction> OnCanceled;
}
public class ScyllaInputActionMap
{
public string MapID { get; }
public string DisplayName { get; }
public UnityEngine.InputSystem.InputActionMap UnityMap { get; }
public bool IsEnabled { get; }
public IReadOnlyList<ScyllaInputAction> Actions { get; }
public int ActionCount { get; }
public ScyllaInputActionMap(UnityEngine.InputSystem.InputActionMap unityMap);
public ScyllaInputActionMap(string mapID, string displayName = null);
public void AddAction (ScyllaInputAction action);
public bool RemoveAction(string actionID);
public ScyllaInputAction GetAction (string actionID);
public bool TryGetAction(string actionID, out ScyllaInputAction action);
public bool HasAction (string actionID);
public void Enable ();
public void Disable();
public event Action<ScyllaInputActionMap> OnEnabled;
public event Action<ScyllaInputActionMap> OnDisabled;
public event Action<ScyllaInputAction> OnActionPerformed;
public event Action<ScyllaInputAction> OnActionStarted;
public event Action<ScyllaInputAction> OnActionCanceled;
}
public class ScyllaInputBinding
{
public Guid BindingID { get; }
public int BindingIndex { get; }
public string ActionID { get; }
public string Path { get; }
public string EffectivePath { get; }
public string DisplayString { get; }
public string Name { get; }
public string Groups { get; }
public bool IsComposite { get; }
public bool IsPartOfComposite { get; }
public bool HasOverride { get; }
public string OverridePath { get; }
public UnityEngine.InputSystem.InputBinding UnityBinding { get; }
public void Refresh(UnityEngine.InputSystem.InputBinding unityBinding);
public string GetDisplayString(InputControlPath.HumanReadableStringOptions options = InputControlPath.HumanReadableStringOptions.OmitDevice);
public bool BelongsToGroup (string group);
public InputDeviceType GetDeviceType ();
}
/* Event payloads published on the SEX bus. */
public class InputActionPerformedEvent : Scylla.Core.Events.ScyllaEvent
{
public string ActionID { get; set; }
public string MapID { get; set; }
public float Value { get; set; }
public Vector2 Vector2Value { get; set; }
public UnityEngine.InputSystem.InputDevice Device { get; set; }
public double Time { get; set; }
}
public class InputActionStartedEvent : Scylla.Core.Events.ScyllaEvent { /* same shape */ }
public class InputActionCanceledEvent : Scylla.Core.Events.ScyllaEvent { /* same shape */ }
}
See the API reference for full signatures, remarks, and the complete event roster.
Composite binding reference
The composites that interact with the modifier disambiguation rule, and what they mean for the action they belong to.
| Composite type | Authored as | Modifier depth | Trigger control path | Notes |
|---|---|---|---|---|
| Simple (non-composite) | Single binding to a control path | 0 | The binding’s EffectivePath | The default. One binding row in the asset editor. Participates in disambiguation only as a “bare” sibling of a modifier composite on the same trigger. |
ButtonWithOneModifier | Composite container + modifier + button | 1 | The button part’s EffectivePath | Fires only when the modifier is held and the button is pressed. Suppresses any bare-key sibling on the same trigger path during disambiguation. |
ButtonWithTwoModifiers | Composite container + modifier1 + modifier2 + button | 2 | The button part’s EffectivePath | Fires only when both modifiers and the button are held. Suppresses both bare-key and one-modifier siblings on the same trigger. |
2DVector (WASD, IJKL) | Composite container + up + down + left + right | 0 (per part) | Each direction part contributes its own path | Used for movement input. Does not participate in modifier disambiguation as a unit because each part is its own trigger; relevant for ReadVector2. |
1DAxis | Composite container + negative + positive | 0 (per part) | Each part contributes its own path | Two-key axis (e.g. Q/E for camera roll). Returns -1, 0, or +1 from ReadFloat. |
OneModifier | Composite container + modifier + binding | 1 | The binding part’s EffectivePath | Generic single-modifier composite for non-button actions (axes, vectors). Same disambiguation rules as ButtonWithOneModifier. |
Best Practices
- Cache
ScyllaInputActionreferences inStart(insideRunWhenReady), not inUpdate.ActionManager.GetAction("Foo")is a dictionary lookup – cheap but not free; the cost adds up on a hot path. The reference stays valid until the owning asset is unregistered, so one lookup at startup is the right shape. - Use fully qualified
"MapID/ActionID"lookup when two maps share an action name. Plain-name lookup picks the first match across all maps, which is order-dependent. Be explicit when the codebase grows past two maps. - Prefer
WasPerformedThisFramefor one-shot transitions; reserveIsPressedfor held-state queries.IsPressedreturnstruefor every frame the binding is held; pairing it with a “did I already handle this” flag in user code duplicates work the action manager already does. ReadFloatandReadVector2handle type mismatches gracefully;ReadValue<T>does not. Use the typed convenience methods for the common cases; reach forReadValueSafe<T>when the action’s type is dynamic; use the unsafeReadValue<T>only when the type match is known and the cost of the try/catch is unacceptable.- Subscribe to
InputActionPerformedEventonce and dispatch onActionIDinside the handler. One handler that switches on the ID is faster and clearer than ten handlers subscribed to the same event type and filtering throughischecks. - Author modifier-bound actions as
ButtonWithOneModifier(orTwo) composites, not as bare actions guarded by a modifier check. The composite approach lets the framework’s disambiguation engage automatically. AuthoringCtrl+Zas “an action bound toZwith a modifier check in the callback” defeats the suppression rule and reintroduces the double-fire bug. - Disable maps for context switches; disable individual actions for cooldowns.
ActionManager.DisableActionMap("Gameplay")cleanly stops every gameplay action when a menu opens. See Contexts for the context-stack approach.action.Disable()is the right shape for a single ability that needs to be unavailable for a few seconds. - Always store the
Listenreturn value. Discarding the subscription token makes the listener effectively permanent. Field-store the token, dispose inOnDestroy(for MonoBehaviours) orOnShutdown(for modules). - Cache
GetBindingsForGroupresults outsideUpdate. The method filters by substring and allocates a list every call. For per-frame HUD code, cache the result onControlSchemeChangedEventinstead. - Use
action.UnityActionandbinding.UnityBindingas escape hatches, not as the default. The wrappers cover the common cases; the underlying Unity objects exist for the rare cases that need specialised access (interaction processors, custom phase tracking). Reach for the wrapper-level API first.
Pitfalls
Related
- Scylla Input
- Getting Started
- Event System
- API reference:
Scylla.Input.IInputActionManager - API reference:
Scylla.Input.ScyllaInputActionMap - API reference:
Scylla.Input.ScyllaInputAction - API reference:
Scylla.Input.ScyllaInputBinding - API reference:
Scylla.Input.InputActionPerformedEvent - API reference:
Scylla.Input.InputActionStartedEvent - API reference:
Scylla.Input.InputActionCanceledEvent