Laat je spel knallen met deeltjeseffecten en kwadranten

Dus je wilt explosies, vuur, kogels of magische spreuken in je spel? Particle-systemen zijn geweldige eenvoudige grafische effecten om je spel wat op te fleuren. Je kunt de speler nog meer wowen door deeltjes in wisselwerking te laten staan ​​met je wereld, af te kaatsen van de omgeving en andere spelers. In deze tutorial zullen we enkele eenvoudige partikeleffecten implementeren, en vanaf hier gaan we verder met het laten terugkomen van de deeltjes van de wereld om hen heen.

We optimaliseren ook dingen door een gegevensstructuur met de naam quadtree te implementeren. Met kwadraten kunt u veel sneller controleren op botsingen dan zonder een en ze zijn eenvoudig te implementeren en te begrijpen.

Notitie: Hoewel deze tutorial geschreven is met HTML5 en JavaScript, zou je in bijna elke game-ontwikkelomgeving dezelfde technieken en concepten moeten kunnen gebruiken.

Lees dit artikel in Chrome, Firefox, IE 9 of een andere browser die HTML5 en Canvas ondersteunt om de in-artikel-demo's te bekijken..
Let op hoe de deeltjes van kleur veranderen als ze vallen en hoe ze de vormen weerkaatsen.

Wat is een deeltjessysteem?

Een particle-systeem is een eenvoudige manier om effecten te genereren, zoals vuur, rook en explosies.

Je maakt een deeltjes-emitter, en dit start kleine "deeltjes" die u kunt weergeven als pixels, kaders of kleine bitmaps. Ze volgen de eenvoudige Newtoniaanse fysica en veranderen van kleur terwijl ze bewegen, wat resulteert in dynamische, aanpasbare grafische effecten.


Het begin van een deeltjessysteem

Ons deeltjessysteem heeft enkele instelbare parameters:

  • Hoeveel deeltjes spuugt het elke seconde uit.
  • Hoe lang een deeltje kan "leven".
  • De kleuren waar elk deeltje doorheen gaat.
  • De positie en hoek waarin de deeltjes zullen spawnen.
  • Hoe snel de deeltjes zullen gaan wanneer ze spawnen.
  • Hoeveel zwaartekracht deeltjes zou moeten beïnvloeden.

Als elk deeltje precies hetzelfde zou worden uitgezet, zouden we gewoon een stroom deeltjes hebben, geen deeltjeseffect. Dus laten we ook instelbare variabiliteit toestaan. Dit geeft ons een paar meer parameters voor ons systeem:

  • Hoeveel hun starthoek kan variëren.
  • Hoeveel hun initiële snelheid kan variëren.
  • Hoeveel hun levensduur kan variëren.

We eindigen met een particle system-klasse die als volgt begint:

 function ParticleSystem (params) // Standaardparameters this.params = // Where particles spawn from pos: new Point (0, 0), // Hoeveel deeltjes spawnen om de tweede deeltjes Percese: 100, // Hoe lang elk deeltje leeft (en hoeveel dit kan variëren) particleLife: 0.5, lifeVariation: 0.52, // Het kleurverloop dat het deeltje door kleuren zal reizen: nieuw verloop ([new Color (255, 255, 255, 1), new Color (0, 0, 0, 0)]), // De hoek waarin het deeltje zal vuren (en hoeveel dit kan variëren) hoek: 0, hoekVariatie: Math.PI * 2, // Het snelheidsbereik dat het deeltje afvuurt op minVelocity: 20, maxVelocity: 50, // De zwaartekrachtvector toegepast op elke zwaartekracht van het deeltje: nieuw punt (0, 30.8), // Een te testen object voor botsingen tegen en bounce-dempingsfactor // voor de botsingen collider: null, bounceDamper: 0,5; // Overschrijf onze standaardparameters met de opgegeven parameters voor (var p in params) this.params [p] = params [p];  this.particles = []; 

Het systeem laten stromen

Elk frame moeten we drie dingen doen: nieuwe deeltjes maken, bestaande deeltjes verplaatsen en de deeltjes tekenen.

Partikels maken

Het maken van deeltjes is vrij eenvoudig. Als we 300 deeltjes per seconde maken en het is 0,05 seconden sinds het laatste frame, maken we 15 deeltjes voor het frame (gemiddeld tot 300 per seconde).

We zouden een eenvoudige lus moeten hebben die er zo uitziet:

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

Onze spawnParticle () functie maakt een nieuw deeltje gebaseerd op de parameters van ons systeem:

 ParticleSystem.prototype.spawnParticle = function (offset) // We willen het deeltje afvuren met een willekeurige hoek en een willekeurige snelheid // binnen de parameters gedicteerd voor dit systeem 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); // Onze initiële snelheid zal bewegen met de snelheid die we hierboven hebben gekozen in de // richting van de hoek die we kozen var velocity = new Point (). FromPolar (angle, speed); // Als we elk afzonderlijk deeltje op "pos" hebben gemaakt, zou elk deeltje // dat in één frame is gemaakt, op dezelfde plaats beginnen. // In plaats daarvan handelen we alsof we het deeltje ononderbroken tussen // dit frame en het vorige frame hebben gemaakt, door het met een bepaalde offset // langs het pad te starten. var pos = this.params.pos.clone (). add (velocity.times (offset)); // Een nieuw deeltjesobject construeren uit de parameters die we hiervoor hebben gekozen.particles.push (nieuw Particle (this.params, pos, velocity, life)); ;

We kiezen onze beginsnelheid vanuit een willekeurige hoek en snelheid. We gebruiken dan de fromPolar () methode om een ​​Cartesiaanse snelheidsvector te maken uit de combinatie hoek / snelheid.

Basis trigonometrie levert de fromPolar methode:

 Point.prototype.fromPolar = function (ang, rad) this.x = Math.cos (ang) * rad; this.y = Math.sin (ang) * rad; geef dit terug; ;

Als u de trigonometrie een beetje moet oppoetsen, is alle trigonometrie die we gebruiken afgeleid van de Unit Circle.

Deeltjesbeweging

De beweging van deeltjes volgt de basiswetgeving van Newton. Deeltjes hebben allemaal een snelheid en positie. Onze snelheid wordt beïnvloed door de zwaartekracht en onze positie verandert evenredig met de zwaartekracht. Eindelijk moeten we het leven van elk deeltje in de gaten houden, anders zouden deeltjes nooit doodgaan, zouden we uiteindelijk te veel hebben en zou het systeem tot stilstand komen. Al deze acties komen proportioneel overeen met de tijd tussen de frames.

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

Partikels tekenen

Eindelijk moeten we onze deeltjes trekken. Hoe u dit in uw game implementeert, varieert sterk van platform tot platform en hoe geavanceerd u wilt dat de rendering wordt. Dit kan zo simpel zijn als het plaatsen van een enkele gekleurde pixel, voor het verplaatsen van een paar driehoeken voor elk deeltje, getekend door een complexe GPU-shader.

In ons geval maken we gebruik van de Canvas API om een ​​kleine rechthoek voor het deeltje te tekenen.

 Particle.prototype.draw = function (ctx, frameTime) // Het is niet nodig om het deeltje te tekenen als het uit het leven is. als (this.isDead ()) terugkomt; // We willen reizen door onze gradiënt van kleuren als de deeltjesleeftijden var lifePercent = 1.0 - this.life / this.maxLife; var color = this.params.colors.getColor (lifePercent); // Stel de kleuren in ctx.globalAlpha = color.a; ctx.fillStyle = color.toCanvasColor (); // Vul de rechthoek in op de positie van het deeltje ctx.fillRect (this.pos.x - 1, this.pos.y - 1, 3, 3); ;

Kleurinterpolatie is afhankelijk van het feit of het platform dat u gebruikt een kleurklasse (of weergave-indeling) levert, of het een interpolator voor u levert en hoe u het hele probleem wilt benaderen. Ik heb een kleine verloopklasse geschreven die eenvoudige interpolatie tussen meerdere kleuren mogelijk maakt, en een kleine kleurklasse die de functionaliteit biedt om tussen twee kleuren te interpoleren.

 Color.prototype.interpolate = functie (%, andere) nieuwe kleur retourneren (this.r + (other.r - this.r) * procent, this.g + (other.g - this.g) * percent, this .b + (other.b - this.b) * percent, this.a + (other.a - this.a) * percent); ; Gradient.prototype.getColor = functie (percentage) // Drijvende-komma-kleurlocatie in de matrix var colorF = percent * (this.colors.length - 1); //Beneden afronden; dit is de opgegeven kleur in de array // onder onze huidige kleur var color1 = parseInt (colorF); //Naar boven afronden; dit is de opgegeven kleur in de array // boven onze huidige kleur var color2 = parseInt (colorF + 1); // Interpoleer tussen de twee dichtstbijzijnde kleuren (met behulp van bovenstaande methode) retourneer this.colors [color1] .interpolate ((colorF - color1) / (color2 - color1), this.colors [color2]); ;

Dit is ons deeltjesstelsel in actie!

Stuiterende deeltjes

Zoals je hierboven in de demo kunt zien, hebben we nu wat basisdeeltjeseffecten. Ze missen echter enige interactie met de omgeving om hen heen. Om deze effecten deel te laten uitmaken van onze gamewereld, laten we ze weerkaatsen op de muren om hen heen.

Om te beginnen, zal het particle systeem nu een collider als een parameter. Het is de taak van de spuiter om een ​​deeltje te vertellen of het ergens in is gecrasht. De stap() methode van een deeltje ziet er nu als volgt uit:

 Particle.prototype.step = function (frameTime) // Sla onze laatste positie op var lastPos = this.pos.clone (); // Verplaats this.velocity.add (this.params.gravity.times (frameTime)); this.pos.add (this.velocity.times (frameTime)); // Kan dit deeltje stuiteren? if (this.params.collider) // Controleer of we iets hebben geraakt var intersect = this.params.collider.getIntersection (nieuwe regel (lastPos, this.pos)); if (intersect! = null) // Zo ja, dan stellen we onze positie opnieuw in en werken we onze velocity // bij om de botsing weer te geven this.pos = lastPos; this.velocity = intersect.seg.reflect (this.velocity) .times (this.params.bounceDamper);  this.life - = frameTime; ;

Elke keer als het deeltje beweegt, vragen we de rijder of het bewegingspad via de getIntersection () methode. Als dit het geval is, stellen we de positie opnieuw in (zodat deze niet binnen de kruising ervan valt) en geven we de snelheid weer.

Een eenvoudige "collider" -implementatie kan er als volgt uitzien:

 // Neemt een verzameling lijnsegmenten die de spelwereldfunctie Collider (lijnen) vertegenwoordigen this.lines = lines;  // Retourneert een willekeurig lijnsegment dat is gekruist door "pad", anders is nul Collider.prototype.getIntersection = function (pad) for (var i = 0; i < this.lines.length; i++)  var intersection = this.lines[i].getIntersection(path); if (intersection) return intersection;  return null; ;

Let op een probleem? Elk deeltje moet bellen collider.getIntersection () en dan elk getIntersection oproep moet controleren tegen elke "muur" in de wereld. Als je 300 deeltjes hebt (een beetje een laag getal) en 200 muren in je wereld (ook niet onredelijk), voer je 60.000 lijnkruising tests uit! Dit kan je spel tot stilstand brengen, vooral met meer deeltjes (of complexere werelden).


Snellere botsingsdetectie met Quadtrees

Het probleem met onze eenvoudige spuitmachine is dat elke muur voor elk deeltje wordt gecontroleerd. Als ons deeltje zich in het kwadrant van de rechterbovenhoek van het scherm bevindt, hoeven we geen tijd te verspillen aan het controleren of het in muren is gedaald die zich alleen in de bodem of de linkerkant van het scherm bevinden. Dus idealiter willen we eventuele controles op kruispunten buiten het kwadrant rechtsboven uitschakelen:


We controleren alleen op botsingen tussen de blauwe stip en de rode lijnen.

Dat is slechts een kwart van de cheques! Laten we nu nog verder gaan: als het deeltje zich in het kwadrant linksboven in het kwadrant rechtsboven van het scherm bevindt, moeten we alleen die wanden in hetzelfde kwadrant controleren:

Quadtrees kun je precies dit doen! In plaats van testen tegen allemaal muren, split je muren in de kwadranten en sub-kwadranten die ze innemen, dus je hoeft maar een paar kwadranten te controleren. Je kunt gemakkelijk van 200 cheques per deeltje naar slechts 5 of 6 gaan.

De stappen om een ​​quadtree te maken zijn als volgt:

  1. Begin met een rechthoek die het volledige scherm vult.
  2. Neem de huidige rechthoek, tel hoeveel "muren" erin vallen.
  3. Als u meer dan drie regels hebt (u kunt een ander nummer kiezen), splitst u de rechthoek in vier gelijke kwadranten. Herhaal stap 2 met elk kwadrant.
  4. Na het herhalen van stap 2 en 3, krijg je een "boom" van rechthoeken, met geen van de kleinste rechthoeken die meer dan drie regels bevatten (of wat je ook kiest).

Een quadree bouwen. De getallen vertegenwoordigen het aantal lijnen binnen het kwadrant, rood is te hoog en moet worden onderverdeeld.

Om onze quadtree te bouwen nemen we een reeks "muren" (lijnsegmenten) als een parameter, en als er te veel in onze rechthoek zitten, dan onderverdelen we in kleinere rechthoeken en het proces herhaalt zich.

 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 (nieuwe QuadTree (x, y, w2, h2)); this.quads.push (nieuwe QuadTree (x + w2, y, w2, h2)); this.quads.push (nieuwe QuadTree (x + w2, y + h2, w2, h2)); this.quads.push (nieuwe QuadTree (x, y + h2, w2, h2)); for (var i = 0; i < this.quads.length; i++)  this.quads[i].addSegments(this.segs);  this.segs = []; ;

U kunt de volledige QuadTree-klasse hier bekijken:

 / ** * @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; 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();   ;

Testen op kruising met een lijnsegment wordt op vergelijkbare wijze uitgevoerd. Voor elke rechthoek doen we het volgende:

  1. Begin met de grootste rechthoek in de kwadratuur.
  2. Controleer of het lijnsegment elkaar snijdt of zich binnen de huidige rechthoek bevindt. Als dat niet het geval is, probeer dan niet verder te zoeken op dit pad.
  3. Als het lijnsegment wel binnen de huidige rechthoek valt of het snijdt, controleert u of de huidige rechthoek kinderhoeken heeft. Als dit het geval is, gaat u terug naar stap 2, maar gebruikt u elk van de onderliggende rechthoeken.
  4. Als de huidige rechthoek geen onderliggende rechthoeken heeft, maar wel een bladknooppunt (dat wil zeggen, het heeft alleen lijnsegmenten als kinderen), test het doellijn segment tegen die lijnsegmenten. Als er een kruispunt is, stuurt u het kruispunt terug. We zijn klaar!

Een quadtree zoeken. We beginnen bij de grootste rechthoek en doorzoeken kleinere en kleinere totdat uiteindelijk individuele lijnsegmenten worden getest. Met de kwadratuur voeren we slechts vier rechthoektests en twee lijntests uit, in plaats van te testen tegen alle 21 lijnsegmenten. Het verschil wordt alleen maar groter met grotere datasets.
 QuadTree.prototype.getIntersection = function (seg) if (! This.rect.overlapsWithLine (seg)) return 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; ;

Als we eenmaal een quadtree bezwaar tegen ons deeltjessysteem als "collider" krijgen we razendsnelle look-ups. Bekijk de interactieve demo hieronder - gebruik je muis om te zien voor welke lijnsegmenten de quadtree moet testen!


Plaats de muisaanwijzer op een (sub) kwadrant om te zien welke lijnsegmenten het bevat.

Stof tot nadenken

Het deeltjessysteem en quadtree gepresenteerd in dit artikel zijn rudimentaire onderwijssystemen. Een paar andere ideeën die u misschien in overweging wilt nemen wanneer u deze zelf implementeert:

  • Misschien wilt u objecten naast lijnsegmenten in de kwadrant houden. Hoe zou je het uitbreiden naar kringen? Squares?
  • Je zou een manier kunnen vinden om individuele objecten op te halen (om hen op de hoogte te stellen dat ze door een deeltje zijn geraakt), terwijl ze nog steeds reflecteerbare segmenten terughalen.
  • De fysica-vergelijkingen lijden aan verschillen die Euler-vergelijkingen in de loop van de tijd opbouwen met onstabiele framesnelheden. Hoewel dit in het algemeen niet van belang is voor een particle systeem, waarom zou je dan niet meer lezen over geavanceerdere bewegingsvergelijkingen? (Neem een ​​kijkje in deze tutorial, bijvoorbeeld.)
  • Er zijn veel manieren waarop u de lijst met deeltjes in het geheugen kunt opslaan. Een array is het eenvoudigst, maar is mogelijk niet de beste keuze, omdat deeltjes vaak uit het systeem worden verwijderd en er vaak nieuwe worden ingevoegd. Een gelinkte lijst kan beter passen, maar heeft een slechte cache-lokaliteit. De beste weergave voor deeltjes kan afhankelijk zijn van het kader of de taal die u gebruikt.
gerelateerde berichten
  • Gebruik Quadtrees om mogelijke botsingen in 2D-ruimte te detecteren