Faceți-vă pop joc cu efecte particule și Quadtrees

Deci vrei vreo explozie, foc, gloanțe sau vrăji magice în jocul tău? Sistemele de particule fac mari efecte grafice simple pentru a-ți îmbogăți puțin jocul. Puteți să jucați mai mult, făcând particulele să interacționeze cu lumea voastră, săriți de mediul înconjurător și alți jucători. În acest tutorial vom implementa câteva efecte particulare simple și de aici vom trece la a face ca particulele să sări de pe glob.

De asemenea, vom optimiza lucrurile prin implementarea unei structuri de date numită quadtree. Quadtrees vă permit să verificați coliziunile mult mai repede decât ați putea fără un singur lucru și sunt ușor de implementat și de înțeles.

Notă: Deși acest tutorial este scris folosind HTML5 și JavaScript, ar trebui să puteți utiliza aceleași tehnici și concepte în aproape orice mediu de dezvoltare a jocului.

Pentru a vedea demo-urile din articol, asigurați-vă că citiți acest articol în Chrome, Firefox, IE 9 sau orice alt browser care acceptă HTML5 și Canvas.
Observați cum particulele își schimbă culoarea în timp ce cade și cum izbesc formele.

Ce este un sistem de particule?

Un sistem de particule este un mod simplu de a genera efecte cum ar fi focul, fumul și exploziile.

Creați a emițător de particule, și acest lucru lansează mici "particule" pe care le puteți afișa sub formă de pixeli, cutii sau bitmap-uri mici. Ele urmează fizica simplă Newtoniană și schimbă culoarea în timp ce se mișcă, rezultând efecte grafice dinamice, personalizabile.


Începutul unui sistem de particule

Sistemul nostru de particule va avea câțiva parametri de acord:

  • Câte particule scapă în fiecare secundă.
  • Cât timp o particulă poate "trăi".
  • Culorile pe care fiecare particulă le va trece prin.
  • Poziția și unghiul în care se vor dezvolta particulele.
  • Cât de repede vor porni particulele când vor cadea.
  • Câtă gravitate ar trebui să producă particule.

Dacă fiecare particulă a dat naștere exact la fel, am avea doar un flux de particule, nu un efect de particule. Deci, permiteți și variabilitatea configurabilă. Acest lucru ne oferă câțiva parametri suplimentari pentru sistemul nostru:

  • Cât de mult poate varia unghiul de lansare.
  • Cât de mult poate varia viteza lor inițială.
  • Cât de mult poate varia durata lor de viață.

Încheiem cu o clasă de sisteme de particule care începe astfel:

 Parametru Parametru Parametru Parametrii Parametrii Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri Parametri ([și noul Culoare (255, 255, 255, 1), noua Culoare (0, 0) 0, 0, 0)]), // Unghiul în care particula se va declanșa la (și cât de mult poate varia aceasta) unghiul: 0, unghiul Variație: Math.PI * 2, // Viteza de acțiune a particulei se va trage la (0, 30.8), // Un obiect pentru a testa coliziuni împotriva și factorul de amortizare a sarcinii // pentru colizorul de coliziuni: null, bounceDamper: 0.5; // Suprascrieți parametrii impliciți cu parametrii furnizați pentru (var p în paramuri) this.params [p] = params [p];  this.particles = []; 

Efectuarea fluxului de sistem

Fiecare cadru trebuie să facă trei lucruri: să creeze particule noi, să mutați particulele existente și să trageți particulele.

Crearea de particule

Crearea particulelor este destul de simplă. Dacă creăm 300 de particule pe secundă și a trecut 0,05 secunde de la ultimul cadru, noi creăm 15 particule pentru cadru (care în medie se ridică la 300 pe secundă).

Ar trebui să avem o bucla simplă care arată astfel:

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

Al nostru spawnParticle () funcția creează o nouă particulă bazată pe parametrii sistemului nostru:

 ParticleSystem.prototype.spawnParticle = funcție (offset) // Vrem să tragem particulele la un unghi aleatoriu și o viteză aleatorie // în parametrii dictați pentru acest sistem var angle = randVariation (this.params.angle, this. params.angleVariation); var viteză = randRange (this.params.minVelocity, this.params.maxVelocity); var life = randVariation (this.params.particleLife, this.params.particleLife * this.params.lifeVariation); // Viteza noastră inițială se va deplasa la viteza pe care am ales-o mai sus în direcția unghiului pe care am ales-o viteza var = nou punct () de la polar (unghi, viteză); // Dacă am creat fiecare particulă la "pos", atunci fiecare particulă // creată într-un cadru ar începe în același loc. // În schimb, acționăm ca și cum am crea particula continuu între // acest cadru și cadrul anterior, pornind-o la un anumit decalaj // de-a lungul căii sale. var pos = this.params.pos.clone () adăugați (velocity.times (offset)); // Construiește un nou obiect de particule din parametrii pe care i-am ales acest parametru.push (Particule noi (acest paragraf, pos, viteză, viață)); ;

Alegem viteza inițială dintr-un unghi și viteză aleatorii. Apoi folosim fromPolar () pentru a crea un vector de viteză carteziană din combinația unghi / viteză.

Trigonometria de bază dă fromPolar metodă:

 Point.prototype.fromPolar = funcție (ang, rad) this.x = Math.cos (ang) * rad; aceasta.y = Math.sin (ang) * rad; returnați acest lucru; ;

Dacă aveți nevoie să citiți ușor trigonometria, toată trigonometria pe care o folosim este derivată din Unitatea cercului.

Mișcarea particulelor

Miscarea particulelor urmeaza legile fundamentale newtoniene. Particulele au toate o viteză și o poziție. Viteza noastră este acționată de forța gravitației, iar poziția noastră se schimbă proporțional cu gravitatea. În cele din urmă, trebuie să ținem evidența vieții fiecărei particule, altfel particulele nu ar muri niciodată, am ajunge la prea multe și sistemul s-ar opri. Toate aceste acțiuni au loc proporțional cu timpul dintre cadre.

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

Desenarea particulelor

În cele din urmă trebuie să tragem particulele noastre. Modul în care implementați acest lucru în jocul dvs. va varia foarte mult pe baza platformei și platformei și cât de avansat doriți ca redarea să fie. Acest lucru poate fi la fel de simplu ca plasarea unui singur pixel colorat, mutarea unei perechi de triunghiuri pentru fiecare particulă, trasată de un shader complex de GPU.

În cazul nostru, vom profita de API Canvas pentru a desena un mic dreptunghi pentru particulă.

 Particle.prototype.draw = funcția (ctx, frameTime) // Nu este nevoie să desenați particula dacă nu este în viață. dacă (this.isDead ()) returnează; // Vrem sa calatorim prin gradientul de culori, pe masura ce varsta particulei var varPercent = 1.0 - this.life / this.maxLife; var culoarea = this.params.colors.getColor (lifePercent); // Configurați culorile ctx.globalAlpha = color.a; ctx.fillStyle = color.toCanvasColor (); // Completați dreptunghiul la poziția particulei ctx.fillRect (this.pos.x - 1, this.pos.y - 1, 3, 3); ;

Interpolarea culorilor depinde de faptul dacă platforma pe care o utilizați furnizează o clasă de culori (sau un format de reprezentare), indiferent dacă furnizează un interpolator pentru dvs. și cum doriți să abordați întreaga problemă. Am scris o clasă de gradienți mici care permite interpolarea ușoară între mai multe culori și o clasă de culoare mică, care oferă funcționalitatea de a interpola între două culori.

 Color.prototype.interpolate = funcție (procent, altul) retur nou culoare (this.r + (other.r - this.r) * procente, this.g + (other.g - this.g) *% .b + (alte.b - this.b) * procente, this.a + (other.a - this.a) * procente); ; Gradient.prototype.getColor = funcție (procente) // Poziția culoare în virgulă mobilă în array var colorF = procente * (this.colors.length - 1); // Rotiți în jos; aceasta este culoarea specificată în array // sub culoarea noastră curentă var color1 = parseInt (colorF); //A rotunji; aceasta este culoarea specificată în matricea // deasupra culorii noastre actuale var color2 = parseInt (colorF + 1); // Interpolate între cele două cele mai apropiate culori (folosind metoda de mai sus) returnează acest. Culori [color1] .interpolate ((colorF - color1) / (color2 - color1), this.colors [color2]); ;

Iată sistemul nostru de particule în acțiune!

Punturile de particule

După cum puteți vedea în demo-ul de mai sus, acum avem câteva efecte particulare de bază. Ele nu au nicio interacțiune cu mediul din jurul lor. Pentru ca aceste efecte să facă parte din lumea jocurilor noastre, le vom face să se sară de pe pereții din jurul lor.

Pentru a începe, sistemul de particule va lua acum Collider ca parametru. Va fi sarcina colocviatorului de a spune unei particule dacă s-a prăbușit în orice. Etapa() metoda unei particule arată acum:

 Particle.prototype.step = function (frameTime) // Salveaza ultima pozitie var lastPos = this.pos.clone (); // mutați acest.velocity.add (this.params.gravity.times (frameTime)); this.pos.add (this.velocity.times (frameTime)); // Poate această particulă să sară? if (this.params.collider) // Verificați dacă am lovit ceva var intersect = this.params.collider.getIntersection (new Line (lastPos, this.pos)); if (intersect! = null) // Dacă da, ne reinițializăm poziția și ne actualizăm viteza // pentru a reflecta coliziunea this.pos = lastPos; this.velocity = intersect.seg.reflect (this.velocity) .times (this.params.bounceDamper);  this.life - = frameTime; ;

Acum, de fiecare dată când particula se mișcă, îi întrebăm pe ciocan dacă calea sa de mișcare sa "ciocnit" prin intermediul getIntersection () metodă. Dacă este așa, ne reinițializăm poziția (astfel încât să nu se afle în interiorul intersecției) și să reflecte viteza.

O implementare de bază "collider" ar putea arăta astfel:

 // Creează o colecție de segmente de linie care reprezintă funcția globală de joc Collider (lines) this.lines = lines;  // Returnează orice segment de linie intersectat de "path", altfel Collider.prototype.getIntersection = funcția (calea) pentru (var i = 0; i < this.lines.length; i++)  var intersection = this.lines[i].getIntersection(path); if (intersection) return intersection;  return null; ;

Observați o problemă? Fiecare particulă trebuie să sune collider.getIntersection () și apoi fiecare getIntersection apelul trebuie să verifice împotriva oricărui "zid" din lume. Dacă aveți 300 de particule (un fel de număr scăzut) și 200 de pereți în lumea voastră (nu nerezonabile), efectuați 60 000 de teste de intersecție a liniei! Acest lucru vă poate împiedica jocul, mai ales cu mai multe particule (sau cu lumi mai complexe).


Detectarea mai rapidă a coliziunii cu quadtrees

Problema cu colizorul simplu este că verifică fiecare perete pentru fiecare particulă. Dacă particulele noastre se află în cadranul din partea dreaptă superioară a ecranului, nu ar trebui să pierdem timpul verificând dacă s-au prăbușit în pereți care se află doar în partea de jos sau din stânga ecranului. Deci, în mod ideal, vrem să eliminăm orice verificări pentru intersecții în afara cadranului de sus-dreapta:


Pur și simplu verificăm coliziuni între punctul albastru și liniile roșii.

Acesta este doar un sfert din cecuri! Acum să mergem și mai departe: dacă particula se află în cadranul stânga sus al cvadrantului din dreapta-sus al ecranului, ar trebui să verificăm doar acei pereți din același cvadrant:

Quadtrees vă permit să faceți exact acest lucru! Mai degrabă decât să testeze împotriva toate pereți, vă împărțiți pereții în cadrane și subcadrante pe care le ocupă, deci trebuie doar să verificați câteva cadrane. Puteți trece cu ușurință de la 200 de cecuri pe particulă la doar 5 sau 6.

Pașii pentru a crea un quadtree sunt după cum urmează:

  1. Începeți cu un dreptunghi care umple întregul ecran.
  2. Luați dreptunghiul curent, numărați câte "pereți" intră în el.
  3. Dacă aveți mai mult de trei linii (puteți alege un alt număr), împărțiți dreptunghiul în patru cadrane egale. Repetați Pasul 2 cu fiecare cvadrant.
  4. După repetarea Pașilor 2 și 3, veți termina cu un "copac" de dreptunghiuri, cu nici unul dintre cele mai mici dreptunghiuri care conțin mai mult de trei linii (sau orice ați ales).

Construirea unui quadtree. Numerele reprezintă numărul de linii din cadrul cadranului, roșu fiind prea mare și trebuie subdivizat.

Pentru a construi quadtree noastre luăm un set de "pereți" (segmente de linie) ca parametru, și dacă prea multe sunt cuprinse în dreptunghiul nostru, vom subdiviza în dreptunghiuri mai mici, iar procesul se repetă.

 QuadTree.prototype.addSegments = funcția (segs) pentru (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 = funcția () var w2 = această.recv./ 2, h2 = această.recv.h / 2, x = această.recv.x, y = această.recv.y; this.quads.push (noul QuadTree (x, y, w2, h2)); this.quads.push (noul QuadTree (x + w2, y, w2, h2)); this.quads.push (noul QuadTree (x + w2, y + h2, w2, h2)); this.quads.push (noul QuadTree (x, y + h2, w2, h2)); pentru (var i = 0; i < this.quads.length; i++)  this.quads[i].addSegments(this.segs);  this.segs = []; ;

Puteți vedea întreaga clasă QuadTree aici:

 / ** * @constructor * / funcția QuadTree (x, y, w, h) this.thresh = 4; this.segs = []; this.quads = []; acest lucru este corect = nou Rect2D (x, y, w, h);  QuadTree.prototype.addSegments = funcția (segs) pentru (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 = funcția (seg) if (! This.rect.overlapsWithLine (seg)) returnează null; pentru (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();   ;

Testarea pentru intersecția cu un segment de linie este efectuată într-un mod similar. Pentru fiecare dreptunghi facem urmatoarele:

  1. Începeți cu cel mai mare dreptunghi din quadtree.
  2. Verificați dacă segmentul de linie se intersectează sau se află în interiorul dreptunghiului curent. Dacă nu, nu vă deranjați să faceți mai multe teste pe această cale.
  3. Dacă segmentul de linie intră în dreptul dreptunghiului curent sau îl intersectează, verificați dacă dreptunghiul actual are dreptunghiuri copil. În caz contrar, reveniți la pasul 2, dar folosiți fiecare dintre dreptunghiurile copilului.
  4. Dacă dreptunghiul curent nu are dreptunghiuri pentru copii, este vorba de a nodul frunzelor (adică, are doar segmente de linie ca și copii), testați segmentul de linie țintă împotriva acelor segmente de linie. Dacă unul este o intersecție, întoarceți intersecția. Am terminat!

Căutarea unui Quadtree. Începem la cel mai mare dreptunghi și căutăm pe cele mai mici și mai mici, până când testează în cele din urmă segmentele individuale de linie. Cu quadtree, vom efectua numai patru teste dreptunghiulare și două teste de linie, în loc de a testa toate cele 21 de segmente de linie. Diferența devine mai dramatică cu seturi de date mai mari.
 QuadTree.prototype.getIntersection = funcția (seg) if (! This.rect.overlapsWithLine (seg)) returnează null; pentru (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; ;

Odată ce trecem a quadtree obiect pentru sistemul nostru de particule ca "collider", obținem căutări rapide. Consultați demo-ul interactiv de mai jos - utilizați mouse-ul pentru a vedea care segmente de linie vor fi testate de quadtree!


Plasați cursorul pe un (sub-) cvadrant pentru a vedea ce segmente de linie conțin.

Hrana pentru minte

Sistemul de particule și quadtree prezentate în acest articol sunt sisteme de învățământ rudimentar. Unele alte idei pe care ar trebui să le luați în considerare atunci când le implementați:

  • S-ar putea să doriți să țineți obiecte pe lângă segmente de linie în quadtree. Cum l-ați extinde pentru a include cercurile? pătrate?
  • S-ar putea să doriți o modalitate de a recupera obiecte individuale (pentru a le anunța că au fost lovite de o particulă), în timp ce în continuare se regăsesc segmente reflectabile.
  • Ecuațiile fizicii suferă de discrepanțe pe care ecuațiile lui Euler le construiesc de-a lungul timpului cu rate ale cadrelor instabile. Deși acest lucru nu va avea în general importanță pentru un sistem de particule, de ce să nu citiți mai multe ecuații avansate de mișcare? (Uitați-vă la acest tutorial, de exemplu.)
  • Există numeroase moduri în care puteți memora lista de particule în memorie. O matrice este cea mai simplă, dar nu poate fi cea mai bună alegere, având în vedere că particulele sunt adesea scoase din sistem și adesea inserate noi. O listă legată se poate potrivi mai bine, dar are o localitate proastă de cache. Cea mai bună reprezentare pentru particule poate depinde de cadrul sau limba pe care o utilizați.
postări asemănatoare
  • Utilizați quadtrees pentru a detecta coliziuni probabile în spațiul 2D