Input Systems, State Machines, Events
📦 Unity packages from today's class:
Before importing both packages, make sure to install the Input System package on your Editor as well.
📚 Other relevant resources to today's topic:
- Extra notes on Setting up Mouse Cursor Inputs: May be less relevant for project 2, but could be something to explore for other projects.
- Button map for Arcade Cocktail Cabinet
- Gamepad Tester: Online tool for testing gamepad input; useful for understanding how joystick buttons are mapped.
- Unity's Rollaball Tutorial: Moving the Player: A simple example of how to set up player movement using Unity's new Input System.
To recap, we've already learned how to map inputs to certain lines of code using KeyCodes and GetKey.
However, as we get more ambitious with our projects, it makes sense to use a more organised solution for mapping input controls to specific actions.
Legacy Input Solution: the Input Manager
From the most recent version of Unity's Manual (Unity 6, 6.000)
Unity has a built-in legacy input solution called the Input Manager. This is not the recommended workflow, as it is less flexible than the Input System Package, and will be removed in future versions of Unity.
As of now, Unity legacy input system is still their default solution.
Input Manager
Go to Edit > Project Settings > Input Manager.
The Input Manager comes with a default set of actions that are already mapped to keyboard and joystick controls. These actions can be called using their names.
Directional Input from Keyboard and Joysticks
Use Input.GetAxis to read analog values from a gamepad. The output range would be from -1 to 1 along both horizontal and vertical axes.
The joystick is mapped to the axes Horizontal and Vertical by default, which also read the WASD and Arrow keys.

float moveX = Input.GetAxis("Horizontal");
float moveY = Input.GetAxis("Vertical");
It's sometimes useful to convert these inputs into a vector, for instance if you are using directional input to change a character's velocity:
Vector3 heading = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
Get Button Presses from Keyboards and Gamepads
The GetKey functions are great for getting keyboard input, but Unity also lets us detect keyboard and gamepad inputs all at once using the GetButton functions.
if(Input.GetButton("Jump"))
{
Debug.Log("space or joystick button 3 are pressed");
}
if(Input.GetButtonDown("Fire1"))
{
Debug.Log("left ctrl or left mb or joystick button 0 was just pressed down");
}
if(Input.GetButtonUp("Fire2"))
{
Debug.Log("left alt or right mb or joystick button 1 was just released");
}
You can customize which string values and button mappings the input system uses for these functions in the Input Manager, but in most cases the default settings work well. You have access to Jump, Fire 1, Fire 2, Fire 3, which map to the four face buttons on a gamepad, to several commonly used keys on the keyboard, and in some cases to the buttons on the mouse.
Unity's (New) Input System Package
The Input System is Unity's latest solution for binding controls to actions. It's meant to consolidate everything input-related in a single interface, so you can do the following from the Input Action Editor:
- map actions across different types of control devices;
- organise action maps for different contexts (e.g. player controls, UI controls)
- determine how input values should be processed, and how buttons should be interacted with.
- managing inputs for local multiplayer projects.
Installing the Input System
Go to Window > Package Manager > look for "Input System" in the Unity Registry, and click Install. The Unity Editor will ask you to restart your project, so make sure to save any changes beforehand!
It's still possible to use both the old Input Manager and the new Input System package at the same time (most use cases for this are to help ease the transition across both input solutions.)
You can set the method for handling input in Edit > Project Settings > in the Player tab, look in "Other Settings", and scroll down to Active Input Handling.
Making an Input Action Asset for Player Controls
First, add a Player Input component to your Player GameObject.
Next, create an new Input Action Asset. You can click the "Create Actions..." button from the Player Input component to create an asset with Unity's presets; or start from scratch by right clicking in your Project asset tab > Create > Input Actions.
Input Action Asset
Double click on your Input Action Asset to open up the Input Action Editor window.
This is where you can store and manage the following information:
- Action Map: separates a set of actions for a specific context (eg. Player movement, UI controls)
- Actions: contained within Action Maps; single events that can be triggered with the assigned inputs.
- Bindings: contained within Actions; determines which physical controls will trigger the Action.
- Control Scheme: which device contains the specific controls that players can use; could also be used to assign player controls in local multiplayer settings.
Changes in this editor have to be saved regularly. You could also toggle "Auto Save" at the top of the window.
Action Callback Functions
One easy way to access action events in scripts is to use the callback functions for each action.
For example, if you create an Action called "Jump", the Input System will map the name of this action to a callback function called "OnJump".
The same rule applies for any action of any name:
- "Move" gets mapped to "OnMove"
- "Fire" gets mapped to "OnFire"
- "CustomAction" gets mapped to "OnCustomAction"
... and so forth!
This means we can decide what happens when the action gets triggered by calling the function in scripts.
Before we can access these functions, we must include the Input System namespace at the top of our script:
using UnityEngine.InputSystem;
For a button type action called Jump:
void OnJump(){
// write a code for the player jump motion here!
}
If we need to use the values from our action inputs, we can pass them as arguments for our functions in this way:
Vector2 movement;
void OnMove(InputValue moveValue)
{
// move takes an input value of type Vector2
// so we should get a Vector2 value.
movement = moveValue.Get<Vector2>() * speed;
}
You may also check whether a button is pressed using InputValue.isPressed -- though this will only work if the Action triggers on both the Press and Release, which can be done with a Press Interaction for that Action or by using a Value Action Type.
bool modifierPressed;
void OnModifier(InputValue value)
{
modifierPressed = value.isPressed;
}
Local Multiplayer Input Setup
Local Multiplayer for Legacy Input Manager
Two Players on One Keyboard
To set up input for multiple players using the keyboard, start by opening the first "Horizontal" axis. This axis has a positive and negative button, as well as an alternate positive and negative button. This means that this single axis reads from both left/right arrows AND from A and D at the same time.
Right click on the horizontal axis and select "Duplicate Array Element", and then modify both versions of the horizontal axis.
I've set it up so that player 1's horizontal axis is called "Horizontal1" and player2's is called "Horizontal2". Horizontal1 corresponds to A and D, and Horizontal2 corresponds to the left/right arrow keys.
Repeat this process for the vertical axes.
Now we need to write a script that reads from a given input axis depending on the player.
Here I use a public int to represent which player the script should be controlled by. I add the int to the end of "Horizontal" and "Vertical" to read from the correct input axis.
public class PlayerCharacter: MonoBehaviour
{
public int playerNum;
void Update()
{
float h = Input.GetAxis("Horizontal" + playerNum);
float v = Input.GetAxis("Vertical" + playerNum);
}
}
Two Players with Two Controllers
Find the second set of Horizontal and Vertical inputs and open them up. These correspond to joysticks.
Notice that these have slightly different settings than the first pair of axes.
Repeat the process we did for the keyboard bindings: duplicate each axis so that there is a separate Horizontal1 and Horizontal2, and a Vertical1 and Vertical2
Finally, for each axis find the dropdown next to "Joy Num" and change it from "Get Motion From All Joysticks" to the correct joystick for that axis. Vertical1 should read from Joystick1 etc.
Reading Buttons from two joysticks
You can repeat the same process as our other input axes, duplicating and renaming the jump button field. However, for buttons you need to also need to specify which joystick the axis reads from in the field "positive button"
You can read the button inputs like so:
public class PlayerCharacter: MonoBehaviour
{
public int playerNum;
void Update()
{
float h = Input.GetAxis("Horizontal"+playerNum);
float v = Input.GetAxis("Vertical" + playerNum);
//true the MOMENT the button is pressed
bool jumpPressed = Input.GetButtonDown("Jump" + playerNum);
//true WHILE the button is held down
bool jumpHeld = Input.GetButton("Jump" + playerNum);
//true the MOMENT the button is RELEASED
bool jumpReleased = Input.GetButtonUp("Jump" + playerNum);
}
}
Local Multiplayer for New Input System
In your Input Action Asset for the Player Input component, create a new control scheme for each player. Here, I've labelled them P1 and P2.
When you create a new control scheme, you may also specify which control device is required for each player controller.
Once you start binding keys to each action for every player controller device, you can select which control scheme this binding is for on the right column.
In your Scene Hierarchy, duplicate your player GameObject (the one that contains the PlayerInput component) to make a second player. For each player object, set the Default Scheme in the PlayerInput component to the correct player.
Player Input Manager for Local Multiplayer
The New Input system also has a built-in local multiplayer functionality using their Player Input Manager component.
- Notification Behaviour: You could change this to Invoke Unity Events to call multiple methods simultaneously when a new player joins.
- Join Behaviour: Have players join automatically with a button press, or set up a custom join button.
- Player Prefab: This will be the prefab that gets instantiated when a new player joins.
- Enable Split-Screen: Enables a split-screen view when a new player joins.
Remember that there are also other methods for setting up the above features without having to use the Player Input Manager (this is one of Unity's attempts to automate or standardise workflows for achieving standard results... )
ScreenToWorldPoint and Raycasts
These methods are commonly used for cursor input -- the notes for those are included this document: Extra notes on Setting up Mouse Cursor Inputs.
We'll learn how to use these for detecting objects and pinpointing positions in our game scene through our UI canvas screen.
Screen Space and World Space
Screen Space is defined in 2 dimensions (X,Y). This space refers to the 2D area of your Unity game window.
In screen space, the bottom-left of the screen or window is at (0, 0). The top-right of the screen or window is the height and width of the screen in pixels, at (Screen.width, Screen.height).
World Space is defined in 3 dimensions (X,Y,Z). This space refers to the virtual 3D space inside your game.
Sometimes it's useful to translate coordinates across these two spaces, e.g. when we're trying to aim at a specific object or position in our 3D scene through 2D screen coordinates.
Camera.ScreenToWorldPoint
ScreenToWorldPoint() is a method for determining how a position in screen space corresponds to world space depending on where the camera is looking, whether the camera uses perspective or orthographic projection, and how "deep" into the scene you want that positional coordinate to be.
If you get a reference to the camera in your scene, you can convert from a screen coordinate to a world coordinate with Camera.ScreenToWorldPoint.
In this unity package demo example, I used this method to aim at the direction in which my projectile will be launched. Here we're using a UI element to determine our target coordinate in screen space.
In this case, ScreenToWorldPoint gives us a point ten units in front of the camera, with the same apparent position on screen as our UI target element.
Camera camera;
public RectTransform targetAim;
Vector3 targetAimPos; //position of our target aim on the screen (we'll also assign the z-coordinate later)
Vector3 worldPos; //position of where our target would be pointing at in worldspace from the screen.
void Start()
{
camera = GetComponent<Camera>();
// or "camera = Camera.main;", which refers to the Main camera assigned in your scene.
}
private void Update()
{
// store our target aim's anchored position inside a Vector3 variable
// -- z coordinate would be zero.
// note the difference in syntax for getting
// a Rect Transform position (RectTransform.anchoredPosition)
// compared to a Transform component position (transform.position)
targetAimPos = targetAim.anchoredPosition;
// assign the z coordinate for targetAimPos
// -- this will determine how far away from the camera our projectile will spawn.
targetAimPos.z = 10f;
worldPos = Camera.main.ScreenToWorldPoint(targetAimPos);
if (Input.GetButtonDown("Fire1"))
{
Shoot();
}
}
void Shoot()
{
// calculate projection direction
Vector3 shootDirection = worldPos - projectileSpawnPoint.position;
// shoot!
currentProjectile.GetComponent<Rigidbody>().isKinematic = false;
currentProjectile.GetComponent<Rigidbody>().AddForce(shootDirection.normalized * shootForce, ForceMode.Impulse);
// turn this script off after shooting.
// we will turn it back on when we have picked up another projectile.
this.enabled = false;
}
Raycasting from the camera
If you want to interact with objects in a 3D scene from a 2D screen space, at some point you will have to use raycasting.
First, you need to get a Ray that passes from the camera, through your cursor, into the scene.
A Ray contains two vectors, which represent the ray's origin point in space and its direction vector.
Then, you can use the ray to raycast against the ground or other objects in your scene.
In the same demo example as above, we're using the same target UI element to pick up projectiles in our scene.
Camera camera;
public RectTransform targetAim;
void Start()
{
camera = GetComponent<Camera>(); //or Camera.main
}
void Update()
{
Ray targetAimRay = camera.ScreenPointToRay(targetAim.anchoredPosition);
RaycastHit hitInfo;
if (Physics.Raycast(targetAimRay, out hitInfo))
{
/*
Debug.Log(hitInfo.point); //prints the position in the scene that the raycast hit...
*/
//or do something else, depending on what you hit!
if (hitInfo.collider.CompareTag("Projectile"))
{
Debug.Log("Found Ball!");
//pick up this projectile if we press the button "Fire1"
if (Input.GetButtonDown("Fire1"))
{
PickUpProjectile();
}
}
}
}
State Machines
adapted from Game Programming Patterns by Robert Nystrom.
The Problem
We have a player character that jumps. Push the jump button, and they should jump. Simple enough:
void Update()
{
if(Input.GetButtonDown("Jump"))
{
yVelocity = JUMP_VELOCITY;
setGraphics(IMAGE_JUMP);
}
}
Spot the bug?
There’s nothing to prevent “air jumping” — keep hammering Jump while the player is in the air, and they will float forever. The simple fix is to add an isJumping
 boolean field to our player that tracks when they're jumping, and then do:
void Update()
{
if(Input.GetButtonDown("Jump") && isJumping == false)
{
yVelocity = JUMP_VELOCITY;
setGraphics(IMAGE_JUMP);
isJumping = true;
}
}
Another Problem
Next, we want the player to duck if the down button is pressed while they're on the ground and stand back up when the button is released:
void Update()
{
if (Input.GetButtonDown("B") && isJumping == false)
{
// Jump if not jumping...
}
else if (Input.GetButtonDown("Crouch"))
{
if (isJumping == false)
{
setGraphics(IMAGE_DUCK);
}
}
else if (Input.GetButtonUp("Crouch"))
{
setGraphics(IMAGE_STAND);
}
}
Spot the bug this time?
With this code, the player could:
- Press down to duck.
- Press Jump to jump from a ducking position.
- Release down while still in the air.
The player will switch to their standing graphic in the middle of the jump. Time for another Boolean variable…
void Update()
{
if (Input.GetButtonDown("Jump"))
{
if (!isJumping && !isDucking)
{
// Jump...
}
}
else if (Input.GetButtonDown("Crouch"))
{
if (!isJumping)
{
isDucking = true;
setGraphics(IMAGE_DUCK);
}
}
else if (Input.GetButtonUp("Crouch"))
{
if (isDucking)
{
isDucking = false;
setGraphics(IMAGE_STAND);
}
}
}
Suppose we want to add other moves, like a cool dive attack if the player presses down in the middle of a jump, or a dash that's only available on the ground. To add these features with this approach, we would need to add more Boolean variables, and keep adding onto our increasingly complicated series of if / else statements...
Clearly, we need another approach!
Finite State Machines
Grab a pen and paper, then draw a flow chart that maps out each and every possible action the player could be doing in a box: standing, jumping, ducking, and diving.
When the player can respond to a button press in one of those states, draw an arrow from that box, label it with that button, and connect it to the state they change to.
...and you've just created a finite state machine!
Finite state machines (FSM) came out of a branch of computer science called automata theory, whose family of data structures also includes the famous Turing machine. FSMs are the simplest member of that family.
The gist is:
- You have a fixed set of states that the machine can be in. For our example, that’s standing, jumping, ducking, and diving.
- The machine can only be in one state at a time. Our player can’t be jumping and standing simultaneously. In fact, preventing that is one reason we’re going to use an FSM.
- A sequence of inputs or events is sent to the machine. In our example, that’s the raw button presses and releases.
- Each state has a set of transitions, each associated with an input and pointing to a state. When an input comes in, if it matches a transition for the current state, the machine changes to the state that transition points to.
For example, pressing down while standing transitions to the ducking state. Pressing down while jumping transitions to diving. If no transition is defined for an input on the current state, the input is ignored.
In their pure form, that’s the whole banana: states, inputs, and transitions. You can draw it out like a little flowchart. Unfortunately, the compiler doesn’t recognize our scribbles, so how do we go about implementing one?
Implementing a State Machine
Lets start simple with strings and if
statements...
string state = "ground"
void Update()
{
if(state == "ground")
{
GroundUpdate();
}
else if(state == "jumping")
{
JumpUpdate();
}
else if(state == "dive")
{
DiveUpdate();
}
}
We write a function to encapsulate each state: GroundUpdate
, JumpUpdate
, and DiveUpdate
. For now, this function can handle transitions as well. Our GroundUpdate
function could look like this:
void GroundUpdate()
{
//if pressing jump,
if(Input.GetButtonDown("Jump"))
{
//transition to jump state
state = "jumping";
}
else
{
//control ground movement here
}
}
This solution basically works, but is vulnerable to typos. If we ever misspell or capitalize one of our state names, the game will stop working.
Consider using an enum to prevent this problem!
Enumerations
Read Microsoft's documentation for C# Enumerations.
enum State{Ground, Jump, Dive}; //define the members of the enum type State here
State currentState = State.Ground;
void Update()
{
if(currentState == State.Ground)
GroundUpdate();
else if(currentState == State.Jump)
JumpUpdate();
else if(currentState == State.Dive)
DiveUpdate();
}
With an enum
, we can define specific values with clear names. Your text editor will autocomplete the names of each member of the enum
for you, which prevents errors from typos.
In many cases, an enum
and a series of if / else
statements will be totally adequate to implement a simple state machine. It's good not to over-engineer if you can help it.
You might also use a switch
statement instead of a series of if
statements, which looks like this:
enum State{Ground, Jump, Dive};
State currentState = State.Ground;
void Update()
{
switch(currentState)
{
case State.Ground: GroundUpdate(); break;
case State.Jump: JumpUpdate(); break;
case State.Dive: DiveUpdate(); break;
}
}
State Machines and Game Logic
There are many other cases in which it is useful to think in terms of discreet states and transitions between them.
States can be useful for structuring the overall logic or turn order of a game. Suppose we are creating a bowling game. On a player's turn, the player should be able to choose:
- their initial position,
- the direction they are aiming at,
- the spin of the ball,
- the amount of force the ball is thrown with
Then, once the ball is thrown, the following should happen automatically:
- the player watches the ball as it travels down the lane towards the pins
- once the pins are knocked down, score is evaluated and displayed
- If the player has used both throws, the pins are reset for the next player
Although this is mostly a linear sequence, we can still think in terms of states to describe the order of events in our game. Plotting our bowling game as a flow chart reveals a surprising level of complexity:
It's important to remember that there is no single best way to implement a state machine. A state machine is simply a pattern you can use to think through organizing your code.
The state machine describing our bowling game is significantly more complicated than our jump example, controlling sound, physics, animation, input, timers, different menus and screens and more.
Rather than encapsulate the logic for our entire game inside one script, like our earlier example, we could break each state out into it's own script, on a different game object.
Each state GameObject could contain a canvas with UI elements nested inside. Then we can activate and deactivate each game object to begin and end it's state.
We might also think of Position, Aim, Spin, and Force as sub-states of a larger state called "Player Turn". The Player Turn state could have it's own script, which stores the values chosen during the Position, Aim, Spin, and Force sub-states, and applies them to the ball only at the end of the turn....
Animation States
Finally it's worth noting that Unity controls animations using a kind of state machine called an animator controller. We'll discuss this more in another week, but the editor for this should look familiar.
UnityEvent
Remember our OnClick() event for setting up interactivity with UI Buttons?
OnClick() is a UnityEvent that belongs to the Button component class. We add listeners through the inspector, so when the event is invoked (ie. the event happens), the button calls the functions that are listening to this event at the same time.
We can set up customised UnityEvents for other objects and scripts (such as triggers, collisions, etc.) UnityEvents uses the namespace using UnityEngine.Events;
-- make sure to include this at the top of your script.
using UnityEngine;
using UnityEngine.Events;
public class EventHandler : MonoBehaviour
{
public UnityEvent onCollisionEnter, onCollisionExit;
void OnCollisionEnter(Collision col){
onCollisionEnter.Invoke();
}
void OnCollisionExit(Collision col){
onCollisionExit.Invoke();
}
}
UnityEvents can be useful for connecting different scripts in your game, and executing multiple functions at the same time upon a game state change.
For example, when your player dies, the following scripts need to be run:
- play the player character's lose animation.
- update UI to show lose screen UI elements.
- stop receiving player input.
- the player score needs to be reset.
- the scene needs to be changed.
UnityEvents also keeps your game programming architecture modular using something called the Observer Pattern. Our event listeners are observers that passively observe whether the event has been invoked or not, and can act in response to an event, but they don't interfere with other listeners, nor do they depend on each other.
The observer pattern keeps things manageable and organised when we want to set up complex event systems! This allows your objects to communicate but stay loosely coupled using a “one-to-many” dependency. When one object changes states, all dependent objects get notified automatically.
Some course reminders
- Project 2 Prototype Playtest is due next Tuesday.