libGDX Android con OpenGL ES e Layout XML – Guida Completa in Java

libGDX Android con OpenGL ES e layout XML

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.

Commenti

Rispondi

Scopri di più da App Tutorial

Abbonati ora per continuare a leggere e avere accesso all'archivio completo.

Continua a leggere