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..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.
Ons deeltjessysteem heeft enkele instelbare parameters:
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:
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 = [];
Elk frame moeten we drie dingen doen: nieuwe deeltjes maken, bestaande deeltjes verplaatsen en de deeltjes tekenen.
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.
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; ;
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]); ;
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).
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:
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:
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:
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!
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: