Creëer een gezellige, besneeuwde nachtscène met deeltjeseffecten

Effecten van deeltjes zijn heel gebruikelijk in games - het is moeilijk om een ​​moderne game te vinden niet gebruik ze. In deze zelfstudie gaan we kijken hoe je een redelijk complexe deeltjesmotor kunt bouwen en deze gebruiken om een ​​leuke besneeuwde scène te maken. Zet je wollen hoed op en laten we beginnen.

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


Eindresultaat voorbeeld

Klik en sleep met de muis om het sneeuweffect te gebruiken.

Geen flash? Bekijk deze video van de demo op YouTube:


Opstelling

De demo-implementatie hierboven gebruikt AS3 en Flash, met het Starling Framework voor GPU versnelde rendering. Een vooraf gerenderde afbeelding van een 3D-scène (beschikbaar in de brondownload) zal als achtergrond worden gebruikt. In de middelste lagen plaatsen we partikeleffecten en de hele scène wordt vermenigvuldigd met een structuur die de lichttoewijzing vertegenwoordigt.


Effecten van deeltjes

In games moeten we vaak verschillende visuele en bewegingsverschijnselen simuleren en moeten we vaak dynamische visuele effecten weergeven, zoals vuur, rook en regen. Er zijn meerdere manieren waarop we dit kunnen doen; een veel gebruikte methode is om deeltjeseffecten te gebruiken.

Dit omvat het gebruik van veel kleine elementen - meestal afbeeldingen gericht op de camera - maar ze kunnen ook worden gedaan met behulp van 3D-modellen. Het belangrijkste doel is om de positie, schaal, kleur en andere eigenschappen van een groep van vele deeltjes (misschien enkele duizenden) bij te werken. Dit creëert een illusie van hoe het effect eruit ziet in het echte leven.


Moderne deeltjeseffecten: GPU-deeltjes in de nieuwe Unreal Engine 4

In deze zelfstudie simuleren we twee effecten: een sneeuweffect dat zal bestaan ​​uit vele vloksprites, die worden beïnvloed door de wind en zwaartekracht, en een subtiel misteffect dat een handvol grote rooksprites zal gebruiken.


Emitters, colliders en Force Sources implementeren

Typische deeltjeseffecten bestaan ​​uit een of meer deeltjesemitters. Een emitter is de plaats waar deeltjes vandaan komen; het kan verschillende vormen aannemen en verschillende gedragingen hebben. Het voegt de initiële positie en hoek van deeltjes toe en kan ook andere startparameters definiëren, zoals de beginsnelheid.

We zullen één type emitter maken, een vak emitter, wat deeltjes binnen zal spawnen - je raadt het al - een doos die we definiëren.

Om het voor ons gemakkelijker te maken om meer zenders toe te voegen, gebruiken we een programmeerconstructie met de naam interface die definieert dat de klasse die deze implementeert een gedefinieerde set van methoden moet hebben. In ons geval hebben we slechts één methode nodig:

 function generatePosition (): Point

Het implementeren van dit in de box-emitter is super eenvoudig; we nemen een willekeurig punt tussen de minimum en maximum punten die het vak definiëren:

 public function generatePosition (): Point var randomX: Number = Math.random () * (maxPoint.x - minPoint.x) + minPoint.x; var randomY: Number = Math.random () * (maxPoint.y - minPoint.y) + minPoint.y; return new Point (randomX, randomY); 

Om dit voorbeeld interessanter en een beetje geavanceerder te maken, voegen we het concept toe van: a collider en bronnen dwingen.

Een collider is een object dat bestaat uit een of meer geometriedefinities, die aan een deeltjesemitter kunnen worden bevestigd. Wanneer een deeltje een interactie heeft met de collider (dat wil zeggen, wanneer het de geometrie binnengaat), zullen we een evenement krijgen om te beslissen wat we zouden willen doen. Dit wordt gebruikt om te voorkomen dat sneeuwvlokken bewegen wanneer ze tegen de grond botsen.

Op dezelfde manier als bij de emitters gebruiken we een interface waarvoor we de volgende functie moeten implementeren:

 function collides (x: Number, y: Number): Boolean;

Opmerking: dit is een eenvoudige implementatie, dus we nemen alleen de positie in aanmerking bij het controleren op een botsing.

De implementatie van de box-collider is eenvoudig; we controleren of het punt binnen de grenzen van het vak valt:

 openbare functie collides (x: Number, y: Number): Boolean var xInBounds: Boolean = this.minPoint.x < x && this.maxPoint.x > X; var yInBounds: Boolean = this.minPoint.y < y && this.maxPoint.y > y; return xInBounds && yInBounds; 

Het andere type object dat we zullen introduceren, is een krachtbron. Dit zal een effect hebben op de snelheid van een deeltje op basis van de parameters en de positie en massa van het deeltje.

De eenvoudigste bron wordt de richting force source, en we zullen het definiëren met een enkele vector D (gebruikt voor de krachtrichting en kracht). Er wordt geen rekening gehouden met de posities van deeltjes; het past gewoon de kracht op alle deeltjes van dat effect toe. Hiermee kunnen we de zwaartekracht en de wind simuleren - voor wind varieert de richting in de tijd om er realistischer uit te zien.

Een ander type krachtbron zal afhangen van de afstand tussen een bepaald punt en deeltjes, zwakkere verder weg van het centrum. Deze bron wordt bepaald door zijn positie P en sterktefactor S. We zullen dit gebruiken om interactie met de muis met de sneeuw mogelijk te maken.


Typen krachtbronnen die we zullen maken

De krachtbronnen hebben hun eigen interface, waarvoor de volgende methode moet worden geïmplementeerd:

 function forceInPoint (x: Number, y: Number): Point;

Deze keer hebben we echter meerdere implementaties: één voor een richtingskrachtbron en één voor een krachtbron met punten.

De implementatie van de richtingkrachtbron is de eenvoudigste van de twee:

 public function forceInPoint (x: Number, y: Number): Point /// Elk deeltje krijgt dezelfde krachtterugkeer nieuw punt (forceVectorX, forceVectorY); 

Dit is de implementatie van de puntkrachtbron:

 /// x. y zijn de positie van het deeltje public function forceInPoint (x: Number, y: Number): Point /// Direction and distance var differenceX: Number = x - positionX; var differenceY: Number = y - positionY; var distance: Number = Math.sqrt (differenceX * differenceX + differenceY * differenceY); /// Falloff-waarde die de krachtsterkte vermindert var falloff: Number = 1.0 / (1.0 + distance); /// We normaliseren de richting en gebruiken afvloeiing en kracht om de uiteindelijke kracht te berekenen var forceX: Number = differenceX / distance * falloff * strength; var forceY: Number = differenceY / distance * falloff * strength; return new Point (forceX, forceY); 

Merk op dat deze methode continu zal worden aangeroepen. Dit stelt ons in staat om krachtparameters te veranderen wanneer het effect actief is. We zullen deze functie gebruiken om de wind te simuleren en interactiviteit met de muis toe te voegen.


Effect en deeltjesimplementatie

Het grootste deel van de implementatie is in de Effect klasse, die verantwoordelijk is voor het afzetten en bijwerken van deeltjes.

De hoeveelheid deeltjes om te spawnen wordt bepaald door de spawnPerSecond waarde in de bijwerken methode:

 _spawnCounter - = tijd; /// Een lus gebruiken om meerdere deeltjes in een frame te spawnen terwijl (_spawnCounter <= 0)  /// Spawn the number of particles according to the passed time _spawnCounter += (1 / _spawPerSecond) * time; spawnParticle(); 

Updaten is een beetje ingewikkelder. Eerst worden de krachten door de implementatie bijgewerkt, vervolgens wordt de updates van de particle-simulatie genoemd en wordt gecontroleerd op botsingen. Het is ook verantwoordelijk voor het verwijderen van deeltjes wanneer ze niet meer nodig zijn.

 var i: int = 0; /// met de while-lus zodat we de deeltjes uit de container kunnen verwijderen terwijl (d.w.z. < _particles.length)  var particle:Particle = _particles[i]; /// Calculate particle accleration from all forces particle.acceleration = calculateParticleAcceleration(particle); /// Simulate particle particle.update(time); /// Go through the colliders and report collisions if (_colliders && _collisionResponse != null)  for each (var collider:ICollider in _colliders)  if (collider.collides(particle.x, particle.y))  _collisionResponse(particle, collider);    /// remove particle if it's dead if (particle.isDead)  _particles.splice(i, 1); addParticleToThePool(particle); particle.removeFromParent();  else  /// We are in the while loop and need to increment the counter i++;  

Ik heb het belangrijkste onderdeel van de implementatie nog niet genoemd: hoe we deeltjes vertegenwoordigen. De Deeltje klasse zal erven van een object dat we kunnen weergeven (afbeelding) en enkele eigenschappen hebben die de wijziging tijdens de update bewerkstelligen:

  • startingLife - hoe lang een deeltje in leven kan blijven.
  • beweegbaar - of de positie van het deeltje veranderd moet worden (gebruikt om het deeltje op zijn plaats te bevriezen).
  • snelheid - hoeveel het deeltje in een bepaalde hoeveelheid tijd zal bewegen.
  • versnelling - hoeveel de snelheid van het deeltje in een ingestelde hoeveelheid tijd zal veranderen.
  • angularVelocity - hoe snel de rotatie in een ingestelde tijd verandert.
  • Vervagen in - of we een vervagende alpha-waarde gebruiken om het deeltje vloeiend te maken en te vernietigen.
  • alphaModifier - bepaalt de alpha-basiswaarde.
  • massa- - de fysieke massa van het deeltje (gebruikt bij het berekenen van de versnelling door krachten).

Elk deeltje heeft een bijwerken functie die wordt aangeroepen met de tijd delta (dt). Ik wil graag laten zien welk deel van die functie zich bezig houdt met het bijwerken van de positie van deeltjes, wat gebruikelijk is in games:

 /// update positie met snelheid x + = _velocity.x * dt; y + = _velocity.y * dt; /// updatesnelheid met versnelling _velocity.x + = _acceleration.x * dt; _velocity.y + = _acceleration.y * dt;

Dit gebeurt met behulp van Euler-integratie en het heeft nauwkeurigheidsfouten, maar omdat we het alleen voor visuele effecten gebruiken, storen deze ons niet. Als je natuurkundige simulaties belangrijk vindt voor het spelen van het spel, moet je naar andere methoden kijken.


Voorbeeld effecten

Eindelijk zijn we zover dat ik zal uitleggen hoe ik het daadwerkelijke effect kan implementeren. Om een ​​nieuw effect te maken, breiden we het Effect klasse.


Deeltjesstructuren

Laat het sneeuwen

We beginnen met het sneeuweffect. Plaats eerst een box-emitter boven op het scherm en gebruik deze om nogal wat deeltjes te spawnen. Een botser zal worden gebruikt om te detecteren of een deeltje de vloer heeft bereikt. In dat geval stellen we de deeltjes in beweegbaar eigendom aan vals.

Het belangrijkste dat we ervoor moeten zorgen is dat de deeltjes willekeurig genoeg zijn zodat ze geen zichtbare patronen op het scherm creëren, wat de illusie schaadt. We doen dit op een aantal manieren:

  • Willekeurige beginsnelheid - elk deeltje zal op een enigszins andere manier bewegen.
  • Willekeurige schaal - andere dan de verschillende formaten, dit voegt ook meer diepte toe aan het effect waardoor het er meer 3D uitziet.
  • Willekeurige rotatie - zorgt ervoor dat elk deeltje er uniek uitziet, ook al gebruiken ze hetzelfde beeld.

We initialiseren elk sneeuwdeeltje op deze manier:

 particle.fadeInOut = true; /// Leven [3, 4> seconden particle.startingLife = 3 + Math.random (); /// Kleine hoeveelheid beginsnelheid particle.velocity = Point.polar (30, Math.random () * Math.PI * 2.0); /// Willekeurige rotatie [0, 360> graden particle.rotation = Math.PI * 2.0 * Math.random (); /// Willekeurige schaal [0.5, 1> particle.scaleX = particle.scaleY = Math.random () * 0.5 + 0.5;

Om ze een realistische beweging te geven om uit de lucht te vallen, gebruiken we een krachtbron als zwaartekracht. Het zou te gemakkelijk zijn om hier te stoppen, dus we gaan een andere richtingskracht toevoegen om wind te simuleren, die in de tijd zal variëren.

 /// -20 is een willekeurig getal dat goed werkte bij het testen /// (9.81m / s / s is de daadwerkelijke versnelling als gevolg van de zwaartekracht op aarde) var zwaartekracht: DirectionalField = new DirectionalField (0, -9.81 * -20); /// Initialisatie is niet belangrijk; waarden veranderen in de tijd _wind = nieuw DirectionalField (1, 0); /// zet krachten in om dit.forces = nieuw te maken [zwaartekracht, _wind];

We zullen de windwaarde variëren met behulp van een sinusfunctie; dit werd vooral bepaald door experimenten. Voor de x-as verhogen we sinus naar de kracht van 4, waardoor zijn piek scherper wordt. Elke zes seconden zal er een piek zijn, die het effect van een sterke windvlaag produceert. Op de y-as zal wind snel oscilleren tussen -20 en 20.

 /// Bereken windkracht _counter + = tijd; _wind.forceVectorX = Math.pow (Math.sin (_counter) * 0.5 + 0.5, 4) * 150; _wind.forceVectorY = Math.sin (_counter * 100) * 20;

Bekijk de functieplot om een ​​beter begrip te krijgen van wat er gaande is.


De x-as vertegenwoordigt de tijd; de y-as vertegenwoordigt de windsnelheid. (Niet op schaal.)

Voeg wat mist toe

Om het effect te voltooien, gaan we een subtiel misteffect toevoegen, met behulp van een boxemitter die de hele scène beslaat.

Omdat de textuur die we zullen gebruiken voor het deeltje relatief groot is, zal de emitter een klein aantal deeltjes spawnen. Het alfaniveau van het deeltje zal vanaf het begin laag zijn om te voorkomen dat het de scène volledig verduistert. We zullen ze ook zo instellen dat ze langzaam draaien, om een ​​windeffect te simuleren.

 /// Bedek groot deel van het scherm this.emitter = nieuwe box (0, 40, 640, 400); /// We willen slechts een paar van de deeltjes tegelijkertijd op dit scherm .spawnPerSecond = 0,05; this.setupParticle = function (particle: Particle): void /// Beweeg langzaam in één richting particle.velocity = Point.polar (50, Math.random () * Math.PI * 2.0); particle.fadeInOut = true; /// [3, 4> seconden van leven particle.startingLife = 3 + Math.random (); particle.alphaModifier = 0.3; /// Willekeurige rotatie [0, 360> graden particle.rotation = Math.PI * 2.0 * Math.random (); /// Roteren <-0.5, 0.5] radians per second particle.angularVelocity = (1 - Math.random() * 2) * 0.5; /// Set the scale to [1, 2> particle.scaleX = particle.scaleY = Math.random () + 1; ;

Om een ​​beetje meer sfeer aan het voorbeeld toe te voegen, heb ik een lichte textuur toegevoegd die zich op de bovenste lagen van de scène bevindt; het mengen wordt ingesteld op Vermenigvuldigen. De partikeleffecten zullen nu veel interessanter zijn, omdat hun witte basiskleur zal worden aangepast aan het licht, en de scène als geheel meer geïntegreerd zal aanvoelen.


Prestaties verbeteren

Een veelgebruikte manier om de simulatie van veel deeltjes te optimaliseren, is door het concept te gebruiken pooling. Met pooling kunt u objecten opnieuw gebruiken die al zijn gemaakt, maar niet meer nodig zijn.

Het concept is eenvoudig: als we klaar zijn met een bepaald object, plaatsen we het in een "pool"; als we vervolgens een ander object van hetzelfde type nodig hebben, controleren we eerst of er een "reserve" in de pool zit. Als dat zo is, nemen we het gewoon en passen we nieuwe waarden toe. We kunnen een bepaald aantal van deze objecten in de pool invoegen aan het begin van de simulatie om ze voor te bereiden voor later.

Tip: Meer informatie over poolen vindt u in dit artikel.

Een andere manier om deeltjeseffecten te optimaliseren, is door ze vooraf te structureren naar een structuur. Hierdoor verlies je veel flexibiliteit, maar het voordeel is dat het tekenen van een effect hetzelfde zou zijn als het tekenen van een enkele afbeelding. U zou het effect op dezelfde manier animeren als een normale sprite-bladanimatie.


Vuurdeeltjeseffect in spritebladvorm

U moet echter voorzichtig zijn: dit is niet goed geschikt voor effecten op volledig scherm zoals sneeuw, omdat ze veel geheugen in beslag zouden nemen.

Een goedkopere manier om sneeuw te simuleren, zou zijn om een ​​textuur met meerdere vlokken erin te gebruiken, en dan een vergelijkbare simulatie te doen als degene die we deden, maar met veel minder deeltjes. Dit kan worden gemaakt om er goed uit te zien, maar kost extra inspanning.

Hier is een voorbeeld van (van de introscène van Fahrenheit, aka Indigo Prophecy):


Laatste gedachten

Voordat je begint met het schrijven van je eigen deeltjesmotor, moet je controleren of de technologie die je gebruikt om je spel te maken al partikeleffecten bevat, of dat er een bibliotheek van derden bestaat. Toch is het erg handig om te weten hoe ze worden geïmplementeerd, en als je dat goed begrijpt, zou je geen problemen moeten hebben om een ​​bepaalde variant te gebruiken, omdat ze op dezelfde manier worden geïmplementeerd. Deeltjesmotoren kunnen zelfs worden geleverd met editors die een WYSIWYG-manier bieden om hun eigenschappen te bewerken.

Als het effect dat je nodig hebt kan worden weggenomen in een sprite sheet gebaseerd partikeleffect, zou ik TimelineFX aanraden. Het kan worden gebruikt om verbluffende effecten snel te creëren en heeft een grote bibliotheek met effecten die u kunt gebruiken en wijzigen. Helaas is dit niet het meest intuïtieve hulpmiddel en is het al een tijdje niet bijgewerkt.