Sviluppare un Gioco per Android – Lezione 9: Esplosione di Particelle Android Blog Italia.

L’effetto che vogliamo ottenere è simile a quello dei fuochi d’artificio dove il razzo esplode generando attorno quelle che sembrano essere piccole stelle scintillanti (il materiale in combustione). Quello che faremo sarà creare delle particelle dando ad ognuna una forza diversa e mettendole in un punto d’origine. È il caso di ricordare che la forza è una grandezza vettoriale, avente intensità, verso e direzione. Detto questo, vediamo come creare la particella e, successivamente, l’esplosione.
La particella
La particella altro non è che un piccolo rettangolo (può assumere qualsiasi forma, noi utilizzeremo un rettangolo) con delle proprietà. La nostra particella dovrà avere uno stato che indica se è viva o morta, una posizione, la velocità e la direzione. In particolare, la particella sarà viva se il suo colore non è nero e il suo tempo di vita non è stato raggiunto.

La particella e le due componenti della sua velocità
La posizione, in un sistema di coordinate 2D, sarà indicata dalle coordinate x e y, mentre la velocità, essendo un vettore, avrà una componente verticale (vy) e una orizzontale (vx). Essa avrà anche un’età che inizialmente sarà 0, ma verrà incrementata ad ogni aggiornamento di gioco fino al raggiungimento del tempo di vita della particella (lifetime). Vediamo tutte queste proprietà (oltre che quelle inerenti il colore) nell’implementazione della classe Particle.java qui di seguito:
package it.androidblog.lesson9;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
public class Particle {
public static final int STATE_ALIVE = 0; // la particella è viva
public static final int STATE_DEAD = 1; // la particella è morta
public static final int DEFAULT_LIFETIME = 200; // lifetime di default
public static final int MAX_DIMENSION = 5; // massima altezza o larghezza
public static final int MAX_SPEED = 10; // velocità massima
private int state; // particella viva o morta
private float widht; // larghezza della particella
private float height; // altezza della particella
private float x, y; // coordinate
private double xv, yv; // velocità orizzontale e verticale
private int age; // età corrente della particella
private int lifetime; // la particella è morta quando raggiunge questo valore
private int color; // colore della particella
private Paint paint; // uso interno per permettere l'istanzazione
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public float getWidht() {
return widht;
}
public void setWidht(float widht) {
this.widht = widht;
}
public float getHeight() {
return height;
}
public void setHeight(float height) {
this.height = height;
}
public float getX() {
return x;
}
public void setX(float x) {
this.x = x;
}
public float getY() {
return y;
}
public void setY(float y) {
this.y = y;
}
public double getXv() {
return xv;
}
public void setXv(double xv) {
this.xv = xv;
}
public double getYv() {
return yv;
}
public void setYv(double yv) {
this.yv = yv;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getLifetime() {
return lifetime;
}
public void setLifetime(int lifetime) {
this.lifetime = lifetime;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
// metodi d'aiuto -------------------------
public boolean isAlive() {
return this.state == STATE_ALIVE;
}
public boolean isDead() {
return this.state == STATE_DEAD;
}
public Particle(int x, int y) {
this.x = x;
this.y = y;
this.state = Particle.STATE_ALIVE;
this.widht = rndInt(1, MAX_DIMENSION);
this.height = this.widht;
// this.height = rnd(1, MAX_DIMENSION);
this.lifetime = DEFAULT_LIFETIME;
this.age = 0;
this.xv = (rndDbl(0, MAX_SPEED * 2) - MAX_SPEED);
this.yv = (rndDbl(0, MAX_SPEED * 2) - MAX_SPEED);
// stabilizza la velocità diagonale
if (xv * xv + yv * yv > MAX_SPEED * MAX_SPEED) {
xv *= 0.7;
yv *= 0.7;
}
this.color = Color.argb(255, rndInt(0, 255), rndInt(0, 255), rndInt(0, 255));
this.paint = new Paint(this.color);
}
public void reset(float x, float y) {
this.state = Particle.STATE_ALIVE;
this.x = x;
this.y = y;
this.age = 0;
}
// ritorna un interno compreso tra min e max
static int rndInt(int min, int max) {
return (int) (min + Math.random() * (max - min + 1));
}
static double rndDbl(double min, double max) {
return min + (max - min) * Math.random();
}
public void update() {
if (this.state != STATE_DEAD) {
this.x += this.xv;
this.y += this.yv;
// estraiamo l'alpha
int a = this.color >>> 24;
a -= 2; // decrementiamone il valore
if (a <= 0) { // se raggiunge la trasparenza la particella è morta
this.state = STATE_DEAD;
} else {
this.color = (this.color & 0x00ffffff) + (a << 24); // settiamo nuovo alpha
this.paint.setAlpha(a);
this.age++; // incrementiamo l'età della particella
// this.widht *= 1.05;
// this.height *= 1.05;
}
if (this.age >= this.lifetime) { // se raggiunge la sua lifetime è morta
this.state = STATE_DEAD;
}
}
}
public void update(Rect container) {
// aggiornamento con collisioni
if (this.isAlive()) {
if (this.x <= container.left || this.x >= container.right - this.widht) {
this.xv *= -1;
}
if (this.y <= container.top || this.y >= container.bottom - this.height) {
this.yv *= -1;
}
}
update();
}
public void draw(Canvas canvas) {
// paint.setARGB(255, 128, 255, 50);
paint.setColor(this.color);
canvas.drawRect(this.x, this.y, this.x + this.widht, this.y + this.height, paint);
// canvas.drawCircle(x, y, widht, paint);
}
}
Come potete vedere, tenere sotto controllo la creazione di una particella è molto semplice grazie alle sue proprietà. Tuttavia, abbiamo bisogno di rendere casuali le dimensioni e i colori delle particelle (poiché un’esplosione ne crea diverse) e, per questo, abbiamo due metodi di supporto appositi che restituiscono numeri casuali.
Un controllo sulle due componenti della velocità è d’obbligo, affinché la grandezza risultante non sia uguale a quella massima, quindi è necessario stabilizzare vx e vy. Infine, l’ultima cosa che impostiamo è il colore, sempre in maniera random. A questo punto, passiamo a dare un’occhiata al metodo update() della particella.
Esso è piuttosto semplice. Ad ogni aggiornamento, settiamo la posizione della particella in funzione della sua velocità, dopodiché decrementiamo la componente alpha del suo colore. Se essa arriva a zero (la particella diventa trasparente) o l’età ha raggiunto la sua lifetime, la particella muore.
Per quanto riguarda i colori, possiamo utilizzare anche i metodi Android, ma la conoscenza di RGB/ARGB e degli operatori bitwise è molto più veloce come potete vedere voi stessi.
Anche il metodo draw() è abbastanza semplice da capire. Come avrete notato, nel codice ci sono alcune istruzioni sotto forma di commento, il loro utilizzo è intuitivo, provate a modificarle ed utilizzare per ottenere effetti diversi con le vostre esplosioni. Passiamo, ora alla classe Explosion.java.
L’esplosione
L’esplosione non è altro che centinaia di particelle che provengono dallo stesso punto aventi stessa velocità ma direzioni diverse. L’immagine qui di seguito rappresenta i primi 4 aggiornamenti di una semplice esplosione.

Quali sono le proprietà dell’esplosione? Senza dubbio essa avrà un numero di particelle e un array di queste ultime. Naturalmente, l’esplosione sarà valida se esiste almeno una particella viva. L’aggiornamento è molto semplice in quanto si limita all’iterazione del metodo update() su ogni particella. Idem per il metodo draw().
Anche il costruttore non è difficile da capire ed è proprio qui che viene istanziato l’array di particelle. Vediamo il codice:
package it.androidblog.lesson9;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.Log;
public class Explosion {
private static final String TAG = Explosion.class.getSimpleName();
public static final int STATE_ALIVE = 0; // almeno una particella è viva
public static final int STATE_DEAD = 1; // tutte le particelle sono morte
private Particle[] particles; // particelle nell'esplosione
private int x, y; // l'origine dell'esplosione
private float gravity; // gravità dell'esplosione
private float wind; // velocità del vento orizzontale
private int size; // numero delle particelle
private int state; // se è ancora attiva o no
public Explosion(int particleNr, int x, int y) {
Log.d(TAG, "Explosion created at " + x + "," + y);
this.state = STATE_ALIVE;
this.particles = new Particle[particleNr];
for (int i = 0; i < this.particles.length; i++) {
Particle p = new Particle(x, y);
this.particles[i] = p;
}
this.size = particleNr;
}
public Particle[] getParticles() {
return particles;
}
public void setParticles(Particle[] particles) {
this.particles = particles;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public float getGravity() {
return gravity;
}
public void setGravity(float gravity) {
this.gravity = gravity;
}
public float getWind() {
return wind;
}
public void setWind(float wind) {
this.wind = wind;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
// metodi d'aiuto -------------------------
public boolean isAlive() {
return this.state == STATE_ALIVE;
}
public boolean isDead() {
return this.state == STATE_DEAD;
}
public void update() {
if (this.state != STATE_DEAD) {
boolean isDead = true;
for (int i = 0; i < this.particles.length; i++) {
if (this.particles[i].isAlive()) {
this.particles[i].update();
isDead = false;
}
}
if (isDead)
this.state = STATE_DEAD;
}
}
public void update(Rect container) {
if (this.state != STATE_DEAD) {
boolean isDead = true;
for (int i = 0; i < this.particles.length; i++) {
if (this.particles[i].isAlive()) {
this.particles[i].update(container);
// this.particles[i].update();
isDead = false;
}
}
if (isDead)
this.state = STATE_DEAD;
}
}
public void draw(Canvas canvas) {
for(int i = 0; i < this.particles.length; i++) {
if (this.particles[i].isAlive()) {
this.particles[i].draw(canvas);
}
}
}
}
L’esplosione viene generata in onTouchEvent (ricordate MainGamePanel?) e, con tutti i metodi a disposizione nelle due classi sopra riportate, è possibile gestire le esplosioni come si ritene più opportuno. In onTouchEvent controlliamo che l’esplosione sia nulla o che lo stato sia STATE_DEAD e, in quel caso, creiamo una nuova esplosione nell’esatto punto del nostro tocco con il numero di particelle impostate in EXPLOSION_SIZE.
Nel codice completo, che potete trovare qui, non manca il rilevamento delle collisioni grazie all’aggiunta di un bordo per la parete del nostro display. Per generare l’esplosione, come soprascritto, basterà cliccare sul display. Ecco come dovrebbe apparire il risultato del vostro lavoro:

Sviluppare un Gioco per Android – Lezione 9: Esplosione di Particelle Android Blog Italia.


