Input mapping for cross-platform development

Managing inputs in a relatively complex game that targets several platforms with different peripherals can become a massive headache. Keys, touch, mouse, gestures, analog sticks, on-screen sticks, and so on, the variety is just too overwhelming, not to mention that controllers by different manufacturers do not have the same button codes. Sometimes, device codes vary even across platforms!

Without an additional system to aid us in user-input issues, the game's logic code will become a sea of conditional blocks to handle each possibility.

Quite rapidly, an additional layer becomes imperative to spare us the need to worry about which user input peripheral is currently being used and to gain a few years of life expectancy. This recipe introduces a barebones system to achieve this goal. It makes use of concepts such actions and states, which keys are mapped to. The user could potentially configure the said mapping to their taste. Moreover, the system allows different input contexts, because we care about different events when the user is browsing the menu or playing the actual game.

The present recipe explains the concepts, design, and implementation details of the aforementioned system and provides a direction to expand on it.

Getting ready

The input mapping sample requires you to import the sample projects into your Eclipse workspace.

How to do it…

All the code for this recipe can be found in the InputMappingSample.java file and the com.cookbook.samples.inputmapping package. However, before we jump into the inner workings of the system, we are going to pause for a moment and explain some of its fundamental concepts.

Our whole input mapping system is data-driven; interactions are defined in an XML configuration file whose format we will examine later on in this section.

Note

The implementation of this input mapping system is too big to be directly included in this text, so we will only show important snippets. Please refer to the sample code for a full implementation.

Input contexts

An input context defines a set of interactions that the user can trigger for as long as such context is active. For instance, while the player navigates the character selection menu, he or she may go forward or backward, select a character, or return to the previous screen. However, while the game is active, these inputs become invalid, and now, the user may be able to jump, move around, shoot, and so on.

Note

For the purpose of this recipe, we only process keyboard events. However, extending it will be fairly straightforward.

We can classify the various kinds of inputs that a user can enter within a context in different categories, as follows:

  • Action: These are one-time events such as jumping and shooting.
  • State: These are the persistent properties, such as the character wanting to move forward.

This is the skeleton of our InputContext class. It has a name, a set of action listeners, and dictionaries to keep track of the states as well as actions:

public class InputContext extends InputAdapter {
  private String name;
  private ArrayMap<String, Integer> keyStates;
  private ArrayMap<Integer, String> keyActions;
  private ObjectSet<InputActionListener> listeners;

  public InputContext() {}

  public void load(Element contextElement) {}

  public void addListener(InputActionListener listener) {}
  public void removeListener(InputActionListener listener) {}

  public boolean getState(String state) {}
  public String getName() {}

  public boolean keyDown(int keycode) {}
}

The load() method takes an XML element and populates the context with the appropriate data. The addListener() and removeListener() methods are pretty self-explanatory; they just add or remove a listener to or from the set, respectively. To check whether a state is active, we use the getState() method, passing in the name of the state. This state simply checks the keyStates dictionary for its corresponding keycode and checks whether or not the said key is pressed:

public boolean getState(String state) {
  Integer keycode = keyStates.get(state);

  if (keycode != null) {
    return Gdx.input.isKeyPressed(keycode);
  }

  return false;
}

Note

XML and JSON serialization and deserialization will be covered in the The XML parsing primer and JSON serialization and deserialization recipes of Chapter 5, Audio and File I/O.

Let's take a look at the keyDown() event handler. It tries to see if there is an action associated with this key and then notifies all the registered listeners:

public boolean keyDown(int keycode) {
  boolean processed = false;

  String action = keyActions.get(keycode);

  if (action != null) {
    for (InputActionListener listener : listeners) {
      processed = listener.OnAction(action);
      if (processed) {
        break;
      }
    }
  }

  return processed;
}

Input profiles

An input profile contains a set of input contexts and defines a complete configuration instance of potential user interactions. It keeps a dictionary from Strings to InputContext as well as the active context. The constructor takes a FileHandle pointing to the configuration XML file, which we will examine soon.

When the game switches between screens, you could call the setContext() method, passing in the desired context name to adopt the new high-level control scheme. We can get the current context with getContext() or get an arbitrary context by name with getContextByName().

All the event handlers simply defer onto the current context by passing through the event. Consider the following code:

public class InputProfile implements InputProcessor {
  private ArrayMap<String, InputContext> contexts;
  private InputContext context;

  public InputProfile(FileHandle handle) {}

  public void setContext(String contextName) {}
  public InputContext getContext() {}
  public InputContext getContextByName(String name) {}

  public boolean keyDown(int keycode) {}
  public boolean keyUp(int keycode) {}
  public boolean keyTyped(char character) {}
  public boolean touchDown(int screenX, int screenY, int pointer, int {}
  public boolean touchUp(int screenX, int screenY, int pointer, int button) {}
  public boolean touchDragged(int screenX, int screenY, int pointer) {}
  public boolean mouseMoved(int screenX, int screenY) {}
  public boolean scrolled(int amount) {}
}

Alright, this is all very good, but where is the file that defines an input profile? Let's take a look at the profile.xml file in the [cookbook]/android/assets/data/input/ folder:

<InputProfile>
  <context name="Game">
    <states>
      <state name="Crouch"><key code="DOWN"/></state>
      <state name="LookUp"><key code="UP"/></state>
      …
    </states>
    <actions>
      <action name="Jump"><key code="A"/></action>
      <action name="Shoot"><key code="S"/></action>
    </actions>
  </context>
  <context name="MainMenu">
    …
  </context>
</InputProfile>

Event notifications with InputActionListener

States are polled, but we need a mechanism for other systems to subscribe to action events. They can do so by implementing the InputActionListener interface, which only contains the OnAction() method:

public interface InputActionListener {
  public boolean OnAction(String action);
}

Input mapping in action

We have seen how our small input mapping system works internally, but it is now time to see it in action. Take a look at the InputMappingSample.java file. Note that the InputMappingSample class implements the InputActionListener interface.

Events will be logged on the screen, so we are going to keep an array of strings for them:

private static final int MESSAGE_MAX = 15;
private Array<String> messages;

Besides our usual base setup, we add two InputProfile and InputContext references:

private InputProfile profile;
private InputContext gameContext;

In the create() method, we initialize our keys' mappings and create a new InputProfile, passing in our configuration XML file. We then set the Game context and cache it off for ease of use later on. Finally, we tell the context to send action events to the InputMappingSample instance, as follows:

public void create() {
  …
  KeyCodes.init();
  profile = new InputProfile(Gdx.files.internal("data/input/profile.xml"));
  profile.setContext("Game");
  gameContext = profile.getContext();
  gameContext.addListener(this);
  Gdx.input.setInputProcessor(profile);
}

The render() method is quite simple. We first draw the current state at the top using the getState() method on the context and the latest messages at the bottom:

public void render() {
  …
  batch.begin();

  font.draw(batch, gameContext.getState("Crouch") ? "crouching" : "not crouching", 50.0f, SCENE_HEIGHT - 20.0f);
  …

  int numMessages = messages.size;
  for (int i = 0; i < numMessages; ++i) {
    font.draw(batch, messages.get(i), 50.0f, SCENE_HEIGHT - 160.0f - 30.0f * i);
  }

  batch.end();
}

Every time an action event is fired, we add the message to the queue:

public boolean OnAction(String action) {
  addMessage("Action -> " + action);
  return false;
}

When we reach the limit of the message queue, we need to delete the oldest queue:

private void addMessage(String message) {
  messages.add(message);

  if (messages.size > MESSAGE_MAX) {
    messages.removeIndex(0);
  }
}

Now it is a good time to run the sample and play around with it. See how it is easier now to handle game events and states? Rather than dealing with key codes, you can just stick purely to gameplay concepts such as jump or shoot.

How it works…

The following diagram illustrates the class hierarchy of the previously explained system:

How it works…

There's more…

As we have seen, we only work with keyboard inputs for simplicity reasons. However, this does not make this system very useful for real-life applications. A good way to put your newly acquired skills to practice is to extend the code so that it can handle mouse clicks, touch events, and gamepad inputs.

Going further, you could map gestures to specific actions! There are many possibilities.

Naturally, you need to add elements to profile.xml and modify the code that parses it as well as add new event handlers.

This input mapping system does not support more than one local player either, which could be a nice addition you may want to work on yourself.

Other important issues that need taking into account in a production environment are gamepad connections and disconnection events. What happens when a new player joins? Even trickier, what happens when someone accidentally plugs out his or her controller?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.140.188.244