App Tutorial

Pong su Android con OpenGL ES: Guida Completa in Java

Guida rapida: sviluppare Pong su Android con OpenGL ES e Java

Pong è uno dei giochi più semplici e didattici per imparare le basi della grafica 2D. Su Android, usare OpenGL ES 2.0 ti permette di avere pieno controllo sul rendering, imparando concetti riutilizzabili in giochi più complessi.

In questa mini guida creiamo una versione giocabile di Pong (single player vs CPU) in Java puro, usando GLSurfaceView e un renderer personalizzato.

1. Prerequisiti

  • Android Studio (versione recente)
  • Android SDK >= 21 (Android 5.0)
  • Conoscenza base di Java e cicli di gioco
  • Dispositivo o emulatore con supporto OpenGL ES 2.0 (praticamente tutti i device moderni)

2. Setup del progetto e configurazione GLSurfaceView

Crea un nuovo progetto Empty Activity. Nel layout principale sostituisci il TextView con un GLSurfaceView personalizzato o, più semplicemente, useremo il layout in Java creando la vista a runtime.

Esempio di layout XML (opzionale) in activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.opengl.GLSurfaceView
        android:id="@+id/glSurfaceView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

In MainActivity.java collegheremo il GLSurfaceView al nostro GameRenderer e abiliteremo OpenGL ES 2.0.

3. Componenti base: GLSurfaceView e Renderer

GLSurfaceView è una vista specializzata che gestisce il contesto OpenGL. GameRenderer implementa GLSurfaceView.Renderer ed è responsabile del ciclo di rendering: inizializzazione, gestione delle dimensioni e disegno di ogni frame.

Le tre funzioni principali del renderer:

  • onSurfaceCreated(): inizializza lo stato OpenGL, colori di sfondo, ecc.
  • onSurfaceChanged(): reagisce a cambi di dimensione (rotazione, resize).
  • onDrawFrame(): viene chiamata ogni frame; qui aggiorniamo logica di gioco e disegniamo la scena.

4. Codice: Renderer, Paddle, Ball, MainActivity e GameLoop

Qui sotto trovi una mini app completa. I file Java principali sono:

  • MainActivity.java: crea il GLSurfaceView, gestisce il touch.
  • GameRenderer.java: implementa GLSurfaceView.Renderer, game loop e disegno.
  • Paddle.java: rappresenta la racchetta (giocatore e CPU).
  • Ball.java: rappresenta la pallina, movimento e collisioni.

Il game loop è integrato in onDrawFrame(): ad ogni chiamata aggiorniamo le posizioni in base al tempo trascorso e ridisegniamo.

MainActivity.java

package com.example.pongopengl;

import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;

public class MainActivity extends Activity implements View.OnTouchListener {

    private GLSurfaceView glSurfaceView;
    private GameRenderer renderer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        glSurfaceView = new GLSurfaceView(this);

        // Dichiarare che vogliamo usare OpenGL ES 2.0
        glSurfaceView.setEGLContextClientVersion(2);

        renderer = new GameRenderer(this);
        glSurfaceView.setRenderer(renderer);

        // Render continui per avere un game loop semplice
        glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);

        glSurfaceView.setOnTouchListener(this);
        setContentView(glSurfaceView);
    }

    @Override
    protected void onPause() {
        super.onPause();
        glSurfaceView.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        glSurfaceView.onResume();
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        // Usiamo la posizione X del tocco per muovere la racchetta del giocatore
        if (event != null) {
            final float x = event.getX();

            // Inviamo l'evento al renderer sul thread GL
            glSurfaceView.queueEvent(new Runnable() {
                @Override
                public void run() {
                    renderer.onTouch(x);
                }
            });
            return true;
        }
        return false;
    }
}

Paddle.java

package com.example.pongopengl;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.opengles.GL10;

public class Paddle {

    // Coordinate del rettangolo (due triangoli) in coordinate normalizzate
    private FloatBuffer vertexBuffer;
    private float width;
    private float height;

    // Posizione centrale
    public float x;
    public float y;

    // Velocità usata per l'IA o per eventuali animazioni
    public float speed = 1.5f;

    // Colore RGBA
    private float[] color = {1f, 1f, 1f, 1f};

    public Paddle(float width, float height, float startX, float startY) {
        this.width = width;
        this.height = height;
        this.x = startX;
        this.y = startY;

        updateBuffer();
    }

    private void updateBuffer() {
        float left = x - width / 2f;
        float right = x + width / 2f;
        float top = y + height / 2f;
        float bottom = y - height / 2f;

        float[] vertices = {
                left,  top,
                left,  bottom,
                right, bottom,
                left,  top,
                right, bottom,
                right, top
        };

        ByteBuffer bb = ByteBuffer.allocateDirect(vertices.length * 4);
        bb.order(ByteOrder.nativeOrder());
        vertexBuffer = bb.asFloatBuffer();
        vertexBuffer.put(vertices);
        vertexBuffer.position(0);
    }

    public void setPosition(float x, float y) {
        this.x = x;
        this.y = y;
        updateBuffer();
    }

    public void draw(GL10 gl) {
        // Abilita array di vertici
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        gl.glVertexPointer(2, GL10.GL_FLOAT, 0, vertexBuffer);

        // Imposta colore
        gl.glColor4f(color[0], color[1], color[2], color[3]);

        // Disegna 6 vertici (2 triangoli)
        gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 6);

        gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
    }

    // Controllo collisione con la pallina (asse Y dipende da renderer)
    public boolean collidesWith(Ball ball) {
        float left = x - width / 2f;
        float right = x + width / 2f;
        float top = y + height / 2f;
        float bottom = y - height / 2f;

        float bx = ball.x;
        float by = ball.y;
        float br = ball.radius;

        return bx + br > left && bx - br < right &&
               by + br > bottom && by - br < top;
    }
}

Ball.java

package com.example.pongopengl;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.opengles.GL10;

public class Ball {

    public float x;
    public float y;
    public float vx;
    public float vy;
    public float radius;

    private FloatBuffer vertexBuffer;
    private int segments = 32;
    private float[] color = {1f, 1f, 1f, 1f};

    public Ball(float radius, float startX, float startY, float vx, float vy) {
        this.radius = radius;
        this.x = startX;
        this.y = startY;
        this.vx = vx;
        this.vy = vy;

        buildCircle();
    }

    private void buildCircle() {
        // Creiamo un triangolo a ventaglio per il cerchio
        float[] vertices = new float[(segments + 2) * 2];
        vertices[0] = 0f;
        vertices[1] = 0f;

        for (int i = 0; i <= segments; i++) {
            double angle = 2.0 * Math.PI * i / segments;
            float dx = (float) Math.cos(angle) * radius;
            float dy = (float) Math.sin(angle) * radius;
            vertices[(i + 1) * 2] = dx;
            vertices[(i + 1) * 2 + 1] = dy;
        }

        ByteBuffer bb = ByteBuffer.allocateDirect(vertices.length * 4);
        bb.order(ByteOrder.nativeOrder());
        vertexBuffer = bb.asFloatBuffer();
        vertexBuffer.put(vertices);
        vertexBuffer.position(0);
    }

    public void update(float dt) {
        // Aggiorna posizione in base alla velocità e al delta time
        x += vx * dt;
        y += vy * dt;
    }

    public void bounceHorizontal() {
        vx = -vx;
    }

    public void bounceVertical() {
        vy = -vy;
    }

    public void draw(GL10 gl) {
        gl.glPushMatrix();

        // Trasla la matrice alla posizione della pallina
        gl.glTranslatef(x, y, 0f);

        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        gl.glVertexPointer(2, GL10.GL_FLOAT, 0, vertexBuffer);

        gl.glColor4f(color[0], color[1], color[2], color[3]);
        gl.glDrawArrays(GL10.GL_TRIANGLE_FAN, 0, segments + 2);

        gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
        gl.glPopMatrix();
    }
}

GameRenderer.java

package com.example.pongopengl;

import android.content.Context;
import android.opengl.GLSurfaceView;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

public class GameRenderer implements GLSurfaceView.Renderer {

    private Context context;

    private Paddle playerPaddle;
    private Paddle cpuPaddle;
    private Ball ball;

    // Limiti del mondo in coordinate normalizzate
    // Useremo -1 .. 1 per X e Y
    private float worldLeft = -1f;
    private float worldRight = 1f;
    private float worldTop = 1f;
    private float worldBottom = -1f;

    private long lastTimeNs;

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

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        // Colore di sfondo nero
        gl.glClearColor(0f, 0f, 0f, 1f);

        // Abilita il 2D senza profondità
        gl.glDisable(GL10.GL_DEPTH_TEST);

        // Inizializza oggetti di gioco
        float paddleWidth = 0.4f;
        float paddleHeight = 0.05f;

        playerPaddle = new Paddle(paddleWidth, paddleHeight, 0f, -0.9f);
        cpuPaddle = new Paddle(paddleWidth, paddleHeight, 0f, 0.9f);

        ball = new Ball(0.04f, 0f, 0f, 0.6f, 0.8f);

        lastTimeNs = System.nanoTime();
    }

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

        // Impostiamo una proiezione ortografica semplice da -1 a 1
        gl.glMatrixMode(GL10.GL_PROJECTION);
        gl.glLoadIdentity();
        gl.glOrthof(worldLeft, worldRight, worldBottom, worldTop, -1f, 1f);

        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();
    }

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

        update(dt);

        gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

        gl.glLoadIdentity();

        // Disegna oggetti di gioco
        playerPaddle.draw(gl);
        cpuPaddle.draw(gl);
        ball.draw(gl);
    }

    private void update(float dt) {
        // Aggiorna la pallina
        ball.update(dt);

        // Rimbalzi sui bordi sinistro e destro
        if (ball.x - ball.radius < worldLeft) {
            ball.x = worldLeft + ball.radius;
            ball.bounceHorizontal();
        } else if (ball.x + ball.radius > worldRight) {
            ball.x = worldRight - ball.radius;
            ball.bounceHorizontal();
        }

        // Controllo collisione con racchette
        if (playerPaddle.collidesWith(ball) && ball.vy < 0) {
            ball.bounceVertical();
            // Aggiungi un po' di variazione in base alla posizione di impatto
            ball.vx += (ball.x - playerPaddle.x) * 0.5f;
        }

        if (cpuPaddle.collidesWith(ball) && ball.vy > 0) {
            ball.bounceVertical();
            ball.vx += (ball.x - cpuPaddle.x) * 0.5f;
        }

        // Rimbalzo su bordi alto/basso (reset se esce per semplicità)
        if (ball.y - ball.radius < worldBottom) {
            resetBall(-1);
        } else if (ball.y + ball.radius > worldTop) {
            resetBall(1);
        }

        // IA semplice: la CPU segue la X della pallina
        float targetX = ball.x;
        float dx = targetX - cpuPaddle.x;
        float maxMove = cpuPaddle.speed * dt;
        if (Math.abs(dx) > maxMove) {
            dx = Math.signum(dx) * maxMove;
        }
        float newCpuX = cpuPaddle.x + dx;
        newCpuX = clamp(newCpuX, worldLeft + 0.2f, worldRight - 0.2f);
        cpuPaddle.setPosition(newCpuX, cpuPaddle.y);
    }

    private void resetBall(int direction) {
        ball.x = 0f;
        ball.y = 0f;
        ball.vx = 0.6f * direction;
        ball.vy = 0.8f * direction;
    }

    private float clamp(float v, float min, float max) {
        return Math.max(min, Math.min(max, v));
    }

    // Chiamato dalla MainActivity quando l'utente tocca lo schermo
    public void onTouch(float screenX) {
        // Convertiamo la coordinata di schermo in mondo [-1, 1]
        // Qui assumiamo una trasformazione semplice: normalizziamo rispetto alla larghezza.
        // Per semplicità, supponiamo che la larghezza dello schermo sia mappata in [-1, 1].

        // In un progetto reale, passeresti anche la larghezza dello schermo
        // o useresti un tocco relativo (0..1). Qui semplifichiamo:
        float normalizedX = (screenX / 1080f) * 2f - 1f; // 1080 come esempio
        normalizedX = clamp(normalizedX, worldLeft + 0.2f, worldRight - 0.2f);
        playerPaddle.setPosition(normalizedX, playerPaddle.y);
    }
}

Nota: per una gestione precisa del touch, passa la larghezza reale dello schermo al renderer o calcola normalizedX usando un fattore derivato in onSurfaceChanged().

5. Mini app completa e prossimi passi

Con questi quattro file Java (MainActivity, GameRenderer, Paddle, Ball) hai una versione funzionante di Pong single player contro una semplice IA. Puoi estendere il progetto aggiungendo punteggi, suoni, menu, livelli di difficoltà e migliorando la fisica della pallina.

Come prossimo passo, prova a:

  • Implementare un sistema di punteggio e reset della partita.
  • Aggiungere effetti grafici (colori, linee centrali, animazioni).
  • Ottimizzare il controllo touch (drag continuo, multi-touch).
  • Portare la logica su OpenGL ES 2.0 con shader (vertex/fragment) per avere più controllo sul rendering.