App Tutorial

Tetris su Android con OpenGL ES: Guida in Java

Tetris per Android con OpenGL ES 2.0 (Java)

Tetris è un puzzle game a blocchi a caduta; qui realizziamo una mini app completa in Java che usa OpenGL ES 2.0 per disegnare i tetromini con grafica 2D accelerata su Android.

Prerequisiti

  • Android Studio (ultima versione stabile)
  • minSdkVersion 21, targetSdkVersion recente
  • In build.gradle (Module: app) abilita OpenGL ES 2.0:
android {
    defaultConfig {
        applicationId "com.example.tetrisgl"
        minSdk 21
        targetSdk 34
    }

    defaultConfig {
        vectorDrawables.useSupportLibrary = true
    }
}

// Opzionale: forza il requisito OpenGL ES 2.0 nel manifest
// <uses-feature android:glEsVersion="0x00020000" android:required="true" />

Architettura del progetto

  • MainActivity: crea e mostra la GLSurfaceView (TetrisView).
  • TetrisView: estende GLSurfaceView, collega il renderer e gestisce input base.
  • GameRenderer: implementa GLSurfaceView.Renderer, gestisce loop, logica e disegno.
  • Tetromino: rappresenta forme I, O, T, S, Z, L, J con colori diversi.
  • Board: griglia 10×20, collision detection e rimozione linee.

MainActivity.java

package com.example.tetrisgl;

import android.app.Activity;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;

public class MainActivity extends Activity {
    private TetrisView tetrisView;

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

        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                WindowManager.LayoutParams.FLAG_FULLSCREEN);

        tetrisView = new TetrisView(this);
        setContentView(tetrisView);
    }

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

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

TetrisView.java

package com.example.tetrisgl;

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

public class TetrisView extends GLSurfaceView {

    private final GameRenderer renderer;
    private float startX, startY;
    private static final int SWIPE_THRESHOLD = 50;

    public TetrisView(Context context) {
        super(context);
        setEGLContextClientVersion(2);
        renderer = new GameRenderer(context);
        setRenderer(renderer);
        setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = event.getX();
                startY = event.getY();
                return true;
            case MotionEvent.ACTION_UP:
                float dx = event.getX() - startX;
                float dy = event.getY() - startY;

                if (Math.abs(dx) < SWIPE_THRESHOLD && Math.abs(dy) < SWIPE_THRESHOLD) {
                    renderer.rotateCurrent(); // tap: ruota
                } else if (Math.abs(dx) > Math.abs(dy)) {
                    if (dx > 0) renderer.moveRight();
                    else renderer.moveLeft();
                } else {
                    if (dy > 0) renderer.softDrop();
                }
                return true;
        }
        return super.onTouchEvent(event);
    }
}

Tetromino.java

package com.example.tetrisgl;

import java.util.Random;

public class Tetromino {
    public enum Type { I, O, T, S, Z, L, J }

    public Type type;
    public int[][] shape; // matrice 4x4
    public float r, g, b, a;
    public int x, y; // posizione sulla board

    private static final Random random = new Random();

    public Tetromino(Type type) {
        this.type = type;
        this.shape = createShape(type);
        setColor(type);
        this.x = 3;
        this.y = 0;
    }

    public static Tetromino randomTetromino() {
        Type[] values = Type.values();
        return new Tetromino(values[random.nextInt(values.length)]);
    }

    private int[][] createShape(Type type) {
        // 4x4; 1 = blocco, 0 = vuoto
        switch (type) {
            case I:
                return new int[][]{
                        {0, 0, 0, 0},
                        {1, 1, 1, 1},
                        {0, 0, 0, 0},
                        {0, 0, 0, 0}
                };
            case O:
                return new int[][]{
                        {0, 1, 1, 0},
                        {0, 1, 1, 0},
                        {0, 0, 0, 0},
                        {0, 0, 0, 0}
                };
            case T:
                return new int[][]{
                        {0, 1, 0, 0},
                        {1, 1, 1, 0},
                        {0, 0, 0, 0},
                        {0, 0, 0, 0}
                };
            case S:
                return new int[][]{
                        {0, 1, 1, 0},
                        {1, 1, 0, 0},
                        {0, 0, 0, 0},
                        {0, 0, 0, 0}
                };
            case Z:
                return new int[][]{
                        {1, 1, 0, 0},
                        {0, 1, 1, 0},
                        {0, 0, 0, 0},
                        {0, 0, 0, 0}
                };
            case L:
                return new int[][]{
                        {0, 0, 1, 0},
                        {1, 1, 1, 0},
                        {0, 0, 0, 0},
                        {0, 0, 0, 0}
                };
            case J:
            default:
                return new int[][]{
                        {1, 0, 0, 0},
                        {1, 1, 1, 0},
                        {0, 0, 0, 0},
                        {0, 0, 0, 0}
                };
        }
    }

    private void setColor(Type type) {
        // I=ciano, O=giallo, T=viola, S=verde, Z=rosso, L=arancione, J=blu
        a = 1.0f;
        switch (type) {
            case I: r = 0.0f; g = 1.0f; b = 1.0f; break;      // ciano
            case O: r = 1.0f; g = 1.0f; b = 0.0f; break;      // giallo
            case T: r = 0.5f; g = 0.0f; b = 0.5f; break;      // viola
            case S: r = 0.0f; g = 1.0f; b = 0.0f; break;      // verde
            case Z: r = 1.0f; g = 0.0f; b = 0.0f; break;      // rosso
            case L: r = 1.0f; g = 0.5f; b = 0.0f; break;      // arancione
            case J: r = 0.0f; g = 0.0f; b = 1.0f; break;      // blu
        }
    }

    public void rotateClockwise() {
        int n = 4;
        int[][] res = new int[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                res[j][n - 1 - i] = shape[i][j];
            }
        }
        shape = res;
    }
}

Board.java

package com.example.tetrisgl;

public class Board {
    public static final int WIDTH = 10;
    public static final int HEIGHT = 20;

    // -1 = vuoto, altrimenti indice colore
    private int[][] cells = new int[HEIGHT][WIDTH];

    public Board() {
        clear();
    }

    public void clear() {
        for (int y = 0; y < HEIGHT; y++) {
            for (int x = 0; x < WIDTH; x++) {
                cells[y][x] = -1;
            }
        }
    }

    public boolean isInside(int x, int y) {
        return x >= 0 && x < WIDTH && y >= 0 && y < HEIGHT;
    }

    public boolean isFree(int x, int y) {
        return isInside(x, y) && cells[y][x] == -1;
    }

    public boolean canPlace(Tetromino t, int newX, int newY) {
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 4; j++) {
                if (t.shape[i][j] == 0) continue;
                int bx = newX + j;
                int by = newY + i;
                if (!isInside(bx, by) || cells[by][bx] != -1) {
                    return false;
                }
            }
        }
        return true;
    }

    public void lockTetromino(Tetromino t, int colorIndex) {
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 4; j++) {
                if (t.shape[i][j] == 0) continue;
                int bx = t.x + j;
                int by = t.y + i;
                if (isInside(bx, by)) {
                    cells[by][bx] = colorIndex;
                }
            }
        }
    }

    public int clearLines() {
        int lines = 0;
        for (int y = 0; y < HEIGHT; y++) {
            boolean full = true;
            for (int x = 0; x < WIDTH; x++) {
                if (cells[y][x] == -1) {
                    full = false;
                    break;
                }
            }
            if (full) {
                lines++;
                // sposta tutto verso il basso
                for (int yy = y; yy > 0; yy--) {
                    System.arraycopy(cells[yy - 1], 0, cells[yy], 0, WIDTH);
                }
                for (int x = 0; x < WIDTH; x++) {
                    cells[0][x] = -1;
                }
            }
        }
        return lines;
    }

    public int[][] getCells() {
        return cells;
    }
}

GameRenderer.java (loop, touch, shader GLSL)

package com.example.tetrisgl;

import android.content.Context;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.os.Handler;

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 {

    private final Context context;
    private final Board board = new Board();
    private Tetromino current;
    private int score = 0;
    private boolean gameOver = false;

    private Handler handler = new Handler();
    private long dropInterval = 500; // ms

    private int program;
    private int aPositionLocation;
    private int uColorLocation;

    private FloatBuffer vertexBuffer;

    private float aspect;

    private final Runnable gameLoop = new Runnable() {
        @Override
        public void run() {
            if (!gameOver) {
                updateGame();
                handler.postDelayed(this, dropInterval);
            }
        }
    };

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

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        GLES20.glClearColor(0f, 0f, 0f, 1f);
        String vertexShaderCode =
                "attribute vec2 aPosition;\n" +
                "varying vec2 vPos;\n" +
                "void main() {\n" +
                "  vPos = aPosition;\n" +
                "  gl_Position = vec4(aPosition, 0.0, 1.0);\n" +
                "}";
        String fragmentShaderCode =
                "precision mediump float;\n" +
                "uniform vec4 uColor;\n" +
                "void main() {\n" +
                "  gl_FragColor = uColor;\n" +
                "}";

        int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
        int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
        program = GLES20.glCreateProgram();
        GLES20.glAttachShader(program, vertexShader);
        GLES20.glAttachShader(program, fragmentShader);
        GLES20.glLinkProgram(program);

        float[] squareCoords = {
                -0.5f, 0.5f,
                -0.5f, -0.5f,
                0.5f, -0.5f,
                0.5f, 0.5f
        };

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

        GLES20.glUseProgram(program);
        aPositionLocation = GLES20.glGetAttribLocation(program, "aPosition");
        uColorLocation = GLES20.glGetUniformLocation(program, "uColor");

        handler.postDelayed(gameLoop, dropInterval);
    }

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

    @Override
    public void onDrawFrame(GL10 gl) {
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        GLES20.glUseProgram(program);

        GLES20.glEnableVertexAttribArray(aPositionLocation);
        GLES20.glVertexAttribPointer(aPositionLocation, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer);

        drawBoard();
        drawCurrent();

        GLES20.glDisableVertexAttribArray(aPositionLocation);
    }

    private void updateGame() {
        if (gameOver) return;
        if (board.canPlace(current, current.x, current.y + 1)) {
            current.y++;
        } else {
            int colorIndex = current.type.ordinal();
            board.lockTetromino(current, colorIndex);
            int lines = board.clearLines();
            score += lines * 100;
            current = Tetromino.randomTetromino();
            current.x = 3;
            current.y = 0;
            if (!board.canPlace(current, current.x, current.y)) {
                gameOver = true;
            }
        }
    }

    public void moveLeft() {
        if (!gameOver && board.canPlace(current, current.x - 1, current.y)) {
            current.x--; }
    }

    public void moveRight() {
        if (!gameOver && board.canPlace(current, current.x + 1, current.y)) {
            current.x++; }
    }

    public void softDrop() {
        if (!gameOver && board.canPlace(current, current.x, current.y + 1)) {
            current.y++; }
    }

    public void rotateCurrent() {
        if (gameOver) return;
        Tetromino clone = new Tetromino(current.type);
        clone.shape = new int[4][4];
        for (int i = 0; i < 4; i++) {
            System.arraycopy(current.shape[i], 0, clone.shape[i], 0, 4);
        }
        clone.x = current.x;
        clone.y = current.y;
        clone.rotateClockwise();
        if (board.canPlace(clone, clone.x, clone.y)) {
            current.rotateClockwise();
        }
    }

    private void drawBoard() {
        int[][] cells = board.getCells();
        for (int y = 0; y < Board.HEIGHT; y++) {
            for (int x = 0; x < Board.WIDTH; x++) {
                int c = cells[y][x];
                if (c != -1) {
                    float[] color = colorFromIndex(c);
                    GLES20.glUniform4fv(uColorLocation, 1, color, 0);
                    drawCell(x, y);
                }
            }
        }
    }

    private void drawCurrent() {
        if (current == null) return;
        float[] color = {current.r, current.g, current.b, current.a};
        GLES20.glUniform4fv(uColorLocation, 1, color, 0);
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 4; j++) {
                if (current.shape[i][j] == 0) continue;
                int bx = current.x + j;
                int by = current.y + i;
                drawCell(bx, by);
            }
        }
    }

    private float[] colorFromIndex(int index) {
        Tetromino.Type type = Tetromino.Type.values()[index];
        Tetromino temp = new Tetromino(type);
        return new float[]{temp.r, temp.g, temp.b, temp.a};
    }

    private void drawCell(int x, int y) {
        float cellWidth = 2.0f / Board.WIDTH;
        float cellHeight = 2.0f / Board.HEIGHT;

        float left = -1.0f + x * cellWidth;
        float right = left + cellWidth;
        float top = 1.0f - y * cellHeight;
        float bottom = top - cellHeight;

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

        vertexBuffer.position(0);
        vertexBuffer.put(coords);
        vertexBuffer.position(0);

        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 4);
    }

    private int loadShader(int type, String shaderCode) {
        int shader = GLES20.glCreateShader(type);
        GLES20.glShaderSource(shader, shaderCode);
        GLES20.glCompileShader(shader);
        return shader;
    }

    public int getScore() {
        return score;
    }

    public boolean isGameOver() {
        return gameOver;
    }
}

Shader GLSL (in linea)

I sorgenti dei due shader sono già inclusi in GameRenderer nelle stringhe vertexShaderCode e fragmentShaderCode, ma per chiarezza riportiamo qui il codice GLSL:

// Vertex shader
attribute vec2 aPosition;
varying vec2 vPos;
void main() {
    vPos = aPosition;
    gl_Position = vec4(aPosition, 0.0, 1.0);
}

// Fragment shader
precision mediump float;
uniform vec4 uColor;
void main() {
    gl_FragColor = uColor;
}

Mini app funzionante: loop, controlli, punteggio, game over

Il loop di gioco è gestito dal Handler in GameRenderer (metodo updateGame()), mentre i controlli touch (swipe sinistra/destra/basso e tap per ruotare) sono in TetrisView. Il punteggio viene aggiornato in base alle linee rimosse e lo stato di game over è esposto da isGameOver(); puoi mostrare punteggio e stato aggiungendo una TextView sopra la GLSurfaceView in un layout XML.

Conclusione e possibili miglioramenti

Questa base ti offre un Tetris OpenGL ES 2.0 minimale ma completo: grafica 2D accelerata, input touch e logica essenziale. Da qui puoi aggiungere anteprima del prossimo pezzo, salvataggio high score, effetti sonori, livelli di difficoltà e shader più avanzati (bordo, ombre, animazioni) mantenendo la stessa struttura.