Lag ditt spillpop med partikkeleffekter og quadtrees

Så du vil ha eksplosjoner, brann, kuler eller magiske tryllekunstnere i spillet ditt? Partikkelsystemer gjør store, enkle grafiske effekter for å spice opp spillet litt. Du kan wow spilleren enda mer ved å få partikler til å samhandle med din verden, spretter av miljøet og andre spillere. I denne opplæringen vil vi gjennomføre noen enkle partikkeleffekter, og herfra vil vi fortsette å gjøre partiklene sprette av verden rundt dem.

Vi vil også optimalisere ting ved å implementere en datastruktur kalt en quadtree. Quadtrees lar deg kontrollere kollisjoner mye raskere enn du kunne uten en, og de er enkle å implementere og forstå.

Merk: Selv om denne opplæringen er skrevet ved hjelp av HTML5 og JavaScript, bør du kunne bruke de samme teknikkene og konseptene i nesten hvilket som helst spillutviklingsmiljø.

For å se demoer i artikkelen må du lese denne artikkelen i Chrome, Firefox, IE 9 eller en annen nettleser som støtter HTML5 og lærred.
Legg merke til hvordan partiklene bytter farge når de faller, og hvordan de spretter av figurene.

Hva er et partikkelsystem?

Et partikkelsystem er en enkel måte å generere effekter som brann, røyk og eksplosjoner.

Du oppretter en partikkel emitter, og dette lanserer små "partikler" som du kan vise som piksler, bokser eller små bitmaps. De følger simple newtonske fysikk og bytter farge når de beveger seg, noe som resulterer i dynamiske, tilpassbare, grafiske effekter.


Starten av et partikkelsystem

Vårt partikkelsystem vil ha noen justerbare parametere:

  • Hvor mange partikler spretter det hvert sekund.
  • Hvor lenge en partikkel kan "leve".
  • Fargene hver partikkel vil overgå gjennom.
  • Stillingen og vinkelen partiklene vil gyte fra.
  • Hvor fort partiklene vil gå når de gyter.
  • Hvor mye tyngdekraft skal påvirke partikler.

Hvis hver partikkel oppsto akkurat det samme, ville vi bare ha en strøm av partikler, ikke en partikkel-effekt. Så la oss også tillate konfigurerbar variabilitet. Dette gir oss noen flere parametere for vårt system:

  • Hvor mye startvinkelen kan variere.
  • Hvor mye deres innledende hastighet kan variere.
  • Hvor mye levetid kan variere.

Vi ender med en partikkelsystemklasse som starter slik:

 funksjon ParticleSystem (params) // Standardparametre this.params = // Hvor partikler hekker fra pos: nytt punkt (0, 0), // Hvor mange partikler henter hver sekund partiklerPerSekund: 100, // Hvor lenge hver partikkel lever (og hvor mye dette kan variere) particleLife: 0.5, lifeVariation: 0.52, // Fargegradienten partikkelen vil bevege seg gjennom farger: Ny Gradient ([ny farge (255, 255, 255, 1), ny farge (0, 0, 0, 0)]), / / ​​Vinkelen partikkelen vil brenne av ved (og hvor mye dette kan variere) vinkel: 0, vinkelVariasjon: Math.PI * 2, // Hastighetsområdet partikkelen vil brenne av ved minVelocity: 20, maxVelocity: 50, // Gravitasjonsvektoren på hver partikkel tyngdekraft: nytt punkt (0, 30.8), // Et objekt for å teste for kollisjoner mot og sprette dempningsfaktor // for kollisjonene collider: null, bounceDamper: 0,5; // Overstyr standardparametrene med medfølgende parametere for (var p i params) this.params [p] = params [p];  this.particles = []; 

Gjøre System Flow

Hver ramme vi trenger å gjøre tre ting: lage nye partikler, flytte eksisterende partikler og tegne partiklene.

Opprette partikler

Å lage partikler er ganske enkelt. Hvis vi lager 300 partikler per sekund, og det har vært 0,05 sekunder siden den siste rammen, oppretter vi 15 partikler for rammen (som er gjennomsnittlig til 300 per sekund).

Vi burde ha en enkel sløyfe som ser slik ut:

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

Våre spawnParticle () funksjonen skaper en ny partikkel basert på systemets parametere:

 ParticleSystem.prototype.spawnParticle = function (offset) // Vi vil brenne partikkelen i en tilfeldig vinkel og en tilfeldig hastighet // innenfor parametrene diktert for dette systemet var vinkel = randVariasjon (this.params.angle, dette. params.angleVariation); var hastighet = randRange (this.params.minVelocity, this.params.maxVelocity); var livet = randVariasjon (this.params.particleLife, this.params.particleLife * this.params.lifeVariation); // Vår første hastighet vil bevege seg med den hastigheten vi valgte over i // retningen av vinkelen vi valgte var hastighet = nytt punkt (). Fra polar (vinkel, hastighet); // Hvis vi opprettet hver enkelt partikkel på "pos", vil hver partikkel // som er opprettet i en ramme, starte på samme sted. // I stedet virker vi som om vi laget partikkelen kontinuerlig mellom // denne rammen og den forrige rammen, ved å starte den med en viss offset // langs sin bane. var pos = this.params.pos.clone (). add (hastighet.times (offset)); // Konstruer en ny partikkelobjekt fra parametrene vi valgte this.particles.push (new Particle (this.params, pos, speed, life)); ;

Vi velger vår innledende hastighet fra en tilfeldig vinkel og hastighet. Vi bruker deretter fromPolar () metode for å lage en kartesisk hastighetsvektor fra vinkel / hastighetskombinasjonen.

Grunnleggende trigonometri gir fromPolar metode:

 Point.prototype.fromPolar = funksjon (ang, rad) this.x = Math.cos (ang) * rad; this.y = Math.sin (ang) * rad; returnere dette; ;

Hvis du trenger å pusse opp på trigonometri litt, er alle trigonometriene vi bruker, avledet fra Unit Circle.

Partikkelbevegelse

Partikkelbevegelse følger grunnleggende newtonske lover. Partikler har alle en hastighet og posisjon. Vår hastighet opptrer av tyngdekraften, og vår posisjon endres proporsjonalt til tyngdekraften. Til slutt må vi holde styr på hver partikkels liv, ellers vil partikler aldri dø, vi vil ende opp med å ha for mange, og systemet slår seg til slutt. Alle disse handlingene skjer proporsjonalt til tiden mellom rammene.

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

Tegning Partikler

Til slutt må vi tegne våre partikler. Hvordan du implementerer dette i spillet ditt vil variere sterkt fra plattform til plattform, og hvor avansert du vil at gjengivelsen skal være. Dette kan være like enkelt som å plassere en enkelt farget piksel, for å flytte et par trekanter for hver partikkel, tegnet av en kompleks GPU-skygger.

I vårt tilfelle vil vi dra nytte av Canvas API for å tegne et lite rektangel for partikkelen.

 Particle.prototype.draw = funksjon (ctx, frameTime) // Ikke nødvendig å tegne partikkelen hvis den er ute av livet. hvis (this.isDead ()) returnerer; // Vi ønsker å reise gjennom vår gradient av farger som partikkelalderen var lifePercent = 1.0 - this.life / this.maxLife; var farge = this.params.colors.getColor (lifePercent); // Sett opp fargene ctx.globalAlpha = color.a; ctx.fillStyle = color.toCanvasColor (); // Fyll ut rektangelet ved partikkelens posisjon ctx.fillRect (this.pos.x - 1, this.pos.y - 1, 3, 3); ;

Fargeinterpolering avhenger av om plattformen du bruker, leverer en fargeklasse (eller representasjonsformat), om det gir en interpolator for deg, og hvordan du vil nærme seg hele problemet. Jeg skrev en liten gradientklasse som muliggjør enkel interpolering mellom flere farger og en liten fargeklasse som gir funksjonaliteten til å interpolere mellom to farger.

 Color.prototype.interpolate = funksjon (prosent, annet) return new Color (this.r + (andre.r - this.r) * prosent, this.g + (other.g - this.g) * prosent, dette .b + (other.b - this.b) * prosent, dette.a + (andre.a - dette.a) * prosent); ; Gradient.prototype.getColor = funksjon (prosent) // Flytpunktsfargeplassering i array var colorF = percent * (this.colors.length - 1); //Runde ned; dette er den angitte fargen i gruppen // under vår nåværende farge var color1 = parseInt (colorF); // runde opp; dette er den angitte fargen i gruppen // over vår nåværende farge var color2 = parseInt (colorF + 1); // Interpolere mellom de to nærmeste farger (bruk ovenfor metode) returnere dette. Farger [color1] .interpolate ((colorF - color1) / (color2 - color1), this.colors [color2]); ;

Her er vårt partikkelsystem i bruk!

Bouncing Particles

Som du kan se i demonstrasjonen ovenfor, har vi nå noen grunnleggende partikkeleffekter. De mangler imidlertid samspill med omgivelsene rundt dem. For å gjøre disse effektene en del av vår spillverden, skal vi få dem til å hoppe av veggene rundt dem.

For å starte, vil partikkelsystemet nå ta en Collider som en parameter. Det vil være colliderens jobb å fortelle en partikkel om den har krasjet inn i noe. De skritt() Metoden til en partikkel ser nå slik ut:

 Particle.prototype.step = function (frameTime) // Lagre vår siste posisjon var lastPos = this.pos.clone (); // Flytt this.velocity.add (this.params.gravity.times (frameTime)); this.pos.add (this.velocity.times (frameTime)); // Kan denne partikkelen sprette? hvis (this.params.collider) // Sjekk om vi treffer noe var krysser = this.params.collider.getIntersection (new Line (lastPos, this.pos)); hvis (krysse! = null) // Hvis så, nullstiller vi vår posisjon, og oppdaterer hastigheten // for å gjenspeile kollisjonen this.pos = lastPos; this.velocity = intersect.seg.reflect (this.velocity) .x (this.params.bounceDamper);  this.life - = frameTime; ;

Nå hver gang partikkelen beveger seg, spør vi collideren om bevegelsesveien har "kollidert" via getIntersection () metode. I så fall tilbakestiller vi sin posisjon (slik at den ikke er innenfor det som skjæres), og reflekterer hastigheten.

En grunnleggende "collider" -implementering kan se slik ut:

 // Tar en samling av linjesegmenter som representerer spillverdenens funksjon Collider (linjer) this.lines = lines;  // Returnerer ethvert linjesegment som er krysset av "banen", ellers null Collider.prototype.getIntersection = funksjon (sti) for (var i = 0; i < this.lines.length; i++)  var intersection = this.lines[i].getIntersection(path); if (intersection) return intersection;  return null; ;

Legg merke til et problem? Hver partikkel må ringe collider.getIntersection () og så hver getIntersection samtale må sjekke mot hver "veggen" i verden. Hvis du har 300 partikler (slags lavt antall) og 200 vegger i din verden (ikke urimelig heller), utfører du 60 000 linjeskåringstest! Dette kan slipe spillet ditt til slutt, spesielt med flere partikler (eller mer komplekse verdener).


Hurtigere kollisjonsdeteksjon med quadtrees

Problemet med vår enkle collider er at den sjekker hver vegg for hver partikkel. Hvis vår partikkel er i skjermens øverste høyre kant, bør vi ikke kaste bort tid på å sjekke om den krasjet i vegger som bare er nederst eller til venstre på skjermen. Så ideelt vil vi kutte ut eventuelle kontroller for kryssninger utenfor den øverste høyre kvadranten:


Vi ser bare etter sammenstøt mellom den blå prikken og de røde linjene.

Det er bare en fjerdedel av kontrollene! La oss gå enda lenger: Hvis partikkelen er i øvre venstre kvadrant i skjermens øvre høyre kvadrant, må vi bare sjekke disse veggene i samme kvadrant:

Quadtrees lar deg gjøre akkurat dette! Snarere enn å teste mot alle vegger, deler du vegger i kvadranter og delkvadranter de okkuperer, så du trenger bare å sjekke noen kvadranter. Du kan enkelt gå fra 200 sjekker per partikkel til bare 5 eller 6.

Fremgangsmåten for å lage en quadtree er som følger:

  1. Start med et rektangel som fyller hele skjermen.
  2. Ta gjeldende rektangel, telle hvor mange "vegger" faller inn i den.
  3. Hvis du har mer enn tre linjer (du kan velge et annet tall), del rektangelet i fire like kvadranter. Gjenta trinn 2 med hver kvadrant.
  4. Etter å ha gjentatt trinn 2 og 3, ende du med et "tre" av rektangler, med ingen av de minste rektanglene som inneholder mer enn tre linjer (eller hva du valgte).

Bygg en quadtree. Tallene representerer antall linjer i kvadranten, rødt er for høyt og trenger å dele inn.

For å bygge vår quadtree tar vi et sett med "vegger" (linjesegmenter) som en parameter, og hvis for mange er inneholdt i rektangelet, deler vi inn i mindre rektangler, og prosessen gjentas.

 QuadTree.prototype.addSegments = funksjon (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 (ny QuadTree (x, y, w2, h2)); this.quads.push (ny QuadTree (x + w2, y, w2, h2)); this.quads.push (ny 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 = []; ;

Du kan se hele QuadTree-klassen her:

 / ** * @constructor * / funksjon QuadTree (x, y, w, h) this.thresh = 4; this.segs = []; this.quads = []; this.rect = new Rect2D (x, y, w, h);  QuadTree.prototype.addSegments = funksjon (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 = funksjon (seg) hvis (! This.rect.overlapsWithLine (seg)) returnere null; for (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();   ;

Testing for kryss mot et linjesegment utføres på lignende måte. For hvert rektangel gjør vi følgende:

  1. Start med det største rektangelet i quadtree.
  2. Kontroller om linjesegmentet skjærer eller er inne i det nåværende rektangel. Hvis det ikke gjør det, ikke bry deg om å gjøre noe mer testing nedover denne banen.
  3. Hvis linjesegmentet faller innenfor det gjeldende rektangelet eller krysser det, kontroller om det nåværende rektangel har noen barnrektangler. Hvis det gjør det, gå tilbake til trinn 2, men bruk hver av barnets rektangler.
  4. Hvis det nåværende rektangel ikke har barnetrektangler, men det er a bladknutepunkt (det vil si at den bare har linjesegmenter som barn), test mållinjesegmentet mot disse linjesegmentene. Hvis en er et skjæringspunkt, returner du krysset. Vi er ferdige!

Søker på en Quadtree. Vi starter ved det største rektangelet og søker mindre og mindre, til endelig testing av enkeltlinjesegmenter. Med quadtree utfører vi bare fire rektangeltester og to linjetester, i stedet for å teste mot alle 21 linjesegmenter. Forskjellen vokser bare mer dramatisk med større datasett.
 QuadTree.prototype.getIntersection = funksjon (seg) hvis (! This.rect.overlapsWithLine (seg)) returnere null; for (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; ;

Når vi passerer en firertre protester mot vårt partikkelsystem som "collider" vi får lynrask oppslag. Ta en titt på den interaktive demoen nedenfor - bruk musen til å se hvilke linjesegmenter quadtree må teste mot!


Flytt over en (sub) kvadrant for å se hvilke linjesegmenter den inneholder.

Noe å tenke på

Partikkelsystemet og quadtree presentert i denne artikkelen er rudimentære læringssystemer. Noen andre ideer du kanskje vil vurdere når du implementerer disse selv:

  • Du vil kanskje holde objekter i tillegg til linjestykker i quadtree. Hvordan ville du utvide den til å inkludere sirkler? Squares?
  • Du vil kanskje ha en måte å hente individuelle objekter på (for å varsle dem om at de har blitt rammet av en partikkel), mens du fremdeles henter reflekterbare segmenter.
  • Fysikk-ligningene lider av uoverensstemmelser at Euler-ligninger bygger opp over tid med ustabile rammebetingelser. Selv om dette ikke generelt betyr noe for et partikkelsystem, hvorfor ikke les videre på mer avanserte bevegelsesbevegelser? (Ta en titt på denne opplæringen, for eksempel.)
  • Det er mange måter du kan lagre listen over partikler i minnet. En matrise er enklest, men kan ikke være det beste valget, da partikler ofte blir fjernet fra systemet, og nye er ofte satt inn. En koblet liste kan passe bedre, men har dårlig cache lokalitet. Den beste representasjonen for partikler kan avhenge av rammen eller språket du bruker.
Relaterte innlegg
  • Bruk Quadtrees til å oppdage sannsynlige kollisjoner i 2D-rom