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.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.
Vårt partikkelsystem vil ha noen justerbare parametere:
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:
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 = [];
Hver ramme vi trenger å gjøre tre ting: lage nye partikler, flytte eksisterende partikler og tegne partiklene.
Å 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 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; ;
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]); ;
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).
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:
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:
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:
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!
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: