Author: TheTinySteini
Wie vielleicht einige von euch mitbekommen haben, hab auch ich angefangen, ein Partikelsystem zu coden. Mittlerweile habe ich mich jedoch schon wieder anderen Dingen zugewendet, deshalb stelle ich euch hier meinen Code zur Verfügung, vielleicht könnt ihr ja etwas damit anfangen. Allerdings muss ich gleich vorweg sagen, dass der Code noch überhaupt nicht fertig ist. Zwar kann man Particles darstellen, aber die Organisation der Particles in einzelne Systeme beispielsweise fehlt noch komplett. Auch andere Teile des Codes sind noch in einer Rohfassung. Es ist also weniger ein Tutorial, sondern mehr ein Grundlagen-Artikel - deswegen habe ich absichtlich nicht meinen kompletten Code hier gepostet sondern nur die Funktionen und Datenstrukturen, ihr müsst euch also die Dateien selber zusammenstellen und einfügen, was noch fehlt. Wer nur dem Text folgt und dann ein perfektes System erwartet, den muss ich deshalb enttäuschen. Man sollte also schon Bock dazu haben, auf eigene Faust den Code weiterzuentwickeln (oder sich einfach nur Anregungen daraus zu holen).
Jedes einzelne Partikel besteht aus einem Sprite, hat Position, Geschwindigkeit, Farbe, Größe und einiges mehr. Eine Wasserfontäne kann man zum Beispiel mit vielen kleinen blauen Partikeln darstellen, die senkrecht nach oben fliegen und dann im Bogen zur Erde zurückfallen. Ändert man die Farbe in ein orange-rot, macht die Partikel etwas größer und lässt das Zurückfallen weg, hat man eine Flamme. Auf ähnliche Art lassen sich auch Smokepuffs, Blutspritzer, aufgewirbelter Staub, sogar Lensflares darstellen.
Da ein Particle also einige Eigenschaften aufweist (Position usw., eben die oben genannten), scheint es am sinnvollsten zu sein, jedes Particle durch eine struct zu repräsentieren. Diese struct wird dann gefüllt mit Variablen, die wir für unser Particle brauchen. Gut, wie diese struct genau aussehen soll, werden wir uns später noch überlegen. Wichtiger ist jetzt erstmal, die Particles irgendwie zu organisieren. Hier gibt es viele verschiedene Ansätze. Zum Beispiel könnte man ein Array anlegen, das für die Maximalzahl an Particles Speicherplatz bietet. Diese Methode ist ziemlich schnell, weil der Speicherplatz nicht dynamisch zugewiesen werden muss. Leider gibt es auch einige Nachteile. So kann das Löschen eines Particles Probleme bereiten: Will man alle nachfolgenden Particles verschieben, wenn man ein Particle in der Mitte des Arrays löscht? Das ist sehr zeitaufwändig. Alternativ kann man den Eintrag einfach mit NULL belegen, aber dann muss man zum Hinzufügen eines Particles das Array nach einem leeren Platz durchsuchen. Zudem bleibt der Speicherplatz für die maximale Anzahl an Particles ständig belegt, selbst dann, wenn nur wenige Particles dargestellt werden. Eine mögliche Verbesserung wäre, die Speicherverwaltung halbdynamisch zu gestalten, also beispielsweise Particles in 100er-Arrays hinzuzufügen. Die Methode, die ich verwendet habe, basiert auf einer Linked List. Jedes Particle hat einen Pointer auf das folgende Particle. Dadurch kann man Particles schnell und einfach löschen, in dem man die Pointer der „Nachbar-Particles“ anpasst und dann das Particle deleted. Um ein neues Particle zu erstellen, erstellt man eine neue Instanz der Particle-Struct und hängt sie an den Anfang der Linked List. Nun wird auch nur soviel Speicherplatz verbraucht, wie wirklich notwendig ist. Im Prinzip eine gute Lösung, hat allerdings den Nachteil, dass für jedes neue Particle neu Speicherplatz zugewiesen werden muss, was natürlich die Geschwindigkeit beeinflusst. Ich hab testweise einen Mechanismus eingebaut, der „ausgebrannte“ Particles nicht gleich löscht, sondern einfach in eine andere Linked List hängt. Werden nun neue Particles erzeugt, wird zuerst in dieser Linked List geschaut, ob's noch freie Particles gibt, erst wenn es wirklich nötig ist, wird neuer Speicher belegt. Allerdings kann ich euch nicht sagen, ob diese Lösung Sinn macht, ich hab keine direkten Vergleichtests gestartet, und vielleicht ist das Jonglieren mit den beiden Linked Lists sogar langsamer… wie gesagt, mein System ist nicht wirklich ausgereift.
Die Organisation der Particles ist der nächste Schritt, er ist in meinem Particle-System noch überhaupt nicht ausgereift. Das Prinzip ist folgendes: Eine Klasse namens CParticleRenderer verwaltet die Particles, hat also die Pointer in die Linked Lists, berechnet die Particles und stellt sie letztlich auf dem Bildschirm dar. Will man ein Particle erzeugen, ruft man CParticleRenderer::Create() auf und bekommt im Erfolgsfall einen Pointer auf eine Particle-struct zurück. Diese struct füllt man aus und muss sich danach nicht mehr um das Particle kümmern, alles weitere übernimmt CParticleRenderer.
Diese Organisation ist zwar sehr angenehm, aber hat leider einige Nachteile. Zum Beispiel sind die Particles nicht in logische Gruppen sortiert - wenn man etwa eine Wasserfontäne darstellen will, wäre es besser, wenn alle Particles, die zu dieser Fontäne gehören, auch zusammenbleiben würden - außerdem braucht man sehr viele Membervariablen der Particle-Struct, um möglichst viele Darstellungsmöglichkeiten abzudecken. Eine mögliche Lösung wäre es, statt die Particles direkt von CParticleRenderer verwalten zu lassen, den CParticleRenderer in CParticleManager oder so umzubenennen und ihm eine Linked List auf z.B. CParticleSystems zu geben. Diese CParticleSystems gibt's dann in unterschiedlichen „Geschmacksrichtungen“, also etwa für Fontäne, Feuer, Rauch etc. - alles abgeleitet von einer Basisklasse. Diese CParticleSystems haben dann eine eigene Linked List auf ihre Particles und eine komplett eigene Darstellungs- und Berechnungsroutine. Somit erstellt man keine einzelnen Particles mehr, sondern immer ganze Systeme. Diese brauchen dann nur noch ein paar Member-Variablen, die einen zum Beispiel die Farbe und Größe - aber natürlich auch die Position - varieren lassen. Entsprechend kann man dann auch die Particles auf sehr wenige Variablen reduzieren. Ein anderer Ansatz wäre, für verschiedene Zwecke auch verschiedene Particles zu erzeugen, mit jeweils anderen Eigenschaften. Dies hat den Vorteil, dass man sehr schnell auch geringe Mengen an Particles erzeugen kann, etwa für ein paar Blutspritzer - ein ganzes System für 5-6 Particles zu erzeugen, wäre mit Kanonen auf Spatzen geschossen… Ihr seht, es gibt verschiedene Möglichkeiten (und noch viel mehr als ich angesprochen hab) - ihr müsst selbst entscheiden, wie ihr eure Particles organisiert, je nachdem, was für Ansprüche ihr an euer Particle-System habt, wie flexibel es sein soll usw.
Wie schon oben angesprochen, setze ich in meinem System hauptsächlich auf zwei Datenstrukturen, eine „normale“ struct für die Particles und eine Klasse für das Rendering-System. Hier erstmal die Definition der Particle struct:
struct particles_t { float age; float life; vec3_t origin; vec3_t vel; float fadein_time; float color[4]; float d_color[4]; float color_end[4]; float size[2]; float d_size[2]; float size_end[2]; float gravity; // gravity amount applied vec3_t customgrav; // custom velocity change per second float bounce; // that much velocity is reflected upon collision int flags; particles_t *next; // linked list functionality };
Tja, wie ihr seht, ist das ein ganz schöner Haufen Zeug. Einige Variablen davon sollte man besser auslagern, etwa die x_end-Variablen, denn diese werden nur zur Initialisierung gebraucht. Wenn man ein System entwirft, dass ähnlich wie oben beschrieben mit Klassen für jede Fontäne usw. arbeitet, dann kann (und sollte!) man noch einiges mehr auslagern.
Was die einzelnen Variabeln genau bedeuten, werden wir noch später sehen, wichtig sind denke ich erstmal die Haupt-Variablen age und lifetime. Age enthält das Alter des Particles in Sekunden, lifetime entsprechend die Lebensdauer des Particles. Wenn also age >= lifetime ist, wird das Particle entfernt. flags kann (wer hätte das gedacht…) Flags enthalten, die zum Beispiel regeln, ob ein Particle bei Kollision entfernt werden soll. Bei mir sehen die möglichen Flags so aus:
#define PARTICLE_FLAG_NONE 0x0 #define PARTICLE_FLAG_FADEIN 0x01 #define PARTICLE_FLAG_COLLIDE 0x02 #define PARTICLE_FLAG_CUSTOMGRAV 0x04 #define PARTICLE_FLAG_COLORFADE 0x08 #define PARTICLE_FLAG_SIZE 0x10
origin und vel sollten klar sein, *next ist der schon weiter oben angesprochene Zeiger auf das nächste Particle.
Gehen wir am besten gleich weiter zum CParticleRenderer:
class CParticleRenderer { public: CParticleRenderer(); // custom constructor/destructor ~CParticleRenderer(); void Init(); // set up things that can't be handled in constructor void Reset() { // reset system, should be called after levelchange ClearAll(); m_fInitialized = false; } particles_t *Create(); // find space for new particle void ClearFreed(); // clear our lists void ClearAll(); void Update(); // gets called every frame, calls Draw() int Draw(); // the main function void CalcDeltas( particles_t* p ); bool m_fInitialized; float m_flFrametime; // time between calls private: int m_iSprite; // the sprite we're going to use model_s *m_pSprite; float m_flLastTime; int m_iParticleCount; // amount of particles to control memory usage particles_t *m_pParticles; // index into linked list particles_t *m_pFreed; };
Ok, das meiste hab ich schon mit Kommentaren versehen, es wird vermutlich klarer, wenn ihr den eigentlichen Code vor Augen habt, also los geht's!
Gut, ich denke mal, wir fangen der Reihe nach an. Also - der Konstruktor zuerst:
CParticleRenderer::CParticleRenderer() { m_iParticleCount = 0; m_pParticles = NULL; m_pFreed = NULL; m_fInitialized = false; }
Naja, spricht eigentlich für sich - die diversen Variablen werden auf NULL gesetzt, vor allem die Linked-List-Pointer, damit wir uns keine Probleme durch wilde Zeiger einhandeln.
Als nächstes die Init-Funktion. Hier wird alles erledigt, was im Konstruktor noch nicht geschehen kann. Beispielsweise sind zu dem Zeitpunkt, wenn der Konstruktor aufgerufen wird, die gEngfuncs noch nicht vorhanden.
void CParticleRenderer::Init() { char sz[256]; sprintf( sz, "sprites/flare3.spr" ); m_iSprite = SPR_Load( sz ); m_pSprite = (struct model_s *)gEngfuncs.GetSpritePointer( m_iSprite ); m_flLastTime = gEngfuncs.GetClientTime(); m_fInitialized = true; }
Bedarf eigentlich keiner weiteren Erklärung, wir holen uns halt den Spritepointer (den müssen wir übrigens nach nem Levelchange erneuern!), und setzen die restlichen Variablen.
Kommen wir zum interessanteren Teil - Create():
particles_t *CParticleRenderer::Create() { if( m_iParticleCount < MAX_PARTICLES ) { particles_t *p; // check whether we have some free particles // left over from last call if( m_pFreed ) { p = m_pFreed; m_pFreed = p->next; } else p = new particles_t; // feed it into our linked list p->next = m_pParticles; m_pParticles = p; m_iParticleCount++; // reset particle's age p->age = 0.0; return p; } else { gEngfuncs.Con_Printf("> %d particles!\n", MAX_PARTICLES); return NULL; } }
Schon umfangreicher. Hier seht ihr auch mein oben beschriebenes (zweifelhaftes ;) Konzept der doppelten Linked-List. Denn zuerst wird überprüft, ob in der Liste der freien Particles ein Particle existiert. Falls ja, wird dieses Particle genommen, ansonsten wird ein neues alloziiert. Dann wird dieses Particle in unsere Haupt-Linked-List gefüttert, und zwar immer direkt an den Anfang. Das war's im Prinzip auch schon - Linked Lists sollte ich euch nicht erklären müssen, das würde den Rahmen dieses Tutorials bei weitem sprengen. Wenn ihr noch nicht über Linked Lists bescheid wisst, solltet ihr euch schleunigst darüber informieren, denn diese Kenntnisse sollten zum Grundrepertoire eines Programmierers zählen. Die Überprüfung auf MAX_PARTICLES ist ein Sicherheitscheck, denn wenn jemand doch zu großzügig mit den Particle-Einstellungen umgegangen ist (wenn etwa mehr Particles pro Sekunde erstellt werden als verschwinden), wird so effektiv verhindert, dass der gesamte Speicher mit Particles zugemüllt wird. Ich hab bei mir MAX_PARTICLES per #define auf 1500 gesetzt, auf aktuellen Rechnern sollten aber deutlich mehr möglich sein.
Zwei kleine Funktionen sorgen dafür, dass die Linked Lists recht einfach geleert werden können:
void CParticleRenderer::ClearAll() { particles_t *p; ClearFreed(); while( m_pParticles ) { p = m_pParticles; m_pParticles = p->next; delete p; m_iParticleCount--; } } void CParticleRenderer::ClearFreed() { particles_t *p; // delete every particle that was not reused while( m_pFreed ) { p = m_pFreed; m_pFreed = p->next; delete p; } }
So, der Destruktor ist in dem Zusammenhang noch wichtig, ist nicht viel, es müssen halt alle alloziierten Particles freigegeben. Dafür wird einfach ClearAll() aufgerufen:
CParticleRenderer::~CParticleRenderer() { ClearAll(); }
So, kommen wir zur Hauptroutine, die jedes Frame aufgerufen wird:
void CParticleRenderer::Update() { // calculate time between frames m_flFrametime = gEngfuncs.GetClientTime() - m_flLastTime; // draw particles if there are any if( m_iParticleCount > 0 && m_fInitialized ) Draw(); m_flLastTime = gEngfuncs.GetClientTime(); }
Spricht für sich, denke ich. Also gehen wir lieber gleich weiter zum richtig dicken Brocken, Draw():
int CParticleRenderer::Draw() { vec3_t v_forward, v_right, v_up, point; particles_t *p, *pprev, *pnext; int contents; float a; // r, g, b, a float gravity = CVAR_GET_FLOAT( "sv_gravity" ); // prepare for drawing gEngfuncs.pTriAPI->SpriteTexture( m_pSprite, 0 ); gEngfuncs.pTriAPI->RenderMode( kRenderTransAdd ); gEngfuncs.pTriAPI->CullFace( TRI_NONE ); gEngfuncs.pTriAPI->Begin( TRI_QUADS ); // split up player's viewangles AngleVectors(v_angles, v_forward, v_right, v_up ); pprev = NULL; p = m_pParticles; while( p ) { // store this so we can always determine the next particle pnext = p->next; // in-/decrease alpha a = (p->color[3] += p->d_color[3] * m_flFrametime); if( p->flags & PARTICLE_FLAG_FADEIN ) { float delta = (p->fadein_time - p->age); if( delta < 0.0 ) p->flags &= ~PARTICLE_FLAG_FADEIN; else { a = p->color[3] * (1.0 - delta / p->fadein_time); } } // colorfade if( p->flags & PARTICLE_FLAG_COLORFADE ) { p->color[0] += p->d_color[0] * m_flFrametime; p->color[1] += p->d_color[1] * m_flFrametime; p->color[2] += p->d_color[2] * m_flFrametime; } if( p->flags & PARTICLE_FLAG_SIZE ) { p->size[0] += p->d_size[0] * m_flFrametime; p->size[1] += p->d_size[1] * m_flFrametime; } // calculate normal & custom gravity p->vel.z -= gravity * m_flFrametime * p->gravity; if( p->flags & PARTICLE_FLAG_CUSTOMGRAV ) p->vel = p->vel + p->customgrav * m_flFrametime; // move p->origin = p->origin + p->vel * m_flFrametime; // collision detection if( p->flags & PARTICLE_FLAG_COLLIDE ) { contents = gEngfuncs.PM_PointContents( p->origin, NULL ); if( contents != CONTENTS_EMPTY ) { if( p->bounce == 0.0 ) // kill on collision { p->life = 0.0; // this will kill the particle } else { //UNDONE: this is physically nonsense p->vel.z = -(p->vel.z * p->bounce); } } } // check whether this particle burned out if( p->age >= p->life ) { if( pprev ) pprev->next = pnext; else // particle was at head of list m_pParticles = pnext; // link over into freed list p->next = m_pFreed; m_pFreed = p; m_iParticleCount--; } else { pprev = p; // now draw that tiny quad // the triapi calls are pretty similar to opengl gEngfuncs.pTriAPI->Color4f( p->color[0], p->color[1], p->color[2], a ); gEngfuncs.pTriAPI->Brightness( 1 ); gEngfuncs.pTriAPI->TexCoord2f( 0, 0 ); point = p->origin; gEngfuncs.pTriAPI->Vertex3fv( point ); gEngfuncs.pTriAPI->TexCoord2f( 0, 1 ); point = p->origin + v_right * p->size[0]; gEngfuncs.pTriAPI->Vertex3fv( point ); gEngfuncs.pTriAPI->TexCoord2f( 1, 1 ); point = p->origin + v_right * p->size[0] + v_up * p->size[1]; gEngfuncs.pTriAPI->Vertex3fv( point ); gEngfuncs.pTriAPI->TexCoord2f( 1, 0 ); point = p->origin + v_up * p->size[1]; gEngfuncs.pTriAPI->Vertex3fv( point ); // increase the particles age // we do this at the end so that the particles really go through // this loop with age == 0.0 the first time p->age += m_flFrametime; } p = pnext; } gEngfuncs.pTriAPI->End(); gEngfuncs.pTriAPI->RenderMode( kRenderNormal ); return 1; }
Gut, den Code sollten wir besser mal auseinandernehmen. Die ersten vier TriAPI-Aufrufe setzen für uns den richten Rendermodus, die Textur der Particles und so weiter. Falls ihr es nicht wisst, die TriAPI ist genau wie OpenGL eine Solid-State-Engine, Einstellungen bleiben also so lange erhalten, bis man sie ändert. Deswegen können wir gleich am Anfang in den Modus TRI_QUADS zur Darstellung von Rechtecken gehen und brauchen dann nur noch die einzelnen Koordinaten zu übergeben, die TriAPI macht automatisch aus je vier Koordinaten ein Particle. Genauso ist es auch mit der Textur, sie wird am Anfang gesetzt und gilt dann für alle folgenden Particles.
Warum wir uns die viewangles vom Spieler holen (extern vec3_t v_angles am Anfang der Datei nicht vergessen!), wird später klar. Wir fangen erstmal an, durch unsere Linked List zu marschieren. Erstmal noch ein Wort zu den d_-Variablen. Sie enthalten die Differenz zwischen Endwert und Startwert, dividiert durch die Lebensdauer des Particles. Dadurch erhalten wir die Abnahme/Zunahme des Werts pro Sekunde. Wenn wir beispielsweise für Alpha einen Startwert von 1.0 und einen Endwert von 0.0 haben und das Particle 2.0 Sekunden lebt, dann erhalten wir einen Wert von (0.0-1.0)/2.0 = -0.5. Der Alpha-Wert nimmt also um 0.5 Einheiten pro Sekunde ab. Dadurch können wir nun den Alpha-Wert für jeden Frame ganz einfach berechnen. Er ist einfach der aktuelle Alpha-Wert minus die Abnahme pro Sekunde multipliziert mit der Zeit zwischen zwei Frames. Häh? Ok, ein Beispiel: Wir haben einen aktuellen Alpha-Wert von 1.0, der Wert soll um 0.5 pro Sekunde abnehmen, und die Zeit zwischen zwei Frames ist 0.05 Sekunden (entspricht 20fps - oft ist der Wert also noch deutlich niedriger). Also ergibt sich für den neuen Alphawert: 1.0 + (-0.5 * 0.05) = 0.975. Die Berechnung ist für die Farbe, den Alpha-Wert und die Größe identisch. Ob bestimmte Berechnungen überhaupt ausgeführt werden, wird anhand von Flags bestimmt. Ist etwa PARTICLE_FLAG_COLORFADE nicht gesetzt, dann wird sich um das Berechnen einer neuen Farbe gar nicht erst gekümmert. Hat auch den Vorteil, dass man sich um die Werte in d_color[0..2] gar keine Gedanken machen muss - diese können also uninitialisiert bleiben (z.B. noch Werte vom Vorgängerpartikel enthalten, wenn eine struct wiederverwendet wurde, wie oben beschrieben).
Der FADEIN-Code bewirkt, dass ein Particle nicht gleich mit voller Helligkeit startet. Denn dann kann es durch Überlagerung von mehreren Particles zu unschönen Effekten kommen (fast weiße Flecken). Ich werde den Code hier nicht genauer erklären, schaut ihn euch einfach an und setzt ein paar Beispielzahlen ein, um zu verstehen, wie's funktioniert. Es kann durchaus sein, dass mein Fadein-Code zu umständlich ist, ich hab selber erstmal ziemlich überlegen müssen, bis ich auf eine Lösung gekommen bin.
Da p→vel schon die Bewegung des Particles pro Sekunde beinhaltet, brauchen wir den Wert nur noch mit der Frametime zu multiplizieren und zum origin hinzuzuaddieren, um die neue Position des Particles zu erhalten. Vorher wird noch die Schwerkraft in die Beschleunigung in z-Richtung eingerechnet. Da die volle Schwerkraft die Particles extrem schnell nach unten zieht, kann man mit p→gravity einen Multiplikator angeben und so zum Beispiel nur die halbe Schwerkraft wirken lassen. Auch eine eigene Schwerkraft kann man angeben, zum Beispiel einen leichten Seitwärts-Drift, um Wind nachzuahmen.
Danach gibt's den Kollisionscode. Schaut ihn euch am besten gar nicht erst an, er funktioniert zwar, aber erstens ist der Aufruf von PointContents ziemlich langsam, und zweitens kann es bei schneller Bewegung der Particles durchaus sein, dass der zurückgelegte Weg zwischen zwei Frames einige Units groß ist, die Particles also durch eine dünne Wand fliegen können, ohne dass PointContents etwas davon merkt. Eine Traceline wäre besser, aber noch langsamer. Wer eine Idee hat, kann sich ja melden - ein guter Ansatzpunkt wäre zum Beispiel die Kollisionsabfrage der Temp-Entities in entity.cpp. Eine Traceline hätte zudem den Vorteil, dass man den Bounce-Code vernünftig schreiben könnte. So wird einfach nur der Wert für die Geschwindigkeit in z-Richtung umgekehrt. Das ist zwar auf waagerechten Flächen in Ordnung, aber auf schrägen Flächen absoluter Nonsens. Dort müsste man die Neigung der Fläche in die Berechnung mit einbeziehen. Bei PointContents bekommt man die nicht, bei einer Traceline schon, und zwar über plane.normal.
Im nächsten Schritt überprüfen wir, ob die maximale Lebenszeit bereits überschritten wurde. Falls ja, wird das Particle freigegeben (oder besser gesagt: es wird in die Liste der freigegebenen Particles verschoben).
Falls das Particle nicht gelöscht wurde, wird es jetzt an die TriAPI übergeben. Jetzt kommen auch die oben bereits genannten Viewangles zum Einsatz. Denn alle Particles sollen ja so ausgerichtet sein, dass sie dem Spieler die volle Fläche zuwenden. Sonst könnte man die Particles von der Seite sehen - und da sie eine Tiefe von 0 haben, sieht man sie dann gar nicht. Übrigens kann es teilweise durchaus von Bedeutung sein, die Particles nicht zum Spieler auszurichten. Etwa dann, wenn man langgezogene Sparks darstellt (diese müssen sich dann auch noch in die richtige Richtung drehen, dürfen also nicht senkrecht ausgerichtet sein), oder Blut, dass auf dem Boden klebt (lecker… :-|).
Ganz zum Schluss erhöhen wir schließlich noch das „Alter“ des Particles und wenden uns dann dem nächsten Particle zu.
Eine Funktion fehlt uns noch, CalcDeltas() nämlich, die uns die d_-Variablen berechnet:
void CParticleRenderer::CalcDeltas( particles_t* p ) { if( p ) { for( int i=0; i < 4; i++ ) { p->d_color[i] = (p->color_end[i] - p->color[i]) / p->life; } p->d_size[0] = (p->size_end[0] - p->size[0]) / p->life; p->d_size[1] = (p->size_end[1] - p->size[1]) / p->life; } }
Gut, damit wären wir auch schon fast am Ende von diesem Grundlagen-Tutorial. Es fehlt eigentlich nur noch ein Beispiel, wie man das System nun aufrufen kann:
void Draw_Particles() { static vec3_t origin; cl_entity_t *player; particles_t *p; if( !gParticleRenderer.m_fInitialized ) { gParticleRenderer.Init(); // Load it up with some bogus data player = gEngfuncs.GetLocalPlayer(); if ( !player ) return; origin = player->origin; } int iCount = gParticleRenderer.m_flFrametime * 450.0f; for( int i = 0; i < iCount; i++ ) { if( p = gParticleRenderer.Create() ) { p->life = RANDOM_FLOAT(0.8, 1.5); p->vel.x = RANDOM_FLOAT(-20.0, 20.0); p->vel.y = RANDOM_FLOAT(100.0, 200.0); p->vel.z = RANDOM_FLOAT(5.0, 10.0); p->origin = origin; p->vel = p->vel; p->color[0] = 0.0; p->color[1] = 0.0; p->color[2] = 0.5; p->color[3] = 0.7; p->color_end[0] = 1.0; p->color_end[1] = 0.5; p->color_end[2] = 0.0; p->color_end[3] = 0.0; p->fadein_time = 0.1; p->size[0] = 3.0; p->size[1] = 3.0; p->size_end[0] = 40.0; p->size_end[1] = 40.0; p->gravity = -0.1; p->flags = (PARTICLE_FLAG_FADEIN|PARTICLE_FLAG_COLORFADE|PARTICLE_FLAG_SIZE); gParticleRenderer.CalcDeltas( p ); } } gParticleRenderer.Update(); }
Ich habe also einfach eine globale Instanz der CParticleRenderer-Klasse, nämlich gParticleRenderer, erstellt. Man kann zum Beispiel die CParticleRenderer-Klasse in eine eigene Datei, z.B. particle.cpp, stellen, dort gParticleRenderer definieren und dann über eine Header-Datei überall dort #includen, wo man Zugriff auf gParticleRenderer braucht. Aufgerufen wird die Draw_Particles() dann in der HUD_DrawTransparentTriangles(). Wenn man später dann ein vernünftig organisiertes System zusammengecodet hat, würde man nur noch gParticleRenderer.Update() aufrufen, der Rest wird dann an den Stellen erledigt, wo man die Particles erzeugt, also bei den Waffen, bei den Stepsounds, bei den Smokepuffs, wo auch immer…
Und wie immer verteile ich auch noch ein paar Credits an blair für einige Anregungen und für's Probelesen, für's Probelesen an Andy und für Anregungen, Grundlagen und winzige Codesnippets an Another1.
Dieses Tutorial stammt aus der ehemaligen Sammlung des resourcecode.de und konnte dank der freundlichen Zustimmung des Autors in das thewall-Wiki übertragen werden.