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.