Making a Hack’n’slash #19: Showing cross-platform input displays

Let’s do a quick user feature for our UI!

This article is also available on Medium.

We’ve had quite a lot of tutorials on our inventory and loot systems – those were cool but pretty dense! Today, let’s take a step back and work on a more “player-level feature”: the input displays.

Basically, what I want to do today is make sure that the players get on-screen hints of the available inputs depending on the current context to help remember all the various actions:

More precisely, we are going to:

  • show easy-to-read button icons to tell the players what device input actions are linked to
  • insure those displays are cross-platform (and adapt to your current main device type)
  • adapt the displays to the current context (for example, the loot panel or the inventory panel)

Preparing our InputManager

First of all, we’re going to need to upgrade our InputManager class to know what is our current device, catch whenever a new device is plugged in, and of course “convert” an action reference to the proper input button(s).

I’m going to assume the following:

  • I’ll have either a “Desktop” input layout (with a keyboard and a mouse), or an “XBox” input layout – the other controller types would work the same, but it would overcrowd our scripts quite a lot 😉
  • to get the “current device”, I’ll list all the current devices and consider just the top of this list; for a gamepad, it will be only the first item but for the Desktop layout, I’ll need to use both the first and the second to have both the mouse and the keyboard
  • action references will have a format like: <action_map>:<action_name> (for example: Player:Loot)

To begin with, let’s get our current devices and map them to specific “input device types” that we set from an enum:

Then, we’ll make sure that when we plug in or out a new input device, we update our list of devices; we’ll also trigger an event so that other objects in the scene can get notified of this device change:

The next step is to add some code to get the device input matching a given action. In short, the idea is to go through the actions in the given map and check if/when one of the actions is associated with a control from our current device(s). If we don’t find any, we’ll return an empty reference; else, we’ll return the name of the input button.

However, because some of our actions require a combination of input (like the <Shift> + <X> to drop an inventory item stack on desktops), we have to make sure we can return multiple buttons if need be.

Also, we have to properly handle special cases like the “move” action where we have a composite binding. To do this, since they aren’t too many, I’ve decided to define a handmade _ACTION_TO_CONTROL_OVERRIDES mapping to bypass these issues and instantly return an input.

We have to be careful, though: since our InputManager now has a bit of logic in its Awake(), OnEnable() and OnDisable() methods, we have to add it to our “MANAGER” object in the scene:

Importing our input button icons

Now, we need to prepare some assets (that we’ll soon turn into Addressables) to show in our interface based on those input button names.

You can find various sets of such images, but one I like is the free pack of controllers & keyboard prompts from theawesomeguys:

If you download and unzip this package, you’ll see that you have subdirectories for the different possible input devices (keyboards, mouses, PlayStation gamepads, Xbox controllers…). Feel free to take a look and get the right icons for your expected devices – I’ll go with the white keyboard set and the Xbox 360 icons 🙂

However, because our scripts will try to find image paths based on the input actions in our action maps, we need to make sure the names match!

This means that, first, we need to rename our folders “Desktop” and “Xbox”, like the values in our InputDeviceType enum. Then, we also have to rename the icons inside to work with the exact action names. For example, here’s a peek at how I changed some of the keyboard icon files:

Finally, let’s make sure all those resources are defined as Unity sprites and as Addressables in our project. Just like before, we just need to select the files (luckily, we can select all the files in a folder at once), change the image type and then toggle the Addressables check on:

Setting up the input display UI elements

Now that our assets are ready, it’s time to use those in our scene and populate it with some input display placeholders. What I want to do is have a row of input hints at the bottom of the screen that I can set and show when needed:

Note: you can of course have more than five placeholders, but I’d wager that if you have more than five inputs to show at the same time, then you probably have a (too?) complex gameplay…

This UI element will be a container with a Horizontal Layout Group component that contains five instances of a prefab, the “InputDisplay”:

Basically, in the “Input Display”, I need two elements side-by-side: the icon(s) of the key(s) to press, and a text for the name of the associated action. However, to better handle the key combinations (like <Shift> + <X>), I found it easiest to also prepare a second “icon” element that is actually a small container with two sprites side by side and a “+” text in the middle):

You should obviously have only one active child active at a time among “Icon” and “IconWithModifier” – we’ll make sure of that in the code, but you should probably set it up properly when the game starts, too 😉

For more details on how this prefab is organised, check out the Github repository 🚀

All those components will be referenced and updated by a new script, the InputDisplayer:

Of course, don’t forget to add it on the prefab. You can play around with the text colour option if it doesn’t work with your overall theme and/or 3D objects colours, and if one of you placeholders should have a given action display by default, you can fill its “Text Display” and “Action Path” fields:

The next step is to actually add some logic to the InputDisplayer to get the right icon(s) depending on the inputs associated with the given action path. In short:

  • we’ll use our method from before, ActionToControl(), to get the exact name of the control matching the action
  • we’ll get our current input device type
  • we’ll combine the two to make the path of our Addressable image file

Since the Addressable loading is asynchronous, I’ll extract the function that actually assigns the resulting objects to my components so I can call it when necessary:

Finally, I’ll create some entrypoints to call my _OnDeviceChanged() method in various contexts: at the start if I already have an action to display, whenever the deviceChanged event is invoked or with a manual public function, SetDisplay():

And just to better encapsulate this complexity, I’ll create a second script to put on the parent UI container (the row of five “Input Display” instances), the InputDisplayers class:

Now, we have an easy way to show or hide our input displays!

Showing inputs for the loot/inventory panels

So let’s use all of this for our loot and inventory panels to give some context to the players 🙂

The idea is simply to call our SetupDisplayers() method when we open and close the inventory or the loot panels to either update or reset the input displays:

Since our displays can easily be modified at runtime, we can even do something pretty cool with the inventory “grab item” action – we can have the action display conditionally say “Grab” or “Release” depending on the current state of the grabbing!

We should also make sure that, when the game first starts, the input displays are properly hidden – so let’s turn them off in the InGameMenuManager:

Note however that, since my input displays are always shown at the top of the bottom and take a given height, I’ve had to “shrink” my menu panel’s contents to accommodate for these new dimensions:

If you test this out, you’ll see that you can easily check the valid controls in any state of the game:

Boost: displaying the in-game loot input

The last thing I want to do today is take advantage of our new input display system to “expand” our “Loot” text above the loot bags.

Overall, we need to do the same as before:

  • we add an image above the UI text we already have in the “Loot Bag” prefab, and we group both the sprite and the text in a sub-hierarchy to make it easy to toggle on and off:
  • then, in the LootBagManager, we update our reference (don’t forget to re-assign it in the Inspector!) and we re-import our Addressable loading logic from the InputDisplayer to update the sprite in the instance:

We can even use our deviceChanged event and call our _SetInputDisplay() function if it is invoked, so that the input is automatically updated:

Now, if you run the game, you’ll see that the loot bags have this extra data about “pressing the right-mouse button to collect” in an intuitive way for the players:

Thanks to these hints, thanks to the fact the info only appears when you’re close enough and also thanks to the shared culture of video gamers, we can assume that this sprite alone can fully replace several sentences explaining how to pick up items!

Conclusion

Today, we worked on a sidebar feature for the players and added some input displays to make it less taxing to remember all the available controls. This was a bit of an auxiliary thing, but it’s a nice improvement of our current cross-platform input system 😉

With that said, next time, we’ll go back to our inventory/equipment features, and start to work on equipping items to boost the player’s stats…

Leave a Reply

Your email address will not be published.