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:

  1. Setting the Game's Input Type
  2. Making Input Handlers
  3. Creating New Input Handlers
  4. Using Action Components
  5. Adding New Input Types
  6. 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;
  }
  ...