Erstellen Sie einen Neon Vector Shooter für iOS: Partikeleffekte
German (Deutsch) translation by Tatsiana Bochkareva (you can also view the original English article)
In dieser Reihe von Tutorials zeige ich Ihnen, wie Sie einen von Geometry Wars inspirierten Twin-Stick-Shooter mit Neongrafiken, verrückten Partikeleffekten und großartiger Musik für iOS mit C++ und OpenGL ES 2.0 erstellen. In diesem Teil werden wir Explosionen und visuelles Flair hinzufügen.
Überblick
In der bisherigen Serie haben wir das Gameplay eingerichtet und virtuelle Gamepad-Steuerelemente hinzugefügt. Als nächstes werden wir Partikeleffekte hinzufügen.
Partikeleffekte entstehen durch die Herstellung einer großen Anzahl kleiner Partikel. Sie sind sehr vielseitig und können verwendet werden, um fast jedem Spiel Flair zu verleihen. In Shape Blaster werden wir Explosionen mit Partikeleffekten machen. Wir werden auch Partikeleffekte verwenden, um Abgasfeuer für das Schiff des Spielers zu erzeugen und den Schwarzen Löchern visuelles Flair zu verleihen. Außerdem schauen wir uns an, wie Partikel mit der Schwerkraft der Schwarzen Löcher interagieren.
Wechseln Sie zu Release Builds für Geschwindigkeitsgewinne
Bis jetzt haben Sie wahrscheinlich Shape Blaster mit allen Standard-debug-Builds des Projekts erstellt und ausgeführt. Während dies in Ordnung und großartig ist, wenn Sie Ihren Code debuggen, deaktiviert das Debuggen die meisten Geschwindigkeits- und mathematischen Optimierungen, die durchgeführt werden können, und aktiviert alle Zusicherungen im Code.
Wenn Sie den Code von nun an im Debug-Modus ausführen, werden Sie feststellen, dass die Bildrate dramatisch sinkt. Dies liegt daran, dass wir auf ein Gerät abzielen, das im Vergleich zu einem Desktop-Computer oder sogar einem Laptop weniger RAM, weniger CPU-Takt und weniger 3D-Hardware aufweist.
An diesem Punkt können Sie optional das Debuggen deaktivieren und den "Release" -Modus aktivieren. Der Release-Modus bietet uns eine vollständige Compiler- und Mathematikoptimierung sowie das Entfernen von nicht verwendetem Debugging-Code und Zusicherungen.
Wenn Sie das Projekt geöffnet haben, wählen Sie das Menü Produkt, Schema und dann Schema bearbeiten....



Das folgende Dialogfenster wird geöffnet. Wählen Sie auf der linken Seite des Dialogfelds Ausführen und ändern Sie unter Build Configuration das Popup-Element von Debug auf Release.



Sie werden die Geschwindigkeitsgewinne sofort bemerken. Der Vorgang kann leicht rückgängig gemacht werden, wenn Sie das Programm erneut debuggen müssen: Wählen Sie einfach Debug statt Release, und Sie sind fertig.
Tipp: Beachten Sie jedoch, dass für jede solche Schemaänderung eine vollständige Neukompilierung des Programms erforderlich ist.
Die Partikelverwaltungsklasse
Zunächst erstellen wir eine ParticleManager-Klasse, in der alle Partikel gespeichert, aktualisiert und gezeichnet werden. Wir werden diese Klasse so allgemein gestalten, dass sie problemlos in anderen Projekten wiederverwendet werden kann, aber dennoch von Projekt zu Projekt angepasst werden muss. Um den ParticleManager so allgemein wie möglich zu halten, ist er nicht dafür verantwortlich, wie die Partikel aussehen oder sich bewegen. Wir werden das woanders erledigen.
Partikel neigen dazu, schnell und in großer Zahl erzeugt und zerstört zu werden. Wir werden einen Objektpool verwenden, um zu vermeiden, dass große Mengen Müll entstehen. Dies bedeutet, dass wir eine große Anzahl von Partikeln im Voraus zuweisen und diese Partikel dann weiterhin wiederverwenden.
Wir werden auch dafür sorgen, dass der ParticleManager eine feste Kapazität hat. Dies vereinfacht dies und stellt sicher, dass wir unsere Leistungs- oder Speicherbeschränkungen nicht überschreiten, indem wir zu viele Partikel erzeugen. Wenn die maximale Anzahl von Partikeln überschritten wird, werden die ältesten Partikel durch neue ersetzt. Wir machen den ParticleManager zu einer generischen Klasse. Auf diese Weise können wir benutzerdefinierte Statusinformationen für die Partikel speichern, ohne sie fest in das zu codierenParticleManager selbst.
Wir werden auch eine Particle klasse erstellen:
1 |
class Particle |
2 |
{
|
3 |
public:
|
4 |
ParticleState mState; |
5 |
tColor4f mColor; |
6 |
tVector2f mPosition; |
7 |
tVector2f mScale; |
8 |
tTexture* mTexture; |
9 |
float mOrientation; |
10 |
float mDuration; |
11 |
float mPercentLife; |
12 |
|
13 |
public:
|
14 |
Particle() |
15 |
: mScale(1,1), |
16 |
mPercentLife(1.0f) { } |
17 |
};
|
Die Particle klasse verfügt über alle Informationen, die zum Anzeigen eines Partikels und zum Verwalten seiner Lebensdauer erforderlich sind. Der ParticleState dient dazu, zusätzliche Daten zu speichern, die wir möglicherweise für unsere Partikel benötigen. Welche Daten benötigt werden, hängt von den gewünschten Partikeleffekten ab. Es kann verwendet werden, um Geschwindigkeit, Beschleunigung, Rotationsgeschwindigkeit oder alles andere zu speichern, was Sie benötigen.
Um die Partikel besser verwalten zu können, benötigen wir eine Klasse, die als kreisförmiges Array fungiert. Dies bedeutet, dass Indizes, die normalerweise außerhalb der Grenzen liegen, stattdessen am Anfang des Arrays umbrochen werden. Dies macht es einfach, die ältesten Partikel zuerst zu ersetzen, wenn wir keinen Platz mehr für neue Partikel in unserem Array haben. Dazu fügen wir im ParticleManager Folgendes als verschachtelte Klasse hinzu:
1 |
class CircularParticleArray |
2 |
{
|
3 |
protected:
|
4 |
std::vector<Particle> mList; |
5 |
size_t mStart; |
6 |
size_t mCount; |
7 |
|
8 |
public:
|
9 |
CircularParticleArray(int capacity) |
10 |
{
|
11 |
mList.resize((size_t)capacity); |
12 |
}
|
13 |
|
14 |
size_t getStart() { return mStart; } |
15 |
void setStart(size_t value) { mStart = value % mList.size(); } |
16 |
size_t getCount() { return mCount; } |
17 |
void setCount(size_t value) { mCount = value; } |
18 |
size_t getCapacity() { return mList.size(); } |
19 |
|
20 |
Particle& operator [](const size_t i) |
21 |
{
|
22 |
return mList[(mStart + i) % mList.size()]; |
23 |
}
|
24 |
|
25 |
const Particle& operator [](const size_t i) const |
26 |
{
|
27 |
return mList[(mStart + i) % mList.size()]; |
28 |
}
|
29 |
};
|
Wir können das mStart-Mitglied so einstellen, dass angepasst wird, wo der Index Null in unserem CircularParticleArray im zugrunde liegenden Array entspricht, und mCount wird verwendet, um zu verfolgen, wie viele aktive Partikel in der Liste enthalten sind. Wir werden sicherstellen, dass das Partikel am Index Null immer das älteste Partikel ist. Wenn wir das älteste Teilchen durch ein neues ersetzen, erhöhen wir einfach mStart, wodurch das kreisförmige Array im Wesentlichen gedreht wird.
Nachdem wir unsere Hilfsklassen haben, können wir die ParticleManager-Klasse ausfüllen. Wir brauchen eine neue Mitgliedsvariable und einen Konstruktor.
1 |
CircularParticleArray mParticleList; |
2 |
|
3 |
ParticleManager::ParticleManager(int capacity) |
4 |
: mParticleList(capacity) |
5 |
{
|
6 |
}
|
Wir erstellen mParticleList und füllen es mit leeren Partikeln. Der Konstruktor ist der einzige Ort, an dem der ParticleManager Speicher zuweist.
Als Nächstes fügen wir die Methode createParticle() hinzu, mit der ein neues Partikel unter Verwendung des nächsten nicht verwendeten Partikels im Pool oder des ältesten Partikels erstellt wird, wenn keine nicht verwendeten Partikel vorhanden sind.
1 |
void ParticleManager::createParticle(tTexture* texture, const tVector2f& position, const tColor4f& tint, float duration, const tVector2f& scale, const ParticleState& state, float theta) |
2 |
{
|
3 |
size_t index; |
4 |
|
5 |
if (mParticleList.getCount() == mParticleList.getCapacity()) |
6 |
{
|
7 |
index = 0; |
8 |
mParticleList.setStart(mParticleList.getStart() + 1); |
9 |
}
|
10 |
else
|
11 |
{
|
12 |
index = mParticleList.getCount(); |
13 |
mParticleList.setCount(mParticleList.getCount() + 1); |
14 |
}
|
15 |
|
16 |
Particle& ref = mParticleList[index]; |
17 |
|
18 |
ref.mTexture = texture; |
19 |
ref.mPosition = position; |
20 |
ref.mColor = tint; |
21 |
|
22 |
ref.mDuration = duration; |
23 |
ref.mPercentLife = 1.0f; |
24 |
ref.mScale = scale; |
25 |
ref.mOrientation = theta; |
26 |
ref.mState = state; |
27 |
}
|
Partikel können jederzeit zerstört werden. Wir müssen diese Partikel entfernen und gleichzeitig sicherstellen, dass die anderen Partikel in derselben Reihenfolge bleiben. Wir können dies tun, indem wir die Liste der Partikel durchlaufen und gleichzeitig verfolgen, wie viele zerstört wurden. Während wir gehen, bewegen wir jedes aktive Teilchen vor alle zerstörten Teilchen, indem wir es gegen das erste zerstörte Teilchen austauschen. Sobald alle zerstörten Partikel am Ende der Liste sind, deaktivieren wir sie, indem wir die Variable mCount der Liste auf die Anzahl der aktiven Partikel setzen. Zerstörte Partikel verbleiben im zugrunde liegenden Array, werden jedoch nicht aktualisiert oder gezeichnet.
ParticleManager::update() aktualisiert jedes Partikel und entfernt zerstörte Partikel aus der Liste:
1 |
void ParticleManager::update() |
2 |
{
|
3 |
size_t removalCount = 0; |
4 |
|
5 |
for (size_t i = 0; i < mParticleList.getCount(); i++) |
6 |
{
|
7 |
Particle& ref = mParticleList[i]; |
8 |
ref.mState.updateParticle(ref); |
9 |
ref.mPercentLife -= 1.0f / ref.mDuration; |
10 |
|
11 |
Swap(mParticleList, i - removalCount, i); |
12 |
|
13 |
if (ref.mPercentLife < 0) |
14 |
{
|
15 |
removalCount++; |
16 |
}
|
17 |
}
|
18 |
|
19 |
mParticleList.setCount(mParticleList.getCount() - removalCount); |
20 |
}
|
21 |
|
22 |
void ParticleManager::Swap(typename ParticleManager::CircularParticleArray& list, size_t index1, size_t index2) const |
23 |
{
|
24 |
Particle temp = list[index1]; |
25 |
list[index1] = list[index2]; |
26 |
list[index2] = temp; |
27 |
}
|
Das letzte, was in ParticleManager implementiert werden muss, ist das Zeichnen der Partikel:
1 |
void ParticleManager::draw(tSpriteBatch* spriteBatch) |
2 |
{
|
3 |
for (size_t i = 0; i < mParticleList.getCount(); i++) |
4 |
{
|
5 |
Particle particle = mParticleList[(size_t)i]; |
6 |
|
7 |
tPoint2f origin = particle.mTexture->getSurfaceSize() / 2; |
8 |
spriteBatch->draw(2, particle.mTexture, tPoint2f((int)particle.mPosition.x, (int)particle.mPosition.y), tOptional<tRectf>(), |
9 |
particle.mColor, |
10 |
particle.mOrientation, origin, particle.mScale); |
11 |
}
|
12 |
}
|
Die Partikelstatusklasse
Als Nächstes erstellen Sie eine benutzerdefinierte Klasse oder Struktur, um das Aussehen der Partikel in Shape Blaster anzupassen. In Shape Blaster gibt es verschiedene Arten von Partikeln, die sich geringfügig unterscheiden. Daher erstellen wir zunächst eine enum für den Partikeltyp. Wir benötigen auch Variablen für die Geschwindigkeit und die Anfangslänge des Partikels.
1 |
class ParticleState |
2 |
{
|
3 |
public:
|
4 |
enum ParticleType |
5 |
{
|
6 |
kNone = 0, |
7 |
kEnemy, |
8 |
kBullet, |
9 |
kIgnoreGravity
|
10 |
};
|
11 |
|
12 |
public: |
13 |
tVector2f mVelocity; |
14 |
ParticleType mType; |
15 |
float mLengthMultiplier; |
16 |
|
17 |
public: |
18 |
ParticleState(); |
19 |
ParticleState(const tVector2f& velocity, ParticleType type, float lengthMultiplier = 1.0f); |
20 |
|
21 |
ParticleState getRandom(float minVel, float maxVel); |
22 |
void updateParticle(Particle& particle); |
23 |
};
|
Jetzt können wir die update() -Methode des Partikels schreiben. Es ist eine gute Idee, diese Methode schnell zu machen, da sie möglicherweise für eine große Anzahl von Partikeln aufgerufen werden muss.
Wir fangen einfach an. Fügen wir ParticleState die folgende Methode hinzu:
1 |
void ParticleState::updateParticle(Particle& particle) |
2 |
{
|
3 |
tVector2f vel = particle.mState.mVelocity; |
4 |
|
5 |
particle.mPosition += vel; |
6 |
particle.mOrientation = Extensions::toAngle(vel); |
7 |
|
8 |
// denormalized floats cause significant performance issues
|
9 |
if (fabs(vel.x) + fabs(vel.y) < 0.00000000001f) |
10 |
{
|
11 |
vel = tVector2f(0,0); |
12 |
}
|
13 |
|
14 |
vel *= 0.97f; // Particles gradually slow down |
15 |
particle.mState.mVelocity = vel; |
16 |
}
|
Wir werden gleich zurückkommen und diese Methode verbessern. Lassen Sie uns zunächst einige Partikeleffekte erstellen, damit wir unsere Änderungen tatsächlich testen können.
Feindliche Explosionen
Deklarieren Sie in GameRoot einen neuen ParticleManager und rufen Sie dessen Methoden update() und draw() auf:
1 |
// in GameRoot
|
2 |
protected: |
3 |
ParticleManager mParticleManager; |
4 |
|
5 |
public: |
6 |
ParticleManager* getParticleManager() |
7 |
{
|
8 |
return &mParticleManager; |
9 |
}
|
10 |
|
11 |
// in GameRoot's constructor
|
12 |
GameRoot::GameRoot() |
13 |
: mParticleManager(1024 * 20), |
14 |
mViewportSize(800, 600), |
15 |
mSpriteBatch(NULL) |
16 |
{
|
17 |
}
|
18 |
|
19 |
// in GameRoot::onRedrawView()
|
20 |
mParticleManager.update(); |
21 |
mParticleManager.draw(mSpriteBatch); |
Außerdem deklarieren wir eine neue Instanz der tTexture-Klasse in der Art-Klasse mit dem Namen mLineParticle für die Textur des Partikels. Wir laden es wie die Sprites des anderen Spiels:
1 |
//In Art's constructor
|
2 |
mLineParticle = new tTexture(tSurface("laser.png")); |
Lassen wir jetzt Feinde explodieren. Wir werden die Enemy::wasShot() -Methode wie folgt ändern:
1 |
void Enemy::wasShot() |
2 |
{
|
3 |
mIsExpired = true; |
4 |
|
5 |
for (int i = 0; i < 120; i++) |
6 |
{
|
7 |
float speed = 18.0f * (1.0f - 1 / Extensions::nextFloat(1, 10)); |
8 |
ParticleState state(Extensions::nextVector2(speed, speed), ParticleState::kEnemy, 1); |
9 |
|
10 |
tColor4f color(0.56f, 0.93f, 0.56f, 1.0f); |
11 |
GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getLineParticle(), mPosition, color, 190, 1.5f, state); |
12 |
}
|
13 |
}
|
Dadurch entstehen 120 Partikel, die mit unterschiedlichen Geschwindigkeiten in alle Richtungen nach außen schießen. Die zufällige Geschwindigkeit wird so gewichtet, dass sich Partikel eher in der Nähe der maximalen Geschwindigkeit bewegen. Dies führt dazu, dass sich mehr Partikel am Rand der Explosion befinden, wenn sie sich ausdehnt. Die Partikel halten 190 Frames oder etwas mehr als drei Sekunden.
Sie können das Spiel jetzt ausführen und beobachten, wie Feinde explodieren. Es müssen jedoch noch einige Verbesserungen für die Partikeleffekte vorgenommen werden.
Das erste Problem ist, dass die Partikel nach Ablauf ihrer Dauer abrupt verschwinden. Es wäre schöner, wenn sie sanft ausblenden könnten, aber gehen wir etwas weiter und lassen die Partikel heller leuchten, wenn sie sich schnell bewegen. Es sieht auch gut aus, wenn wir sich schnell bewegende Partikel verlängern und sich langsam bewegende Teilchen verkürzen.
Ändern Sie die Methode ParticleState.UpdateParticle() wie folgt (Änderungen werden hervorgehoben).
1 |
void ParticleState::updateParticle(Particle& particle) |
2 |
{
|
3 |
tVector2f vel = particle.mState.mVelocity; |
4 |
|
5 |
particle.mPosition += vel; |
6 |
particle.mOrientation = Extensions::toAngle(vel); |
7 |
|
8 |
float speed = vel.length(); |
9 |
float alpha = tMath::min(1.0f, tMath::min(particle.mPercentLife * 2, speed * 1.0f)); |
10 |
alpha *= alpha; |
11 |
|
12 |
particle.mColor.a = alpha; |
13 |
|
14 |
particle.mScale.x = particle.mState.mLengthMultiplier * tMath::min(tMath::min(1.0f, 0.2f * speed + 0.1f), alpha); |
15 |
|
16 |
// denormalized floats cause significant performance issues
|
17 |
if (fabs(vel.x) + fabs(vel.y) < 0.00000000001f) |
18 |
{
|
19 |
vel = tVector2f(0,0); |
20 |
}
|
21 |
|
22 |
vel *= 0.97f; // Particles gradually slow down |
23 |
particle.mState.mVelocity = vel; |
24 |
}
|
Die Explosionen sehen jetzt viel besser aus, aber sie haben alle die gleiche Farbe.

Wir können ihnen mehr Abwechslung geben, indem wir zufällige Farben wählen. Eine Methode zur Erzeugung zufälliger Farben besteht darin, die roten, blauen und grünen Komponenten zufällig auszuwählen. Dies führt jedoch zu vielen stumpfen Farben, und wir möchten, dass unsere Partikel ein neonlichtes Aussehen haben. Wir können mehr Kontrolle über unsere Farben haben, indem wir sie im HSV-Farbraum angeben. HSV steht für Farbton, Sättigung und Wert. Wir möchten Farben mit einem zufälligen Farbton, aber einer festen Sättigung und einem festen Wert auswählen. Wir brauchen eine Hilfsfunktion, die aus HSV-Werten eine Farbe erzeugen kann.
1 |
tColor4f ColorUtil::HSVToColor(float h, float s, float v) |
2 |
{
|
3 |
if (h == 0 && s == 0) |
4 |
{
|
5 |
return tColor4f(v, v, v, 1.0f); |
6 |
}
|
7 |
|
8 |
float c = s * v; |
9 |
float x = c * (1 - abs(int32_t(h) % 2 - 1)); |
10 |
float m = v - c; |
11 |
|
12 |
if (h < 1) return tColor4f(c + m, x + m, m, 1.0f); |
13 |
else if (h < 2) return tColor4f(x + m, c + m, m, 1.0f); |
14 |
else if (h < 3) return tColor4f(m, c + m, x + m, 1.0f); |
15 |
else if (h < 4) return tColor4f(m, x + m, c + m, 1.0f); |
16 |
else if (h < 5) return tColor4f(x + m, m, c + m, 1.0f); |
17 |
else return tColor4f(c + m, m, x + m, 1.0f); |
18 |
}
|
Jetzt können wir Enemy::wasShot() so ändern, dass zufällige Farben verwendet werden. Um die Explosionsfarbe weniger eintönig zu machen, wählen wir für jede Explosion zwei nahegelegene Schlüsselfarben aus und interpolieren sie für jedes Partikel linear um einen zufälligen Betrag:
1 |
void Enemy::wasShot() |
2 |
{
|
3 |
mIsExpired = true; |
4 |
|
5 |
float hue1 = Extensions::nextFloat(0, 6); |
6 |
float hue2 = fmodf(hue1 + Extensions::nextFloat(0, 2), 6.0f); |
7 |
tColor4f color1 = ColorUtil::HSVToColor(hue1, 0.5f, 1); |
8 |
tColor4f color2 = ColorUtil::HSVToColor(hue2, 0.5f, 1); |
9 |
|
10 |
for (int i = 0; i < 120; i++) |
11 |
{
|
12 |
float speed = 18.0f * (1.0f - 1 / Extensions::nextFloat(1, 10)); |
13 |
ParticleState state(Extensions::nextVector2(speed, speed), ParticleState::kEnemy, 1); |
14 |
|
15 |
tColor4f color = Extensions::colorLerp(color1, color2, Extensions::nextFloat(0, 1)); |
16 |
GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getLineParticle(), mPosition, color, 190, 1.5f, state); |
17 |
}
|
18 |
}
|
Die Explosionen sollten wie in der folgenden Animation aussehen:

Sie können mit der Farbgenerierung nach Ihren Wünschen herumspielen. Eine alternative Technik, die gut funktioniert, besteht darin, eine Reihe von Farbmustern für Explosionen von Hand auszuwählen und zufällig aus Ihren vorgewählten Farbschemata auszuwählen.
Kugelexplosionen
Wir können die Kugeln auch explodieren lassen, wenn sie den Bildschirmrand erreichen. Wir werden im Wesentlichen dasselbe tun, was wir für feindliche Explosionen getan haben.
Ändern wir Bullet::update() wie folgt:
1 |
if (!tRectf(0, 0, GameRoot::getInstance()->getViewportSize()).contains(tPoint2f((int32_t)mPosition.x, (int32_t)mPosition.y))) |
2 |
{
|
3 |
mIsExpired = true; |
4 |
|
5 |
|
6 |
for (int i = 0; i < 30; i++) |
7 |
{
|
8 |
GameRoot::getInstance()->getParticleManager()->createParticle( |
9 |
Art::getInstance()->getLineParticle(), |
10 |
mPosition, |
11 |
tColor4f(0.67f, 0.85f, 0.90f, 1), 50, 1, |
12 |
ParticleState(Extensions::nextVector2(0, 9), ParticleState::kBullet, 1)); |
13 |
}
|
14 |
}
|
Möglicherweise stellen Sie fest, dass es verschwenderisch ist, den Partikeln eine zufällige Richtung zu geben, da mindestens die Hälfte der Partikel sofort vom Bildschirm abweicht (mehr, wenn die Kugel in einer Ecke explodiert). Wir könnten zusätzliche Arbeit leisten, um sicherzustellen, dass Partikel nur Geschwindigkeiten erhalten, die der Wand gegenüberliegen, der sie zugewandt sind. Stattdessen nehmen wir einen Hinweis aus Geometry Wars und lassen alle Partikel von den Wänden abprallen, sodass alle Partikel, die sich außerhalb des Bildschirms befinden, zurückgeworfen werden.
Fügen Sie ParticleState.UpdateParticle() irgendwo zwischen der ersten und der letzten Zeile die folgenden Zeilen hinzu:
1 |
tVector2f pos = particle.mPosition; |
2 |
int width = (int)GameRoot::getInstance()->getViewportSize().width; |
3 |
int height = (int)GameRoot::getInstance()->getViewportSize().height; |
4 |
|
5 |
// collide with the edges of the screen
|
6 |
if (pos.x < 0) |
7 |
{
|
8 |
vel.x = (float)fabs(vel.x); |
9 |
}
|
10 |
else if (pos.x > width) |
11 |
{
|
12 |
vel.x = (float)-fabs(vel.x); |
13 |
}
|
14 |
|
15 |
if (pos.y < 0) |
16 |
{
|
17 |
vel.y = (float)fabs(vel.y); |
18 |
}
|
19 |
else if (pos.y > height) |
20 |
{
|
21 |
vel.y = (float)-fabs(vel.y); |
22 |
}
|
Schiffsexplosion des Spielers
Wir werden eine wirklich große Explosion machen, wenn der Spieler getötet wird. Ändern Sie PlayerShip::kill() wie folgt:
1 |
void PlayerShip::kill() |
2 |
{
|
3 |
PlayerStatus::getInstance()->removeLife(); |
4 |
mFramesUntilRespawn = PlayerStatus::getInstance()->getIsGameOver() ? 300 : 120; |
5 |
|
6 |
tColor4f explosionColor = tColor4f(0.8f, 0.8f, 0.4f, 1.0f); |
7 |
|
8 |
for (int i = 0; i < 1200; i++) |
9 |
{
|
10 |
float speed = 18.0f * (1.0f - 1 / Extensions::nextFloat(1.0f, 10.0f)); |
11 |
tColor4f color = Extensions::colorLerp(tColor4f(1,1,1,1), explosionColor, Extensions::nextFloat(0, 1)); |
12 |
ParticleState state(Extensions::nextVector2(speed, speed), ParticleState::kNone, 1); |
13 |
|
14 |
GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getLineParticle(), mPosition, color, 190, 1.5f, state); |
15 |
}
|
16 |
}
|
Dies ähnelt den feindlichen Explosionen, aber wir verwenden mehr Partikel und verwenden immer das gleiche Farbschema. Der Partikeltyp ist ebenfalls auf ParticleState::kNone festgelegt.
In der Demo verlangsamen sich Partikel von feindlichen Explosionen schneller als Partikel vom explodierenden Schiff des Spielers. Dadurch hält die Explosion des Spielers etwas länger an und sieht etwas epischer aus.
Schwarze Löcher erneut besucht
Nachdem wir Partikeleffekte haben, lassen Sie uns die Schwarzen Löcher erneut betrachten und sie mit Partikeln interagieren lassen.
Wirkung auf Partikel
Schwarze Löcher sollten neben anderen Entitäten auch Partikel betreffen. Daher müssen wir ParticleState::updateParticle() ändern. Fügen wir die folgenden Zeilen hinzu:
1 |
if (particle.mState.mType != kIgnoreGravity) |
2 |
{
|
3 |
for (std::list<BlackHole*>::iterator j = EntityManager::getInstance()->mBlackHoles.begin(); j != EntityManager::getInstance()->mBlackHoles.end(); j++) |
4 |
{
|
5 |
tVector2f dPos = (*j)->getPosition() - pos; |
6 |
float distance = dPos.length(); |
7 |
tVector2f n = dPos / distance; |
8 |
vel += 10000.0f * n / (distance * distance + 10000.0f); |
9 |
|
10 |
// add tangential acceleration for nearby particles
|
11 |
if (distance < 400) |
12 |
{
|
13 |
vel += 45.0f * tVector2f(n.y, -n.x) / (distance + 100.0f); |
14 |
}
|
15 |
}
|
16 |
}
|
Hier ist n der Einheitsvektor, der auf das Schwarze Loch zeigt. Die Anziehungskraft ist eine modifizierte Version der inversen Quadratfunktion:
- Die erste Modifikation ist, dass der Nenner
distance^2 + 10,000; Dies bewirkt, dass sich die Anziehungskraft einem Maximalwert nähert, anstatt gegen unendlich zu tendieren, wenn der Abstand sehr klein wird.- Wenn der Abstand viel größer als 100 Pixel ist, wird der
distance^2viel größer als 10.000. Daher hat das Hinzufügen von 10.000 zumdistance^2einen sehr geringen Effekt, und die Funktion nähert sich einer normalen inversen Quadratfunktion an. - Wenn der Abstand jedoch viel kleiner als 100 Pixel ist, hat der Abstand einen geringen Einfluss auf den Wert des Nenners, und die Gleichung wird ungefähr gleich:
vel += n
- Wenn der Abstand viel größer als 100 Pixel ist, wird der
- Die zweite Modifikation ist das Hinzufügen einer Seitwärtskomponente zur Geschwindigkeit, wenn die Partikel nahe genug an das Schwarze Loch heranreichen. Dies dient zwei Zwecken:
- Dadurch drehen sich die Partikel im Uhrzeigersinn in Richtung des Schwarzen Lochs.
- Wenn die Partikel nahe genug kommen, erreichen sie ein Gleichgewicht und bilden einen leuchtenden Kreis um das Schwarze Loch.
Tipp: Um einen Vektor V um 90° im Uhrzeigersinn zu drehen, nehmen Sie (V.Y, -V.X). Um es um 90 ° gegen den Uhrzeigersinn zu drehen, nimm (-V.Y, V.X).
Partikel produzieren
Ein Schwarzes Loch erzeugt zwei Arten von Partikeln. Erstens sprüht es regelmäßig Partikel aus, die um es herum kreisen. Zweitens, wenn ein Schwarzes Loch geschossen wird, sprüht es spezielle Partikel aus, die nicht von seiner Schwerkraft beeinflusst werden.
Fügen Sie der BlackHole::WasShot() -Methode den folgenden Code hinzu:
1 |
float hue = fmodf(3.0f / 1000.0f * tTimer::getTimeMS(), 6); |
2 |
tColor4f color = ColorUtil::HSVToColor(hue, 0.25f, 1); |
3 |
const int numParticles = 150; |
4 |
float startOffset = Extensions::nextFloat(0, tMath::PI * 2.0f / numParticles); |
5 |
|
6 |
for (int i = 0; i < numParticles; i++) |
7 |
{
|
8 |
tVector2f sprayVel = MathUtil::fromPolar(tMath::PI * 2.0f * i / numParticles + startOffset, Extensions::nextFloat(8, 16)); |
9 |
tVector2f pos = mPosition + 2.0f * sprayVel; |
10 |
ParticleState state(sprayVel, ParticleState::kIgnoreGravity, 1.0f); |
11 |
|
12 |
GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getLineParticle(), pos, color, 90, 1.5f, state); |
13 |
}
|
Dies funktioniert größtenteils genauso wie die anderen Partikelexplosionen. Ein Unterschied besteht darin, dass wir den Farbton basierend auf der insgesamt verstrichenen Spielzeit auswählen. Wenn Sie mehrmals hintereinander auf das Schwarze Loch schießen, wird sich der Farbton der Explosionen allmählich drehen. Dies sieht weniger chaotisch aus als die Verwendung zufälliger Farben, lässt aber dennoch Variationen zu.
Für das umlaufende Partikelspray müssen wir der BlackHole-Klasse eine Variable hinzufügen, um die Richtung zu verfolgen, in die wir derzeit Partikel sprühen:
1 |
protected:
|
2 |
int mHitPoints; |
3 |
float mSprayAngle; |
4 |
|
5 |
BlackHole::BlackHole(const tVector2f& position) |
6 |
: mSprayAngle(0) |
7 |
{
|
8 |
...
|
9 |
}
|
Jetzt fügen wir der BlackHole::update() -Methode Folgendes hinzu.
1 |
// The black holes spray some orbiting particles. The spray toggles on and off every quarter second.
|
2 |
if ((tTimer::getTimeMS() / 250) % 2 == 0) |
3 |
{
|
4 |
tVector2f sprayVel = MathUtil::fromPolar(mSprayAngle, Extensions::nextFloat(12, 15)); |
5 |
tColor4f color = ColorUtil::HSVToColor(5, 0.5f, 0.8f); |
6 |
tVector2f pos = mPosition + 2.0f * tVector2f(sprayVel.y, -sprayVel.x) + Extensions::nextVector2(4, 8); |
7 |
ParticleState state(sprayVel, ParticleState::kEnemy, 1.0f); |
8 |
|
9 |
GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getLineParticle(), pos, color, 190, 1.5f, state); |
10 |
}
|
11 |
|
12 |
// rotate the spray direction
|
13 |
mSprayAngle -= tMath::PI * 2.0f / 50.0f; |
Dies führt dazu, dass die Schwarzen Löcher Spritzer von lila Partikeln sprühen, die einen Ring bilden, der das Schwarze Loch umkreist, wie folgt:
Schiffsabgasfeuer
Gemäß den Gesetzen der geometrischen Neonphysik treibt sich das Schiff des Spielers an, indem es einen Strom feuriger Partikel aus seinem Auspuffrohr spritzt. Mit unserer Partikel-Engine ist dieser Effekt einfach zu erzielen und verleiht der Schiffsbewegung ein visuelles Flair.
Während sich das Schiff bewegt, erzeugen wir drei Partikelströme: einen Mittelstrom, der direkt aus dem Schiffsrücken herausfeuert, und zwei Seitenströme, deren Winkel sich relativ zum Schiff hin und her drehen. Die beiden Seitenströme schwenken in entgegengesetzte Richtungen, um ein sich kreuzendes Muster zu bilden. Die Seitenströme haben eine rötlichere Farbe, während der Mittelstrom eine heißere gelb-weiße Farbe hat. Die folgende Animation zeigt den Effekt:



Damit das Feuer heller leuchtet, wird das Schiff zusätzliche Partikel abgeben, die so aussehen:

Diese Partikel werden getönt und mit den regulären Partikeln gemischt. Der Code für den gesamten Effekt ist unten dargestellt:
1 |
void PlayerShip::MakeExhaustFire() |
2 |
{
|
3 |
if (mVelocity.lengthSquared() > 0.1f) |
4 |
{
|
5 |
mOrientation = Extensions::toAngle(mVelocity); |
6 |
|
7 |
float cosA = cosf(mOrientation); |
8 |
float sinA = sinf(mOrientation); |
9 |
tMatrix2x2f rot(tVector2f(cosA, sinA), |
10 |
tVector2f(-sinA, cosA)); |
11 |
|
12 |
float t = tTimer::getTimeMS() / 1000.0f; |
13 |
|
14 |
tVector2f baseVel = Extensions::scaleTo(mVelocity, -3); |
15 |
|
16 |
tVector2f perpVel = tVector2f(baseVel.y, -baseVel.x) * (0.6f * (float)sinf(t * 10.0f)); |
17 |
tColor4f sideColor(0.78f, 0.15f, 0.04f, 1); |
18 |
tColor4f midColor(1.0f, 0.73f, 0.12f, 1); |
19 |
tVector2f pos = mPosition + rot * tVector2f(-25, 0); // position of the ship's exhaust pipe. |
20 |
const float alpha = 0.7f; |
21 |
|
22 |
// middle particle stream
|
23 |
tVector2f velMid = baseVel + Extensions::nextVector2(0, 1); |
24 |
GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getLineParticle(), |
25 |
pos, tColor4f(1,1,1,1) * alpha, 60.0f, tVector2f(0.5f, 1), |
26 |
ParticleState(velMid, ParticleState::kEnemy)); |
27 |
GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getGlow(), pos, midColor * alpha, 60.0f, tVector2f(0.5f, 1), |
28 |
ParticleState(velMid, ParticleState::kEnemy)); |
29 |
|
30 |
// side particle streams
|
31 |
tVector2f vel1 = baseVel + perpVel + Extensions::nextVector2(0, 0.3f); |
32 |
tVector2f vel2 = baseVel - perpVel + Extensions::nextVector2(0, 0.3f); |
33 |
GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getLineParticle(), |
34 |
pos, tColor4f(1,1,1,1) * alpha, 60.0f, tVector2f(0.5f, 1), |
35 |
ParticleState(vel1, ParticleState::kEnemy)); |
36 |
GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getLineParticle(), |
37 |
pos, tColor4f(1,1,1,1) * alpha, 60.0f, tVector2f(0.5f, 1), |
38 |
ParticleState(vel2, ParticleState::kEnemy)); |
39 |
|
40 |
GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getGlow(), |
41 |
pos, sideColor * alpha, 60.0f, tVector2f(0.5f, 1), |
42 |
ParticleState(vel1, ParticleState::kEnemy)); |
43 |
GameRoot::getInstance()->getParticleManager()->createParticle(Art::getInstance()->getGlow(), |
44 |
pos, sideColor * alpha, 60.0f, tVector2f(0.5f, 1), |
45 |
ParticleState(vel2, ParticleState::kEnemy)); |
46 |
}
|
47 |
}
|
In diesem Code ist nichts hinterhältiges los. Wir verwenden eine Sinusfunktion, um den Schwenkeffekt in den Seitenströmen zu erzeugen, indem wir ihre Seitengeschwindigkeit über die Zeit variieren. Für jeden Stream erstellen wir zwei überlappende Partikel pro Frame: ein halbtransparentes, weißes LineParticle und ein farbiges Glow-Partikel dahinter. AnrufMakeExhaustFire() am Ende von PlayerShip.Update(), unmittelbar bevor die Schiffsgeschwindigkeit auf Null gesetzt wird.
Abschluss
Mit all diesen Partikeleffekten sieht Shape Blaster ziemlich cool aus. Im letzten Teil dieser Serie werden wir einen weiteren großartigen Effekt hinzufügen: das Warping-Hintergrundgitter.



