Quick answer: Subscribe to one of started/performed/canceled per logical event — usually performed. Unsubscribe in OnDisable. Use the Input Debugger to list active subscribers.
You bind “Fire” to the left mouse button. The player clicks once; your method runs twice; two bullets spawn from one click. Disabling the second behavior makes the double-fire disappear, so you know it’s subscription-related, but the cause isn’t obvious from the editor inspector.
Cause 1: Subscribing to Both started and performed
For a default Press interaction, started and performed fire in the same frame on key-down. Subscribing both:
// Wrong — both callbacks fire on a single press
fireAction.started += OnFire;
fireAction.performed += OnFire;
Most code samples on the internet show subscribing to one of them. Make sure you didn’t paste both during refactoring:
// Right — one subscription per logical event
fireAction.performed += OnFire;
Use started only if you specifically want to fire at the moment of interaction start (e.g., for charging mechanics). Use performed for “press to fire”. Use canceled for “release the held key.”
Cause 2: Multiple Player Inputs in the Scene
If you use PlayerInput components and accidentally have two PlayerInput components in the scene — for example, after duplicating a player prefab in a level — both subscribe to the same action and both invoke your OnFire method.
Check with Hierarchy → Find → t:PlayerInput. You should see exactly one for a single-player game.
Cause 3: Subscribing in Both Awake and OnEnable
The Input System recommends subscribing in OnEnable and unsubscribing in OnDisable. If you also subscribe in Awake by accident, every enable/disable cycle accumulates subscriptions:
void Awake()
{
fireAction.performed += OnFire; // runs once, never undone
}
void OnEnable()
{
fireAction.performed += OnFire; // runs again, doubles
fireAction.Enable();
}
void OnDisable()
{
fireAction.performed -= OnFire; // removes only one subscription
fireAction.Disable();
}
Delete the Awake subscription. Move everything to OnEnable/OnDisable pairs. The Input System safely tolerates extra Enable() calls, but C# event subscriptions don’t deduplicate — every += adds a fresh handler.
Cause 4: Generated C# Wrapper Class Instantiated Multiple Times
If you use the “Generate C# Class” checkbox on your .inputactions asset, each instantiation of that class allocates its own copy of the action map. If two MonoBehaviours each new their own instance, you have two action maps observing the same device, and pressing a key fires events through both maps.
Use a single instance and pass it around, or use InputActionAsset.singleton conventions:
public class InputManager : MonoBehaviour
{
public static Controls Controls { get; private set; }
void Awake()
{
if (Controls != null) { Destroy(gameObject); return; }
Controls = new Controls();
DontDestroyOnLoad(gameObject);
}
}
Other scripts then reference InputManager.Controls rather than creating their own.
Verifying
Open Window → Analysis → Input Debugger. Select your action. The Listeners panel lists every callback currently subscribed. If you see your OnFire method twice, you’re double-subscribed; if you see it once, the duplication is at a different layer (multiple PlayerInputs, multiple action map instances). Add a counter to OnFire:
void OnFire(InputAction.CallbackContext ctx)
{
Debug.Log($"Fire on frame {Time.frameCount}, phase={ctx.phase}");
}
One log per click after the fix is in. Two logs — keep digging through the four causes above.
“In the new Input System, a callback firing twice always traces back to two subscriptions, not two events. Find the second subscription.”
Always pair += in OnEnable with -= in OnDisable. The Input Debugger’s Listeners tab is the fastest way to spot duplicates.