Gestire Input Touch e Eventi con OpenGL ES in libGDX su Android – Parte 3
L’input è il cuore di qualsiasi gioco: senza un buon sistema di gestione dei tocchi e delle gesture, anche il miglior rendering OpenGL ES serve a poco. In Parte 1 abbiamo preparato il progetto Android con libGDX, layout XML e rendering base. In Parte 2 abbiamo costruito un game loop stabile con delta time e animazioni. In questa Parte 3 ci concentriamo su input touch, multi-touch e gesture, sempre con codice Java pronto da copiare.

Se ti servono ripassi sui concetti di base di input touch nativo Android, puoi dare un’occhiata anche a onTouchEvent su Android, mentre per una guida OpenGL ES più generale puoi vedere OpenGL ES su Android in Java e il gioco Pong completo con OpenGL ES. Per una panoramica più ampia su libGDX c’è anche il tutorial libGDX.
1. Come funziona il libGDX input touch Android
libGDX input touch Android è il tema di questa guida: vedremo come gestire eventi, multi-touch e gesture con codice Java pronto da copiare. La Parte 1 ci ha guidati nella preparazione del progetto Android con libGDX, layout XML e rendering base. Con la Parte 2 abbiamo costruito un game loop stabile con delta time e animazioni. Questa Parte 3 entra nel dettaglio dell’input, con supporto completo a multi-touch e GestureDetector.
A livello pratico, nel create() del tuo ApplicationListener (o Game libGDX) registrerai un InputProcessor che riceverà callback come touchDown(), touchUp(), touchDragged() e keyDown(). All’interno di queste callback aggiornerai lo stato del gioco: posizioni di oggetti, flag di input, ecc.
2. Implementare un InputProcessor libGDX per il touch Android in Java
Partiamo con un esempio completo di InputProcessor in Java, pensato per integrarsi con il codice della Parte 1 e Parte 2. Supponiamo di avere una classe MyGame che estende ApplicationAdapter.
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
public class MyGame extends ApplicationAdapter {
private OrthographicCamera camera;
// Posizione di un rettangolo di esempio nel mondo (coordinate logiche)
private float rectX = 0f;
private float rectY = 0f;
// Dimensioni logiche del mondo (es. 800x480)
private static final float WORLD_WIDTH = 800f;
private static final float WORLD_HEIGHT = 480f;
@Override
public void create() {
camera = new OrthographicCamera();
camera.setToOrtho(false, WORLD_WIDTH, WORLD_HEIGHT);
// Registra l'InputProcessor
Gdx.input.setInputProcessor(new MyInputProcessor());
}
@Override
public void render() {
float delta = Gdx.graphics.getDeltaTime();
// Aggiorna la logica del gioco qui se serve (in base a flag di input)
Gdx.gl.glClearColor(0f, 0f, 0f, 1f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
camera.update();
// Usa camera.combined nel tuo batch / shader come visto nelle parti precedenti
// Disegna qui il rettangolo in (rectX, rectY)
}
private class MyInputProcessor implements InputProcessor {
@Override
public boolean keyDown(int keycode) {
// Esempio: tasto BACK esce dal gioco
if (keycode == Input.Keys.BACK || keycode == Input.Keys.ESCAPE) {
Gdx.app.exit();
return true;
}
return false;
}
@Override
public boolean keyUp(int keycode) {
return false;
}
@Override
public boolean keyTyped(char character) {
return false;
}
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
// Gestiremo più avanti la conversione in coordinate del mondo
// Per ora logghiamo solo
Gdx.app.log("Input", "touchDown x=" + screenX + " y=" + screenY + " pointer=" + pointer);
return true; // evento gestito
}
@Override
public boolean touchUp(int screenX, int screenY, int pointer, int button) {
Gdx.app.log("Input", "touchUp x=" + screenX + " y=" + screenY + " pointer=" + pointer);
return true;
}
@Override
public boolean touchDragged(int screenX, int screenY, int pointer) {
Gdx.app.log("Input", "touchDragged x=" + screenX + " y=" + screenY + " pointer=" + pointer);
return true;
}
@Override
public boolean mouseMoved(int screenX, int screenY) {
return false;
}
@Override
public boolean scrolled(float amountX, float amountY) {
return false;
}
}
}
Questo esempio stampa semplicemente i tocchi a log. Nei prossimi paragrafi lo estendiamo per spostare oggetti, gestire multi-touch e integrare le gesture.
3. Tocco singolo in libGDX Android: muovere un rettangolo sullo schermo
Obiettivo: quando tocchi lo schermo, un rettangolo nel mondo 2D si sposta nella posizione toccata. Il problema è che le coordinate tocco sono in pixel schermo (origine in alto a sinistra, Y crescente verso il basso), mentre il nostro mondo OpenGL/orthographic usa origine in basso a sinistra (Y crescente verso l’alto).
Serve quindi una conversione da coordinate schermo a coordinate mondo. Con una OrthographicCamera è molto semplice grazie a camera.unproject(). Vediamo il codice completo con il rettangolo che segue il tocco.
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.Vector3;
public class SingleTouchExample extends ApplicationAdapter {
private OrthographicCamera camera;
private ShapeRenderer shapeRenderer;
// Rettangolo di esempio
private float rectWidth = 50f;
private float rectHeight = 50f;
private float rectX = 400f; // centro iniziale
private float rectY = 240f;
private static final float WORLD_WIDTH = 800f;
private static final float WORLD_HEIGHT = 480f;
private final Vector3 tmpUnproject = new Vector3();
@Override
public void create() {
camera = new OrthographicCamera();
camera.setToOrtho(false, WORLD_WIDTH, WORLD_HEIGHT);
shapeRenderer = new ShapeRenderer();
Gdx.input.setInputProcessor(new InputProcessor() {
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
// Converte le coordinate da schermo a mondo
tmpUnproject.set(screenX, screenY, 0);
camera.unproject(tmpUnproject);
// Sposta il rettangolo in modo che sia centrato nel punto toccato
rectX = tmpUnproject.x - rectWidth / 2f;
rectY = tmpUnproject.y - rectHeight / 2f;
return true;
}
@Override public boolean keyDown(int keycode) { return false; }
@Override public boolean keyUp(int keycode) { return false; }
@Override public boolean keyTyped(char character) { return false; }
@Override public boolean touchUp(int screenX, int screenY, int pointer, int button) { return false; }
@Override public boolean touchDragged(int screenX, int screenY, int pointer) { return false; }
@Override public boolean mouseMoved(int screenX, int screenY) { return false; }
@Override public boolean scrolled(float amountX, float amountY) { return false; }
});
}
@Override
public void render() {
Gdx.gl.glClearColor(0f, 0f, 0.2f, 1f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
camera.update();
shapeRenderer.setProjectionMatrix(camera.combined);
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
shapeRenderer.rect(rectX, rectY, rectWidth, rectHeight);
shapeRenderer.end();
}
@Override
public void dispose() {
shapeRenderer.dispose();
}
}
Il punto chiave è camera.unproject(), che si occupa di:
- invertire l’asse Y (da origine in alto a origine in basso),
- convertire da pixel a coordinate del mondo in base a viewport e proiezione.
4. Multi-touch Android con libGDX: due dita controllano due oggetti
libGDX supporta il multi-touch usando il parametro pointer delle callback e il metodo Gdx.input.isTouched(pointer). Puoi associare ogni pointer a un oggetto diverso. Esempio: due dita controllano due rettangoli indipendenti.
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.Vector3;
public class MultiTouchExample extends ApplicationAdapter {
private OrthographicCamera camera;
private ShapeRenderer shapeRenderer;
private static final float WORLD_WIDTH = 800f;
private static final float WORLD_HEIGHT = 480f;
// Due rettangoli controllati da due dita diverse
private float rect1X = 100f, rect1Y = 200f;
private float rect2X = 600f, rect2Y = 200f;
private float rectW = 60f, rectH = 60f;
// Ricordiamo quale pointer controlla quale rettangolo (-1 = nessuno)
private int rect1Pointer = -1;
private int rect2Pointer = -1;
private final Vector3 tmpUnproject = new Vector3();
@Override
public void create() {
camera = new OrthographicCamera();
camera.setToOrtho(false, WORLD_WIDTH, WORLD_HEIGHT);
shapeRenderer = new ShapeRenderer();
Gdx.input.setInputProcessor(new InputProcessor() {
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
// Assegna il pointer al primo rettangolo libero
if (rect1Pointer == -1) {
rect1Pointer = pointer;
moveRectToTouch(1, screenX, screenY);
} else if (rect2Pointer == -1) {
rect2Pointer = pointer;
moveRectToTouch(2, screenX, screenY);
}
return true;
}
@Override
public boolean touchUp(int screenX, int screenY, int pointer, int button) {
// Se il pointer rilasciato controllava un rettangolo, liberalo
if (pointer == rect1Pointer) {
rect1Pointer = -1;
} else if (pointer == rect2Pointer) {
rect2Pointer = -1;
}
return true;
}
@Override
public boolean touchDragged(int screenX, int screenY, int pointer) {
if (pointer == rect1Pointer) {
moveRectToTouch(1, screenX, screenY);
} else if (pointer == rect2Pointer) {
moveRectToTouch(2, screenX, screenY);
}
return true;
}
private void moveRectToTouch(int rectIndex, int screenX, int screenY) {
tmpUnproject.set(screenX, screenY, 0);
camera.unproject(tmpUnproject);
if (rectIndex == 1) {
rect1X = tmpUnproject.x - rectW / 2f;
rect1Y = tmpUnproject.y - rectH / 2f;
} else {
rect2X = tmpUnproject.x - rectW / 2f;
rect2Y = tmpUnproject.y - rectH / 2f;
}
}
@Override public boolean keyDown(int keycode) { return false; }
@Override public boolean keyUp(int keycode) { return false; }
@Override public boolean keyTyped(char character) { return false; }
@Override public boolean mouseMoved(int screenX, int screenY) { return false; }
@Override public boolean scrolled(float amountX, float amountY) { return false; }
});
}
@Override
public void render() {
Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
camera.update();
shapeRenderer.setProjectionMatrix(camera.combined);
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
// Rettangolo 1 (rosso)
shapeRenderer.setColor(1f, 0f, 0f, 1f);
shapeRenderer.rect(rect1X, rect1Y, rectW, rectH);
// Rettangolo 2 (verde)
shapeRenderer.setColor(0f, 1f, 0f, 1f);
shapeRenderer.rect(rect2X, rect2Y, rectW, rectH);
shapeRenderer.end();
}
@Override
public void dispose() {
shapeRenderer.dispose();
}
}
Quando ti serve sapere in polling se un dito è ancora premuto, puoi usare Gdx.input.isTouched(pointer), ad esempio nel tuo render() per applicare forze o movimenti continui.
5. GestureDetector libGDX Android: tap, swipe, pinch-to-zoom
Per le gesture complesse libGDX fornisce GestureDetector, che avvolge un GestureListener (tipicamente GestureAdapter) e chiama metodi come tap(), longPress(), fling() (swipe veloce), pinch() e zoom().
Vediamo un esempio pratico: uno swipe (fling) cambia il colore di sfondo, un pinch-to-zoom fa zoom in/out della camera.
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputMultiplexer;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.input.GestureDetector;
import com.badlogic.gdx.input.GestureDetector.GestureAdapter;
public class GestureExample extends ApplicationAdapter {
private OrthographicCamera camera;
private ShapeRenderer shapeRenderer;
private float bgR = 0f, bgG = 0f, bgB = 0f;
private static final float WORLD_WIDTH = 800f;
private static final float WORLD_HEIGHT = 480f;
@Override
public void create() {
camera = new OrthographicCamera();
camera.setToOrtho(false, WORLD_WIDTH, WORLD_HEIGHT);
shapeRenderer = new ShapeRenderer();
GestureDetector gestureDetector = new GestureDetector(new GestureAdapter() {
@Override
public boolean fling(float velocityX, float velocityY, int button) {
// Cambio colore sfondo in base alla direzione dello swipe
if (Math.abs(velocityX) > Math.abs(velocityY)) {
// Swipe orizzontale
if (velocityX > 0) {
// Swipe verso destra
bgR = 0f; bgG = 0f; bgB = 1f; // blu
} else {
// Swipe verso sinistra
bgR = 1f; bgG = 0f; bgB = 0f; // rosso
}
} else {
// Swipe verticale
if (velocityY > 0) {
// Swipe verso il basso
bgR = 0f; bgG = 1f; bgB = 0f; // verde
} else {
// Swipe verso l'alto
bgR = 1f; bgG = 1f; bgB = 0f; // giallo
}
}
return true;
}
@Override
public boolean zoom(float initialDistance, float distance) {
// Pinch-to-zoom: distanza più grande = zoom out, più piccola = zoom in
float ratio = initialDistance / distance;
camera.zoom = clamp(ratio, 0.5f, 2f);
return true;
}
private float clamp(float v, float min, float max) {
return Math.max(min, Math.min(max, v));
}
});
// Se vuoi usare più sorgenti di input, usa un InputMultiplexer
InputMultiplexer multiplexer = new InputMultiplexer();
multiplexer.addProcessor(gestureDetector);
// multiplexer.addProcessor(altroInputProcessor);
Gdx.input.setInputProcessor(multiplexer);
}
@Override
public void render() {
Gdx.gl.glClearColor(bgR, bgG, bgB, 1f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
camera.update();
shapeRenderer.setProjectionMatrix(camera.combined);
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
// Disegna un semplice rettangolo che occupa parte dello schermo per percepire lo zoom
shapeRenderer.rect(100, 100, 200, 200);
shapeRenderer.end();
}
@Override
public void dispose() {
shapeRenderer.dispose();
}
}
Il GestureDetector si occupa da solo di tenere traccia di più tocchi e riconoscere le gesture. Tu ti concentri solo sulla logica da eseguire in risposta.
6. Overlay Android con libGDX: Button XML che comunica con OpenGL ES
Spesso nei giochi OpenGL ES serve un overlay UI nativo Android: ad esempio un bottone che resetta la posizione di un oggetto. Questo bottone vive nel layout XML Android e il suo listener gira sul thread UI. Per modificare lo stato del gioco in sicurezza devi passare l’operazione al thread di libGDX con Gdx.app.postRunnable().
Supponiamo di avere un Activity che contiene una AndroidApplicationFragment (o simile) con il gioco libGDX e un bottone nativo:
<!-- res/layout/activity_main.xml -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/gameFragment"
android:name="com.mio.package.LibGdxGameFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/btnReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Reset"
android:layout_margin="16dp"
android:layout_gravity="bottom|end" />
</FrameLayout>
Nell’Activity recuperiamo un riferimento al gioco e usiamo postRunnable() per resettare la posizione di un oggetto:
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
import com.badlogic.gdx.Gdx;
public class MainActivity extends AppCompatActivity {
// Riferimento al gioco libGDX (devi esporlo dal fragment o dall'adapter)
private MyGame myGame;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btnReset = findViewById(R.id.btnReset);
btnReset.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Siamo sul thread UI Android: usiamo postRunnable
Gdx.app.postRunnable(new Runnable() {
@Override
public void run() {
if (myGame != null) {
myGame.resetObjectPosition();
}
}
});
}
});
}
}
Nel tuo MyGame implementerai il metodo resetObjectPosition() che modifica lo stato del mondo (es. rectX, rectY). Questa logica sarà eseguita in sicurezza nel thread GL di libGDX.
7. Formula di conversione coordinate touch in libGDX Android
Se non vuoi usare la camera (ad esempio per un caso molto semplice) puoi convertire le coordinate manualmente. Ricorda che su Android la coordinata Y è invertita rispetto a OpenGL.
Dato:
screenX,screenY: coordinate del tocco in pixel,screenWidth,screenHeight: dimensioni dello schermo in pixel,worldWidth,worldHeight: dimensioni logiche del mondo.
La formula di conversione è:
float worldX = (screenX / (float) screenWidth) * worldWidth;
float worldY = (1f - screenY / (float) screenHeight) * worldHeight; // Y invertita
Snippet Java riutilizzabile in libGDX:
public class TouchConverter {
private final float worldWidth;
private final float worldHeight;
public TouchConverter(float worldWidth, float worldHeight) {
this.worldWidth = worldWidth;
this.worldHeight = worldHeight;
}
public void screenToWorld(int screenX, int screenY, float screenWidth, float screenHeight,
Vector2 outWorld) {
float nx = screenX / screenWidth;
float ny = screenY / screenHeight;
outWorld.x = nx * worldWidth;
outWorld.y = (1f - ny) * worldHeight; // inverti Y
}
}
Nella maggior parte dei casi, però, usare camera.unproject() rimane la soluzione più robusta, soprattutto se usi viewport, letterboxing o scaling complessi (come spesso succede in un gioco reale, ad esempio in un clone di Pong come in questa guida completa).
8. Conclusione: libGDX input touch Android – Parte 4 in arrivo
In questa Parte 3 abbiamo visto come gestire input touch e gesture in libGDX su Android in modo pratico: da InputProcessor base a multi-touch con più dita, dal controllo diretto di oggetti sullo schermo al riconoscimento di swipe e pinch-to-zoom con GestureDetector, fino a integrare un overlay Android nativo che comunica in sicurezza con il thread GL tramite Gdx.app.postRunnable(). Abbiamo anche definito una formula riutilizzabile per convertire coordinate touch in coordinate del mondo OpenGL, così da avere una base solida per qualsiasi scenario di libGDX input touch Android.
Nella Parte 4 ci concentreremo sull’audio: effetti sonori leggeri e reattivi per il gameplay usando SoundPool su Android e le API audio di libGDX. Se vuoi intanto approfondire l’audio nativo, dai un’occhiata a questa guida su SoundPool.
Rispondi