Så du vill ha explosioner, eld, kulor eller magiska stavar i ditt spel? Partikelsystem gör stora enkla grafiska effekter för att krydda ditt spel lite. Du kan wow spelaren ännu mer genom att få partiklar att interagera med din värld, studsa av miljön och andra spelare. I denna handledning kommer vi att genomföra några enkla partikeleffekter, och här kommer vi att fortsätta att göra partiklarna studsa av världen runt dem.
Vi kommer också att optimera saker genom att implementera en datastruktur kallad quadtree. Quadtrees gör att du kan kontrollera kollisioner mycket snabbare än du kan utan en, och de är enkla att implementera och förstå.
Notera: Även om denna handledning skrivs med HTML5 och JavaScript, bör du kunna använda samma tekniker och begrepp i nästan vilken spelutvecklingsmiljö som helst.
För att se demo-artikeln, se till att du läser den här artikeln i Chrome, Firefox, IE 9 eller någon annan webbläsare som stöder HTML5 och Canvas.Ett partikelsystem är ett enkelt sätt att generera effekter som brand, rök och explosioner.
Du skapar en partikelemitterare, och detta lanserar små "partiklar" som du kan visa som pixlar, lådor eller små bitmappar. De följer enkla newtonska fysik och byter färg när de rör sig, vilket resulterar i dynamiska, anpassningsbara, grafiska effekter.
Vårt partikelsystem kommer att ha några inställbara parametrar:
Om varje partikel gödde exakt samma, skulle vi bara ha en ström av partiklar, inte en partikeleffekt. Så låt oss också tillåta konfigurerbar variabilitet. Detta ger oss några fler parametrar för vårt system:
Vi hamnar med en partikelsystemsklass som börjar så här:
Funktion ParticleSystem (params) // Standardparametrar this.params = // Där partiklar hyser från pos: new Point (0, 0), // Hur många partiklar spolas varje sekund partiklarPerSekund: 100, // Hur länge varje partikel lever (och hur mycket detta kan variera) particleLife: 0.5, lifeVariation: 0.52, // Färggradienten partikeln kommer att resa genom färger: Ny Gradient ([ny färg (255, 255, 255, 1), ny färg (0, 0, 0, 0)]), // Vinkeln som partikeln kommer att släcka vid (och hur mycket detta kan variera) Vinkel: 0, VinkelVariation: Math.PI * 2, // Hastighetsintervallet som partikeln kommer att avfyra vid minVelocity: 20, maxVelocity: 50, // Gravitationsvektorn applicerad på varje partikel gravitation: Ny punkt (0, 30.8), // Ett objekt att testa för kollisioner mot och studsa dämpningsfaktor // för nämnda kollisioner kollider: null, bounceDamper: 0.5; // Åsidosätta våra standardparametrar med angivna parametrar för (var p i parametrar) this.params [p] = params [p]; this.particles = [];
Varje ram måste vi göra tre saker: skapa nya partiklar, flytta befintliga partiklar och dra partiklarna.
Att skapa partiklar är ganska enkelt. Om vi skapar 300 partiklar per sekund, och det har varit 0,05 sekunder sedan den sista bilden skapar vi 15 partiklar för ramen (som är genomsnittliga till 300 per sekund).
Vi borde ha en enkel slinga som ser ut så här:
var newParticlesThisFrame = this.params.particlesPerSecond * frameTime; för (var i = 0; i < newParticlesThisFrame; i++) this.spawnParticle((1.0 + i) / newParticlesThisFrame * frameTime);
Vår spawnParticle ()
funktion skapar en ny partikel baserat på våra systemparametrar:
ParticleSystem.prototype.spawnParticle = function (offset) // Vi vill avfyra partikeln i en slumpvinkel och en slumpmässig hastighet // inom parametrarna dikterade för detta system varvinkel = randVariation (this.params.angle, this. params.angleVariation); var hastighet = randRange (this.params.minVelocity, this.params.maxVelocity); var liv = randVariation (this.params.particleLife, this.params.particleLife * this.params.lifeVariation); // Vår initialhastighet kommer att röra sig med den hastighet vi valde ovan i // riktningen av den vinkel vi valde varhastighet = Ny punkt (). FrånPolar (vinkel, hastighet); // Om vi skapade varje enskild partikel vid "pos", skulle varje partikel // som skapades i en ram startas på samma plats. // I stället fungerar vi som om vi skapade partikeln kontinuerligt mellan // den här ramen och den föregående ramen, genom att starta den med en viss förskjutning // längs dess väg. var pos = this.params.pos.clone (). add (hastighet.times (offset)); // Konstruera ett nytt partikelobjekt från parametrarna vi valde this.particles.push (new Particle (this.params, pos, hastighet, liv)); ;
Vi väljer vår initialhastighet från slumpvis vinkel och hastighet. Vi använder sedan fromPolar ()
metod för att skapa en kartesisk hastighetsvektor från vinkel / hastighetskombinationen.
Grundläggande trigonometri ger fromPolar
metod:
Point.prototype.fromPolar = funktion (ang, rad) this.x = Math.cos (ang) * rad; this.y = Math.sin (ang) * rad; returnera detta; ;
Om du behöver borsta på trigonometri lite, är allt trigonometri som vi använder härledda från Unit Circle.
Partikelrörelse följer grundläggande newtonska lagar. Partiklar har alla hastighet och position. Vår hastighet påverkas av tyngdkraften, och vår position förändras proportionellt mot gravitationen. Slutligen måste vi hålla koll på varje partikels liv, annars skulle partiklar aldrig dö, vi skulle sluta ha för många, och systemet skulle halka ner. Alla dessa åtgärder sker proportionellt till tiden mellan ramar.
Particle.prototype.step = function (frameTime) this.velocity.add (this.params.gravity.times (frameTime)); this.pos.add (this.velocity.times (frameTime)); this.life - = frameTime; ;
Slutligen måste vi rita våra partiklar. Hur du implementerar detta i ditt spel varierar kraftigt från plattform till plattform, och hur avancerad du vill att rendering ska vara. Detta kan vara lika enkelt som att placera en enda färgad pixel för att flytta ett par trianglar för varje partikel, ritad av en komplex GPU-skuggare.
I vårt fall utnyttjar vi Canvas API för att rita en liten rektangel för partikeln.
Particle.prototype.draw = funktion (ctx, frameTime) // Ingen anledning att dra partikeln om den är borta. om (this.isDead ()) returnerar; // Vi vill resa genom vår färggradient eftersom partikelålderna var lifePercent = 1.0 - this.life / this.maxLife; var färg = this.params.colors.getColor (lifePercent); // Ställ in färgerna ctx.globalAlpha = color.a; ctx.fillStyle = color.toCanvasColor (); // Fyll i rektangeln vid partikelns position ctx.fillRect (this.pos.x - 1, this.pos.y - 1,3,3); ;
Färginterpolering beror på om plattformen du använder levererar en färgklass (eller representationsformat), om det ger en interpolator för dig och hur du vill närma sig hela problemet. Jag skrev en liten gradient klass som möjliggör enkel interpolering mellan flera färger och en liten färg klass som ger funktionaliteten att interpolera mellan två färger.
Color.prototype.interpolate = funktion (procent, annat) returnera ny färg (this.r + (other.r - this.r) * procent, this.g + (other.g - this.g) * procent, detta .b + (andra.b - this.b) * procent, this.a + (andra.a - this.a) * procent); ; Gradient.prototype.getColor = funktion (procent) // Flytpunktsfärg plats inom array var colorF = percent * (this.colors.length - 1); //Avrunda nedåt; Detta är den angivna färgen i arrayen // under vår nuvarande färg var color1 = parseInt (colorF); //Runda upp; Detta är den angivna färgen i arrayen // ovanför vår nuvarande färg var color2 = parseInt (colorF + 1); // Interpolera mellan de två närmaste färgerna (med hjälp av ovanstående metod) returnera this.colors [color1] .interpolate ((colorF - color1) / (color2 - color1), this.colors [color2]); ;
Som du kan se i demo ovan, nu har vi några grundläggande partikeleffekter. De saknar dock någon interaktion med omgivningen kring dem. För att göra dessa effekter till en del av vår spelvärld kommer vi att få dem att studsa av väggarna runt dem.
För att starta kommer partikelsystemet nu att ta en collider som en parameter. Det kommer att vara colliderens jobb att berätta för en partikel om den har kraschat in i någonting. De steg()
Metoden för en partikel ser nu ut så här:
Particle.prototype.step = function (frameTime) // Spara vår senaste position var lastPos = this.pos.clone (); // Flytta this.velocity.add (this.params.gravity.times (frameTime)); this.pos.add (this.velocity.times (frameTime)); // Kan denna partikel studsa? om (this.params.collider) // Kontrollera om vi slår vad som helst som skärs = this.params.collider.getIntersection (new Line (lastPos, this.pos)); om (korsa! = null) // Om så, vi återställer vår position och uppdaterar vår hastighet // för att återspegla kollisionen this.pos = lastPos; this.velocity = intersect.seg.reflect (this.velocity) x (this.params.bounceDamper); this.life - = frameTime; ;
Nu varje gång partikeln rör sig, frågar vi kollideraren om dess rörelsebana har "kolliderat" via getIntersection ()
metod. Om så är fallet återställer vi sin position (så att den inte ligger inuti vad den korsade) och reflekterar hastigheten.
Ett grundläggande "collider" -implementering kan se ut så här:
// Tar en samling linjesegment som representerar spelvärldsfunktionen Collider (linjer) this.lines = lines; // Returnerar något linjesegment som skärs av "sökväg", annars null Collider.prototype.getIntersection = funktion (sökväg) för (var i = 0; i < this.lines.length; i++) var intersection = this.lines[i].getIntersection(path); if (intersection) return intersection; return null; ;
Lägg märke till ett problem? Varje partikel behöver ringa collider.getIntersection ()
och sedan varje getIntersection
samtal måste kontrolleras mot varje "vägg" i världen. Om du har 300 partiklar (typ av lågt antal) och 200 väggar i din värld (inte orimligt), utför du 60 000 linjekorsningstest! Detta skulle kunna stoppa ditt spel, särskilt med fler partiklar (eller mer komplexa världar).
Problemet med vår enkla collider är att det kontrollerar varje vägg för varje partikel. Om vår partikel ligger i skärmens övre högra kvadrant, borde vi inte slösa bort tiden om det kraschade i väggar som bara finns längst ner eller till vänster på skärmen. Så idealiskt vill vi klippa ut några kontroller för korsningar utanför den högra högra kvadranten:
Det är bara en fjärdedel av kontrollerna! Låt oss gå ännu längre: om partikeln befinner sig i den övre vänstra kvadranten i skärmens övre högra kvadrant, behöver vi bara kolla dessa väggar i samma kvadrant:
Quadtrees låter dig göra just detta! Snarare än att testa mot Allt väggar delar du väggar i kvadranterna och subkvadranterna de upptar, så du behöver bara kolla några kvadranter. Du kan enkelt gå från 200 kontroller per partikel till bara 5 eller 6.
Stegen för att skapa en quadtree är som följer:
För att bygga vår quadtree tar vi en uppsättning "väggar" (linjesegment) som en parameter, och om alltför många finns i vår rektangel delas vi in i mindre rektanglar och processen upprepas.
QuadTree.prototype.addSegments = funktion (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 (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 (ny QuadTree (x, y + h2, w2, h2)); för (var i = 0; i < this.quads.length; i++) this.quads[i].addSegments(this.segs); this.segs = []; ;
Du kan se hela QuadTree-klassen här:
/ ** * @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 = funktion (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 = funktion (seg) om (! This.rect.overlapsWithLine (seg)) returnera null; för (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(); ;
Testning för skärning mot ett linjesegment utförs på ett liknande sätt. För varje rektangel gör vi följande:
QuadTree.prototype.getIntersection = funktion (seg) om (! This.rect.overlapsWithLine (seg)) returnera null; för (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 passerar en quadtree
protesterar mot vårt partikelsystem som "collider" vi får blixtiga snabba uppslag. Kolla in den interaktiva demon nedan - använd musen för att se vilka linjesegment som quadtree skulle behöva testa mot!
Partikelsystemet och quadtree som presenteras i denna artikel är rudimentära undervisningssystem. Några andra idéer du kanske vill överväga när du implementerar dessa själv: