Using Unity’s New Input System to Simulate GetKey()

Seona Bellamy
5 min readJun 7, 2021

--

We’re back with the new Unity Input System again, because I ran into an interesting challenge and learned a bunch of new things. So I figured I’d share them with you. While not strictly necessary, you may find it helpful by starting with my previous articles on the system:

Part 1 | Part 2 | Part 3 | Part 4

Now that that’s out of the way, let’s move on to the new stuff. Namely, how the #$%! do you do something similar to the old Input.GetKey()? This used to be how you did something while a key was being pressed, and it would stop happening automatically when the key was released. Similarly, Input.GetMouseButton() did the same for mouse buttons.

But, I hear you say, in Part 2 of this series we got the player moving while we were holding the direction keys! They stopped moving when we released the key! Isn’t that the same thing?

Well, yes and no. The movement action we set up in Part 2 used the “value” action type and the “Vector2” control type. This means it was leveraging a bunch of built-in logic that Unity’s new Input System provides to do just this sort of thing. But what if we wanted to change the Player’s speed if they hold down the Shift key? While the key is being held, they move at twice their normal speed. Then they go back to normal speed when the key is released.

It turned out that this was a lot harder than it looked. Then I discovered that the secret lay in the Player Input component.

When you add a Player Input component to an object, the default behaviour is “Send Messages”. There are other options, however. What we want to do is change the behaviour to “Invoke Unity Events”.

This gives us a bunch of new options, but the most important is that it lets us take advantage of the full Callback Context that the Input System provides. The Callback Context has three main options that we care about:

  • CallbackContext.started — This fires when the input is first triggered, kind of like the old Input.GetKeyDown().
  • CallbackContext.performed — This one is a little harder to understand. It fires immediately after CallbackContext.started, then again if the value changes during the Input Action. Additionally, after it fires the status of the action is returned to “started” without CallbackContext.started actually being fired again. No, I’m not sure why they did it this way either.
  • CallbackContext.canceled — This one fires at the end of the Input Action, sort of like the old Input.GetKeyUp().

So we can create a new Thruster action in our GameControl, and bind it to the Left Shift key. Then we go into our Player script and add the following variables:

[SerializeField]
private float _thrusterMultiplier = 2f;
private float _thrustSpeed = 1f;

Then the following method:

public void OnThruster(InputAction.CallbackContext context)
{
if (context.performed)
{
_thrustSpeed = _thrusterMultiplier;
}
else if (context.canceled)
{
_thrustSpeed = 1f;
}
}

Here you can see that we’re letting the Input System pass us the CallbackContext and then checking what the context is. If the context is “Performed” then we set the _thrustSpeed to our _thrustMultipler (which is serialised so that we can tweak it in the Editor if we want). If the context is “Canceled”, meaning that the key has been released, then we set it back to 1.

In the Update() loop, we now add that speed to our movement calculation:

transform.Translate(_moveVec * _moveSpeed * _thrustSpeed * Time.deltaTime);

If we aren’t holding the Shift key, the _thrustSpeed is 1 and so no change occurs. But while the Shift key is held down the speed changes to 2 and so the overall movement speed is doubled.

One extra thing we need to be mindful of here: All actions in a given map need to work the same way. So now that we’ve decided to go with the Unity Events style of handling we need to update our existing Player methods:

public void OnMove(InputAction.CallbackContext context)
{
_moveVec = context.ReadValue<Vector2>();
}

public void OnFire(InputAction.CallbackContext context)
{
if (context.performed && Time.time > _nextFire)
{
_nextFire = Time.time + _fireRate;

if (_tripleShotActive)
{
GameObject shot = Instantiate(_tripleShotPrefab);
shot.transform.position = transform.position;
}
else
{
GameObject laser = PoolManager.Instance.RequestLaser();
laser.transform.position = transform.position + _laserOffset;
}

onLaserFired?.Invoke();
}
}

Both methods now take in the CallbackContext. OnMove() is now reading the value from that context rather than being passed it directly. OnFire() is checking if the context is “Performed” (i.e. either of the fire buttons have been pressed) as well as checking if the cooldown is finished. Now these actions will continue to work as expected.

Before this will run, however, we need to go back to the Editor and hook this up to the Player Input component. As soon as we changed the behaviour type to “Invoke Unity Events”, we got a new set of options for Events. If we expand this we see options for our two action maps, Player and UI. If we expand the Player option we see the familiar Unity event boxes for each of our Actions — just as if we were working with a UI button.

A big gotcha here is that you don’t drag the actual script directly into the event binding. What you want to drag is the script as attached to an object in your hierarchy. In this case, both the script and the Player Input we’re using are attached to the Player object, so we drag the Player (Script) component onto the event (arrow 1). Then we can select the appropriate method from it just like we’re used to doing for UI buttons.

Helpfully, Unity groups all of the appropriate Input Events at the top of the list. This also helps you know if you forgot to set one up properly.

Now you can run your game and all the previously-working inputs should still be working. In addition, when you hold the Left Shift key while moving your player will move at double speed!

--

--