Crea il tuo gioco pop con effetti particellari e Quadtrees

Quindi vuoi esplosioni, fuoco, proiettili o incantesimi nel tuo gioco? I sistemi di particelle rendono grandi e semplici effetti grafici per ravvivare un po 'il tuo gioco. Puoi stupire ancora di più il giocatore facendo interagire particelle con il tuo mondo, rimbalzando fuori dall'ambiente e dagli altri giocatori. In questo tutorial implementeremo alcuni semplici effetti particellari e da qui passeremo a far rimbalzare le particelle del mondo che le circonda.

Ottimizzeremo anche le cose implementando una struttura dati chiamata quadtree. Quadtrees ti permettono di verificare le collisioni molto più rapidamente di quanto potresti senza, e sono semplici da implementare e capire.

Nota: Sebbene questo tutorial sia scritto usando HTML5 e JavaScript, dovresti essere in grado di utilizzare le stesse tecniche e concetti in quasi tutti gli ambienti di sviluppo di giochi.

Per vedere le demo in-article, assicurati di leggere questo articolo in Chrome, Firefox, IE 9 o qualsiasi altro browser che supporta HTML5 e Canvas.
Nota come le particelle cambiano colore mentre cadono e come rimbalzano sulle forme.

Cos'è un sistema di particelle?

Un sistema di particelle è un modo semplice per generare effetti come fuoco, fumo ed esplosioni.

Tu crei a emettitore di particelle, e questo lancia piccole "particelle" che è possibile visualizzare come pixel, scatole o piccole bitmap. Seguono la semplice fisica newtoniana e cambiano colore mentre si muovono, con effetti grafici dinamici e personalizzabili.


L'inizio di un sistema di particelle

Il nostro sistema di particelle avrà alcuni parametri sintonizzabili:

  • Quante particelle sputa ogni secondo.
  • Per quanto tempo una particella può "vivere".
  • I colori che ogni particella attraverserà.
  • La posizione e l'angolo di cui verranno generate le particelle.
  • Quanto velocemente le particelle andranno quando si generano.
  • Quanta gravità dovrebbe avere effetto sulle particelle.

Se ogni particella generasse esattamente la stessa cosa, avremmo solo un flusso di particelle, non un effetto particellare. Quindi permettiamo anche la variabilità configurabile. Questo ci dà alcuni altri parametri per il nostro sistema:

  • Quanto può variare il loro angolo di lancio.
  • Quanto la loro velocità iniziale può variare.
  • Quanto la loro vita può variare.

Finiamo con una classe di sistema di particelle che inizia così:

 function ParticleSystem (params) // Parametri predefiniti this.params = // Dove particelle spawn da pos: new Point (0, 0), // Quante particelle generano ogni secondo particellaPerSecond: 100, // Quanto dura ogni particella (e quanto può variare) particleLife: 0.5, lifeVariation: 0.52, // Il gradiente di colori che la particella percorrerà attraverso i colori: new Gradient ([new Color (255, 255, 255, 1), new Color (0, 0, 0, 0)]), // L'angolo che la particella sparerà a (e quanto può variare) angolo: 0, angleVariation: Math.PI * 2, // L'intervallo di velocity che la particella sparerà a minVelocity: 20, maxVelocity: 50, // Il vettore di gravità applicato a ciascuna gravità di particella: new Point (0, 30.8), // Un oggetto da verificare per le collisioni contro e fattore di smorzamento di rimbalzo // per detto collisore delle collisioni: null, bounceDamper: 0.5; // Sovrascrive i nostri parametri di default con i parametri forniti per (var p in params) this.params [p] = params [p];  this.particles = []; 

Rendere il flusso del sistema

Ogni frame abbiamo bisogno di fare tre cose: creare nuove particelle, spostare particelle esistenti e disegnare le particelle.

Creazione di particelle

Creare particelle è piuttosto semplice. Se stiamo creando 300 particelle al secondo e sono passati 0,05 secondi dall'ultimo fotogramma, creiamo 15 particelle per il fotogramma (che è in media di 300 al secondo).

Dovremmo avere un ciclo semplice simile a questo:

 var newParticlesThisFrame = this.params.particlesPerSecond * frameTime; per (var i = 0; i < newParticlesThisFrame; i++)  this.spawnParticle((1.0 + i) / newParticlesThisFrame * frameTime); 

Nostro spawnParticle () la funzione crea una nuova particella basata sui parametri del nostro sistema:

 ParticleSystem.prototype.spawnParticle = function (offset) // Vogliamo sparare la particella su un angolo casuale e una velocità casuale // all'interno dei parametri dettati per questo sistema var angle = randVariation (this.params.angle, this. params.angleVariation); var speed = randRange (this.params.minVelocity, this.params.maxVelocity); var life = randVariation (this.params.particleLife, this.params.particleLife * this.params.lifeVariation); // La nostra velocità iniziale si muoverà alla velocità che abbiamo scelto sopra nella // direzione dell'angolo che abbiamo scelto var velocity = new Point (). FromPolar (angle, speed); // Se creiamo ogni singola particella in "pos", allora ogni particle // creata all'interno di un frame inizierà nello stesso punto. // Invece, agiamo come se avessimo creato la particella continuamente tra // questo frame e il frame precedente, avviandolo a un certo offset // lungo il suo percorso. var pos = this.params.pos.clone (). add (velocity.times (offset)); // Contromettere un nuovo oggetto particella dai parametri che abbiamo scelto this.particles.push (new Particle (this.params, pos, velocity, life)); ;

Scegliamo la nostra velocità iniziale da un angolo e una velocità casuali. Quindi usiamo il fromPolar () metodo per creare un vettore di velocità cartesiano dalla combinazione angolo / velocità.

La trigonometria di base produce il fromPolar metodo:

 Point.prototype.fromPolar = function (ang, rad) this.x = Math.cos (ang) * rad; this.y = Math.sin (ang) * rad; restituiscilo; ;

Se hai bisogno di rispolverare un po 'la trigonometria, tutta la trigonometria che stiamo usando deriva dal Circle Circle.

Movimento delle particelle

Il movimento delle particelle segue le leggi newtoniane di base. Le particelle hanno tutte una velocità e una posizione. La nostra velocità è influenzata dalla forza di gravità e la nostra posizione varia proporzionalmente alla gravità. Infine, dobbiamo tenere traccia della vita di ogni particella, altrimenti le particelle non morirebbero mai, finiremmo per averne troppe e il sistema si arresterebbe. Tutte queste azioni si verificano in proporzione al tempo tra i frame.

 Particle.prototype.step = function (frameTime) this.velocity.add (this.params.gravity.times (frameTime)); this.pos.add (this.velocity.times (frameTime)); this.life - = frameTime; ;

Disegno di particelle

Alla fine dobbiamo disegnare le nostre particelle. Il modo in cui implementate questo gioco nel vostro gioco varia notevolmente da una piattaforma all'altra e quanto avanzato volete che il rendering sia. Questo può essere semplice come posizionare un singolo pixel colorato, per spostare una coppia di triangoli per ogni particella, disegnata da uno shader GPU complesso.

Nel nostro caso, trarremo vantaggio dall'API Canvas per disegnare un piccolo rettangolo per la particella.

 Particle.prototype.draw = function (ctx, frameTime) // Non è necessario disegnare la particella se è fuori dalla vita. se (this.isDead ()) restituisce; // Vogliamo viaggiare attraverso il nostro gradiente di colori quando la particella invecchia var lifePercent = 1.0 - this.life / this.maxLife; var color = this.params.colors.getColor (lifePercent); // Imposta i colori ctx.globalAlpha = color.a; ctx.fillStyle = color.toCanvasColor (); // Riempi il rettangolo nella posizione della particella ctx.fillRect (this.pos.x - 1, this.pos.y - 1, 3, 3); ;

L'interpolazione dei colori dipende dal fatto che la piattaforma che si sta utilizzando fornisca una classe di colori (o un formato di rappresentazione), sia che fornisca un interpolatore per te, sia come si desidera affrontare l'intero problema. Ho scritto una piccola classe di gradiente che consente una facile interpolazione tra più colori e una piccola classe di colori che fornisce la funzionalità per interpolare tra due colori qualsiasi.

 Color.prototype.interpolate = function (percent, other) return new Color (this.r + (other.r - this.r) * percento, this.g + (other.g - this.g) * percento, this .b + (other.b - this.b) * percento, this.a + (other.a - this.a) * percento); ; Gradient.prototype.getColor = function (percent) // Posizione del virgola mobile all'interno dell'array var colorF = percent * (this.colors.length - 1); // Arrotondare; questo è il colore specificato nell'array // sotto il nostro colore attuale var color1 = parseInt (colorF); //Arrotondare; questo è il colore specificato nell'array // sopra il nostro colore corrente var color2 = parseInt (colorF + 1); // Interpola tra i due colori più vicini (usando il metodo sopra) restituisce this.colors [color1] .interpolate ((colorF - color1) / (color2 - color1), this.colors [color2]); ;

Ecco il nostro sistema di particelle in azione!

Bouncing di particelle

Come puoi vedere nella demo qui sopra, ora abbiamo alcuni effetti particellari di base. Non hanno alcuna interazione con l'ambiente che li circonda, però. Per rendere questi effetti parte del nostro mondo di gioco, li faremo rimbalzare sui muri intorno a loro.

Per iniziare, il sistema di particelle ora prenderà un Collider come parametro Sarà compito del collettore dire a una particella se si è schiantato contro qualcosa. Il passo() il metodo di una particella ora si presenta così:

 Particle.prototype.step = function (frameTime) // Salva la nostra ultima posizione var lastPos = this.pos.clone (); // Sposta this.velocity.add (this.params.gravity.times (frameTime)); this.pos.add (this.velocity.times (frameTime)); // Può questa particella rimbalzare? if (this.params.collider) // Controlla se colpiamo qualcosa var intersect = this.params.collider.getIntersection (new Line (lastPos, this.pos)); if (intersect! = null) // Se è così, ripristiniamo la nostra posizione e aggiorniamo la nostra velocità // per riflettere la collisione this.pos = lastPos; this.velocity = intersect.seg.reflect (this.velocity) .times (this.params.bounceDamper);  this.life - = frameTime; ;

Ora ogni volta che la particella si muove, chiediamo al collisore se il suo percorso di movimento si è "scontrato" attraverso il getIntersection () metodo. Se è così, ripristiniamo la sua posizione (in modo che non sia all'interno di qualsiasi cosa intersecata) e rifletta la velocità.

Un'implementazione di base del "collisore" potrebbe assomigliare a questo:

 // Prende una raccolta di segmenti di linea che rappresentano la funzione del mondo di gioco Collider (lines) this.lines = lines;  // Restituisce qualsiasi segmento di linea intersecato da "percorso", altrimenti null Collider.prototype.getIntersection = function (path) for (var i = 0; i < this.lines.length; i++)  var intersection = this.lines[i].getIntersection(path); if (intersection) return intersection;  return null; ;

Noti un problema? Ogni particella deve chiamare collider.getIntersection () e poi ogni getIntersection la chiamata deve essere controllata contro ogni "muro" nel mondo. Se hai 300 particelle (una specie di basso numero) e 200 muri nel tuo mondo (non è neanche irragionevole), stai eseguendo 60.000 test di intersezione di linea! Questo potrebbe rallentare il tuo gioco, specialmente con più particelle (o mondi più complessi).


Rilevamento di collisioni più rapido con Quadtrees

Il problema con il nostro collisore semplice è che controlla ogni parete per ogni particella. Se la nostra particella si trova nel quadrante in alto a destra dello schermo, non dovremmo perdere tempo a controllare se si è schiantato contro muri che si trovano solo nella parte inferiore o sinistra dello schermo. Quindi idealmente vogliamo tagliare qualsiasi controllo per le intersezioni al di fuori del quadrante in alto a destra:


Controlliamo solo le collisioni tra il punto blu e le linee rosse.

Questo è solo un quarto dei controlli! Ora andiamo ancora oltre: se la particella si trova nel quadrante in alto a sinistra del quadrante in alto a destra dello schermo, dovremmo solo controllare quei muri nello stesso quadrante:

Quadtrees ti permettono di fare esattamente questo! Piuttosto che provare contro tutti muri, dividi i muri nei quadranti e nei sub-quadranti che occupano, quindi devi solo controllare alcuni quadranti. Puoi facilmente passare da 200 assegni per particella a soli 5 o 6.

I passaggi per creare un quadrilatero sono i seguenti:

  1. Inizia con un rettangolo che riempie l'intero schermo.
  2. Prendi il rettangolo corrente, conta quanti "muri" cadono all'interno.
  3. Se hai più di tre linee (puoi scegliere un numero diverso), dividi il rettangolo in quattro quadranti uguali. Ripeti il ​​passaggio 2 con ciascun quadrante.
  4. Dopo aver ripetuto i passaggi 2 e 3, si finisce con un "albero" di rettangoli, con nessuno dei rettangoli più piccoli contenenti più di tre linee (o qualsiasi cosa tu abbia scelto).

Costruire un quadrifoglio. I numeri rappresentano il numero di linee all'interno del quadrante, il rosso è troppo alto e deve essere suddiviso.

Per costruire il nostro quadruplo prendiamo un set di "muri" (segmenti di linea) come parametro, e se troppi sono contenuti nel nostro rettangolo, suddividiamo in rettangoli più piccoli, e il processo si ripete.

 QuadTree.prototype.addSegments = function (segs) for (var i = 0; i < segs.length; i++)  if (this.rect.overlapsWithLine(segs[i]))  this.segs.push(segs[i]);   if (this.segs.length > 3) this.subdivide (); ; QuadTree.prototype.subdivide = function () var w2 = this.rect.w / 2, h2 = this.rect.h / 2, x = this.rect.x, y = this.rect.y; this.quads.push (nuovo QuadTree (x, y, w2, h2)); this.quads.push (nuovo QuadTree (x + w2, y, w2, h2)); this.quads.push (nuovo QuadTree (x + w2, y + h2, w2, h2)); this.quads.push (nuovo QuadTree (x, y + h2, w2, h2)); per (var i = 0; i < this.quads.length; i++)  this.quads[i].addSegments(this.segs);  this.segs = []; ;

Puoi vedere la classe QuadTree completa qui:

 / ** * @constructor * / function QuadTree (x, y, w, h) this.thresh = 4; this.segs = []; this.quads = []; this.rect = new Rect2D (x, y, w, h);  QuadTree.prototype.addSegments = function (segs) for (var i = 0; i < segs.length; i++)  if (this.rect.overlapsWithLine(segs[i]))  this.segs.push(segs[i]);   if (this.segs.length > this.thresh) this.subdivide (); ; QuadTree.prototype.getIntersection = function (seg) if (! This.rect.overlapsWithLine (seg)) return null; per (var i = 0; i < this.segs.length; i++)  var s = this.segs[i]; var inter = s.getIntersection(seg); if (inter)  var o = ; return s;   for (var i = 0; i < this.quads.length; i++)  var inter = this.quads[i].getIntersection(seg); if (inter) return inter;  return null; ; QuadTree.prototype.subdivide = function()  var w2 = this.rect.w / 2, h2 = this.rect.h / 2, x = this.rect.x, y = this.rect.y; this.quads.push(new QuadTree(x, y, w2, h2)); this.quads.push(new QuadTree(x + w2, y, w2, h2)); this.quads.push(new QuadTree(x + w2, y + h2, w2, h2)); this.quads.push(new QuadTree(x, y + h2, w2, h2)); for (var i = 0; i < this.quads.length; i++)  this.quads[i].addSegments(this.segs);  this.segs = []; ; QuadTree.prototype.display = function(ctx, mx, my, ibOnly)  var inBox = this.rect.containsPoint(new Point(mx, my)); ctx.strokeStyle = inBox ? '#FF44CC' : '#000000'; if (inBox || !ibOnly)  ctx.strokeRect(this.rect.x, this.rect.y, this.rect.w, this.rect.h); for (var i = 0; i < this.quads.length; i++)  this.quads[i].display(ctx, mx, my, ibOnly);   if (inBox)  ctx.strokeStyle = '#FF0000'; for (var i = 0 ; i < this.segs.length; i++)  var s = this.segs[i]; ctx.beginPath(); ctx.moveTo(s.a.x, s.a.y); ctx.lineTo(s.b.x, s.b.y); ctx.stroke();   ;

Il test per l'intersezione con un segmento di linea viene eseguito in modo simile. Per ogni rettangolo facciamo quanto segue:

  1. Inizia con il rettangolo più grande nel quadrilatero.
  2. Controlla se il segmento di linea si interseca o si trova all'interno del rettangolo corrente. In caso contrario, non preoccuparti di eseguire ulteriori test su questo percorso.
  3. Se il segmento di linea rientra nel rettangolo corrente o lo interseca, controlla se il rettangolo corrente ha rettangoli figlio. In caso affermativo, tornare al passaggio 2, ma utilizzando ciascuno dei rettangoli secondari.
  4. Se il rettangolo corrente non ha rettangoli secondari ma è a nodo fogliare (cioè, ha solo segmenti di linea come figli), testare il segmento di linea di destinazione contro quei segmenti di linea. Se uno è un'intersezione, restituisci l'intersezione. Abbiamo chiuso!

Alla ricerca di un Quadtree. Iniziamo dal rettangolo più grande e cerchiamo sempre più piccoli, finché non testiamo i singoli segmenti di linea. Con il quadrifoglio, eseguiamo solo quattro test rettangolari e due test linea, invece di test su tutti i 21 segmenti di linea. La differenza cresce solo più drammatica con set di dati più grandi.
 QuadTree.prototype.getIntersection = function (seg) if (! This.rect.overlapsWithLine (seg)) return null; per (var i = 0; i < this.segs.length; i++)  var s = this.segs[i]; var inter = s.getIntersection(seg); if (inter)  var o = ; return s;   for (var i = 0; i < this.quads.length; i++)  var inter = this.quads[i].getIntersection(seg); if (inter) return inter;  return null; ;

Una volta che passiamo a quadtree oggetto del nostro sistema particellare come "collisore", otteniamo ricerche fulminee. Dai un'occhiata alla demo interattiva qui sotto: usa il mouse per vedere quali segmenti di linea dovrebbero essere testati con il quadrifoglio!


Passa il mouse su un (sotto) quadrante per vedere quali segmenti di linea contiene.

Cibo per la mente

Il sistema di particelle e il quadrifoglio presentati in questo articolo sono sistemi di insegnamento rudimentali. Altre idee che potresti prendere in considerazione quando implementate queste te stesso:

  • Potresti voler tenere oggetti oltre ai segmenti di linea nel quadrilatero. Come lo espanderesti per includere le cerchie? Piazze?
  • Si potrebbe desiderare un modo per recuperare singoli oggetti (per avvisarli che sono stati colpiti da una particella), mentre si stanno ancora recuperando segmenti riflettenti.
  • Le equazioni fisiche soffrono di discrepanze nel fatto che le equazioni di Eulero si accumulano nel tempo con frame rate instabili. Sebbene questo non sia generalmente un problema per un sistema di particelle, perché non leggere su equazioni di moto più avanzate? (Dai un'occhiata a questo tutorial, per esempio.)
  • Esistono molti modi per memorizzare l'elenco di particelle in memoria. Una matrice è la più semplice, ma potrebbe non essere la scelta migliore dato che le particelle vengono spesso rimosse dal sistema e quelle nuove spesso inserite. Una lista collegata può adattarsi meglio ma ha una scarsa localizzazione della cache. La migliore rappresentazione per le particelle può dipendere dal framework o dalla lingua che stai utilizzando.
Post correlati
  • Usa Quadtrees per rilevare le probabili collisioni nello spazio 2D