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.