
libGDX Android con OpenGL ES e Layout XML in Java
1. Introduzione
In questa guida vediamo come usare libGDX su Android integrando il rendering OpenGL ES con un normale layout XML. L’obiettivo è semplice: avere una scena 2D/3D dentro un GLSurfaceView e sovrapporre sopra di essa viste Android native come Button e TextView.
Seguendo passo passo configurerai le dipendenze Gradle, creerai un layout XML con un GLSurfaceView a schermo intero, implementerai un renderer OpenGL ES 2.0 in Java che disegna un triangolo e collegherai un pulsante nativo che cambia il colore di sfondo in modo thread-safe.
2. Setup Gradle per libGDX Android con OpenGL ES
Partiamo da un progetto Android standard (minSdk 21) con un solo modulo app. Aggiungiamo libGDX 1.12.1 nel build.gradle di modulo, includendo backend Android, core e librerie native per le principali architetture.
File: app/build.gradle
plugins {
id "com.android.application"
}
android {
namespace "com.example.libgdxxml"
compileSdk 34
defaultConfig {
applicationId "com.example.libgdxxml"
minSdk 21
targetSdk 34
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
implementation "androidx.appcompat:appcompat:1.7.0"
implementation "com.google.android.material:material:1.12.0"
// libGDX 1.12.1 backend Android
implementation "com.badlogicgames.gdx:gdx-backend-android:1.12.1"
// Core libGDX (logica e modelli condivisibili con altri target)
implementation "com.badlogicgames.gdx:gdx:1.12.1"
// Natives per Android (armeabi-v7a, arm64-v8a, x86, x86_64)
implementation "com.badlogicgames.gdx:gdx-platform:1.12.1:natives-armeabi-v7a"
implementation "com.badlogicgames.gdx:gdx-platform:1.12.1:natives-arm64-v8a"
implementation "com.badlogicgames.gdx:gdx-platform:1.12.1:natives-x86"
implementation "com.badlogicgames.gdx:gdx-platform:1.12.1:natives-x86_64"
}
3. Layout XML (activity_main.xml)
Ora creiamo il layout principale dell’app. Usiamo un FrameLayout come root, con all’interno un GLSurfaceView che occupa tutto lo schermo e un Button Android sovrapposto in basso al centro.
File: app/src/main/res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".AndroidLauncher">
<android.opengl.GLSurfaceView
android:id="@+id/glSurfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/btnCambiaColore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cambia colore"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="32dp" />
</FrameLayout>
Grazie al FrameLayout, il Button viene disegnato sopra il GLSurfaceView, permettendoti di avere una UI nativa sovrapposta al contenuto OpenGL.
4. GameRenderer.java – Renderer OpenGL ES per libGDX Android
Creiamo ora il renderer OpenGL ES 2.0. Implementeremo GLSurfaceView.Renderer per disegnare un triangolo al centro dello schermo. Avremo anche una variabile volatile che rappresenta il colore di sfondo, e un metodo pubblico cambiaColore() che alterna rosso, verde e blu.
File: app/src/main/java/com/example/libgdxxml/GameRenderer.java
package com.example.libgdxxml;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class GameRenderer implements GLSurfaceView.Renderer {
// Colore di sfondo corrente (RGBA), usato da thread diversi
private volatile float clearR = 1.0f;
private volatile float clearG = 0.0f;
private volatile float clearB = 0.0f;
private volatile float clearA = 1.0f;
// Stato per cambiare colore
private int colorIndex = 0; // 0 = rosso, 1 = verde, 2 = blu
// OpenGL
private int programId;
private int aPositionLocation;
private int uColorLocation;
private FloatBuffer vertexBuffer;
// Vertici del triangolo (x, y) in clip space
private static final float[] TRIANGLE_VERTICES = {
0.0f, 0.5f, // alto
-0.5f, -0.5f, // sinistra
0.5f, -0.5f // destra
};
// Shader GLSL inline
private static final String VERTEX_SHADER_SOURCE =
"attribute vec2 a_Position;\n" +
"void main() {\n" +
" gl_Position = vec4(a_Position, 0.0, 1.0);\n" +
"}";
private static final String FRAGMENT_SHADER_SOURCE =
"precision mediump float;\n" +
"uniform vec4 u_Color;\n" +
"void main() {\n" +
" gl_FragColor = u_Color;\n" +
"}";
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// Inizializza il buffer dei vertici
ByteBuffer bb = ByteBuffer.allocateDirect(TRIANGLE_VERTICES.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(TRIANGLE_VERTICES);
vertexBuffer.position(0);
// Compila shader e linka il programma
int vertexShader = compileShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER_SOURCE);
int fragmentShader = compileShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER_SOURCE);
programId = GLES20.glCreateProgram();
GLES20.glAttachShader(programId, vertexShader);
GLES20.glAttachShader(programId, fragmentShader);
GLES20.glLinkProgram(programId);
int[] linkStatus = new int[1];
GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] == 0) {
String log = GLES20.glGetProgramInfoLog(programId);
GLES20.glDeleteProgram(programId);
throw new RuntimeException("Errore nel link del programma: " + log);
}
// Recupera le location
aPositionLocation = GLES20.glGetAttribLocation(programId, "a_Position");
uColorLocation = GLES20.glGetUniformLocation(programId, "u_Color");
// Imposta colore di sfondo iniziale (rosso)
GLES20.glClearColor(clearR, clearG, clearB, clearA);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// Aggiorna il viewport quando cambia la dimensione
GLES20.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
// Usa il colore di sfondo corrente
GLES20.glClearColor(clearR, clearG, clearB, clearA);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
// Usa il programma
GLES20.glUseProgram(programId);
// Imposta colore del triangolo (bianco)
GLES20.glUniform4f(uColorLocation, 1.0f, 1.0f, 1.0f, 1.0f);
// Abilita e collega l'attributo posizione
vertexBuffer.position(0);
GLES20.glEnableVertexAttribArray(aPositionLocation);
GLES20.glVertexAttribPointer(
aPositionLocation,
2,
GLES20.GL_FLOAT,
false,
2 * 4,
vertexBuffer
);
// Disegna il triangolo
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
// Pulisce lo stato
GLES20.glDisableVertexAttribArray(aPositionLocation);
GLES20.glUseProgram(0);
}
// Cambia il colore di sfondo in modo ciclico: rosso → verde → blu → rosso...
public void cambiaColore() {
colorIndex = (colorIndex + 1) % 3;
switch (colorIndex) {
case 0: // rosso
clearR = 1.0f; clearG = 0.0f; clearB = 0.0f; break;
case 1: // verde
clearR = 0.0f; clearG = 1.0f; clearB = 0.0f; break;
case 2: // blu
clearR = 0.0f; clearG = 0.0f; clearB = 1.0f; break;
}
}
private int compileShader(int type, String source) {
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, source);
GLES20.glCompileShader(shader);
int[] compiled = new int[1];
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
if (compiled[0] == 0) {
String log = GLES20.glGetShaderInfoLog(shader);
GLES20.glDeleteShader(shader);
throw new RuntimeException("Errore nella compilazione dello shader: " + log);
}
return shader;
}
}
5. AndroidLauncher.java (MainActivity con GLSurfaceView)
Passiamo ora alla MainActivity, che in questo esempio si chiama AndroidLauncher. La classe estende AppCompatActivity (non AndroidApplication di libGDX), usa il layout XML creato prima, inizializza il GLSurfaceView con OpenGL ES 2.0 e collega il Button per cambiare il colore di sfondo usando queueEvent().
File: app/src/main/java/com/example/libgdxxml/AndroidLauncher.java
package com.example.libgdxxml;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
public class AndroidLauncher extends AppCompatActivity {
private GLSurfaceView glSurfaceView;
private GameRenderer renderer;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Recupera le view dal layout XML
glSurfaceView = findViewById(R.id.glSurfaceView);
Button btnCambiaColore = findViewById(R.id.btnCambiaColore);
// Configura il GLSurfaceView per OpenGL ES 2.0
glSurfaceView.setEGLContextClientVersion(2);
// Crea e collega il renderer
renderer = new GameRenderer();
glSurfaceView.setRenderer(renderer);
glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
// Gestione click del pulsante: usa queueEvent per eseguire sul GL thread
btnCambiaColore.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
glSurfaceView.queueEvent(new Runnable() {
@Override
public void run() {
renderer.cambiaColore();
}
});
}
});
}
@Override
protected void onPause() {
super.onPause();
glSurfaceView.onPause();
}
@Override
protected void onResume() {
super.onResume();
glSurfaceView.onResume();
}
}
6. Comunicazione thread-safe tra UI thread e GL thread
Nel modello di rendering di Android, il GLSurfaceView ha un suo thread dedicato (GL thread) per eseguire le chiamate OpenGL. Gli eventi della UI (come il click sul Button) arrivano invece sul main thread. Toccare direttamente lo stato OpenGL dal main thread è pericoloso e può causare crash o glitch di rendering.
Per questo esiste glSurfaceView.queueEvent(Runnable): qualunque Runnable passato a questo metodo viene eseguito sul GL thread, in coda al ciclo di rendering. In questo modo puoi modificare in sicurezza variabili usate da onDrawFrame(), creare o distruggere risorse OpenGL, ecc.
Nell’esempio usiamo queueEvent() nel listener del pulsante:
btnCambiaColore.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
glSurfaceView.queueEvent(new Runnable() {
@Override
public void run() {
renderer.cambiaColore();
}
});
}
});
Il metodo cambiaColore() viene quindi eseguito sempre sul GL thread e aggiorna le variabili volatile del colore di sfondo. Il frame successivo, onDrawFrame() legge questi valori aggiornati e il colore di sfondo cambia in modo fluido e sicuro, senza dover usare lock espliciti.
7. Risultato finale
A questo punto, avviando l’app, vedrai:
- un triangolo bianco disegnato al centro dello schermo tramite OpenGL ES 2.0;
- uno sfondo colorato (rosso, verde o blu) dietro il triangolo;
- un pulsante Android nativo “Cambia colore” sovrapposto in basso al centro.
Ogni volta che tocchi il pulsante, il colore di sfondo cambia ciclicamente, dimostrando come la UI nativa possa controllare in modo thread-safe la scena OpenGL.
8. Conclusioni – libGDX Android con OpenGL ES e Layout XML
In questa guida hai visto come integrare libGDX in un progetto Android mantenendo il controllo completo del layout XML e delle viste native. Hai configurato Gradle, creato un layout con GLSurfaceView, scritto un renderer OpenGL ES 2.0 in Java e collegato un pulsante Android che comunica in modo sicuro con il GL thread tramite queueEvent(). Per una panoramica su come creare un gioco Android in Java, consulta anche la guida introduttiva ai giochi Android in Java. Se vuoi vedere altri giochi completi sviluppati con OpenGL ES su Android, puoi seguire le guide a Gioco Pong su Android con OpenGL ES, Arkanoid su Android con OpenGL ES, Tetris su Android con OpenGL ES e Pac-Man su Android con OpenGL ES.
Nel prossimo articolo della serie su libGDX vedremo come passare da un semplice triangolo a una piccola scena: aggiungeremo più oggetti, introdurremo trasformazioni con matrici (scala, rotazione, traslazione) e inizieremo a usare texture, mantenendo sempre l’integrazione con layout XML e UI nativa Android.
Rispondi