In this third devlog, we’re going to look at how we handled complex
input requirements inside the first Unity “Open Project”.
"Open Projects" are small, open-source games where the creator community is free
to collaborate and contribute actively to the game development journey in any of its aspects.
The first open project is an action-adventure titled “Chop Chop”.
You can find the link to the GitHub project and all the other relevant
links to learn more and get involved in the video description below.
We are making the game using Unity 2019.4 LTS,
which will guarantee us the best stability during development.
But since we wanted to make the contents of this devlog series available to you in
the latest Unity version, we upgraded the project to work on Unity 2021.1
You can find this 2021.1 version of the project on a Git branch
on the original repository called devlogs/3-input.
“Chop-Chop” is a single-player game where you control a character from a third-person
perspective. We started with the intention of supporting Gamepads first, but, since
not everyone has a gamepad readily available, we also added support for mouse and keyboard.
You can move, look around, jump, and many more things like interact,
fight, initiate a dialogue, pause the game and navigate through different menus.
All these actions are bound to different physical inputs that can change depending
on the device used. So naturally, we decided to utilise the “Input system” package.
Thanks to an “InputAction” asset, which you can find inside the folder Settings → Input,
it is possible to bind actions defined by a name to different kinds of physical inputs,
potentially two or more per Action.
For example, we associated the “Jump” Action both
to the Space button on the keyboard and to a button on the gamepad.
This abstracts the input logic, allowing us to listen for these actions without caring about
which device is used to produce them, which improves scalability and multi-device support.
For instance, it would be very easy to port this
game to consoles by just adding the right additional action bindings.
As a second tier in this system, Actions are then grouped in an “Action Map” which
defines a set of controls linked by a common purpose. At the moment we have one for Gameplay,
one for Menus, and one for Dialogues.
Once the InputAction asset is setup, there are multiple ways to access its data.
The simplest approach would be to use the component-based workflow, which we recommend
to get started quickly, useful when prototyping. However, we decided to go with another slightly
more elaborate way, to get more control. We ask Unity to automatically generate a C# wrapper class
for the InputActions, which in turn exposes a bunch of Interfaces to implement in scripts.
This removes the need to manually look up Actions and Action Maps using their names by string,
and also provides an easier way to set up callbacks.
Now, instead of referencing this C# wrapper in any script that needs to respond to input,
we decided to build a layer in-between this input detection C# wrapper and the
end scripts which need to access that input. There are a couple of
reasons why we’ve built this intermediate layer, and we’ll go over them in a second.
Initially, this layer has been conceived of as a MonoBehaviour script but,
thanks to a suggestion from a community member, it later became a ScriptableObject.
This means that now we can access this input layer from any script,
due to the fact that ScriptableObjects are assets and thus, scene-independent.
By the way, if you’re curious about how we’ve implemented and designed our
game architecture around ScriptableObjects, check out the previous devlog, linked below.
You can find this ScriptableObject named InputReader, under ScriptableObjects → Input.
Now, let’s now dive into the InputReader class.
Inside this class, we listen to all the Actions of the Action Maps and,
for each one, we raise a UnityAction.
The scripts that need inputs can now subscribe to these UnityActions and respond accordingly.
For example, each time the keyboard or gamepad
buttons related to the “Attack” Action are pressed, the “OnAttack” method is called.
The method receives a “CallbackContext” data structure that contains information
about the input being processed, like its phase and its value, if applicable.
Actions will go through several possible phases, and each one of them can be useful for a certain
scenario. For instance, the “performed” phase doesn’t happen when an input begins,
but when it is confirmed, since it is possible to setup an Action so that
the input is considered valid only when the button is pressed for a minimum amount of time.
In this case, we invoke the “attackEvent” when the attack input is performed.
A good example where we take advantage of the phase property is in the “OnJump” function. Here,
we distinguish two cases. When the input becomes Performed, we raise the “jumpEvent” and, on button
release, we raise the “jumpCanceledEvent”. This allows the character script to process one button
as two separate events, which in turn allows us to better control the arc of the jump. We apply
an upwards push as soon as the input enters the “Performed” phase, and we stop doing that
when it becomes “Canceled” or after 0.4 seconds, allowing gravity to bring the character back down.
We implemented a more complex scenario to control the camera movement. The requirement
is that to rotate the camera, the player can use either the mouse or the gamepad’s right stick.
But we want to rotate the camera only if the right mouse button is pressed.
In our CameraManager script, we subscribe to the cameraMoveEvent
and, inside this function, we perform some checks on the input before rotating the camera.
In particular, in the handler that receives the event for the mouse movement,
we check that the Right Mouse Button is currently pressed before we actually move the camera.
As for the camera speed, since mouse delta and joystick provide values of wildly different scale,
we adjust the scale using “processors” directly within the InputActions interface,
so in the code we get a clean vector2 of similar scale regardless of the device used.
As we mentioned before, all the possible actions
that the player can perform are grouped into Action Maps.
An “Action Map” is a set of controls dedicated to a particular moment in the game.
There are times in a game where you might need to change the way input behaves,
and differentiate between normal gameplay, a dialogue phase, menus or cutscenes, and more.
For instance, inside a menu, to confirm a selection you use a specific button.
This button may be the same physical input that
you use to perform a jump or an attack during the gameplay phase.
For that reason, the Input System allows creating multiple action maps
and switching between them at runtime, enabling and disabling entire groups of Actions at once.
Since our InputReader layer is a ScriptableObject,
we are able to switch between action maps at any point calling the dedicated methods on it.
Also, some of the Actions in the mapping retain the same
name across ActionMaps to keep consistency. For example, the “MoveSelection” Action exists
in both Menus and Dialogue Action Maps. This allows us to use a single method in
the InputReader to react to both actions, since even though they belong to different ActionMaps,
they are supposed to behave in the same way in these two game scenarios.
To use the Input System to navigate menus and interact with the UI, we used a combination
of built-in components and a couple of custom components we designed for our specific needs.
As a start, the generic “EventSystem” component works in conjunction with the
one called “Input System UI Input Module”, coming from the Input System package.
These two take care of broadcasting navigation events.
With this simple setup, we can already move our selection and activate buttons.
However, we also wanted to allow navigation of the UI with either mouse or, as an alternative,
one between gamepad or keyboard. This involves some extra code because,
while the keyboard and joypad move the highlight to adjacent controls, the mouse cursor can jump
to any button it enters, and that’s what the script “MenuSelectionHandler” takes care of.
In addition to this, we wanted the highlight to disappear when the mouse begins to move,
to simulate a fully mouse-driven UI.
For instance, when we move across a menu with the mouse, the system detects which elements
the mouse is currently hovering on and, if a click occurs, the selection should be activated.
If we move again using either the keyboard or the gamepad,
we should resume the navigation from the last element selected by the mouse.
You can explore this simple implementation of a hybrid UI in the scripts called
“MenuSelectionHandler” and “MultiInputButton”, both located in the folder “Scripts > Menu”.
Inside them, you will easily see how we inject our
custom behaviour in the regular flow of events of the Unity UI.
As with many parts of this project, this is just the beginning implementation!
Let’s now see what our next goals are with the input in the game.
One of the things we want to add is the possibility of rebinding Actions.
Covering this is very important for accessibility, to let each user choose the controls that fit
them best, and to accommodate the usage of custom-made controllers.
The runtime re-binding feature was shown in a recent video about the input system,
so if you want to know more you can find the link in the description below.
Also, with regards to binding of inputs, we need to make sure to properly communicate and show
them to the player during gameplay and in menus. For this, another important thing to implement
is a UI to show input mapping in the menus, for both schemes: gamepad, and mouse and keyboard.
Keep yourself updated on the first Open Project by following the roadmap,
the daily changes in the GitHub repository, and the bi-weekly live streams. The project will
keep improving and changing during development on the main branch, but remember that you can
always find this 2021.1 version in a branch on the repository called devlogs/3-input-system.
We can’t wait to see your feedback and contributions!
Thanks for watching!