Quick answer: An action callback fires for started, performed, and canceled. Subscribe only to the phase you want (usually .performed), or branch on context.phase.
A jump handler runs two or three times per button press. The same method is subscribed to multiple action events — or it’s subscribed to the action itself, which fires every phase.
Subscribe to One Phase
jumpAction.performed += OnJump; // fires once, when the action completes
void OnJump(InputAction.CallbackContext ctx)
{
Jump();
}
.performed is the “it happened” phase for a button. .started fires on press-begin, .canceled on release — subscribing to all three runs your handler three times.
Or Branch on Phase
void OnJump(InputAction.CallbackContext ctx)
{
if (ctx.phase != InputActionPhase.Performed) return;
Jump();
}
If you must subscribe broadly, gate on ctx.phase at the top of the handler.
Press vs Hold vs Release
- started: charge-up begins, hold UI appears.
- performed: the action fired (with an interaction, this respects Tap/Hold timing).
- canceled: released / interaction failed.
Map each to the right gameplay moment instead of treating them as duplicates.
Verifying
One button press = one jump. Charge mechanics use started/performed/canceled deliberately. No phantom double-inputs.
“Actions fire per phase. Subscribe to the one you mean, or check context.phase.”
Also unsubscribe in OnDisable — a handler still wired after the object is gone is another way callbacks ‘multiply’.