Using the Input System - UQcsse3200/2024-studio-1 GitHub Wiki
Introduction
This page provides details on how to use the Input System, and is a follow on page to Input Handling Overview.
Page Overview:
- Setting the Game's Input Type
- Making Input Handlers
- Creating New Input Handlers
- Using Action Components
- Adding New Input Types
- Implementing a New Input Interface
Usage
Setting the Game's Input Type
The input type is set in InputService
. This will determine the type of input factory created. The following is an example of setting the input type to KEYBOARD
.
private static final InputFactory.InputType inputType = InputFactory.InputType.KEYBOARD;
Making Input Handlers
Input handlers can be created using the Input Factory. The following is an example to create a type-agnostic input handler for the player:
InputComponent inputComponent = ServiceLocator.getInputService().getInputFactory().createForPlayer();
Creating New Input Handlers
Create a new input handler which extends InputComponent
. InputComponent
registers itself with the InputService
on creation with the default priority of 0
. To set a custom priority level, the input handler's constructor should call super({ priority-level })
.
This is an example of creating a new input handler for the tree with a priority level to 5.
public class NewInputComponent extends InputComponent {
public NewInputComponent() {
super(5);
}
...
}
InputComponent
has methods to handle each type of input event (e.g. keyDown, scrolled, etc.) and by default these methods do not process the input. Therefore, input handlers should override all relevant input handling methods.
As an example, let's extend the input handler to handle typing the character a
.
public class NewInputComponent extends InputComponent {
public NewInputComponent() {
super(5);
}
@Override
public boolean keyTyped(char character) {
if (character == 'a') {
// handle input here
return true;
}
return false;
}
}
Note: For games supporting multiple input types, you will need to create a type-specific input handler for each input type. For example, if your game supports KEYBOARD
and TOUCH
, you would need to create KeyboardNewInputComponent
and TouchNewInputComponent
.
To register the new input handlers with the input factories, add an abstract method for creating the input handler to InputFactory
.
public abstract InputComponent createForPlayer();
Then implement these methods within the input type-specific factories.
Within KeyboardInputFactory
:
@Override
public InputComponent createForExample() {
return new KeyboardNewInputComponent();
}
Within TouchInputFactory
:
@Override
public InputComponent createForExample() {
return new TouchNewInputComponent();
}
Using Action Components
By adding action components to entities, we can ensure that the entity acts consistently regardless of the input type that the game is receiving. We do this by abstracting away the entity's action logic into its own component and having the input component's call this functionality directly or through the events system. This avoids us having to write out the action code within each of the entity's input components, reducing code duplication and the chance of having inconsistent entity behaviour across input types.
Let's compare the code to make the player attack with and without the action component, PlayerAction
.
Without the action component
Within KeyboardPlayerInputComponent
:
@Override
public boolean keyDown(int keycode) {
switch (keycode) {
case Keys.SPACE:
Sound attackSound = ServiceLocator.getResourceService().getAsset("sounds/Impact4.ogg", Sound.class);
attackSound.play();
return true;
default:
return false;
}
}
Within TouchPlayerInputComponent
:
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
Sound attackSound = ServiceLocator.getResourceService().getAsset("sounds/Impact4.ogg", Sound.class);
attackSound.play();
return true;
}
With the action component
Within PlayerAction
:
@Override
public void create() {
// add 'player attack' event listener
entity.getEvents().addListener("attack", this::attack);
}
void attack() {
Sound attackSound = ServiceLocator.getResourceService().getAsset("sounds/Impact4.ogg", Sound.class);
attackSound.play();
}
Within KeyboardPlayerInputComponent
:
@Override
public boolean keyDown(int keycode) {
switch (keycode) {
case Keys.SPACE:
// trigger 'player attack' event
entity.getEvents().trigger("attack");
return true;
default:
return false;
}
}
Within TouchPlayerInputComponent
:
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
// trigger 'player attack' event
entity.getEvents().trigger("attack");
return true;
}
You can see that the code is cleaner and less prone to errors as if the player's actions change, we only need to update it in one place.
Adding New Input Types
InputComponent
currently implements the libgdx InputProcessor
and GestureDetector.GestureListener
interfaces. This means that it can support any input handlers that use only keyboard, mouse and touch gesture input. If the input type you are looking to create is not supported by these two interfaces, first follow the steps in "Implementing a New Input Interface" before coming back to this section.
For the purpose of explaining how to add a new input type, we will look at how TOUCH
was added into the game.
Within InputFactory
:
Add the new input type to InputType
.
public enum InputType {
KEYBOARD,
TOUCH
}
Add the option to create a Touch InputFactory in createFromInputType()
.
public static InputFactory createFromInputType(InputType inputType) {
...
if (inputType == InputType.KEYBOARD) {
return new KeyboardInputFactory();
} else if (inputType == InputType.TOUCH) {
return new TouchInputFactory();
}
...
}
Create a touch input factory which returns touch-specific input components. It should have methods for all the entity's within the game with custom input components, i.e. for the starting game this means the player and debug terminal.
Within TouchInputFactory
:
@Override
public InputComponent createForPlayer() {
return new TouchPlayerInputComponent();
}
@Override
public InputComponent createForTerminal() {
return new TouchTerminalInputComponent();
}
Create touch-specific input components for all the entity's within the game with custom input components.
As an example we will look at how the KEYBOARD
input component KeyboardPlayerInputComponent
handles the player attack differently to the TOUCH
input component TouchPlayerInputComponent
.
For KEYBOARD
, within KeyboardPlayerInputComponent
the player attack is triggered by pressing space
:
@Override
public boolean keyDown(int keycode) {
switch (keycode) {
...
case Keys.SPACE:
// player attack
entity.getEvents().trigger("attack");
return true;
...
}
}
For TOUCH
, within TouchPlayerInputComponent
the player attack is triggered by touching down on the game (with a mouse or via a touch device):
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
// player attack
entity.getEvents().trigger("attack");
return true;
}
Implementing a New Input Interface
For the purpose of explaining how to support a new input interface, we will pretend that the game does not yet support Touch Gesture input and will thus will go through the steps of adding it.
Set InputComponent
and InputService
to implement the new input interface.
public abstract class InputComponent extends Component
implements InputProcessor, GestureDetector.GestureListener {
public class InputService implements InputProcessor, GestureDetector.GestureListener {
Add methods to InputComponent
which return false
for each new input methods. By default InputComponent
will not handle any input, as these methods will be overridden by the actual input handlers.
@Override
public boolean fling(float velocityX, float velocityY, int button) {
return false;
}
@Override
public boolean longPress(float x, float y) {
return false;
}
...
Add methods to InputService
which iterate through the registered input handlers for each of the new input methods.
@Override
public boolean fling(float velocityX, float velocityY, int button) {
for (InputComponent inputHandler : inputHandlers) {
if (inputHandler.fling(velocityX, velocityY, button)) {
return true;
}
}
return false;
}
@Override
public boolean longPress(float x, float y) {
for (InputComponent inputHandler : inputHandlers) {
if (inputHandler.longPress(x, y)) {
return true;
}
}
return false;
}
...