An Introduction to Unity 2019’s Input System Package

Nick Suda
17 min readJul 20, 2021

--

(originally published January 21, 2021 on nicksuda.com)

Often times, when building something interactive in Unity, you’ll find that you need to utilize more than one set of input controls to test your work accurately. One common scenario would be a multi-platform video game that runs both on PCs and consoles, and so you need to evaluate how your character controller feels both with a keyboard & mouse and a gamepad. A more trendy, contemporary example would be comparing the UX of a massive multi-user social platform that is operated with touch controls on mobile phones and spatial motion controllers in VR.

As the variety of contexts that Unity is utilized in the real world has grown, many of the engineering assumptions built into its design from early on have been stretched far beyond their initial scope of building a conventional computer game. What’s more, like nearly everything else in Unity, there are many different ways to access control input from the environment’s subsystems. Let’s talk about those ways, culminating in the solution in the form of the new Input System Package deployed in Unity 2019.1.

Method 1: Polling the Input Class Directly

When hacking together a quick first prototype of something in Unity, most people will begin by accessing the Input class directly, in the Update loop of whatever script they’re working in.

NOTE: Each of the code examples in this article will break out to a GitHub Gist code sample if you click through to them.

The simplest way to do it.

This will work no matter where you ask for it, but now your implementation of user input is scattered across various scripts, making it difficult to keep track of, and it’s tied to a very context-specific logic. Plus, you’re checking for at least one boolean every frame, which is a brute force way of being notified of this change in input state.

In the above image, we’re checking for keyboard, mouse, and touch controls within the same runtime context. We could try to split that up using pre-processor statements for clarity’s sake, but with this one change, our code has gotten much more verbose, harder to debug, and we’d have to implement this platform distinction everywhere in the project that we’re using the Input class.

Yuck.

The next-most obvious step from this level of implementation is to separate the user’s intentions from their input hardware’s features. This is the difference between asking for when the user jumped, instead of whether they pressed the spacebar with a keyboard & mouse and/or the B button with a gamepad. Ideally, when we’re implementing a character controller, we’d only want to care that a jump request happened and do something with that, without falling into a verbose rabbit hole of edge cases and pre-processor statements to keep track of which device we’re reading from. This is where Unity’s legacy feature, the Input Manager, comes in.

Method 2: Utilizing the Input Manager

The Input Manager works as the kind of abstraction layer that we’re looking for, but it’s not very fun to work with. It has an obtuse, unfriendly workflow. For instance, it asks you to type inputs in as matched string keywords, and has you describe the expectations of even the most simple input (like key presses) by the features of the most complicated input that it supports, for example where center biases and bipolar values could be expected, like with an analog stick.

The image below is the default list of “axes” defined in the Input Manager, with typical mappings that you’d expect from a keyboard, mouse, and/or gamepad. They’re referenced with user-defined names, e.g. Vertical and Fire1. Everything is obtusely expressed as an “axis” to be referenced, thanks to the “joystick”-centric bias of this system.

Wait, how many ways are there for me to move “up” on-screen? And how many devices let me do that?

These “axes” are read in script like this:

You’ll notice that in addition to video game-y sounding axes like Fire1 and Jump, there are also multiple axes called Horizontal, Vertical and Submit, that (were we to expand them) we would see are mapped to very similar keyboard keys and/or “joystick” buttons. These are organized in a totally flat manner, which obscures how many sources can send that same message, especially when one axis can already have alternative mappings by definition.

There’s an additional layer of complexity to account for when we start to think about the different modes that a Unity program can be in, especially a video game. At the very least, you’re probably going to be re-using these same hardware inputs to navigate a menu outside of the core gameplay loop. The Input Manager does not discriminate by a “usage mode” (e.g. combat vs. options menu) in a meaningful way for us to see at a high level.

Many times, developers won’t even use the Input Manager. They’ll either poll the Input class directly as we did before, at the expense of fragmentation, or they’ll do The Unity Thing To Do and buy a third-party asset that will manage input abstraction for them.

It’s up to us to look for a mouse click in the form of the Fire1 axis and a mouse click in the form of the Submit axis only when we need them respectively. Surely we could have only certain axes available for polling depending on the “mode” we’re in?

Method 3: The New Input System

The new Input System that Unity introduced in version 2019.1 addresses the concerns of multiple device classes, program modes and usage fragmentation that we’ve identified so far, and in a much friendlier and cleaner way. If you’d like to read more about its utility in their own words, I encourage you to read their excellent blog post from October 2019.

I won’t be covering every aspect of using the Input System in this guide, but we will look at how it addresses the problems we’ve discussed, and get as far as being able to tap into its events as callbacks in your scripts, getting as far as we did with the previous methods. With that out of the way, here’s a quick look at its main interface when you load it up with the default settings:

Straight away, we can observe some important features. First, there’s a new concept called an Action Map that serves as the separation point we needed for distinguishing between different usage modes in the program. (In this case, Player vs. UI.)

We can also see that an Action Map contains Actions, which are the distinction between the high-level — well, actions — we’d like to keep track of, and the individual Bindings that they are mapped to depending on the physical device used. There is a clear labeling of which Binding belongs to which device (e.g. Gamepad vs. Pointer vs. XR Controller).

Each child Binding of an Action is equivalent to any of its siblings. At a glance, we can tell that the W, A, S, and D keys on a keyboard and the left analog stick on a gamepad will both move the player. This reads so much cleaner compared to the “axis” list from the Input Manager, where the scope of each item was quite varied in scale, with low-level hardware features (e.g. Mouse ScrollWheel) and high-level game actions (e.g. Jump) living on the same plane organizationally.

By taking a peek at the drop-down menu in the upper left that says All Control Schemes, we see that we now have consistent language to describe individual physical devices as Control Schemes. We get a very clear list of examples by expanding this menu:

By selecting a specific Control Scheme, we can see the Bindings filtered to only the entries that are relevant to that scheme. For example if we select Keyboard&Mouse, we only see WASD, mouselook, and left mouse click for moving, looking, and firing respectively. There’s also duplication of WASD with the arrow keys, mapped to the exact same Bindings.

Okay, this is really cool. Right away it seems like something we’d want to go to the trouble of incorporating if it’s going to better help us keep track of the input control contexts that we care about, when we want to care about them. So let’s go over actually hooking up to it.

Setting Up the New Input System

Note that most of the actual steps I’m covering here are also addressed in Unity’s documentation for installing and using the Input System Package.

We’ll be starting from a fresh Unity project for this example. I’m using Unity 2019.4.18f1. Just as an amusing note, if we look at the Input Manager settings on a default project in the Project Settings menu, you can see they literally advise you to use the Input System instead.

If you’re new to some of the newer features of the Unity workflow, which tend to be loaded in via the Package Manager, it’s a bit of a cognitive load to get on board with at first. But the new Input System is an excellent opportunity to get acquainted with these aspects, so let’s dive in by switching over from the Input Manager to the Input System Package and discuss using the Package Manager in the process.

The Input System is available as a Package in the Package Manager. As of their shift to the annual release system, Unity has changed their strategy for supporting many new features so that they are part of a modular list of optional Packages that comprise the Unity Registry. This means that their engineers can go deep on engine integration with canonical version tracking without increasing the size of a basic Unity installation.

This is a great move when you compare it alongside Unreal Engine, which trades the power of a monolithic feature set for a HUGE size, both for the Editor and for the builds it generates. But I digress. Head over to Window -> Package Manager, wait a moment for the Unity Registry to populate, and then select Input System from the list.

Click Install and hit Yes when the prompt comes up asking you about needing to enable the native platform backends to support the Input System. Your project will restart after doing so.

If we head back to the Project Settings, you’ll see there’s a new entry below Input Manager that says Input System Package. We don’t need to do anything here, but I just wanted to point out the existence of the first Asset that we can generate which is associated with this package, the Input Settings Asset.

One thing you can do with this Asset is define whether or not you want to use an exclusive list of Control Surfaces to support, versus automatically accepting input from any Control Surface that Unity detects input from while it’s running.

If you open the drop-down menu for this device list, you can see how dramatically the context of Unity’s supported input peripherals has expanded since the early video game-centric days. Straight away, you can see they’ve added support for tablet pens, serial sensors, VR controllers and more directly into the native device class list.

You can also define some behavior around “deadzone” ranges, as well as the amount of time between taps in touchscreen controls for certain gestures like double-taps and holds to be recognized. This is a simple top-level re-organization of stuff we used to have to define on a per-axis basis in the old Input Manager. Now, it’s all covered by a single settings configuration that only generates a separate Asset if we decide we want to override these default settings. We don’t need to muck around in any of this, so you don’t need to hit the button that says Create settings asset.

The main Component responsible for interfacing with the Input System is called Player Input. Drop one of these onto a new GameObject in your project.

Before we go any further, you’ll notice already the helpful hint that the Unity engineers have added to this component under Behavior. The Input System delivers its events as a set of callbacks, and they are automatically listed in this hint box. Because we have not yet created an Input Action Asset, the only events this Component will report as of now are connection, disconnection and remapping of devices that are feeding input to Unity. These callbacks are available to scripts as OnDeviceLost, OnDeviceRegained, and OnControlsChanged respectively.

To create a new Input Action Asset, click the button that says Create Actions… and give the resulting Asset a name. Drag the file (it should be selected in the File Browser) into the Actions field of the Player Input Component. You can double click the Asset to view its Action Maps, Actions, and Bindings. It should look the same as the example Input Action Asset at the top of this section.

After dragging in the Asset reference, the Player Input Component should look like this:

Notice that in addition to the OnDeviceLost, OnDeviceRegained, and OnControlsChanged method invocations that we had before adding the Input Action Asset, we now have many more that match the Actions defined in both of the respective Action Maps of the Input Action Asset.

Each of the new method invocations comes from the Actions defined in the Input Action Map. The Player Input Component accommodates several different approaches for receiving these events. Next, we’ll take a look at how to use some of these methods.

Reading Input Actions from Scripts

There are a ton of different ways to send and react to events in Unity. Events can take the form of a C# Event, a UnityEvent, a UnityAction, by employing the use of Reactive Extensions, on and on. It is an open-ended and somewhat endless discourse in the Unity developer community as to what the “best” approach is. I also have strong opinions on this matter, but perhaps we will save that for another day.

In order to accommodate maximum flexibility in this regard, the engineers who created the Input System allow you to specify how you’d like to receive input events from a list of several options in the Behavior field. Click here if you want an in-depth guide from the Unity engineers for each method.

The four styles of event sent by the Input System: SendMessage(), BroadcastMessage(), Unity Events and C# Events.

For demonstration purposes, we will look at how to read the Actions that are equivalent to our raw polling of the Input class from the beginning of this article, and our use of the Horizontal, Vertical, and Fire1 axes in the default axis list of the old Input Manager.

We can assume that looking for the “W” key on the keyboard is part of moving forward in a first/third-person character controller, and that looking for a mouse click is for firing a weapon. Both would be part of the Player Input Action Map in the default Input Action Asset, as Move and Fire respectively.

The default method used by the Input System is one of the oldest and most inefficient ways of passing method invocations between classes in Unity: the use of SendMessage. This approach uses reflection at runtime, since it uses a string to invoke a method name on a Component, so it’s very brute force and has no error handling for invoking a method that doesn’t exist. Nevertheless, it’s one of the oldest tricks in the book. So, if I wanted to make use of the Move and Fire Actions from our Input Action Map using the Send Messages Behavior, it would look like this:

The Fire Action has no return type, it’s just an empty event letting you know something has happened. Therefore, we do not need to feed in an InputValue argument to read it.

The Move Action, however, is always going to be returning what is equivalent to the combination of the Horizontal and Vertical axes in the old Input Manager, and so its return type is a Vector2. The values in this Vector2 are normalized and bi-polar, meaning that the horizontal component reads -1.0 at its leftmost extent and 1.0 at its rightmost extent and that the vertical component reads -1.0 at its bottom-most extent and 1.0 and its top-most extent.

Since in my case I am reading this value from the WASD arrangement on a keyboard, these values will return as discrete values, where diagonal cardinal directions will return with a magnitude of +/- 0.7. For example, if the value returned by the method is (0.0, 1.0), I know I am holding down the W key and thus moving my character North. If the value I’m receiving is (-0.7, -0.7), I’m holding down the S and A keys and moving my character to the Southwest.

Using SendMessage works well enough as a basic validation, but the thought of sending many Vector2s while moving an analog stick in the Update loop via an internal string lookup that traverses the GameObject hierarchy every frame makes me shudder. It’s also only limited to Components that live on the same GameObject as the Player Input Component itself.

I prefer to instead use the Use Unity Events option in the Behavior field of the Player Input Component. While UnityEvents have their own problems with being a rather large blob of memory allocated to invoke often very simple payloads, I like that they expose public fields for dynamic mapping within the Inspector. They’re also much easier to read when skimming through a project. It’s great for the sake of prototyping and for collaboration.

Upon switching the Behavior field of the Player Input Component from Send Messages to Use Unity Events, you’ll notice right away that the structure of the bottom of the Component in the Inspector has changed significantly. We now have expandable fields for each of the Action Maps in the Input Action Asset, as well as UnityEvent fields for the basic device connectivity events (OnDeviceLost, OnDeviceRegained, and OnControlsChanged from earlier).

If we expand the Player field, we’ll find our old friends Move and Fire right up top. They send out a new class called CallbackContext.

Here’s how we write methods to drag-and-drop into these UnityEvent fields:

Since we are no longer beholden the brittle limitation of exactly matching a method name as we would be with the Send Messages Behavior, we can call these methods whatever we want. They just have to be public void functions and take in an InputAction.CallbackContext object. Then we hook them up like so:

… and likewise with hooking up to Fire:

UnityEvents are publicly exposed in the Inspector, and they’re expandable lists. This means that we can hook up these callbacks to any script on any Component anywhere in the GameObject hierarchy.

… and now we’ve closed the loop.

Switching Action Maps

(shout out to Nathan Savant for making me realize I need to include this part!)

It’d be missing the point of this fancy new layer of abstraction if we didn’t discuss how to switch Action Maps at runtime, now that Actions with input mappings that overlap with each other are cleanly separated into different modal contexts.

Looking at the structure of the Inspector for the Player Input Component, you’ll see that there are expandable fields for each of the Action Maps defined in the Input Action Asset that we’ve pointed it to. In this case, one for Player and one for UI. We can hook our reaction methods up to the corresponding Action fields separately.

We then call PlayerInput.SwitchCurrentActionMap(), which takes in a string for the name of the Action Map that we’re looking for inside of the Input Action Asset that the Player Input Component is using; in this case UI.

Switching Action Maps at runtime using PlayerInput.SwitchCurrentActionMap().

We’ve set the Player Input Component’s default Action Map to Player, so we’re primed for locomotion and combat by default. In this demo script, we’re simulating playing the game for five seconds and then pausing and opening up a menu by making the Start() method switch Action Maps on a coroutine that’s five seconds long.

If you were to enter Play Mode and give it window focus, you’d see during the first five seconds that if you press any arrow keys on your keyboard, the OnMove() method would receive those inputs and print them to the console. After the first five seconds, the Player Input Component is set to using UI as its current Action Map instead of Player, and thus those same arrow key presses will now be sent to OnNavigate() instead. Access to the same arrow key presses is mutually exclusive based on the Player Input Component’s currentActionMap field.

Conclusion

In leveraging the Input System, we’ve managed to solve all of the issues we previously encountered with other forms of checking for user input in a Unity program. We no longer have to concern ourselves with the specific features of a device, thanks to their abstraction as Control Schemes with Bindings to Actions. Furthermore, we are able to make a clean semantic distinction between Actions that are only used in certain program modes via the use of Action Maps. And if we use UnityEvents to call our Input System events, we can pretty clearly see everywhere that we’re using certain Actions within our Scene.

There are many key aspects of using the new Input System to the fullest of its capabilities that I have not covered in this article, such as working with the Player Input Manager Component to handle input from multiple local players simultaneously. But I hope this walkthrough has helped you understand the history of handling user input in Unity as the needs for such a feature have grown increasingly complex, and how the use of the new Input System has found an elegant way to address the concerns of previous approaches.

We’re barely scratching the surface in terms of why this new system is so cool. In another article for this blog, I plan on discussing the extensibility of the Input System by implementing a custom device, and how that can help better integrate virtual input that often falls outside the realm of traditional inputs. I hope you’ll join me for that one, and please do not be afraid to leave a comment if you need additional clarity on anything I’ve covered within this post!

If you’d like a slightly more hands-on supplemental overview of this material that was written and supervised by a team of people instead of just me, Unity created an excellent guide covering the Input System basics. Check that out here!

--

--

Nick Suda

XR Dev · Interaction Designer · Musician · Creative Technologist