Making a Hack’n’slash #2: Setting up cross-platform inputs

Let’s continue our hack’n’slash and discover Unity’s new input system!

This article is also available on Medium.

In the first episode of this series, we prepared a core feature for our hack’n’slash game: the ability to move our player and have the camera follow it around! But, at the moment, this movement uses keyboard inputs and it’s a bit limited.

Today, I want to improve this system and switch over to Unity’s new input system so that we can make the controls work both on desktop (with keyboard + mouse hardware) or on consoles (with gamepad controllers) – by the end of this tutorial, we’ll have cross-platform inputs.

Why use the new input system?

Now – the truth is that, it looks quite simple to rely on Unity’s old input system, and its direct GetAxis() or GetInput() methods. You can get some axes values and quickly integrate them in your C# logic.

So… why should we change and use the new one?

Well, there are a few perks to using this other system: although it is still in development and only available as an additional package at the moment, it already has some really interesting features and it absolutely deserves we take a look at it 😉

Why is the new system interesting?

As summarised by OneWheelStudio in this video, although it’s a bit complex to apprehend at first, the new input system is pretty powerful – it particularly shines in three areas:

  • cross-platform controls are easy: you can quickly setup an input scheme that works with a keyboard, an Xbox controller, a PlayStation gamepad, etc. while keeping all devices consistent and in sync
  • the inputs can use events: instead of continuously polling and checking for buttons in the Update() function, for most inputs you can subscribe to some specific events and assign them callbacks that are managed on their own, automatically, if the input is indeed activated – which is way more optimised!
  • there are plenty of quick-wins & facilitators like composite binding, setting the input by pressing it on your controller, switching between multiple action maps depending on the current context…

Note: polling and regular update is still required for continuous inputs like sticks however, as we’ll see later on in this tutorial 🙂

So even though it requires a bit more preliminary work, the new input system actually does a lot of the heavy lifting under the hood and ultimately creates a more robust architecture.

How is the new system organised?

Now, you might be wondering how exactly this new system works, and how it can provide us with all these new features.

Basically, the new input system relies on the following principles and assets:

  • first, you need to define an input actions asset in your project: this is the top-level object that you’ll instantiate, update and refer to to access the input system, configure it and interact with it at runtime
  • then, inside this asset, you’ll define one or more action maps: these contain mappings between hardware inputs and actions in your game (for example: pressing the Spacebar, or the Y button, to jump). Something important is that each map is linked to a specific user context, such as the UI in the menu, or the normal 3D scene, or a special car-driving level… this is very useful to easily remap the controls to different actions depending on the current state of the game!
  • finally, each action map lists one or more bindings: those are the low-level objects that match a key or a device input slot to an actual action in-game and that are listened for in the C# script with the callbacks. Again, a nice thing with this new system is that, thanks to those bindings, you can easily set up cross-platform schemes just by linking different hardware inputs from various controller types to the same action event.

Why you should use actions instead of direct inputs

Ok – we’ve just said that this new system relies on bindings to properly map player inputs to actions. But why is that interesting? Can’t we just say directly in the C# code: “if I press the Spacebar, then have the player jump”? Wouldn’t that be more readable?

Well, yes, but it would also impose some heavy constraints!

The most basic form of input is indeed one that directly links a hardware input (i.e. a button or an axis value) to a function in the game:

But this technique is completely dependent on the device and makes it pretty hard to re-configure afterwards. Adding another type of controller means extending your Update() with a bunch of if-checks, and the players can’t re-map the controls in-game to better suit their preferences!

That’s why, usually, you don’t do direct binding but rather add an intermediary component: the action.

Here, rather than referencing a specific device button or axis, you trigger an intermediary action that then calls a given function. This action is purely abstract but the nice thing is that can be triggered by one or more inputs and you can even change these inputs at runtime without impacting the action-event-function part of the chain!

Back to our jump example — after introducing this concept of action, you could declare a “Jump” action, then have a gamepad or a keyboard run it and also have this action trigger the “Jump” event that, in turn, uses the “Jump” function as callback:

Or you could allow the players to do some re-mapping and specify their own controls instead of the default ones:

This kind of input system is way more flexible than direct bindings and it’s easier to update for designers than a C# script.

Importing and enabling the new input system in your project

Alright — enough talking, let’s get to work! 🙂

First of all, to get the new input system, you’ll need to use Unity’s 2019.2 version or newer. But then, the system is not yet built-in: you’ll have to import this package in your project before you can use it. So, go to your package manager and, in the Unity Registry, install the Input System package:

Once it’s done installing, Unity should warn you about needing to “switch to the new system”.

The idea is that to make the transition easier and avoid too many breaking changes, Unity’s team decided to let both systems coexist, but you can also decide to switch to the new one completely. So you can click “Yes” right now, or set this option in your Project Settings > Player panel later.

To make use of the new system, you can pick either the “Input System Package (New)” or the “Both” option. If you choose “Both”, then the old system will continue to work as well. This can be very sweet if you have other packages or previous code that rely on the old input system… but here, our codebase is still pretty clean and fresh, so we can commit to the “new” option:

Important note: depending on your type of Cinemachine setup, you might need to be careful with the input system change. For our hack’n’slash, we are just using a simple translation logic, so there is no issue; but for example, if your camera depends on the mouse position for the rotation, then you might benefit from using the “Both” option. And also, for more info on this specific case and another possible workaround, you can check out this article by S. Montambault 😉

Preparing our input assets

Ok – now that we have the system installed and ready, the first thing we need to do is create our input actions asset, the maps and the bindings.

To begin with, let’s create an input actions object in our Assets/ folder – I’ll name it “DefaultInputActions”:

If you select this asset, you’ll see in the Inspector that you can click the “Edit Asset” to open the asset’s configuration panel:

For now, the actions are completely empty, so the panel shows us just a set of three columns that will eventually list ou maps, our bindings and their settings. You can add a new action map with the “+” sign at the top of the first column:

I’ll call my first action map “Player”. Also, I’ll make sure to check the “Auto-Save” button at the top so that whenever I update the asset I keep my progress 😉

Now, if you add an action map to the inputs, it will contain a default binding you can edit to setup your first action mapping:

Let’s rename this input to “Move” and use it for our player movement. To change the name of the action, simply double-click on it and type in the new name. We also need to set the type of action – here, we’re going to get the equivalent of our horizontal/vertical axes by using a Vector2 value:

Then we need to define some bindings for this action so that it is actually linked to hardware inputs. In our case, we want to handle three types of controls: the gamepad left stick, the keyboard WASD keys and the keyboard arrow keys.

For the gamepad, we can simply edit the default empty binding and set its path to the left stick of a gamepad controller. To do this, you can either use the dropdown menu and navigate to the “Gamepad” sub-menu:

Or you can directly type in the full path of the hardware control:

The nice thing with this path is that it is agnostic of the gamepad controller you’re using; so it will work for the left stick of an Xbox or a Playstation controller, for example 🙂

For the WASD and arrow keys, it’s a bit more complex because we actually want to map several keys to the same bindings – focusing on the first set of inputs, what we want is for the W/A/S/D keys to all contribute to this “move” action.

To do this, we can take advantage of another cool feature of this new input system: composite bindings. Basically, it’s a way of grouping together multiple inputs and defining them as “parts” of the same single binding. This way, you don’t have to manually check for each: instead, they will automatically be coalesced into a valid Vector2 value for our “move” action, and so we’ll be able to read it back just as we would with the left stick. (It’s just that we won’t have as much precision as with a joystick, so the values will be pure 0s and 1s)

To create a new composite binding for this action, right-click on the action and choose the “Add Up\Down\Left\Right Composite” option:

Then, all you have to do is fill in the paths of each part of the composite:

And of course, you can do the same for the arrow keys by creating and configuring a second composite binding:

The last thing we need to do is to wrap it with a C# class so that it can be called and used by our C# scripts. The idea is to create an auto-generated C# class that represents this input actions asset and makes it easy to access all of its data.

This will avoid us having to remember that our input is in the “Player” map, and that it’s named “Move”; instead, we’ll have a class that gives our IDE an auto-complete hint and validates the path to the binding we wrote is valid 😉

It’s quite straight-forward to create this class – we’ll just select our input actions asset and, back in its Inspector, check the “Generate C# Class” toggle. Note that you can change the path of the auto-generated script (I prefer to put it in my Scripts/ subfolder) and that you can give it a specific namespace if you want. The name of the class is based on the name of your asset by default but you can also change it to something else.

When you’re done picking the options, don’t forget to hit “Apply” at the bottom to actually validate all of this and create the class in your project folder!

Updating our C# script

All of this looks pretty nice, right? We’re all set up to move our hero either with the gamepad or the keyboard 🙂

The thing is that at the moment, our PlayerController script from last time will compile… but it won’t do nothing except errors if you try to run the game! Because we’ve changed the input system, our old way of getting the “Horizontal” and “Vertical” axes to move our CharacterController doesn’t work anymore…

Let’s fix this! We have to update the PlayerController.cs file a bit to use the new input system, get our input actions thanks to the C# DefaultInputActions wrapper class and read data from them at runtime to fill our CharacterController Move() call.

First, let’s instantiate the DefaultPlayerActions class we’ve just generated to have a unique input manager instance. We can create it in our Awake() function by using the C# class constructor, and also move our _controller referencing here:

Then, we need to make sure that our inputs are actually enabled. We can do this in the OnEnable() and OnDisable() built-in entry points (that are called automatically by Unity whenever the game object is enabled or disabled). We just have to import the InputSystem package and use the .Player.Move field from our auto-generated C# wrapper class:

Again, I’ll cache the reference to my _moveAction so that I can read values from it in my Update() method.

However, because I’m now getting a Vector2 value, I need to change the type of my _move variable, to extract the X and Y components and to re-assign them to the proper axis:

And tadaa! We now have the same player movement feature as before… but it works both with the keyboard and the gamepad controllers for inputs 🙂

Conclusion

In this second episode, we’ve refactored our input system and prepared everything for handling cross-platform inputs in the future! Our player can now be moved either with the WASD/arrow keys or the left joystick on controllers.

Next time, we’ll improve the visuals of our game by importing an animated 3D model for our hero (to replace the red capsule), setting up its Animator and linking it to the move logic…

Leave a Reply

Your email address will not be published.