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.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.
Il nostro sistema di particelle avrà alcuni parametri sintonizzabili:
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:
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 = [];
Ogni frame abbiamo bisogno di fare tre cose: creare nuove particelle, spostare particelle esistenti e disegnare le 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.
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; ;
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]); ;
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).
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:
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:
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:
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!
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: