Sviluppare Pong per Android con OpenGL ES 2.0 e Java
In questa guida completa vedremo come sviluppare una versione semplificata ma completa del classico gioco Pong per Android, utilizzando OpenGL ES 2.0 e Java. L’obiettivo è fornire un punto di partenza solido per sviluppatori Android che vogliono imparare le basi della grafica 2D con OpenGL ES, la struttura di un game loop e la gestione dell’input touch.
1. Introduzione: perché Pong è un ottimo progetto didattico
Pong, pubblicato originariamente nel 1972, è uno dei primi videogiochi arcade della storia. Il gameplay è estremamente semplice: due racchette che si muovono verticalmente ai bordi dello schermo e una palla che rimbalza tra loro. Proprio questa semplicità lo rende un progetto ideale per imparare:
- come strutturare un game loop su Android;
- come disegnare primitive 2D con OpenGL ES 2.0;
- come gestire input touch per controllare la racchetta del giocatore;
- come implementare una semplice IA per l’avversario;
- come organizzare il codice in classi responsabili (renderer, entità di gioco, utility).
Alla fine di questa guida avrai un prototipo giocabile di Pong e una base solida su cui costruire giochi 2D più complessi.
2. Cosa serve per iniziare
Per seguire questa guida ti servono i seguenti prerequisiti:
- Android Studio (versione recente, ad esempio Flamingo o successiva);
- SDK minimo: API 21 (Android 5.0) o superiore;
- Conoscenza di base di Java e dei concetti base di Android (Activity, ciclo di vita, viste).
Quando crei il progetto in Android Studio, scegli un Empty Activity e assicurati che il Minimum SDK sia almeno 21.
2.1 Dipendenze Gradle
OpenGL ES 2.0 è già integrato nell’SDK Android tramite la classe android.opengl.GLES20, quindi non servono librerie esterne. Verifica soltanto che nel tuo AndroidManifest.xml sia dichiarato il requisito di OpenGL ES 2.0:
```xml
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
```
Nel file build.gradle (Module: app) assicurati solo di avere il plugin Android classico e il supporto Java 8 o superiore:
```gradle
android {
compileSdkVersion 34
defaultConfig {
applicationId "com.example.pong"
minSdkVersion 21
targetSdkVersion 34
versionCode 1
versionName "1.0"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
```
3. Cos’è OpenGL ES su Android
OpenGL ES (Open Graphics Library for Embedded Systems) è una API grafica pensata per dispositivi mobili e embedded. Su Android, la versione 2.0 introduce il modello programmabile basato su shader (vertex e fragment shader), che consente un controllo fine sul rendering.
3.1 GLSurfaceView e Renderer
Per usare OpenGL ES in modo semplice su Android, si utilizza la coppia GLSurfaceView + GLSurfaceView.Renderer:
GLSurfaceViewè una vista specializzata che contiene una superficie OpenGL;Rendererè un’interfaccia che fornisce i callback per gestire il ciclo di vita del rendering:onSurfaceCreated,onSurfaceChanged,onDrawFrame.
3.2 Coordinate NDC e VBO
OpenGL ES 2.0 lavora in uno spazio di coordinate chiamato NDC (Normalized Device Coordinates), in cui:
- x va da -1.0 a +1.0 (sinistra → destra)
- y va da -1.0 a +1.0 (basso → alto)
Per disegnare oggetti usiamo array di vertici (float) memorizzati in FloatBuffer e, opzionalmente, in VBO (Vertex Buffer Object) sul lato GPU. In questa guida useremo principalmente FloatBuffer diretti, più facili da capire all’inizio.
3.3 Configurazione base di GLSurfaceView (Java)
Un esempio minimale di configurazione di un GLSurfaceView con un renderer in Java può essere il seguente:
```java
public class SimpleGLView extends GLSurfaceView {
public SimpleGLView(Context context) {
super(context);
// Indichiamo che vogliamo usare OpenGL ES 2.0
setEGLContextClientVersion(2);
// Impostiamo il renderer personalizzato
setRenderer(new SimpleRenderer());
// Render continuo (adatto per i giochi)
setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
}
private static class SimpleRenderer implements GLSurfaceView.Renderer {
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// Imposta il colore di sfondo (RGBA)
GLES20.glClearColor(0f, 0f, 0f, 1f);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// Definisce l'area di disegno
GLES20.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
// Pulisce il buffer colore
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
}
}
```
4. Architettura del progetto Pong
Organizzeremo il progetto in diverse classi per mantenere il codice leggibile e manutenibile:
- MainActivity.java: Activity principale, imposta la finestra a schermo intero e mostra la
GameView. - GameView.java: estende
GLSurfaceView, configura OpenGL ES 2.0, collega ilGameRenderere gestisce l’input touch del giocatore. - GameRenderer.java: implementa
GLSurfaceView.Renderer, contiene il game loop, aggiorna logica e disegna la scena. - Ball.java: rappresenta la palla (posizione, velocità, disegno, aggiornamento e rimbalzi).
- Paddle.java: rappresenta una racchetta (posizione, dimensioni, disegno, collisione con la palla).
- ShaderUtils.java: classe di utilità per compilare e collegare shader OpenGL ES 2.0.
Vediamo ora l’implementazione passo per passo di ciascuna classe.
5. Implementazione passo per passo
5.a GameView.java
GameView estende GLSurfaceView, imposta il renderer e gestisce il tocco del dito sullo schermo per muovere la racchetta del giocatore.
```java
package com.example.pong;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.view.MotionEvent;
public class GameView extends GLSurfaceView {
private final GameRenderer renderer;
public GameView(Context context) {
super(context);
// Usiamo OpenGL ES 2.0
setEGLContextClientVersion(2);
renderer = new GameRenderer(context);
setRenderer(renderer);
// Render continuo per avere un game loop costante
setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// Otteniamo la coordinata Y del tocco per muovere la racchetta del giocatore
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
// Passiamo il valore normalizzato al renderer
renderer.onPlayerTouch(y, getHeight());
return true;
}
return super.onTouchEvent(event);
}
}
```
5.b GameRenderer.java
GameRenderer implementa l’interfaccia GLSurfaceView.Renderer. Qui gestiamo:
- inizializzazione di OpenGL ES (colore di sfondo, shader, ecc.);
- creazione di
BallePaddle; - aggiornamento logico ad ogni frame (movimento, collisioni, punteggio);
- disegno degli oggetti.
```java
package com.example.pong;
import android.content.Context;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class GameRenderer implements GLSurfaceView.Renderer {
private final Context context;
// Entità di gioco
private Ball ball;
private Paddle playerPaddle;
private Paddle aiPaddle;
// Dimensioni dello schermo in NDC
private float aspectRatio = 1.0f;
// Punteggi
private int playerScore = 0;
private int aiScore = 0;
// Velocità massima dell'IA
private static final float AI_SPEED = 0.02f;
// Posizione desiderata della racchetta del giocatore (in NDC)
private float playerTargetY = 0f;
public GameRenderer(Context context) {
this.context = context;
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// Impostiamo il colore di sfondo (nero)
GLES20.glClearColor(0f, 0f, 0f, 1f);
// Abilitiamo il blending per eventuale trasparenza (non strettamente necessario qui)
GLES20.glEnable(GLES20.GL_BLEND);
GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
// Inizializziamo entità di gioco con coordinate NDC
// Racchette: un rettangolo stretto in orizzontale e alto in verticale
float paddleWidth = 0.03f;
float paddleHeight = 0.3f;
playerPaddle = new Paddle(-0.9f, 0f, paddleWidth, paddleHeight); // lato sinistro
aiPaddle = new Paddle(0.9f, 0f, paddleWidth, paddleHeight); // lato destro
// Palla al centro con velocità iniziale
ball = new Ball(0f, 0f, 0.04f, 0.02f, 0.03f);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// Aggiorniamo il viewport
GLES20.glViewport(0, 0, width, height);
// Calcoliamo l'aspect ratio (larghezza / altezza)
aspectRatio = (float) width / (float) height;
// Aggiorniamo le entità se necessario (in questo caso usiamo NDC, quindi non serve molto)
}
@Override
public void onDrawFrame(GL10 gl) {
// Puliamo lo schermo
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
// Aggiorniamo logica di gioco
updateGame();
// Disegniamo gli oggetti
ball.draw();
playerPaddle.draw();
aiPaddle.draw();
}
private void updateGame() {
// Aggiorniamo posizione racchetta giocatore verso il target
updatePlayerPaddle();
// Aggiorniamo IA
updateAI();
// Aggiorniamo palla
ball.update();
// Controlliamo collisioni con le racchette
if (playerPaddle.checkCollision(ball)) {
ball.bounceX();
}
if (aiPaddle.checkCollision(ball)) {
ball.bounceX();
}
// Controlliamo se la palla esce dallo schermo ai lati (punto)
if (ball.getX() < -1.1f) { // punto per l'IA
aiScore++;
resetBall(false);
} else if (ball.getX() > 1.1f) { // punto per il giocatore
playerScore++;
resetBall(true);
}
}
private void updatePlayerPaddle() {
// Muoviamo dolcemente la racchetta verso la posizione target
float dy = playerTargetY - playerPaddle.getCenterY();
float maxStep = 0.08f; // velocità massima della racchetta del giocatore
if (Math.abs(dy) > maxStep) {
dy = Math.signum(dy) * maxStep;
}
playerPaddle.moveBy(0f, dy);
}
private void updateAI() {
// IA molto semplice: muove la racchetta verso la palla con velocità limitata
float targetY = ball.getY();
float dy = targetY - aiPaddle.getCenterY();
if (Math.abs(dy) > AI_SPEED) {
dy = Math.signum(dy) * AI_SPEED;
}
aiPaddle.moveBy(0f, dy);
}
private void resetBall(boolean playerScored) {
// Rimettiamo la palla al centro e invertiamo la direzione orizzontale a seconda di chi ha segnato
float direction = playerScored ? -1f : 1f;
ball.reset(0f, 0f, direction);
}
/**
* Metodo chiamato da GameView quando il giocatore tocca lo schermo.
* @param touchY coordinata Y in pixel
* @param viewHeight altezza della vista in pixel
*/
public void onPlayerTouch(float touchY, int viewHeight) {
// Normalizziamo touchY (0 in alto, viewHeight in basso) in coordinate NDC (-1 in basso, +1 in alto)
float normalizedY = 1f - 2f * (touchY / (float) viewHeight);
playerTargetY = normalizedY;
}
public int getPlayerScore() {
return playerScore;
}
public int getAiScore() {
return aiScore;
}
}
```
5.c Ball.java
Ball gestisce posizione, velocità, logica di rimbalzo sui bordi e disegno di un quadrato che rappresenta la palla. Usiamo un semplice shader che colora il quadrato di bianco.
```java
package com.example.pong;
import android.opengl.GLES20;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
public class Ball {
private float x;
private float y;
private float radius;
private float vx;
private float vy;
private final FloatBuffer vertexBuffer;
private final int program;
// Attributi shader
private int positionHandle;
private int colorHandle;
private int offsetHandle;
// Coordinate di un quadrato centrato in (0,0)
private static final float[] VERTICES = {
-1f, -1f,
1f, -1f,
-1f, 1f,
1f, 1f
};
public Ball(float x, float y, float radius, float vx, float vy) {
this.x = x;
this.y = y;
this.radius = radius;
this.vx = vx;
this.vy = vy;
// Creiamo il buffer dei vertici (quadrato unitario, scalato nello shader)
ByteBuffer bb = ByteBuffer.allocateDirect(VERTICES.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(VERTICES);
vertexBuffer.position(0);
// Creiamo gli shader
String vertexShaderCode =
"attribute vec2 a_Position;\n" +
"uniform vec2 u_Offset;\n" +
"uniform float u_Radius;\n" +
"void main() {\n" +
" vec2 scaledPosition = a_Position * u_Radius;\n" +
" gl_Position = vec4(scaledPosition + u_Offset, 0.0, 1.0);\n" +
"}";
String fragmentShaderCode =
"precision mediump float;\n" +
"uniform vec4 u_Color;\n" +
"void main() {\n" +
" gl_FragColor = u_Color;\n" +
"}";
int vertexShader = ShaderUtils.compileVertexShader(vertexShaderCode);
int fragmentShader = ShaderUtils.compileFragmentShader(fragmentShaderCode);
program = ShaderUtils.linkProgram(vertexShader, fragmentShader);
}
public void draw() {
GLES20.glUseProgram(program);
// Otteniamo gli handle
positionHandle = GLES20.glGetAttribLocation(program, "a_Position");
colorHandle = GLES20.glGetUniformLocation(program, "u_Color");
offsetHandle = GLES20.glGetUniformLocation(program, "u_Offset");
int radiusHandle = GLES20.glGetUniformLocation(program, "u_Radius");
// Passiamo i dati dei vertici
GLES20.glEnableVertexAttribArray(positionHandle);
GLES20.glVertexAttribPointer(positionHandle, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer);
// Colore bianco
GLES20.glUniform4f(colorHandle, 1f, 1f, 1f, 1f);
// Offset (posizione della palla)
GLES20.glUniform2f(offsetHandle, x, y);
// Raggio
GLES20.glUniform1f(radiusHandle, radius);
// Disegniamo il quadrato come strip di triangoli
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GLES20.glDisableVertexAttribArray(positionHandle);
}
public void update() {
// Aggiorniamo la posizione
x += vx;
y += vy;
// Rimbalzo sui bordi superiore e inferiore dello schermo (NDC -1..1)
if (y + radius > 1f) {
y = 1f - radius;
vy = -vy;
} else if (y - radius < -1f) {
y = -1f + radius;
vy = -vy;
}
}
public void bounceX() {
vx = -vx;
}
public float getX() {
return x;
}
public float getY() {
return y;
}
public float getRadius() {
return radius;
}
public void reset(float startX, float startY, float direction) {
this.x = startX;
this.y = startY;
this.vx = Math.abs(vx) * direction; // mantiene la velocità ma cambia direzione
this.vy = Math.abs(vy) * (Math.random() > 0.5 ? 1f : -1f);
}
}
```
5.d Paddle.java
Paddle rappresenta una racchetta. Anche qui utilizziamo un semplice quadrato allungato e lo disegniamo con OpenGL ES 2.0. Implementiamo anche un semplice controllo di collisione AABB (Axis-Aligned Bounding Box) con la palla.
```java
package com.example.pong;
import android.opengl.GLES20;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
public class Paddle {
private float centerX;
private float centerY;
private float halfWidth;
private float halfHeight;
private final FloatBuffer vertexBuffer;
private final int program;
private int positionHandle;
private int colorHandle;
private int offsetHandle;
private int scaleHandle;
// Vertici di un quadrato unitario centrato in (0,0)
private static final float[] VERTICES = {
-1f, -1f,
1f, -1f,
-1f, 1f,
1f, 1f
};
public Paddle(float centerX, float centerY, float width, float height) {
this.centerX = centerX;
this.centerY = centerY;
this.halfWidth = width;
this.halfHeight = height;
ByteBuffer bb = ByteBuffer.allocateDirect(VERTICES.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(VERTICES);
vertexBuffer.position(0);
String vertexShaderCode =
"attribute vec2 a_Position;\n" +
"uniform vec2 u_Offset;\n" +
"uniform vec2 u_Scale;\n" +
"void main() {\n" +
" vec2 scaled = a_Position * u_Scale;\n" +
" gl_Position = vec4(scaled + u_Offset, 0.0, 1.0);\n" +
"}";
String fragmentShaderCode =
"precision mediump float;\n" +
"uniform vec4 u_Color;\n" +
"void main() {\n" +
" gl_FragColor = u_Color;\n" +
"}";
int vertexShader = ShaderUtils.compileVertexShader(vertexShaderCode);
int fragmentShader = ShaderUtils.compileFragmentShader(fragmentShaderCode);
program = ShaderUtils.linkProgram(vertexShader, fragmentShader);
}
public void draw() {
GLES20.glUseProgram(program);
positionHandle = GLES20.glGetAttribLocation(program, "a_Position");
offsetHandle = GLES20.glGetUniformLocation(program, "u_Offset");
scaleHandle = GLES20.glGetUniformLocation(program, "u_Scale");
colorHandle = GLES20.glGetUniformLocation(program, "u_Color");
GLES20.glEnableVertexAttribArray(positionHandle);
GLES20.glVertexAttribPointer(positionHandle, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer);
// Colore bianco
GLES20.glUniform4f(colorHandle, 1f, 1f, 1f, 1f);
// Offset (posizione della racchetta)
GLES20.glUniform2f(offsetHandle, centerX, centerY);
// Scala (mezze dimensioni)
GLES20.glUniform2f(scaleHandle, halfWidth, halfHeight);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GLES20.glDisableVertexAttribArray(positionHandle);
}
public boolean checkCollision(Ball ball) {
float ballX = ball.getX();
float ballY = ball.getY();
float r = ball.getRadius();
float left = centerX - halfWidth;
float right = centerX + halfWidth;
float bottom = centerY - halfHeight;
float top = centerY + halfHeight;
// Collisione AABB semplice
boolean collisionX = ballX + r > left && ballX - r < right;
boolean collisionY = ballY + r > bottom && ballY - r < top;
return collisionX && collisionY;
}
public void moveBy(float dx, float dy) {
centerX += dx;
centerY += dy;
// Manteniamo la racchetta nello schermo (limiti NDC)
if (centerY + halfHeight > 1f) {
centerY = 1f - halfHeight;
} else if (centerY - halfHeight < -1f) {
centerY = -1f + halfHeight;
}
}
public float getCenterY() {
return centerY;
}
}
```
5.e ShaderUtils.java
ShaderUtils racchiude la logica per compilare e collegare shader GLSL, restituendo un int che rappresenta il programma OpenGL ES 2.0.
```java
package com.example.pong;
import android.opengl.GLES20;
import android.util.Log;
public class ShaderUtils {
private static final String TAG = "ShaderUtils";
public static int compileVertexShader(String shaderCode) {
return compileShader(GLES20.GL_VERTEX_SHADER, shaderCode);
}
public static int compileFragmentShader(String shaderCode) {
return compileShader(GLES20.GL_FRAGMENT_SHADER, shaderCode);
}
private static int compileShader(int type, String shaderCode) {
final int shaderId = GLES20.glCreateShader(type);
if (shaderId == 0) {
Log.e(TAG, "Impossibile creare shader");
return 0;
}
GLES20.glShaderSource(shaderId, shaderCode);
GLES20.glCompileShader(shaderId);
final int[] compileStatus = new int[1];
GLES20.glGetShaderiv(shaderId, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
if (compileStatus[0] == 0) {
Log.e(TAG, "Errore compilazione shader: " + GLES20.glGetShaderInfoLog(shaderId));
GLES20.glDeleteShader(shaderId);
return 0;
}
return shaderId;
}
public static int linkProgram(int vertexShaderId, int fragmentShaderId) {
final int programId = GLES20.glCreateProgram();
if (programId == 0) {
Log.e(TAG, "Impossibile creare programma");
return 0;
}
GLES20.glAttachShader(programId, vertexShaderId);
GLES20.glAttachShader(programId, fragmentShaderId);
GLES20.glLinkProgram(programId);
final int[] linkStatus = new int[1];
GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] == 0) {
Log.e(TAG, "Errore link programma: " + GLES20.glGetProgramInfoLog(programId));
GLES20.glDeleteProgram(programId);
return 0;
}
return programId;
}
}
```
5.f MainActivity.java
MainActivity imposta l’app a schermo intero e mostra la GameView come contenuto principale.
```java
package com.example.pong;
import android.app.Activity;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;
public class MainActivity extends Activity {
private GameView gameView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Schermo intero senza title bar
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
gameView = new GameView(this);
setContentView(gameView);
}
@Override
protected void onPause() {
super.onPause();
gameView.onPause();
}
@Override
protected void onResume() {
super.onResume();
gameView.onResume();
}
}
```
6. Gestione del punteggio
Nel codice del GameRenderer abbiamo introdotto due variabili playerScore e aiScore che vengono aggiornate ogni volta che la palla esce dai bordi sinistro o destro dello schermo. Per visualizzare il punteggio ci sono due approcci principali:
- usare un overlay Canvas sopra la superficie OpenGL;
- usare una TextView sovrapposta tramite layout XML.
6.1 TextView sovrapposta (approccio consigliato)
Il modo più semplice e flessibile è creare un layout XML contenente un FrameLayout con dentro la GameView e una TextView sopra di essa. Ad esempio in activity_main.xml:
```xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.pong.GameView
android:id="@+id/game_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/score_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|center_horizontal"
android:textColor="#FFFFFF"
android:textSize="24sp"
android:text="0 : 0" />
</FrameLayout>
```
In MainActivity puoi quindi instanziare la GameView dal layout e aggiornare periodicamente la TextView (ad esempio usando un Handler o sfruttando un callback dal renderer). Qui un esempio semplice con Handler:
```java
public class MainActivity extends Activity {
private GameView gameView;
private TextView scoreText;
private Handler handler = new Handler(Looper.getMainLooper());
private final Runnable scoreUpdater = new Runnable() {
@Override
public void run() {
if (gameView != null) {
GameRenderer renderer = gameView.getRenderer();
if (renderer != null) {
int player = renderer.getPlayerScore();
int ai = renderer.getAiScore();
scoreText.setText(player + " : " + ai);
}
}
handler.postDelayed(this, 200); // aggiorna ogni 200 ms
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.activity_main);
gameView = findViewById(R.id.game_view);
scoreText = findViewById(R.id.score_text);
handler.post(scoreUpdater);
}
public GameRenderer getRenderer() {
return gameView != null ? gameView.getRenderer() : null;
}
}
```
Naturalmente dovrai aggiungere un metodo getRenderer() in GameView che restituisca l’istanza del renderer.
7. IA avversaria semplice
L’IA implementata nel metodo updateAI() di GameRenderer è volutamente semplice: la racchetta dell’avversario si limita a seguire la posizione verticale della palla, con una velocità massima per evitare movimenti istantanei e rendere il gioco più umano.
```java
private void updateAI() {
// Target verticale: la Y della palla
float targetY = ball.getY();
// Differenza tra target e posizione attuale
float dy = targetY - aiPaddle.getCenterY();
// Limitiamo la velocità
if (Math.abs(dy) > AI_SPEED) {
dy = Math.signum(dy) * AI_SPEED;
}
// Muoviamo la racchetta
aiPaddle.moveBy(0f, dy);
}
```
Per rendere il gioco più interessante puoi:
- aggiungere un fattore di errore (offset casuale rispetto alla posizione della palla);
- ridurre la velocità dell’IA quando il giocatore ha pochi punti di svantaggio;
- aumentare la velocità dell’IA con l’aumentare del punteggio del giocatore.
8. Ottimizzazioni e suggerimenti
Una volta che il prototipo di Pong è funzionante, puoi iniziare a ottimizzarlo per ottenere un framerate stabile (idealmente 60 FPS) e ridurre l’uso della memoria.
- Usa FloatBuffer diretti: come abbiamo fatto, crea i buffer con
ByteBuffer.allocateDirect(...)eorder(ByteOrder.nativeOrder()). Questo permette alla JVM di allocare memoria nativa, più efficiente per la GPU. - Evita allocazioni nel game loop: non creare nuovi oggetti dentro
onDrawFrame(),update()o altri metodi chiamati ad ogni frame. Riutilizza array e oggetti esistenti. - Batching e VBO: per giochi più complessi con molti oggetti, valuta l’uso di VBO (Vertex Buffer Object) e di tecniche di batching per ridurre le chiamate a
glDrawArrays. - Profilazione: usa gli strumenti di Android Studio (Profiler) per monitorare FPS, CPU e memoria, identificando colli di bottiglia.
- Delta time: per un movimento più fluido e indipendente dal frame rate, puoi usare un delta time (tempo trascorso tra un frame e l’altro) nei calcoli di update della fisica, invece di usare velocità “per frame”.
9. Conclusione e passi successivi
In questa guida abbiamo visto come creare, passo per passo, una versione base di Pong per Android utilizzando OpenGL ES 2.0 e Java. Hai imparato a:
- configurare un
GLSurfaceViewe unRenderer; - disegnare primitive 2D tramite shader e
FloatBuffer; - implementare un semplice game loop e la logica di gioco (movimento, rimbalzi, collisioni);
- gestire l’input touch per controllare la racchetta del giocatore;
- aggiungere una semplice IA avversaria e un sistema di punteggio.
Per continuare ad ampliare le tue competenze, puoi esplorare:
- Effetti sonori con
SoundPoolo altre librerie audio, per rimbalzi e punti segnati; - Fisica migliorata: angoli di rimbalzo dipendenti dal punto di impatto sulla racchetta, attrito, accelerazioni progressive della palla;
- Grafica avanzata: texture, interpolazioni, animazioni, sfondi dinamici;
- Multiplayer locale (sullo stesso dispositivo) o online (via rete), con sincronizzazione dello stato di gioco;
- migrazione a Kotlin e utilizzo di API più moderne mantenendo la stessa architettura.
Partendo da questo Pong, hai tutte le basi per costruire il tuo piccolo motore 2D personalizzato su Android con OpenGL ES.