Crea uno sparatutto al neon per iOS effetti particellari

In questa serie di tutorial, ti mostrerò come creare uno sparatutto twin-stick ispirato a Geometry Wars, con grafica al neon, effetti pazzeschi e musica fantastica, per iOS che utilizza C ++ e OpenGL ES 2.0. In questa parte, aggiungeremo esplosioni e stile visivo.

Panoramica

Nella serie finora, abbiamo impostato il gameplay e aggiunto i controlli virtuali del gamepad. Successivamente, aggiungeremo effetti particellari.


Avvertenza: forte!

Gli effetti particellari vengono creati facendo un gran numero di piccole particelle. Sono molto versatili e possono essere usati per aggiungere stile a quasi tutti i giochi. In Shape Blaster creeremo esplosioni usando effetti particellari. Useremo anche effetti particellari per creare fuoco di scarico per la nave del giocatore e per aggiungere un tocco visivo ai buchi neri. Inoltre, vedremo come far interagire le particelle con la gravità dai buchi neri.

Passa alle build di rilascio per i guadagni di velocità

Fino ad ora, probabilmente state costruendo ed eseguendo Shape Blaster usando tutti i valori predefiniti mettere a punto costruzione del progetto. Mentre questo è ok e ottimo quando esegui il debug del tuo codice, il debug disattiva la maggior parte delle ottimizzazioni di velocità e matematica che è possibile eseguire, oltre a mantenere attive tutte le asserzioni nel codice.

In effetti, se esegui il codice in modalità di debug da qui in avanti, noterai che l'inizio della frequenza fotogrammi diminuisce drasticamente. Ciò è dovuto al fatto che abbiamo scelto come target un dispositivo che ha una quantità ridotta di RAM, clock clockspeed e hardware 3D più piccolo rispetto a un computer desktop o persino a un laptop.

Quindi a questo punto puoi disattivare il debugging e attivare la modalità "rilascio". La modalità di rilascio ci offre l'ottimizzazione completa del compilatore e della matematica, oltre alla rimozione del codice di debug inutilizzato e delle asserzioni.

Una volta aperto il progetto, scegli il Prodotto menu, schema, poi Modifica schema ... .


Si aprirà la seguente finestra di dialogo. Scegliere Correre sul lato sinistro della finestra di dialogo e da Costruisci la configurazione, cambia l'elemento a comparsa da mettere a punto a pubblicazione.


Noterai immediatamente i guadagni di velocità. Il processo è facilmente reversibile se è necessario eseguire nuovamente il debug del programma: basta scegliere mettere a punto invece di pubblicazione e hai finito.

Mancia: Si noti tuttavia che qualsiasi modifica dello schema come questa richiede una ricompilazione completa del programma.

La classe ParticleManager

Inizieremo creando un ParticleManager classe che memorizzerà, aggiornerà e disegnerà tutte le particelle. Renderemo questa classe abbastanza generale da poter essere facilmente riutilizzata in altri progetti, ma richiederà comunque alcune personalizzazioni da progetto a progetto. Per mantenere il ParticleManager nel modo più generale possibile, non sarà responsabile del modo in cui le particelle appaiono o si muovono; lo gestiremo altrove.

Le particelle tendono a essere create e distrutte rapidamente e in gran numero. Useremo un pool di oggetti per evitare di creare grandi quantità di spazzatura. Ciò significa che alloceremo un gran numero di particelle in primo piano e quindi continueremo a riutilizzare queste stesse particelle.

Faremo anche ParticleManager avere una capacità fissa. Ciò lo semplificherà e ci aiuterà a garantire che non superiamo le nostre prestazioni o limiti di memoria creando troppe particelle. Quando viene superato il numero massimo di particelle, inizieremo a sostituire le particelle più vecchie con quelle nuove. Faremo il ParticleManager una classe generica. Ciò ci consentirà di archiviare le informazioni di stato personalizzate per le particelle senza codificarle nel codice
ParticleManager si.

Creeremo anche un particella classe:

 class Particle public: ParticleState mState; tColor4f mColor; tVector2f mPosition; tVector2f mScale; tTexture * mTexture; float mOrientation; float mDuration; float mPercentLife; public: Particle (): mScale (1,1), mPercentLife (1.0f) ;

Il particella classe ha tutte le informazioni necessarie per visualizzare una particella e gestirne la durata. ParticleState è lì per contenere tutti i dati aggiuntivi di cui potremmo aver bisogno per le nostre particelle. Quali dati sono necessari varieranno a seconda degli effetti particellari desiderati; potrebbe essere utilizzato per memorizzare velocità, accelerazione, velocità di rotazione o qualsiasi altra cosa che potrebbe essere necessario.

Per aiutare a gestire le particelle, avremo bisogno di una classe che funzioni come un array circolare, nel senso che gli indici che normalmente sarebbero fuori limite verranno invece avvolti all'inizio dell'array. Ciò renderà facile sostituire prima le particelle più vecchie se esauriamo lo spazio per nuove particelle nel nostro array. Per questo, aggiungiamo il seguente come una classe annidata in ParticleManager:

 class CircularParticleArray protected: std :: vector mlist; size_t mStart; size_t mCount; public: CircularParticleArray (int capacity) mList.resize ((size_t) capacity);  size_t getStart () return mStart;  void setStart (valore size_t) mStart = valore% mList.size ();  size_t getCount () return mCount;  void setCount (valore size_t) mCount = valore;  size_t getCapacity () return mList.size ();  Particle & operator [] (const size_t i) return mList [(mStart + i)% mList.size ()];  const Particle & operator [] (const size_t i) const return mList [(mStart + i)% mList.size ()]; ;

Possiamo impostare il MStart membro per regolare dove indice zero nel nostro CircularParticleArray corrisponde all'array sottostante, e mCount verrà utilizzato per tenere traccia di quante particelle attive sono presenti nell'elenco. Faremo in modo che la particella a zero indice sia sempre la particella più vecchia. Se sostituiamo la particella più vecchia con una nuova, semplicemente aumenteremo MStart, che sostanzialmente ruota l'array circolare.

Ora che abbiamo le nostre classi di aiuto, possiamo iniziare a compilare il ParticleManager classe. Avremo bisogno di una nuova variabile membro e di un costruttore.

 CircularParticleArray mParticleList; ParticleManager :: ParticleManager (capacità int): mParticleList (capacità) 

Noi creiamo mParticleList e riempirlo con particelle vuote. Il costruttore è l'unico posto in cui il ParticleManager alloca la memoria.

Successivamente, aggiungiamo il createParticle () metodo, che crea una nuova particella usando la particella successiva non utilizzata nel pool, o la particella più vecchia se non ci sono particelle inutilizzate.

 void ParticleManager :: createParticle (trama tTexture *, const tVector2f e posizione, const tColor4f & tint, durata float, const tVector2f & scale, const ParticleState e stato, float theta) size_t index; if (mParticleList.getCount () == mParticleList.getCapacity ()) index = 0; mParticleList.setStart (mParticleList.getStart () + 1);  else index = mParticleList.getCount (); mParticleList.setCount (mParticleList.getCount () + 1);  Particle & ref = mParticleList [indice]; ref.mTexture = texture; ref.mPosition = posizione; ref.mColor = tinta; ref.mDuration = duration; ref.mPercentLife = 1.0f; ref.mScale = scala; ref.mOrientation = theta; ref.mState = state; 

Le particelle possono essere distrutte in qualsiasi momento. Dobbiamo rimuovere queste particelle mentre assicuriamo che le altre particelle rimangano nello stesso ordine. Possiamo farlo ripetendo l'elenco delle particelle tenendo traccia di quante sono state distrutte. Mentre andiamo, spostiamo ogni particella attiva davanti a tutte le particelle distrutte scambiandola con la prima particella distrutta. Una volta che tutte le particelle distrutte sono alla fine della lista, le disattiviamo impostando la lista mCount variabile al numero di particelle attive. Le particelle distrutte rimarranno nell'array sottostante, ma non verranno aggiornate o disegnate.

ParticleManager :: update () gestisce l'aggiornamento di ogni particella e la rimozione di particelle distrutte dall'elenco:

 void ParticleManager :: update () size_t removalCount = 0; per (size_t i = 0; i < mParticleList.getCount(); i++)  Particle& ref = mParticleList[i]; ref.mState.updateParticle(ref); ref.mPercentLife -= 1.0f / ref.mDuration; Swap(mParticleList, i - removalCount, i); if (ref.mPercentLife < 0)  removalCount++;   mParticleList.setCount(mParticleList.getCount() - removalCount);  void ParticleManager::Swap(typename ParticleManager::CircularParticleArray& list, size_t index1, size_t index2) const  Particle temp = list[index1]; list[index1] = list[index2]; list[index2] = temp; 

L'ultima cosa da implementare in ParticleManager sta disegnando le particelle:

 void ParticleManager :: draw (tSpriteBatch * spriteBatch) for (size_t i = 0; i < mParticleList.getCount(); i++)  Particle particle = mParticleList[(size_t)i]; tPoint2f origin = particle.mTexture->getSurfaceSize () / 2; spriteBatch-> draw (2, particle.mTexture, tPoint2f ((int) particle.mPosition.x, (int) particle.mPosition.y), tOptional(), particle.mColor, particle.mOrientation, origin, particle.mScale); 

La classe ParticleState

La prossima cosa da fare è creare una classe o una struttura personalizzata per personalizzare l'aspetto delle particelle in Shape Blaster. Ci saranno diversi tipi di particelle in Shape Blaster che si comportano in modo leggermente diverso, quindi inizieremo creando un enum per il tipo di particella. Avremo anche bisogno di variabili per la velocità della particella e la lunghezza iniziale.

 class ParticleState public: enum ParticleType kNone = 0, kEnemy, kBullet, kIgnoreGravity; public: tVector2f mVelocity; ParticleType mType; float mLengthMultiplier; pubblico: ParticleState (); ParticleState (const tVector2f & velocity, tipo ParticleType, float lengthMultiplier = 1.0f); ParticleState getRandom (float minVel, float maxVel); void updateParticle (Particle & particle); ;

Ora siamo pronti a scrivere le particelle aggiornare() metodo. È una buona idea rendere questo metodo veloce, dal momento che potrebbe essere necessario chiamare un gran numero di particelle.

Inizieremo alla semplicità. Aggiungiamo il seguente metodo a ParticleState:

 void ParticleState :: updateParticle (Particle & particle) tVector2f vel = particle.mState.mVelocity; particle.mPosition + = vel; particle.mOrientation = Extensions :: toAngle (vel); // i float denormalizzati causano problemi di prestazioni significativi se (fabs (vel.x) + fabs (vel.y) < 0.00000000001f)  vel = tVector2f(0,0);  vel *= 0.97f; // Particles gradually slow down particle.mState.mVelocity = vel; 

Torneremo e miglioreremo questo metodo tra un momento. Innanzitutto, creiamo alcuni effetti particellari in modo che possiamo testare realmente le nostre modifiche.

Esplosioni nemiche

Nel GameRoot, dichiarare un nuovo ParticleManager e chiamatelo aggiornare() e disegnare() metodi:

 // in GameRoot protected: ParticleManager mParticleManager; public: ParticleManager * getParticleManager () return & mParticleManager;  // nel costruttore di GameRoot GameRoot :: GameRoot (): mParticleManager (1024 * 20), mViewportSize (800, 600), mSpriteBatch (NULL)  // in GameRoot :: onRedrawView () mParticleManager.update (); mParticleManager.draw (mSpriteBatch);

Inoltre, dichiareremo una nuova istanza di tTexture classe nel Arte classe chiamata mLineParticle per la consistenza della particella. Lo caricheremo come facciamo con gli sprite dell'altro gioco:

 // Nel costruttore di Art mLineParticle = new tTexture (tSurface ("laser.png"));

Ora facciamo esplodere i nemici. Modificheremo il Enemy :: wasShot () metodo come segue:

 void Enemy :: wasShot () mIsExpired = true; per (int i = 0; i < 120; i++)  float speed = 18.0f * (1.0f - 1 / Extensions::nextFloat(1, 10)); ParticleState state(Extensions::nextVector2(speed, speed), ParticleState::kEnemy, 1); tColor4f color(0.56f, 0.93f, 0.56f, 1.0f); GameRoot::getInstance()->getParticleManager () -> createParticle (Art :: getInstance () -> getLineParticle (), mPosition, colore, 190, 1.5f, stato); 

Questo crea 120 particelle che spareranno verso l'esterno con velocità diverse in tutte le direzioni. La velocità casuale è ponderata in modo tale che le particelle abbiano maggiori probabilità di spostarsi vicino alla velocità massima. Questo farà sì che più particelle si trovino sul bordo dell'esplosione mentre si espande. Le particelle durano 190 frame, o poco più di tre secondi.

Ora puoi giocare e vedere i nemici esplodere. Tuttavia, ci sono ancora alcuni miglioramenti da apportare agli effetti particellari.

Il primo problema è che le particelle scompaiono bruscamente una volta esaurita la loro durata. Sarebbe più bello se potessero sfumare gradualmente, ma andiamo un po 'oltre e rendere le particelle più luminose quando si muovono velocemente. Inoltre, è bello se allunghiamo le particelle in rapido movimento e accorciamo quelle lente.

Modifica il ParticleState.UpdateParticle () metodo come segue (le modifiche sono evidenziate).

 void ParticleState :: updateParticle (Particle & particle) tVector2f vel = particle.mState.mVelocity; particle.mPosition + = vel; particle.mOrientation = Extensions :: toAngle (vel); velocità di galleggiamento = vel.length (); float alpha = tMath :: min (1.0f, tMath :: min (particle.mPercentLife * 2, speed * 1.0f)); alfa * = alfa; particle.mColor.a = alfa; particle.mScale.x = particle.mState.mLengthMultiplier * tMath :: min (tMath :: min (1.0f, 0.2f * speed + 0.1f), alpha); // i float denormalizzati causano problemi di prestazioni significativi se (fabs (vel.x) + fabs (vel.y) < 0.00000000001f)  vel = tVector2f(0,0);  vel *= 0.97f; // Particles gradually slow down particle.mState.mVelocity = vel; 

Le esplosioni sembrano molto meglio ora, ma sono tutte dello stesso colore.

Le esplosioni monocromatiche sono un buon inizio, ma possiamo fare di meglio?

Possiamo dare loro più varietà scegliendo colori casuali. Un metodo per produrre colori casuali consiste nel scegliere casualmente i componenti rosso, blu e verde, ma questo produrrà un sacco di colori opachi e vorremmo che le nostre particelle avessero un aspetto di luce al neon. Possiamo avere maggiore controllo sui nostri colori specificandoli nello spazio colore HSV. HSV sta per tonalità, saturazione e valore. Vorremmo scegliere i colori con una tonalità casuale ma con una saturazione e un valore fissi. Abbiamo bisogno di una funzione di supporto che possa produrre un colore dai valori HSV.

 tColor4f ColorUtil :: HSVToColor (float h, float s, float v) if (h == 0 && s == 0) return tColor4f (v, v, v, 1.0f);  float c = s * v; float x = c * (1 - abs (int32_t (h)% 2 - 1)); float m = v - c; se (h < 1) return tColor4f(c + m, x + m, m, 1.0f); else if (h < 2) return tColor4f(x + m, c + m, m, 1.0f); else if (h < 3) return tColor4f(m, c + m, x + m, 1.0f); else if (h < 4) return tColor4f(m, x + m, c + m, 1.0f); else if (h < 5) return tColor4f(x + m, m, c + m, 1.0f); else return tColor4f(c + m, m, x + m, 1.0f); 

Ora possiamo modificare Enemy :: wasShot () usare colori casuali. Per rendere il colore dell'esplosione meno monotono, selezioneremo due colori chiave nelle vicinanze per ogni esplosione e interpoleremo linearmente tra di loro di una quantità casuale per ogni particella:

 void Enemy :: wasShot () mIsExpired = true; float hue1 = Estensioni :: nextFloat (0, 6); float hue2 = fmodf (hue1 + Estensioni :: nextFloat (0, 2), 6.0f); tColor4f color1 = ColorUtil :: HSVToColor (hue1, 0.5f, 1); tColor4f color2 = ColorUtil :: HSVToColor (hue2, 0.5f, 1); per (int i = 0; i < 120; i++)  float speed = 18.0f * (1.0f - 1 / Extensions::nextFloat(1, 10)); ParticleState state(Extensions::nextVector2(speed, speed), ParticleState::kEnemy, 1); tColor4f color = Extensions::colorLerp(color1, color2, Extensions::nextFloat(0, 1)); GameRoot::getInstance()->getParticleManager () -> createParticle (Art :: getInstance () -> getLineParticle (), mPosition, colore, 190, 1.5f, stato); 

Le esplosioni dovrebbero assomigliare all'animazione seguente:


Puoi giocare con la generazione del colore per soddisfare le tue preferenze. Una tecnica alternativa che funziona bene è scegliere una serie di modelli di colori per le esplosioni e selezionare casualmente dalle combinazioni di colori preselezionate.

Esplosioni di proiettili

Possiamo anche far esplodere i proiettili quando raggiungono il bordo dello schermo. Faremo essenzialmente la stessa cosa che abbiamo fatto per le esplosioni nemiche.

Modifichiamo Proiettile :: update () come segue:

 if (! tRectf (0, 0, GameRoot :: getInstance () -> getViewportSize ()). contiene (tPoint2f ((int32_t) mPosition.x, (int32_t) mPosition.y))) mIsExpired = true; per (int i = 0; i < 30; i++)  GameRoot::getInstance()->getParticleManager () -> createParticle (Art :: getInstance () -> getLineParticle (), mPosition, tColor4f (0.67f, 0.85f, 0.90f, 1), 50, 1, ParticleState (Estensioni :: nextVector2 (0, 9) , ParticleState :: kBullet, 1)); 

Potresti notare che dare alle particelle una direzione casuale è dispendioso, perché almeno la metà delle particelle andrà immediatamente fuori dallo schermo (di più se il proiettile esplode in un angolo). Potremmo fare un po 'di lavoro extra per garantire che le particelle abbiano solo velocità opposte al muro che stanno affrontando. Invece, prenderemo spunto da Geometry Wars e faremo rimbalzare tutte le particelle sui muri, così tutte le particelle che si dirigono fuori dallo schermo verranno rimbalzate indietro.

Aggiungi le seguenti linee a ParticleState.UpdateParticle () ovunque tra la prima e l'ultima riga:

 tVector2f pos = particle.mPosition; int width = (int) GameRoot :: getInstance () -> getViewportSize (). width; int height = (int) GameRoot :: getInstance () -> getViewportSize (). height; // collide con i bordi dello schermo se (pos.x < 0)  vel.x = (float)fabs(vel.x);  else if (pos.x > larghezza) vel.x = (float) -fabs (vel.x);  if (pos.y < 0)  vel.y = (float)fabs(vel.y);  else if (pos.y > altezza) vel.y = (float) -fabs (vel.y); 

Esplosione della nave del giocatore

Faremo una grande esplosione quando il giocatore viene ucciso. Modificare PlayerShip :: kill () così:

 void PlayerShip :: kill () PlayerStatus :: getInstance () -> removeLife (); mFramesUntilRespawn = PlayerStatus :: getInstance () -> getIsGameOver ()? 300: 120; tColor4f explosionColor = tColor4f (0.8f, 0.8f, 0.4f, 1.0f); per (int i = 0; i < 1200; i++)  float speed = 18.0f * (1.0f - 1 / Extensions::nextFloat(1.0f, 10.0f)); tColor4f color = Extensions::colorLerp(tColor4f(1,1,1,1), explosionColor, Extensions::nextFloat(0, 1)); ParticleState state(Extensions::nextVector2(speed, speed), ParticleState::kNone, 1); GameRoot::getInstance()->getParticleManager () -> createParticle (Art :: getInstance () -> getLineParticle (), mPosition, colore, 190, 1.5f, stato); 

Questo è simile alle esplosioni nemiche, ma usiamo più particelle e usiamo sempre la stessa combinazione di colori. Anche il tipo di particella è impostato su ParticleState :: kNone.

Nella demo, le particelle provenienti dalle esplosioni nemiche rallentano più velocemente rispetto alle particelle esplosive della nave del giocatore. Ciò rende l'esplosione del giocatore durare un po 'più a lungo e sembrare un po' più epica.

I buchi neri rivisitati

Ora che abbiamo effetti particellari, rivisitiamo i buchi neri e li facciamo interagire con le particelle.

Effetto sulle particelle

I buchi neri dovrebbero influenzare le particelle in aggiunta ad altre entità, quindi dobbiamo modificarle ParticleState :: updateParticle (). Aggiungiamo le seguenti linee:

 if (particle.mState.mType! = kIgnoreGravity) for (std :: list:: iterator j = EntityManager :: getInstance () -> mBlackHoles.begin (); j! = EntityManager :: getInstance () -> mBlackHoles.end (); j ++) tVector2f dPos = (* j) -> getPosition () - pos; distanza di galleggiamento = dPos.length (); tVector2f n = dPos / distance; vel + = 10000.0f * n / (distanza * distanza + 10000.0f); // aggiungi l'accelerazione tangenziale per le particelle vicine se (distanza < 400)  vel += 45.0f * tVector2f(n.y, -n.x) / (distance + 100.0f);   

Qui, n è il vettore dell'unità che punta verso il buco nero. La forza attrattiva è una versione modificata della funzione inversa quadrata:

  • La prima modifica è che il denominatore è distanza ^ 2 + 10.000; questo fa sì che la forza attrattiva si avvicini a un valore massimo invece di tendere verso l'infinito quando la distanza diventa molto piccola.
    • Quando la distanza è molto maggiore di 100 pixel, distanza ^ 2 diventa molto più grande di 10.000. Pertanto, aggiungendo 10.000 a distanza ^ 2 ha un effetto molto piccolo e la funzione si avvicina ad una normale funzione inversa quadrata.
    • Tuttavia, quando la distanza è molto inferiore a 100 pixel, la distanza ha un piccolo effetto sul valore del denominatore e l'equazione diventa approssimativamente uguale a: vel + = n
  • La seconda modifica è l'aggiunta di un componente laterale alla velocità quando le particelle si avvicinano abbastanza al buco nero. Questo ha due scopi:
    1. Rende le particelle a spirale in senso orario verso il buco nero.
    2. Quando le particelle si avvicinano abbastanza, raggiungeranno l'equilibrio e formeranno un cerchio luminoso attorno al buco nero.

Mancia: Per ruotare un vettore, V, 90 ° in senso orario, prendere (V.Y, -V.X). Allo stesso modo, per ruotarlo di 90 ° in senso antiorario, prendere (-V.Y, V.X).

Produrre particelle

Un buco nero produrrà due tipi di particelle. In primo luogo, periodicamente spruzza le particelle che orbitano attorno ad esso. Secondo, quando un buco nero viene sparato, spruzza particelle speciali che non sono influenzate dalla sua gravità.

Aggiungi il seguente codice al BlackHole :: WasShot () metodo:

 float hue = fmodf (3.0f / 1000.0f * tTimer :: getTimeMS (), 6); tColor4f color = ColorUtil :: HSVToColor (hue, 0.25f, 1); const int numParticles = 150; float startOffset = Estensioni :: nextFloat (0, tMath :: PI * 2.0f / numParticles); per (int i = 0; i < numParticles; i++)  tVector2f sprayVel = MathUtil::fromPolar(tMath::PI * 2.0f * i / numParticles + startOffset, Extensions::nextFloat(8, 16)); tVector2f pos = mPosition + 2.0f * sprayVel; ParticleState state(sprayVel, ParticleState::kIgnoreGravity, 1.0f); GameRoot::getInstance()->getParticleManager () -> createParticle (Art :: getInstance () -> getLineParticle (), pos, color, 90, 1.5f, state); 

Questo funziona in gran parte allo stesso modo delle altre esplosioni di particelle. Una differenza è che selezioniamo la tonalità del colore in base al tempo di gioco totale trascorso. Se spari il buco nero più volte in rapida successione, vedrai la tonalità delle esplosioni gradualmente ruotare. Questo sembra meno disordinato rispetto all'uso di colori casuali, pur consentendo variazioni.

Per lo spruzzo di particelle orbitanti, dobbiamo aggiungere una variabile al Buco nero classe per tracciare la direzione in cui attualmente stiamo spruzzando le particelle:

 protetto: int mHitPoints; float mSprayAngle; BlackHole :: BlackHole (const tVector2f e position): mSprayAngle (0) ...

Ora aggiungeremo quanto segue al BlackHole :: update () metodo.

 // I buchi neri spruzzano alcune particelle orbitanti. Lo spray si accende e si spegne ogni quarto di secondo. if ((tTimer :: getTimeMS () / 250)% 2 == 0) tVector2f sprayVel = MathUtil :: fromPolar (mSprayAngle, Extensions :: nextFloat (12, 15)); tColor4f color = ColorUtil :: HSVToColor (5, 0.5f, 0.8f); tVector2f pos = mPosition + 2.0f * tVector2f (sprayVel.y, -sprayVel.x) + Estensioni :: nextVector2 (4, 8); Stato di ParticleState (sprayVel, ParticleState :: kEnemy, 1.0f); GameRoot :: getInstance () -> getParticleManager () -> createParticle (Art :: getInstance () -> getLineParticle (), pos, color, 190, 1.5f, state);  // ruota la direzione dello spruzzo mSprayAngle - = tMath :: PI * 2.0f / 50.0f;

Questo farà sì che i buchi neri spruzzino spruzzi di particelle viola che formeranno un anello che orbita intorno al buco nero, in questo modo:

Ship Exhaust Fire

Come dettato dalle leggi della fisica dei neon geometrici, la nave del giocatore si spinge facendo fuoriuscire un flusso di particelle infuocate fuori dal suo tubo di scarico. Con il nostro motore di particelle installato, questo effetto è facile da realizzare e aggiunge un tocco visivo al movimento della nave.

Mentre la nave si muove, creiamo tre flussi di particelle: un flusso centrale che spara direttamente sul retro della nave, e due flussi laterali i cui angoli ruotano avanti e indietro rispetto alla nave. I due flussi laterali si muovono in direzioni opposte per creare uno schema incrociato. I flussi laterali hanno un colore più rosso, mentre il flusso centrale ha un colore più caldo, giallo-bianco. L'animazione sotto mostra l'effetto:


Per rendere il fuoco più luminoso, avremo la nave emettere particelle aggiuntive che assomigliano a questo:


Queste particelle saranno colorate e mescolate con le particelle regolari. Il codice per l'intero effetto è mostrato di seguito:

 void PlayerShip :: MakeExhaustFire () if (mVelocity.lengthSquared ()> 0.1f) mOrientation = Extensions :: toAngle (mVelocity); float cosA = cosf (mOrientation); float sinA = sinf (mOrientation); tMatrix2x2f rot (tVector2f (cosA, sinA), tVector2f (-sinA, cosA)); float t = tTimer :: getTimeMS () / 1000.0f; tVector2f baseVel = Estensioni :: scaleTo (mVelocity, -3); tVector2f perpVel = tVector2f (baseVel.y, -baseVel.x) * (0.6f * (float) sinf (t * 10.0f)); tColor4f sideColor (0.78f, 0.15f, 0.04f, 1); tColor4f midColor (1.0f, 0.73f, 0.12f, 1); tVector2f pos = mPosition + rot * tVector2f (-25, 0); // posizione del tubo di scarico della nave. const float alpha = 0.7f; // flusso di particelle medie tVector2f velMid = baseVel + Estensioni :: nextVector2 (0, 1); GameRoot :: getInstance () -> getParticleManager () -> createParticle (Art :: getInstance () -> getLineParticle (), pos, tColor4f (1,1,1,1) * alpha, 60.0f, tVector2f (0.5f, 1), ParticleState (velMid, ParticleState :: kEnemy)); GameRoot :: getInstance () -> getParticleManager () -> createParticle (Art :: getInstance () -> getGlow (), pos, midColor * alpha, 60.0f, tVector2f (0.5f, 1), ParticleState (velMid, ParticleState: : kEnemy)); // flussi di particelle laterali tVector2f vel1 = baseVel + perpVel + Estensioni :: nextVector2 (0, 0,3f); tVector2f vel2 = baseVel - perpVel + Estensioni :: nextVector2 (0, 0,3f); GameRoot :: getInstance () -> getParticleManager () -> createParticle (Art :: getInstance () -> getLineParticle (), pos, tColor4f (1,1,1,1) * alpha, 60.0f, tVector2f (0.5f, 1), ParticleState (vel1, ParticleState :: kEnemy)); GameRoot :: getInstance () -> getParticleManager () -> createParticle (Art :: getInstance () -> getLineParticle (), pos, tColor4f (1,1,1,1) * alpha, 60.0f, tVector2f (0.5f, 1), ParticleState (vel2, ParticleState :: kEnemy)); GameRoot :: getInstance () -> getParticleManager () -> createParticle (Art :: getInstance () -> getGlow (), pos, sideColor * alpha, 60.0f, tVector2f (0.5f, 1), ParticleState (vel1, ParticleState: : kEnemy)); GameRoot :: getInstance () -> getParticleManager () -> createParticle (Art :: getInstance () -> getGlow (), pos, sideColor * alpha, 60.0f, tVector2f (0.5f, 1), ParticleState (vel2, ParticleState: : kEnemy)); 

Non c'è nulla di subdolo in questo codice. Usiamo una funzione seno per produrre l'effetto girevole nei flussi laterali variando la loro velocità laterale nel tempo. Per ogni flusso, creiamo due particelle sovrapposte per fotogramma: una semitrasparente, bianca LineParticle, e una particella di luce colorata dietro di esso. Chiamata
MakeExhaustFire () alla fine di PlayerShip.Update (), immediatamente prima di impostare la velocità della nave a zero.

Conclusione

Con tutti questi effetti particellari, Shape Blaster sta diventando piuttosto interessante. Nella parte finale di questa serie, aggiungeremo un altro effetto fantastico: la griglia di sfondo di orditura.