Clone Arkanoid con OpenGL ES e Java su Android

Arkanoid Android OpenGL ES screenshot su dispositivo Android

Arkanoid è un classico breakout game: paddle, pallina, mattoncini, ed è un esempio ideale di Arkanoid Android OpenGL ES per chi vuole studiare la grafica su dispositivi mobile. In particolare, è perfetto per imparare OpenGL ES 2.0 su Android perché unisce rendering 2D, input touch, fisica semplice e gestione della scena. Inoltre, questo tutorial ti guiderà passo dopo passo nella creazione di un’app completa di questo breakout game in Java.

1. Prerequisiti per Arkanoid Android OpenGL ES

Prima di iniziare, innanzitutto assicurati di possedere: conoscenza intermedia di Java e programmazione orientata agli oggetti, Android Studio installato e configurato (versione 4.0 o successiva), basi di OpenGL ES 2.0 (coordinati NDC, shader, buffer objects), e familiarità con il ciclo di vita delle Activity Android. In particolare, se sei nuovo a OpenGL ES, consiglio di leggere la documentazione ufficiale OpenGL ES su Android per una guida introduttiva su rendering 2D prima di affrontare Arkanoid.

Setup GLSurfaceView per il progetto Android

public class GameGLSurfaceView extends GLSurfaceView {
    public GameGLSurfaceView(Context context) {
        super(context);
        setEGLContextClientVersion(2); // OpenGL ES 2.0
        setRenderer(new GameRenderer(context));
        setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // Sposta il paddle in base alla posizione x del dito
        if (event.getAction() == MotionEvent.ACTION_MOVE) {
            GameRenderer.setPaddleTargetX(event.getX());
        }
        return true;
    }
}

2. Struttura del Progetto OpenGL ES Android

Per un’app Android ben organizzata, innanzitutto consiglio questa struttura di classi: Activity principale che gestisce il ciclo di vita e la GLSurfaceView su Android Developers, Renderer implementando GLSurfaceView.Renderer per il rendering e il game loop, classi di gioco (Ball, Paddle, Brick) contenenti la logica fisica, classe CollisionHandler per rilevamenti AABB, e infine classe RectRenderer per il disegno dei rettangoli OpenGL. Di conseguenza, una buona organizzazione facilita il debugging e l’aggiunta di nuove funzionalità.

2. Componenti chiave del Gioco Arkanoid Android

Oggetti fondamentali: pallina (posizione, velocità, rimbalzi), paddle (controllato da touch), mattoncini (griglia di rettangoli distruttibili). In particolare, tutto può essere gestito in coordinate normalizzate (da -1 a 1) per semplificare il calcolo e il rendering. Di conseguenza, questa scelta facilita notevolmente l’implementazione della fisica del gioco.

Struttura degli oggetti nel gioco Breakout Android in Java

public class Ball {
    public float x, y;       // posizione
    public float vx, vy;     // velocità
    public float radius = 0.05f;

    public Ball(float x, float y, float vx, float vy) {
        this.x = x; this.y = y;
        this.vx = vx; this.vy = vy;
    }

    public void update(float dt) {
        x += vx * dt;
        y += vy * dt;

        // Rimbalzo sui bordi orizzontali
        if (x - radius < -1f) { x = -1f + radius; vx = -vx; }
        if (x + radius >  1f) { x =  1f - radius; vx = -vx; }

        // Bordi verticali (in alto si rimbalza, in basso si perde)
        if (y + radius > 1f) { y = 1f - radius; vy = -vy; }
    }
}

3. Disegnare Arkanoid con OpenGL ES 2.0

Per rappresentare pallina, paddle e mattoncini puoi utilizzare rettangoli 2D definiti da due triangoli. In questo modo, ecco un esempio minimale con shader e VBO. Pertanto, la semplicità di questo approccio lo rende ideale per principianti.

Shader e buffer: rendering 2D con OpenGL ES

class RectRenderer {
    private final String vShaderSrc =
        "attribute vec4 aPosition;\n" +
        "uniform vec2 uOffset;\n" +
        "uniform vec2 uScale;\n" +
        "void main() {\n" +
        "  vec2 pos = aPosition.xy * uScale + uOffset;\n" +
        "  gl_Position = vec4(pos, 0.0, 1.0);\n" +
        "}";

    private final String fShaderSrc =
        "precision mediump float;\n" +
        "uniform vec4 uColor;\n" +
        "void main() { gl_FragColor = uColor; }";

    private int program, aPositionLoc, uOffsetLoc, uScaleLoc, uColorLoc;
    private int vbo;

    // Rettangolo NDC [-0.5,0.5]
    private final float[] vertices = {
        -0.5f, -0.5f,
         0.5f, -0.5f,
        -0.5f,  0.5f,
         0.5f,  0.5f
    };

    public void init() {
        program = GLUtil.buildProgram(vShaderSrc, fShaderSrc);
        aPositionLoc = GLES20.glGetAttribLocation(program, "aPosition");
        uOffsetLoc   = GLES20.glGetUniformLocation(program, "uOffset");
        uScaleLoc    = GLES20.glGetUniformLocation(program, "uScale");
        uColorLoc    = GLES20.glGetUniformLocation(program, "uColor");

        int[] buffers = new int[1];
        GLES20.glGenBuffers(1, buffers, 0);
        vbo = buffers[0];
        FloatBuffer vb = ByteBuffer.allocateDirect(vertices.length * 4)
                .order(ByteOrder.nativeOrder()).asFloatBuffer();
        vb.put(vertices).position(0);
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo);
        GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vb.capacity() * 4,
                vb, GLES20.GL_STATIC_DRAW);
    }

    public void draw(float cx, float cy, float sx, float sy, float[] color) {
        GLES20.glUseProgram(program);
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo);
        GLES20.glEnableVertexAttribArray(aPositionLoc);
        GLES20.glVertexAttribPointer(aPositionLoc, 2, GLES20.GL_FLOAT,
                false, 2 * 4, 0);

        GLES20.glUniform2f(uOffsetLoc, cx, cy);
        GLES20.glUniform2f(uScaleLoc, sx, sy);
        GLES20.glUniform4fv(uColorLoc, 1, color, 0);

        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
        GLES20.glDisableVertexAttribArray(aPositionLoc);
    }
}

4. Rilevamento Collisioni AABB in OpenGL ES Android

Per mattoncini e paddle basta un Axis-Aligned Bounding Box. In particolare, la pallina può essere approssimata con un quadrato centrato sulla sua posizione. Inoltre, questo metodo garantisce calcoli veloci e affidabili per il rilevamento delle collisioni.

Gestione delle collisioni AABB con rilevamento preciso

// Ritorna true se A e B si sovrappongono (AABB)
public static boolean intersectAABB(
        float ax1, float ay1, float ax2, float ay2,
        float bx1, float by1, float bx2, float by2) {

    return ax1 < bx2 && ax2 > bx1 &&
           ay1 < by2 && ay2 > by1;
}

// Esempio per pallina vs mattoncino
public static boolean ballHitsBrick(Ball b, Brick r) {
    float ax1 = b.x - b.radius;
    float ay1 = b.y - b.radius;
    float ax2 = b.x + b.radius;
    float ay2 = b.y + b.radius;
    return intersectAABB(ax1, ay1, ax2, ay2,
                         r.left, r.bottom, r.right, r.top);
}

5. Mini App Completa: Arkanoid Android OpenGL ES in Java

Classe GameRenderer essenziale: innanzitutto inizializza pallina, paddle, 6 mattoncini in due righe, quindi aggiorna logica e disegna ogni frame. Infine, tutto questo si ripete continuamente nel ciclo di rendering del gioco.

Game loop e renderer completo in Java

public class GameRenderer implements GLSurfaceView.Renderer {
    private static float paddleTargetX = 0.0f; // in pixel, aggiornato dal touch

    public static void setPaddleTargetX(float x) {
        paddleTargetX = x;
    }

    private final Context context;
    private RectRenderer rectRenderer;
    private Ball ball;

    // Paddle in coordinate mondo (-1..1)
    private float paddleX = 0f;
    private float paddleY = -0.8f;
    private final float paddleHalfW = 0.25f;
    private final float paddleHalfH = 0.05f;

    // Mattoncini
    private static class Brick {
        float left, right, top, bottom;
        boolean alive = true;
    }

    private Brick[] bricks;
    private int width, height;
    private long lastTimeNs;

    public GameRenderer(Context context) {
        this.context = context;
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        GLES20.glClearColor(0f, 0f, 0f, 1f);
        rectRenderer = new RectRenderer();
        rectRenderer.init();

        // Pallina al centro, velocità diagonale
        ball = new Ball(0f, -0.6f, 0.6f, 0.8f);

        // 6 mattoncini: 2 righe x 3 colonne
        bricks = new Brick[6];
        int idx = 0;
        float bw = 0.5f;  // larghezza
        float bh = 0.15f; // altezza
        for (int row = 0; row < 2; row++) {
            for (int col = 0; col < 3; col++) {
                Brick b = new Brick();
                float cx = -0.6f + col * 0.6f;  // centro X
                float cy =  0.4f + row * 0.2f;  // centro Y
                b.left   = cx - bw / 2f;
                b.right  = cx + bw / 2f;
                b.bottom = cy - bh / 2f;
                b.top    = cy + bh / 2f;
                bricks[idx++] = b;
            }
        }

        lastTimeNs = System.nanoTime();
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        this.width = width;
        this.height = height;
        GLES20.glViewport(0, 0, width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        long now = System.nanoTime();
        float dt = (now - lastTimeNs) / 1_000_000_000f;
        lastTimeNs = now;

        update(dt);

        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

        float[] white = {1f, 1f, 1f, 1f};
        float[] blue  = {0.2f, 0.6f, 1f, 1f};
        float[] red   = {1f, 0.3f, 0.3f, 1f};

        // Disegna paddle
        rectRenderer.draw(paddleX, paddleY,
                paddleHalfW * 2, paddleHalfH * 2, white);

        // Disegna pallina (usiamo un piccolo rettangolo)
        rectRenderer.draw(ball.x, ball.y,
                ball.radius * 2, ball.radius * 2, blue);

        // Disegna mattoncini
        for (Brick b : bricks) {
            if (!b.alive) continue;
            float cx = (b.left + b.right) * 0.5f;
            float cy = (b.top + b.bottom) * 0.5f;
            float sx = (b.right - b.left);
            float sy = (b.top - b.bottom);
            rectRenderer.draw(cx, cy, sx, sy, red);
        }
    }

    private void update(float dt) {
        // Converte paddleTargetX da pixel a coordinate mondo [-1,1]
        float ndcX = (paddleTargetX / (float) width) * 2f - 1f;
        // Interpola per movimento più fluido
        paddleX += (ndcX - paddleX) * 10f * dt;

        ball.update(dt);

        // Collisione con paddle
        float ax1 = ball.x - ball.radius;
        float ay1 = ball.y - ball.radius;
        float ax2 = ball.x + ball.radius;
        float ay2 = ball.y + ball.radius;

        float px1 = paddleX - paddleHalfW;
        float px2 = paddleX + paddleHalfW;
        float py1 = paddleY - paddleHalfH;
        float py2 = paddleY + paddleHalfH;

        if (ax1 < px2 && ax2 > px1 && ay1 < py2 && ay2 > py1 && ball.vy < 0) {
            ball.y = py2 + ball.radius;
            ball.vy = -ball.vy; // rimbalzo verticale
            // varia leggermente vx in base al punto di impatto
            float hit = (ball.x - paddleX) / paddleHalfW;
            ball.vx += hit * 0.5f;
        }

        // Collisione con mattoncini
        for (Brick r : bricks) {
            if (!r.alive) continue;
            if (ax1 < r.right && ax2 > r.left &&
                ay1 < r.top   && ay2 > r.bottom) {
                r.alive = false;
                // Semplice inversione verticale
                ball.vy = -ball.vy;
                break;
            }
        }

        // Se la pallina cade sotto lo schermo, reset semplice
        if (ball.y + ball.radius < -1f) {
            ball.x = 0f; ball.y = -0.6f;
            ball.vx = 0.6f; ball.vy = 0.8f;
        }
    }
}

6. Conclusione: Arkanoid Android OpenGL ES

Da qui puoi espandere il clone di Arkanoid aggiungendo innanzitutto suoni con SoundPool, quindi livelli multipli caricati da file, power-up, sistema di punteggio e infine persistenza con SharedPreferences o database. Per ulteriori approfondimenti sui giochi Android, consulta la guida completa ai giochi Android in Java. Inoltre, se hai bisogno di implementare logiche asincrone nel tuo gioco, puoi anche consultare la guida ai Thread Android in Android Studio.

Prossimi passi per il tuo clone Breakout Android con OpenGL ES