Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon


24. October 2013

 

In the previous tutorial we looked at handling mouse and keyboard events, both event driven and polled.  Now we will look at how touch works.  To follow along at this point you need to have a touch enabled device ( multi-touch with a mouse is tricky to say the least! ) although all the code will work in Desktop and HTML targets, you simply wont be able to test it.  Let’s jump right in with an example.  This example shows how to handle multiple simultaneous touches:

 

Multitouch

 

package com.gamefromscratch;

import java.util.HashMap;
import java.util.Map;

import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.BitmapFont.TextBounds;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;

public class InputDemo2 implements ApplicationListener, InputProcessor {
    private SpriteBatch batch;
    private BitmapFont font;
    private String message = "Touch something already!";
    private int w,h;
    
    class TouchInfo {
        public float touchX = 0;
        public float touchY = 0;
        public boolean touched = false;
    }
    
    private Map<Integer,TouchInfo> touches = new HashMap<Integer,TouchInfo>();
    
    @Override
    public void create() {        
        batch = new SpriteBatch();    
        font = new BitmapFont(Gdx.files.internal("data/arial-15.fnt"),false);
        font.setColor(Color.RED);
        w = Gdx.graphics.getWidth();
        h = Gdx.graphics.getHeight();
        Gdx.input.setInputProcessor(this);
        for(int i = 0; i < 5; i++){
            touches.put(i, new TouchInfo());
        }
    }

    @Override
    public void dispose() {
        batch.dispose();
        font.dispose();
    }

    @Override
    public void render() {        
        Gdx.gl.glClearColor(1, 1, 1, 1);
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
        
        batch.begin();
        
        message = "";
        for(int i = 0; i < 5; i++){
            if(touches.get(i).touched)
                message += "Finger:" + Integer.toString(i) + "touch at:" +
                        Float.toString(touches.get(i).touchX) +
                        "," +
                        Float.toString(touches.get(i).touchY) +
                        "\n";
                                
        }
        TextBounds tb = font.getBounds(message);
        float x = w/2 - tb.width/2;
        float y = h/2 + tb.height/2;
        font.drawMultiLine(batch, message, x, y);
        
        batch.end();
    }

    @Override
    public void resize(int width, int height) {
    }

    @Override
    public void pause() {
    }

    @Override
    public void resume() {
    }

    @Override
    public boolean keyDown(int keycode) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean keyUp(int keycode) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean keyTyped(char character) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean touchDown(int screenX, int screenY, int pointer, int button) {
        if(pointer < 5){
            touches.get(pointer).touchX = screenX;
            touches.get(pointer).touchY = screenY;
            touches.get(pointer).touched = true;
        }
        return true;
    }

    @Override
    public boolean touchUp(int screenX, int screenY, int pointer, int button) {
        if(pointer < 5){
            touches.get(pointer).touchX = 0;
            touches.get(pointer).touchY = 0;
            touches.get(pointer).touched = false;
        }
        return true;
    }

    @Override
    public boolean touchDragged(int screenX, int screenY, int pointer) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean mouseMoved(int screenX, int screenY) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean scrolled(int amount) {
        // TODO Auto-generated method stub
        return false;
    }
}

Now when you run it, diagnostic information will be displayed for each finger you are touching with:

photo

 

For each finger it displays the coordinates the finger is touched up, up to a total of 5 fingers. 

So what exactly is going on here?  We create a simple class TouchInfo for holding touch details: if its touched, the X and Y coordinates.  We then create a HashMap of touches with an Integer as the key and a TouchInfo class as the data.  The key will be the index of the finger touching with.  The logic is actually in the touchDown and touchUp event handlers.  On touch down we update the touches map at the index represented by the value pointer.  As you may recall from the previous tutorial the value pointer represents which finger is currently pressed.  When the finger is released, touchUp is fired and we simply clear the touch entry at that location.  Finally in render() we loop through the touches map and display the ones that are touched and where.

At the end of the day, touches is basically identical to mouse clicks, except you can have multiple of them and there are no buttons.  Oh I suppose I should mention that the 5 touch limit in this example was just an number I picked arbitrarily.  LibGDX supports up to 20 touches although no devices do.  The iPad for example can track up to 11, the iPhone tracks up to 5, while the HTC One tracks 10.  On your Google phone you can track how many touches it supports using this app.  That said, 5 is a pretty safe and reasonable number… heck, I dont think I’ve ever used more than 4 on any device.

 

Touch gestures

 

There are a number of common gestures that have become ubiquitous in the mobile world.  Things like pinch to zoom, or flick/fling and long press have become the norm.  Fortunately GDX supports all of these things out of the box.  Let’s jump right into a simple demo:

package com.gamefromscratch;


import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.BitmapFont.TextBounds;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.input.GestureDetector;
import com.badlogic.gdx.input.GestureDetector.GestureListener;
import com.badlogic.gdx.math.Vector2;

public class InputDemo3 implements ApplicationListener, GestureListener {
    private SpriteBatch batch;
    private BitmapFont font;
    private String message = "No gesture performed yet";
    private int w,h;

    
    @Override
    public void create() {        
        batch = new SpriteBatch();    
        font = new BitmapFont(Gdx.files.internal("data/arial-15.fnt"),false);
        font.setColor(Color.RED);
        w = Gdx.graphics.getWidth();
        h = Gdx.graphics.getHeight();
        
        GestureDetector gd = new GestureDetector(this);
        Gdx.input.setInputProcessor(gd);
    }

    @Override
    public void dispose() {
        batch.dispose();
        font.dispose();
    }

    @Override
    public void render() {        
        Gdx.gl.glClearColor(1, 1, 1, 1);
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
        
        batch.begin();
        
        TextBounds tb = font.getBounds(message);
        float x = w/2 - tb.width/2;
        float y = h/2 + tb.height/2;
        
        font.drawMultiLine(batch, message, x, y);
        
        batch.end();
    }

    @Override
    public void resize(int width, int height) {
    }

    @Override
    public void pause() {
    }

    @Override
    public void resume() {
    }

    @Override
    public boolean touchDown(float x, float y, int pointer, int button) {
        // TODO Auto-generated method stub
        return true;
    }

    @Override
    public boolean tap(float x, float y, int count, int button) {
        message = "Tap performed, finger" + Integer.toString(button);
        return true;
    }

    @Override
    public boolean longPress(float x, float y) {
        message = "Long press performed";
        return true;
    }

    @Override
    public boolean fling(float velocityX, float velocityY, int button) {
        message = "Fling performed, velocity:" + Float.toString(velocityX) +
                "," + Float.toString(velocityY);
        return true;
    }

    @Override
    public boolean pan(float x, float y, float deltaX, float deltaY) {
        message = "Pan performed, delta:" + Float.toString(deltaX) +
                "," + Float.toString(deltaY);
        return true;
    }

    @Override
    public boolean zoom(float initialDistance, float distance) {
        message = "Zoom performed, initial Distance:" + Float.toString(initialDistance) +
                " Distance: " + Float.toString(distance);
        return true;
    }

    @Override
    public boolean pinch(Vector2 initialPointer1, Vector2 initialPointer2,
            Vector2 pointer1, Vector2 pointer2) {
        message = "Pinch performed";
        return true;
    }

}

 

If you run it, as you perform various gestures they will be displayed centered on the screen.  Supported gestures include tap, fling(flick), pinch ( two fingers moving closer together ), zoom ( two fingers moving apart ), pan ( one finger hold and slide ) and long press ( tap and hold ) as well as good ole fashion touch.

Just like we implemented InputProcessor to handle touch, mouse and keyboard events, we implement the GestureListener to accept gesture events from LibGDX.  In create() you create a GestureDetector using your GestureListener and once again you register it using Gdx.input.setInputProcessor().  Each different gesture triggers the corresponding even in your GestureListener.  In each, we simple update the displayed message to reflect the most recently performed event.

One important concept we didn’t cover here is configuring your GestureDetector…  how do you determine how long a long touch is, or how long must elapse before a drag becomes a flick?  The simple answer is, using the GestureDetector constructor.  You can read more about it here.

 

Handling multiple InputProcessors

 

So, what if you wanted to handle gestures AND keyboard events… what then?  Fortunately the answer is quite simple, instead of passing setInputProcessor an InputProcessor or GestureDetector, you instead pass in an InputMultiplexer.  Like so:

package com.gamefromscratch;


import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputMultiplexer;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.BitmapFont.TextBounds;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.input.GestureDetector;
import com.badlogic.gdx.input.GestureDetector.GestureListener;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.math.Vector2;

public class InputDemo4 implements ApplicationListener, GestureListener, InputProcessor {
    private SpriteBatch batch;
    private BitmapFont font;
    private String message = "No gesture performed yet";
    private int w,h;

    
    @Override
    public void create() {        
        batch = new SpriteBatch();    
        font = new BitmapFont(Gdx.files.internal("data/arial-15.fnt"),false);
        font.setColor(Color.RED);
        w = Gdx.graphics.getWidth();
        h = Gdx.graphics.getHeight();
        
        InputMultiplexer im = new InputMultiplexer();
        GestureDetector gd = new GestureDetector(this);
        im.addProcessor(gd);
        im.addProcessor(this);
        
        
        Gdx.input.setInputProcessor(im);
    }

    @Override
    public void dispose() {
        batch.dispose();
        font.dispose();
    }

    @Override
    public void render() {        
        Gdx.gl.glClearColor(1, 1, 1, 1);
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
        
        batch.begin();
        
        TextBounds tb = font.getBounds(message);
        float x = w/2 - tb.width/2;
        float y = h/2 + tb.height/2;
        
        font.drawMultiLine(batch, message, x, y);
        
        batch.end();
    }

    @Override
    public void resize(int width, int height) {
    }

    @Override
    public void pause() {
    }

    @Override
    public void resume() {
    }

    @Override
    public boolean touchDown(float x, float y, int pointer, int button) {
        message = "Touch down!";
        Gdx.app.log("INFO", message);
        return true;
    }

    @Override
    public boolean tap(float x, float y, int count, int button) {
        message = "Tap performed, finger" + Integer.toString(button);
        Gdx.app.log("INFO", message);
        return false;
    }

    @Override
    public boolean longPress(float x, float y) {
        message = "Long press performed";
        Gdx.app.log("INFO", message);
        return true;
    }

    @Override
    public boolean fling(float velocityX, float velocityY, int button) {
        message = "Fling performed, velocity:" + Float.toString(velocityX) +
                "," + Float.toString(velocityY);
        Gdx.app.log("INFO", message);
        return true;
    }

    @Override
    public boolean pan(float x, float y, float deltaX, float deltaY) {
        message = "Pan performed, delta:" + Float.toString(deltaX) +
                "," + Float.toString(deltaY);
        Gdx.app.log("INFO", message);
        return true;
    }

    @Override
    public boolean zoom(float initialDistance, float distance) {
        message = "Zoom performed, initial Distance:" + Float.toString(initialDistance) +
                " Distance: " + Float.toString(distance);
        Gdx.app.log("INFO", message);
        return true;
    }

    @Override
    public boolean pinch(Vector2 initialPointer1, Vector2 initialPointer2,
            Vector2 pointer1, Vector2 pointer2) {
        message = "Pinch performed";
        Gdx.app.log("INFO", message);

        return true;
    }

    @Override
    public boolean keyDown(int keycode) {
        message = "Key Down";
        Gdx.app.log("INFO", message);
        return true;
    }

    @Override
    public boolean keyUp(int keycode) {
        message = "Key up";
        Gdx.app.log("INFO", message);
        return true;
    }

    @Override
    public boolean keyTyped(char character) {
        message = "Key typed";
        Gdx.app.log("INFO", message);
        return true;
    }

    @Override
    public boolean touchDown(int screenX, int screenY, int pointer, int button) {
        message = "Touch Down";
        Gdx.app.log("INFO", message);

        return false;
    }

    @Override
    public boolean touchUp(int screenX, int screenY, int pointer, int button) {
        message = "Touch up";
        Gdx.app.log("INFO", message);
        return false;
    }

    @Override
    public boolean touchDragged(int screenX, int screenY, int pointer) {
        message = "Touch Dragged";
        Gdx.app.log("INFO", message);
        return false;
    }

    @Override
    public boolean mouseMoved(int screenX, int screenY) {
        message = "Mouse moved";
        Gdx.app.log("INFO", message);
        return false;
    }

    @Override
    public boolean scrolled(int amount) {
        message = "Scrolled";
        Gdx.app.log("INFO", message);
        return false;
    }

}

Do to the fact multiple events can be fired off at once, in addition to printing them on screen, we also log them using Gdx.app.log().  You can see logged events in the LogCat window in Eclipse:

image

There is also a program called ddms ( its a BAT script on windows ) in the android-sdk tools folder that will display the same information.

image

 

So, that’s what log() does… now back to the code.  The key part here is:

InputMultiplexer im = new InputMultiplexer();
GestureDetector gd = new GestureDetector(this);
im.addProcessor(gd);
im.addProcessor(this);

Gdx.input.setInputProcessor(im);

Essentially you create the multiplexer, then add both the InputProcessor and the GestureDetector to it via addProcessor(), then it is the multiplexer that is passed to setInputProcessor().  Otherwise the code works pretty much exactly the same.  There is two things of critical importance here.  First, the order that processors are added to the multiplexor seems to determine the order that they will have first crack at events that occurred.  Next, and this one is super important, in event handlers if you return true that means that the event is handled.  Think about that for a second, its an important concept to grasp.  While something like a touch up or down event is pretty straight forward, a say… pinch event is not.   In fact, a pinch event is composed of a number of other events, including multiple touch events.  However if you return true out of say, the touchDown event, that event will not bubble through to the gesture detector.  Therefore if you are supporting multi touch, be sure to return false from the more atomic events such as touchDown and TouchUp, so they still get a crack at handling those events!

Programming , ,

blog comments powered by Disqus

Month List

Popular Comments