Home > Guide e tutorial per Android > Sviluppare un Gioco per Android – Lezione 9: Esplosione di Particelle

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

programmazione-android9 Nell’ultima lezione abbiamo visto come creare delle semplici animazioni Sprite e, quest’oggi, è giunta l’ora delle esplosioni di particelle. La domanda alla quale dobbiamo rispondere è la seguente: come creiamo delle esplosioni nel nostro gioco Android? Innanzitutto, cerchiamo di dare una definizione precisa di esplosione: un mucchio di particelle sparse per lo schermo (possono essere pixel, immagini o molto altro ancora) che provengono da più punti anche se noi, per semplicità, assumeremo che queste provengano da un unico punto.

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.

Particle1

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.

Explosion_diagram

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:

Screenshot from 2014-02-28 10:05:49