Mobile
Article

Handling Player Input in Cross-Platform Games with LibGDX

By Travis O'Brien

LibGDX is an open source Java library for creating cross-platform games and applications. In my last tutorial I covered getting the library setup and ready for development. This tutorial will cover handling player input with LibGDX, bringing interactivity to your cross-platform games.

Different Types of Input States.

Handling input is a simple task. If a key is down, then that key should register as being true and likewise for when a key is up. This simplicity can lead to a lot of problems, especially for games. What you want is a simple way to ask libGDX if a specific key is either pressed, down, or released.

But what is the significance of these three different key states? They describe how a user interacted with their keyboard.

  • Pressed: A key was activated and it only triggers for one frame.
  • Down: A key is currently held down.
  • Released: A key was released and it only triggers for one frame.

If you had logic within the rendering function such as:

if (key.pressed())
    print("pressed")

if (key.down())
    print("down")

if (key.released())
    print("released")

You would expect to see something like:

..."pressed" //triggered one frame ..."down" //constant amongst all frames ..."down" ..."down" ..."released" //triggered one from

The same logic could also apply to touch events.

The Input Manager Class

There are many different ways you could implement this class, but I suggest you strive to be as ‘Object Oriented’ as possible which means every key and touch event will be it’s own object.

The first class to declare is the Input Manager itself (which will implement libGDX’s InputProcessor class).

public class InputManager implements InputProcessor {
}

If you’re following along from the previous tutorial and are using IntelliJ, right click within this class and click generate -> override methods. (Due to code bloat, I’m going to leave those generated methods out of these code snippets for now.)

Next, create an inner class called InputState.

NOTE: All three classes below will be inner classes of InputManager.

public class InputState {
    public boolean pressed = false;
    public boolean down = false;
    public boolean released = false;
}

Now create the KeyState and TouchState classes which extend InputState

public class KeyState extends InputState{
    //the keyboard key of this object represented as an integer.
    public int key;

    public KeyState(int key){
        this.key = key;
    }
}

public class TouchState extends InputState{
    //keep track of which finger this object belongs to
    public int pointer;
    //coordinates of this finger/mouse
    public Vector2 coordinates;
    //mouse button
    public int button;
    //track the displacement of this finger/mouse
    private Vector2 lastPosition;
    public Vector2 displacement;

    public TouchState(int coord_x, int coord_y, int pointer, int button){
        this.pointer = pointer;
        coordinates = new Vector2(coord_x, coord_y);
        this.button = button;

        lastPosition = new Vector2(0, 0);
        displacement = new Vector2(lastPosition.x, lastPosition.y);
    }
}

The TouchState class is more complicated due to not only having Pressed, Down, and Released events, but also storing which finger is in control of this object and storing coordinates and a displacement vector for gesture movements.

Here is the base structure of the entire class (excluding stub override methods):

public class InputManager implements InputProcessor {
    public class InputState {
        public boolean pressed = false;
        public boolean down = false;
        public boolean released = false;
    }

    public class KeyState extends InputState{
    //the keyboard key of this object represented as an integer.
        public int key;

        public KeyState(int key){
            this.key = key;
        }
    }

    public class TouchState extends InputState{
        //keep track of which finger this object belongs to
        public int pointer;
        //coordinates of this finger/mouse
        public Vector2 coordinates;        
        //mouse button
        public int button;
        //track the displacement of this finger/mouse
        private Vector2 lastPosition;
        public Vector2 displacement;

        public TouchState(int coord_x, int coord_y, int pointer, int button){
            this.pointer = pointer;
            coordinates = new Vector2(coord_x, coord_y);
            this.button = button;

            lastPosition = new Vector2(0, 0);
            displacement = new Vector2(lastPosition.x,lastPosition.y);
        }
    }
}

Since every key/touch event will be it’s own object, you need to store these within an array. Add two new array fields to InputManager.

public Array<KeyState> keyStates = new Array<KeyState>();
public Array<TouchState> touchStates = new Array<TouchState>();

And add a constructor for InputManager to initialize these two objects.

public InputManager() {
    //create the initial state of every key on the keyboard.
    //There are 256 keys available which are all represented as integers.
    for (int i = 0; i < 256; i++) {
        keyStates.add(new KeyState(i));
    }

    //this may not make much sense right now, but I need to create
    //atleast one TouchState object due to Desktop users who utilize
    //a mouse rather than touch.
    touchStates.add(new TouchState(0, 0, 0, 0));
}

Now you’re ready to control the logic of these objects utilizing the override methods generated. Starting with public boolean keyDown(int keycode).

@Override
public boolean keyDown(int keycode) {
    //this function only gets called once when an event is fired. (even if this key is being held down)

    //I need to store the state of the key being held down as well as pressed
    keyStates.get(keycode).pressed = true;
    keyStates.get(keycode).down = true;

    //every overridden method needs a return value. I won't be utilizing this but it can be used for error handling.
    return false;
}

Next is public boolean keyUp(int keycode).

@Override
public boolean keyUp(int keycode) {
    //the key was released, I need to set it's down state to false and released state to true
    keyStates.get(keycode).down = false;
    keyStates.get(keycode).released = true;
    return false;
}

Now that you have handled the logic for key state, you need a way to access these keys to check their states. Add these three methods to InputManager:

//check states of supplied key
public boolean isKeyPressed(int key){
    return keyStates.get(key).pressed;
}
public boolean isKeyDown(int key){
    return keyStates.get(key).down;
}
public boolean isKeyReleased(int key){
    return keyStates.get(key).released;
}

Everything is taking shape, so now is a good moment to try and explain how everything fits together.

libGDX represents every key as an integer used to grab an element from the keyStates array which returns a single keyState object. You can check the state of that key (Pressed, Down, or Released), which is a boolean.

You’re almost ready to test drive the InputManager but there a few more things to setup. Right now, the only state that is actually functional is the Down state.

When a key is Down. Down is set to true. And when a key is Up, Down is set to false. The other two states, Pressed and Released don’t work properly yet.

These are Trigger keys which should only trigger for one frame and then remain false. Now, once they’re activated, they continue to be True.

You need to implement a new method for InputManager called update which will correctly handle the states of Pressed and Released.

public void update(){
  //for every keystate, set pressed and released to false.
  for (int i = 0; i < 256; i++) {
      KeyState k = keyStates.get(i);
      k.pressed = false;
      k.released = false;
  }
}

The Update method allows you to adjust the InputManager post frame. Anything that needs to be reset will go within this method.

To use the InputManager, you need to instantiate it within the core namespace of the project. Within MyGdxGame, add InputManager as a new field.

public class MyGdxGame extends ApplicationAdapter {
    InputManager inputManager;
}

Within the MyGdxGame constructor, instantiate the InputManager. For InputManager to be useful, you need to pass it to libGDX’s InputProcessor and libGDX will use this new object to process input events. Replace the current create method with the below:

@Override
  public void create () {
      inputManager = new InputManager();
      Gdx.input.setInputProcessor(inputManager);
  }

Now you need to call InputManager‘s update method at the end of the MyGdxGame‘s render method.

You should call update() after all game logic involving input has been processed. Replace the current create method with the below:

@Override
public void render () {
    inputManager.update();
}

Here is how MyGdxGame currently looks:

public class MyGdxGame extends ApplicationAdapter {
    SpriteBatch batch;
    Texture img;
    OrthographicCamera camera;
    InputManager inputManager;

    @Override
    public void create () {
        batch = new SpriteBatch();
        img = new Texture("badlogic.jpg");
        camera = new OrthographicCamera(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
        inputManager = new InputManager();
        Gdx.input.setInputProcessor(inputManager);
    }

    @Override
    public void render () {
        camera.update();
        batch.setProjectionMatrix(camera.combined);

        Gdx.gl.glClearColor(1, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        batch.begin();
        batch.draw(img, 0, 0);
        batch.end();

        inputManager.update();
    }
}

Test this code to make sure everything works as intended.

@Override
    public void render () {

        //testing key states...
        if (inputManager.isKeyPressed(Input.Keys.A)) {
            System.out.println("A Pressed");
        }
        if (inputManager.isKeyDown(Input.Keys.A)) {
            System.out.println("A Down");
        }
        if (inputManager.isKeyReleased(Input.Keys.A)) {
            System.out.println("A Released");
        }

        camera.update();
        batch.setProjectionMatrix(camera.combined);

        Gdx.gl.glClearColor(1, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        batch.begin();
        batch.draw(img, 0, 0);
        batch.end();

        inputManager.update();
    }

You should now have more accurate input events.

The InputManager is almost finished, all that’s left is implementing the logic for handling touch events. Luckily, KeyStates and TouchStates function in the same way. You will now be utilizing the generated touch event methods.

Note: This method is heavily commented so I may repeat myself.

@Override
    public boolean touchDown(int screenX, int screenY, int pointer, int button) {
        //There is always at least one touch event initialized (mouse).
        //However, Android can handle multiple touch events (multiple fingers touching the screen at once).

        //Due to this difference, the input manager will add touch events on the fly if more than one
        //finger is touching the screen.

        //check for existing pointer (touch)
        boolean pointerFound = false;

        //get altered coordinates
        int coord_x = coordinateX(screenX);
        int coord_y = coordinateY(screenY);

        //set the state of all touch state events
        for (int i = 0; i < touchStates.size; i++) {
            TouchState t = touchStates.get(i);
            if (t.pointer == pointer) {
                t.down = true;
                t.pressed = true;

                //store the coordinates of this touch event
                t.coordinates.x = coord_x;
                t.coordinates.y = coord_y;
                t.button = button;

                //recording last position for displacement values
                t.lastPosition.x = coord_x;
                t.lastPosition.y = coord_y;

                //this pointer exists, don't add a new one.
                pointerFound = true;
            }
        }

        //this pointer doesn't exist yet, add it to touchStates and initialize it.
        if (!pointerFound) {
            touchStates.add(new TouchState(coord_x, coord_y, pointer, button));
            TouchState t = touchStates.get(pointer);

            t.down = true;
            t.pressed = true;

            t.lastPosition.x = coord_x;
            t.lastPosition.y = coord_y;
        }
        return false;
    }

One of the main differences between KeyStates and TouchStates is the fact that all KeyStates get initialized within the constructor of InputManager due to a keyboard being a physical device.

You know all the available keys for use, but a touch event is a cross-platform event. A touch on Desktop means the user has clicked the mouse, but a touch on Android means the user has touched the screen with their finger. On top of that, there can only be one touch event on Desktop (mouse), while there can be multiple touches on Android (finger/s).

To handle this problem, add new TouchStates on-the-fly depending on what the user does.

When a user triggers a touch event, you first want to convert it’s screenX and screenY values to something more usable. The coordinate 0,0 is at the upper-left of the screen, and the Y axis flipped. To accommodate this add two simple methods to InputManager to convert these coordinates to make 0,0 at the center of the screen and the Y axis right-side up which will work better with a SpriteBatch object.

private int coordinateX (int screenX) {
        return screenX - Gdx.graphics.getWidth()/2;
    }
private int coordinateY (int screenY) {
        return Gdx.graphics.getHeight()/2 - screenY;
    }

Now you need a way to check if this is a new touch event or if InputManager has already discovered this event and added it to the list of TouchStates. To clarify, the user is holding their device and touches the screen with their right-hand thumb, this will be the first touch event within touchStates and InputManager knows that it can handle at least one touch event. If the user decides to touch the screen with both left-hand and right-hand thumbs, the second touch event will instantiate a new TouchState and add it to touchStates on the fly. InputManager now knows it can process two touch events.

All this works from the pointer variable passed to this method. The pointer variable is an integer that allows you to distinguish between simultaneous touch events.

Every time a user fires a touch event, use pointer to check whether to create a new TouchState or not.

boolean pointerFound = false;

Loop through all available TouchState objects to check if this pointer already exists and set the states of this TouchState.

//set the state of all touch state events
        for (int i = 0; i < touchStates.size; i++) {
            TouchState t = touchStates.get(i);
            if (t.pointer == pointer) {
                t.down = true;
                t.pressed = true;

                //store the coordinates of this touch event
                t.coordinates.x = coord_x;
                t.coordinates.y = coord_y;
                t.button = button;

                //recording last position for displacement values
                t.lastPosition.x = coord_x;
                t.lastPosition.y = coord_y;

                //this pointer exists, don't add a new one.
                pointerFound = true;
            }
        }

Notice how if a TouchState is not found, you create a new one and append it to the touchStates array. Also notice how to access the TouchState.

touchStates.add(new TouchState(coord_x, coord_y, pointer, button));
TouchState t = touchStates.get(pointer);

If one finger touches the screen, pointer is 0 which represents the one finger. Adding that new TouchState object to an array automatically set’s up the correct index value. It’s position in the array is 0 which is the same value as it’s pointer.

If a second finger touches the screen, it’s pointer will be 1. When the new TouchState is added to the array, it’s index value is 1. If you need to find which TouchSate is which, use the given pointer value to find the correct TouchState from touchStates.

Now handle the logic for when a finger is no longer touching the screen.

@Override
    public boolean touchUp(int screenX, int screenY, int pointer, int button) {
        TouchState t = touchStates.get(pointer);
        t.down = false;
        t.released = true;

        return false;
    }

NOTE: I chose not to remove TouchState objects even if they’re no longer used. If a new finger count has been discovered, I think it should stay discovered and ready to be re-used. If you feel differently about this, within the touchUp method is where you would implement the logic for removing a TouchState.

Now calculate the displacement vector of a TouchState to handle finger gestures.

@Override
    public boolean touchDragged(int screenX, int screenY, int pointer) {
        //get altered coordinates
        int coord_x = coordinateX(screenX);
        int coord_y = coordinateY(screenY);

        TouchState t = touchStates.get(pointer);
        //set coordinates of this touchstate
        t.coordinates.x = coord_x;
        t.coordinates.y = coord_y;
        //calculate the displacement of this touchstate based on
        //the information from the last frame's position       
        t.displacement.x = coord_x - t.lastPosition.x;
        t.displacement.y = coord_y - t.lastPosition.y;
        //store the current position into last position for next frame.
        t.lastPosition.x = coord_x;
        t.lastPosition.y = coord_y;

        return false;
    }

Like before with key states, you need to add three methods to access these touch states plus two other methods for getting the coordinates and displacement of a TouchState.

//check states of supplied touch
    public boolean isTouchPressed(int pointer){
        return touchStates.get(pointer).pressed;
    }
    public boolean isTouchDown(int pointer){
        return touchStates.get(pointer).down;
    }
    public boolean isTouchReleased(int pointer){
        return touchStates.get(pointer).released;
    }

    public Vector2 touchCoordinates(int pointer){
        return touchStates.get(pointer).coordinates;
    }
    public Vector2 touchDisplacement(int pointer){
        return touchStates.get(pointer).displacement;
    }

You now have the same problem as before with KeyStates, when only the Down state was working properly. But that makes perfect sense, you never added logic to reset the trigger events.

Returning to the update method, add the following code:

for (int i = 0; i < touchStates.size; i++) {
            TouchState t = touchStates.get(i);

            t.pressed = false;
            t.released = false;

            t.displacement.x = 0;
            t.displacement.y = 0;
        }

Here is the full update method again:

public void update(){
        for (int i = 0; i < 256; i++) {
            KeyState k = keyStates.get(i);
            k.pressed = false;
            k.released = false;
        }
        for (int i = 0; i < touchStates.size; i++) {
            TouchState t = touchStates.get(i);

            t.pressed = false;
            t.released = false;

            t.displacement.x = 0;
            t.displacement.y = 0;
        }
    }

Note: The displacement vector is reset back to (0, 0) on every frame.

Using the InputManager

You’ve already seen an example of using the InputManager for checking KeyStates. Now I will explain using TouchStates.

If the application is running on Desktop and only utilizes the mouse then use the only TouchState avaiable from touchStates.

if (inputManager.isTouchPressed(0)) {
    System.out.println("PRESSED");
 }
 if (inputManager.isTouchDown(0)) {
     System.out.println("DOWN");
     System.out.println("Touch coordinates: " + inputManager.touchCoordinates(0));
     System.out.println("Touch displacement" + inputManager.touchDisplacement(0));
 }
 if (inputManager.isTouchReleased(0)) {
     System.out.println("RELEASED");
 }

If the application is running on Android, you need to loop over all available TouchStates and handle each one individually, but checking a TouchState directly is error prone.

inputManager.touchStates.get(0);

Cover this with a new method added to InputManager:

public TouchState getTouchState(int pointer){
        if (touchStates.size > pointer) {
            return touchStates.get(pointer);
        } else {
            return null;
        }
    }

Now you have a cleaner way of accessing a TouchState with simple error checking.

inputManager.getTouchState(0);

If this was in a for loop to begin with, the bounds check of the for loop would essentially be free from error checking.

for (int i = 0; i < inputManager.touchStates.size; i++) {
            TouchState t = inputManager.getTouchState(i);

            System.out.println("Touch State: " + t.pointer + " : coordinates : " + t.coordinates);
        }

Game On

This Input Manager was designed for controlling entities within a game environment. I first designed the basics of this class a while back for ‘variable’ jump heights for my game character.

I later extended the class to handle input for Android with multiple touch events which is currently used in a game called Vortel. I needed a way to allow the user to control an entity with their fingers, no matter where their fingers were on the screen, and no matter how many fingers were on the screen at once. I achieved this affect by accumulating all displacement vectors from every TouchState, and then applying this new vector to the entity. If you have some free time, please check it out.

I used the variable jump height feature in another game called Ooze. I used the different states of TouchState (Pressed, Down, and Released) to accurately control how high the character jumps depending on how long the user is touching the screen.

I hope this tutorial was useful to you with your game ideas and look forward to your questions and comments below.

No Reader comments

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Mobile, once a week, for free.