Haz un Neon Vector Shooter para iOS Efectos de partículas

En esta serie de tutoriales, te mostraré cómo hacer un shooter de doble palo inspirado en Geometry Wars, con gráficos de neón, efectos de partículas locas y música increíble, para iOS con C ++ y OpenGL ES 2.0. En esta parte, añadiremos explosiones y un toque visual..

Visión general

En la serie hasta ahora, hemos configurado el juego y agregado controles de gamepad virtual. A continuación, añadiremos efectos de partículas..


Advertencia: Alto!

Los efectos de partículas se crean al hacer un gran número de partículas pequeñas. Son muy versátiles y se pueden usar para agregar estilo a casi cualquier juego. En Shape Blaster realizaremos explosiones utilizando efectos de partículas. También usaremos efectos de partículas para crear fuego de escape para la nave del jugador y para agregar un toque visual a los agujeros negros. Además, veremos cómo hacer que las partículas interactúen con la gravedad de los agujeros negros.

Cambio a las versiones de lanzamiento para ganancias de velocidad

Hasta ahora, probablemente has estado construyendo y ejecutando Shape Blaster usando todos los valores predeterminados depurar construcción del proyecto. Si bien esto está bien y es excelente cuando está depurando su código, la depuración desactiva la mayoría de las optimizaciones de velocidad y matemáticas que se pueden realizar, además de mantener activadas todas las aserciones en el código..

De hecho, si ejecuta el código en modo de depuración de aquí en adelante, notará que la velocidad de cuadros comienza a disminuir drásticamente. Esto se debe a que nos dirigimos a un dispositivo que tiene una cantidad reducida de RAM, velocidad de reloj de CPU y hardware 3D más pequeño en comparación con una computadora de escritorio o incluso una computadora portátil.

Así que en este punto, opcionalmente, puede desactivar la depuración y activar el modo de "liberación". El modo de lanzamiento nos brinda una completa compilación y optimización matemática, así como la eliminación de código de depuración no utilizado y aserciones.

Una vez que abra el proyecto, elija la Producto menú, Esquema, entonces Editar esquema ... .


Se abrirá la siguiente ventana de diálogo. Escoger correr en el lado izquierdo del diálogo, y desde Construir la configuración, cambiar el elemento emergente de depurar a lanzamiento.


Notarás las ganancias de velocidad inmediatamente. El proceso se puede revertir fácilmente si necesita depurar el programa nuevamente: simplemente elija depurar en lugar de lanzamiento y tu estas listo.

Propina: Tenga en cuenta que cualquier cambio de esquema como este requiere una recompilación completa del programa.

La clase ParticleManager

Empezaremos creando un Administrador de partículas Clase que almacenará, actualizará y dibujará todas las partículas. Haremos que esta clase sea lo suficientemente general como para poder reutilizarla fácilmente en otros proyectos, pero aún requerirá cierta personalización de proyecto a proyecto. Para mantener el Administrador de partículas lo más general posible, no será responsable de cómo se ven o se mueven las partículas; manejaremos eso en otro lugar.

Las partículas tienden a ser creadas y destruidas rápidamente y en grandes cantidades. Usaremos un conjunto de objetos para evitar la creación de grandes cantidades de basura. Esto significa que asignaremos una gran cantidad de partículas por adelantado y luego seguiremos reutilizando estas mismas partículas..

También haremos Administrador de partículas tener una capacidad fija. Esto lo simplificará y ayudará a garantizar que no excedamos nuestro rendimiento o limitaciones de memoria al crear demasiadas partículas. Cuando se exceda el número máximo de partículas, comenzaremos a reemplazar las partículas más antiguas por otras nuevas. Haremos el Administrador de partículas una clase genérica Esto nos permitirá almacenar información de estado personalizada para las partículas sin tener que codificarla en el
Administrador de partículas sí mismo.

También crearemos un Partícula clase:

 clase Particle public: ParticleState mState; tColor4f mColor; tVector2f mPosition; tVector2f mScale; tTexture * mTexture; orientación flotante; flotación mDuración float mPercentLife; public: Particle (): mScale (1,1), mPercentLife (1.0f) ;

los Partícula La clase tiene toda la información necesaria para mostrar una partícula y administrar su vida útil.. Estado de partícula está ahí para contener cualquier información adicional que podamos necesitar para nuestras partículas. Los datos necesarios variarán dependiendo de los efectos de partículas deseados; podría usarse para almacenar velocidad, aceleración, velocidad de rotación o cualquier otra cosa que pueda necesitar.

Para ayudar a administrar las partículas, necesitaremos una clase que funcione como una matriz circular, lo que significa que los índices que normalmente estarían fuera de límites se ajustarán al principio de la matriz. Esto facilitará la sustitución de las partículas más antiguas primero si nos quedamos sin espacio para nuevas partículas en nuestra matriz. Para esto, agregamos lo siguiente como una clase anidada en Administrador de partículas:

 clase CircularParticleArray protected: std :: vector mList size_t mStart; size_t mCount; público: CircularParticleArray (int capacidad) mList.resize ((size_t) capacidad);  size_t getStart () return mStart;  void setStart (valor size_t) mStart = valor% mList.size ();  size_t getCount () return mCount;  void setCount (size_t value) mCount = value;  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 ()]; ;

Podemos configurar el mStart miembro para ajustar donde el índice cero en nuestra CircularParticleArray corresponde a en la matriz subyacente, y mCount se utilizará para rastrear cuántas partículas activas hay en la lista. Nos aseguraremos de que la partícula en el índice cero sea siempre la partícula más antigua. Si reemplazamos la partícula más antigua por una nueva, simplemente incrementaremos mStart, que esencialmente gira la matriz circular.

Ahora que tenemos nuestras clases de ayuda, podemos comenzar a llenar el Administrador de partículas clase. Necesitaremos una nueva variable miembro, y un constructor..

 CircularParticleArray mParticleList; ParticleManager :: ParticleManager (capacidad int): mParticleList (capacidad) 

Nosotros creamos mParticleList y llenarlo con partículas vacías. El constructor es el único lugar donde el Administrador de partículas asigna memoria.

A continuación, agregamos el createParticle () Método, que crea una nueva partícula utilizando la siguiente partícula no utilizada en el grupo, o la partícula más antigua si no hay partículas no utilizadas.

 void ParticleManager :: createParticle (tTexture * texture, const tVector2f & position, const tColor4f & tint, duración de flotación, const tVector2f & scale, const ParticleState & state, 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);  Partícula & ref = mParticleList [índice]; ref.mTexture = textura; ref.mPosición = posición; ref.mColor = tinte; ref.mDuration = duración; ref.mPercentLife = 1.0f; ref.mScale = scale; ref.mOrientación = theta; ref.mState = estado; 

Las partículas pueden ser destruidas en cualquier momento. Necesitamos eliminar estas partículas mientras aseguramos que las otras partículas permanezcan en el mismo orden. Podemos hacerlo iterando a través de la lista de partículas mientras hacemos un seguimiento de cuántas han sido destruidas. A medida que avanzamos, movemos cada partícula activa frente a todas las partículas destruidas intercambiándola con la primera partícula destruida. Una vez que todas las partículas destruidas están al final de la lista, las desactivamos configurando la lista mCount Variable al número de partículas activas. Las partículas destruidas permanecerán en la matriz subyacente, pero no se actualizarán ni dibujarán.

ParticleManager :: update () Maneja la actualización de cada partícula y elimina las partículas destruidas de la lista:

 void ParticleManager :: update () size_t removalCount = 0; para (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; 

Lo último para implementar en Administrador de partículas Está dibujando las partículas:

 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 clase ParticleState

Lo siguiente que debe hacer es crear una clase o estructura personalizada para personalizar el aspecto de las partículas en Shape Blaster. Habrá varios tipos diferentes de partículas en Shape Blaster que se comportarán de manera ligeramente diferente, así que comenzaremos creando una enumerar para el tipo de partícula. También necesitaremos variables para la velocidad de la partícula y la longitud inicial..

 clase ParticleState public: enum ParticleType kNone = 0, kEnemy, kBullet, kIgnoreGravity; público: tVector2f mVelocity; ParticleType mType; float mLengthMultiplier; público: ParticleState (); ParticleState (const tVector2f & speed, tipo ParticleType, float lengthMultiplier = 1.0f); ParticleState getRandom (float minVel, float maxVel); void updateParticle (Partícula y partícula); ;

Ahora estamos listos para escribir las partículas. actualizar() método. Es una buena idea hacer que este método sea rápido, ya que podría ser necesario para una gran cantidad de partículas..

Vamos a empezar simple. Añadamos el siguiente método a Estado de partícula:

 void ParticleState :: updateParticle (Partícula y partícula) tVector2f vel = particle.mState.mVelocity; partícula.mPosición + = vel; particle.mOrientation = Extensions :: toAngle (vel); // los flotadores desnormalizados causan problemas de rendimiento significativos si (fabs (vel.x) + fabs (vel.y) < 0.00000000001f)  vel = tVector2f(0,0);  vel *= 0.97f; // Particles gradually slow down particle.mState.mVelocity = vel; 

Volveremos y mejoraremos este método en un momento. Primero, vamos a crear algunos efectos de partículas para que podamos probar nuestros cambios.

Explosiones enemigas

En GameRoot, declarar un nuevo Administrador de partículas y llama a su actualizar() y dibujar() métodos:

 // en GameRoot protegido: ParticleManager mParticleManager; público: ParticleManager * getParticleManager () return & mParticleManager;  // en el constructor GameRoot GameRoot :: GameRoot (): mParticleManager (1024 * 20), mViewportSize (800, 600), mSpriteBatch (NULL)  // en GameRoot :: onRedrawView () mParticleManager.update (); mParticleManager.draw (mSpriteBatch);

Además, declararemos una nueva instancia de la tTextura clase en el Art º clase llamada mLinePartícula Para la textura de la partícula. Lo cargaremos como lo hacemos con los sprites del otro juego:

 // En el constructor de Art mLineParticle = new tTexture (tSurface ("laser.png"));

Ahora hagamos explotar a los enemigos. Modificaremos la Enemigo :: wasShot () método de la siguiente manera:

 void Enemy :: wasShot () mIsExpired = true; para (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, color, 190, 1.5f, estado); 

Esto crea 120 partículas que dispararán hacia el exterior con diferentes velocidades en todas las direcciones. La velocidad aleatoria está ponderada de modo que las partículas tienen más probabilidades de viajar cerca de la velocidad máxima. Esto hará que haya más partículas en el borde de la explosión a medida que se expande. Las partículas duran 190 cuadros, o algo más de tres segundos..

Ahora puedes correr el juego y ver explotar a los enemigos. Sin embargo, todavía hay algunas mejoras por hacer para los efectos de partículas..

El primer problema es que las partículas desaparecen bruscamente una vez que se agota su duración. Sería mejor si se desvanecieran suavemente, pero vamos un poco más lejos y hacemos que las partículas se vuelvan más brillantes cuando se mueven rápido. Además, se ve bien si alargamos las partículas en movimiento rápido y acortamos las partículas en movimiento lento.

Modificar el ParticleState.UpdateParticle () Método como sigue (los cambios están resaltados).

 void ParticleState :: updateParticle (Partícula y partícula) tVector2f vel = particle.mState.mVelocity; partícula.mPosición + = vel; particle.mOrientation = Extensions :: toAngle (vel); velocidad de flotación = vel.length (); float alpha = tMath :: min (1.0f, tMath :: min (particle.mPercentLife * 2, speed * 1.0f)); alfa * = alfa; partícula.mColor.a = alfa; partícula.mScale.x = particle.mState.mLengthMultiplier * tMath :: min (tMath :: min (1.0f, 0.2f * speed + 0.1f), alfa); // los flotadores desnormalizados causan problemas de rendimiento significativos si (fabs (vel.x) + fabs (vel.y) < 0.00000000001f)  vel = tVector2f(0,0);  vel *= 0.97f; // Particles gradually slow down particle.mState.mVelocity = vel; 

Las explosiones se ven mucho mejor ahora, pero todas son del mismo color..

Las explosiones monocromáticas son un buen comienzo, pero ¿podemos hacerlo mejor??

Podemos darles más variedad eligiendo colores al azar. Un método para producir colores aleatorios es elegir los componentes rojo, azul y verde al azar, pero esto producirá muchos colores apagados y nos gustaría que nuestras partículas tuvieran una apariencia de luz de neón. Podemos tener más control sobre nuestros colores al especificarlos en el espacio de color HSV. HSV significa tono, saturación y valor. Nos gustaría elegir colores con un tono aleatorio pero con una saturación y un valor fijos. Necesitamos una función auxiliar que pueda producir un color a partir de valores 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; si (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); 

Ahora podemos modificar Enemigo :: wasShot () para utilizar colores al azar. Para hacer que el color de la explosión sea menos monótono, elegiremos dos colores clave cercanos para cada explosión e interpolaremos linealmente entre ellos por una cantidad aleatoria para cada partícula:

 void Enemy :: wasShot () mIsExpired = true; float hue1 = Extensions :: nextFloat (0, 6); float hue2 = fmodf (hue1 + Extensions :: nextFloat (0, 2), 6.0f); tColor4f color1 = ColorUtil :: HSVToColor (hue1, 0.5f, 1); tColor4f color2 = ColorUtil :: HSVToColor (hue2, 0.5f, 1); para (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, color, 190, 1.5f, estado); 

Las explosiones deben verse como la siguiente animación:


Puedes jugar con la generación de colores para que se adapte a tus preferencias. Una técnica alternativa que funciona bien es elegir a mano una serie de patrones de color para las explosiones y seleccionar aleatoriamente entre los esquemas de color preseleccionados.

Explosiones de bala

También podemos hacer explotar las balas cuando llegan al borde de la pantalla. Básicamente haremos lo mismo que hicimos para las explosiones enemigas..

Vamos a modificar Bullet :: update () como sigue:

 if (! tRectf (0, 0, GameRoot :: getInstance () -> getViewportSize ()). contiene (tPoint2f ((int32_t) mPosition.x, (int32_t) mPosition.y))) mIsExpired = true; para (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 (Extensiones :: nextVector2 (0, 9) , ParticleState :: kBullet, 1)); 

Puede notar que dar una dirección aleatoria a las partículas es un desperdicio, porque al menos la mitad de las partículas saldrán inmediatamente de la pantalla (más si la bala explota en una esquina). Podríamos hacer un trabajo adicional para garantizar que las partículas solo tengan velocidades opuestas a la pared que están enfrentando. Sin embargo, en cambio, seguiremos el ejemplo de Geometry Wars y haremos que todas las partículas reboten en las paredes, de modo que cualquier partícula que salga de la pantalla será devuelta.

Agrega las siguientes líneas a ParticleState.UpdateParticle () En cualquier lugar entre las primeras y últimas líneas:

 tVector2f pos = particle.mPosition; int width = (int) GameRoot :: getInstance () -> getViewportSize (). width; int height = (int) GameRoot :: getInstance () -> getViewportSize (). height; // colisionar con los bordes de la pantalla si (pos.x < 0)  vel.x = (float)fabs(vel.x);  else if (pos.x > ancho) vel.x = (float) -fabs (vel.x);  si (pos.y < 0)  vel.y = (float)fabs(vel.y);  else if (pos.y > altura) vel.y = (flotador) -fabs (vel.y); 

Explosión del barco del jugador

Haremos una explosión realmente grande cuando el jugador muere. Modificar PlayerShip :: kill () al igual que:

 void PlayerShip :: kill () PlayerStatus :: getInstance () -> removeLife (); mFramesUntilRespawn = PlayerStatus :: getInstance () -> getIsGameOver ()? 300: 120; tColor4f explosionColor = tColor4f (0.8f, 0.8f, 0.4f, 1.0f); para (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, color, 190, 1.5f, estado); 

Esto es similar a las explosiones enemigas, pero usamos más partículas y siempre usamos el mismo esquema de color. El tipo de partícula también se establece en ParticleState :: kNone.

En la demostración, las partículas de las explosiones enemigas se ralentizan más rápido que las partículas de la nave del jugador que explotan. Esto hace que la explosión del jugador dure un poco más y se vea un poco más épica.

Agujeros negros revisados

Ahora que tenemos efectos de partículas, revisemos los agujeros negros y hagamos que interactúen con las partículas..

Efecto sobre las partículas

Los agujeros negros deberían afectar las partículas además de otras entidades, por lo que necesitamos modificar ParticleState :: updateParticle (). Añadamos las siguientes líneas:

 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; distancia de flotación = dPos.length (); tVector2f n = dPos / distance; vel + = 10000.0f * n / (distancia * distancia + 10000.0f); // agregar aceleración tangencial para partículas cercanas si (distancia < 400)  vel += 45.0f * tVector2f(n.y, -n.x) / (distance + 100.0f);   

aquí, norte Es el vector unitario que apunta hacia el agujero negro. La fuerza atractiva es una versión modificada de la función de cuadrado inverso:

  • La primera modificación es que el denominador es distancia ^ 2 + 10,000; esto hace que la fuerza de atracción se acerque a un valor máximo en lugar de tender al infinito a medida que la distancia se vuelve muy pequeña.
    • Cuando la distancia es mucho mayor que 100 pixeles, distancia ^ 2 se vuelve mucho mayor que 10,000. Por lo tanto, sumando 10,000 a distancia ^ 2 tiene un efecto muy pequeño, y la función se aproxima a una función cuadrada inversa normal.
    • Sin embargo, cuando la distancia es mucho menor que 100 píxeles, la distancia tiene un pequeño efecto sobre el valor del denominador, y la ecuación se vuelve aproximadamente igual a: vel + = n
  • La segunda modificación es agregar un componente lateral a la velocidad cuando las partículas se acercan lo suficiente al agujero negro. Esto tiene dos propósitos:
    1. Hace que las partículas formen una espiral en el sentido de las agujas del reloj hacia el agujero negro..
    2. Cuando las partículas se acerquen lo suficiente, alcanzarán el equilibrio y formarán un círculo brillante alrededor del agujero negro..

Propina: Para rotar un vector, V, 90 ° en el sentido de las agujas del reloj, tomar (V.Y, -V.X). Del mismo modo, para girarlo 90 ° en sentido antihorario, tome (-V.Y, V.X).

Partículas productoras

Un agujero negro producirá dos tipos de partículas. Primero, rociará periódicamente partículas que orbitarán a su alrededor. Segundo, cuando se dispara un agujero negro, rociará partículas especiales que no se ven afectadas por su gravedad.

Agregue el siguiente código a la BlackHole :: WasShot () método:

 float hue = fmodf (3.0f / 1000.0f * tTimer :: getTimeMS (), 6); tColor4f color = ColorUtil :: HSVToColor (tono, 0.25f, 1); const int numParticles = 150; float startOffset = Extensions :: nextFloat (0, tMath :: PI * 2.0f / numParticles); para (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, estado); 

Esto funciona en su mayoría de la misma manera que las otras explosiones de partículas. Una diferencia es que elegimos el tono del color en función del tiempo total transcurrido del juego. Si disparas al agujero negro varias veces en rápida sucesión, verás que el tono de las explosiones gira gradualmente. Esto parece menos desordenado que el uso de colores aleatorios, a la vez que permite variaciones.

Para el rociado de partículas en órbita, necesitamos agregar una variable a la Agujero negro clase para rastrear la dirección en la que actualmente estamos rociando partículas:

 protegido: int mHitPoints; float mSprayAngle; BlackHole :: BlackHole (const tVector2f & position): mSprayAngle (0) …

Ahora añadiremos lo siguiente a la BlackHole :: update () método.

 // Los agujeros negros rocían algunas partículas en órbita. El spray se activa y desactiva cada cuarto de segundo. 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) + Extensions :: nextVector2 (4, 8); Estado de ParticleState (sprayVel, ParticleState :: kEnemy, 1.0f); GameRoot :: getInstance () -> getParticleManager () -> createParticle (Art :: getInstance () -> getLineParticle (), pos, color, 190, 1.5f, estado);  // gire la dirección de rociado mSprayAngle - = tMath :: PI * 2.0f / 50.0f;

Esto hará que los agujeros negros rocien chorros de partículas púrpuras que formarán un anillo que orbita alrededor del agujero negro, de esta manera:

Fuego de escape de la nave

Según lo dictado por las leyes de la física geométrica-neón, la nave del jugador se propulsa lanzando un chorro de partículas ardientes por su tubo de escape. Con nuestro motor de partículas en su lugar, este efecto es fácil de hacer y agrega un toque visual al movimiento de la nave.

A medida que la nave se mueve, creamos tres corrientes de partículas: una corriente central que se dispara directamente desde la parte posterior de la nave, y dos corrientes laterales cuyos ángulos giran de un lado a otro en relación con la nave. Las dos corrientes laterales giran en direcciones opuestas para hacer un patrón entrecruzado. Las corrientes laterales tienen un color más rojo, mientras que la transmisión central tiene un color amarillo-blanco más cálido. La siguiente animación muestra el efecto:


Para hacer que el fuego brille más intensamente, haremos que la nave emita partículas adicionales que se ven así:


Estas partículas se teñirán y se mezclarán con las partículas regulares. El código para el efecto completo se muestra a continuación:

 void PlayerShip :: MakeExhaustFire () if (mVelocity.lengthSquared ()> 0.1f) mOrientation = Extensiones :: 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 = Extensions :: 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); // Posición del tubo de escape de la nave. const float alpha = 0.7f; // flujo de partículas medias tVector2f velMid = baseVel + Extensions :: 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)); // flujos de partículas laterales tVector2f vel1 = baseVel + perpVel + Extensions :: nextVector2 (0, 0.3f); tVector2f vel2 = baseVel - perpVel + Extensions :: 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)); 

No hay nada furtivo en este código. Utilizamos una función sinusoidal para producir el efecto de giro en las corrientes laterales variando su velocidad de lado a lo largo del tiempo. Para cada flujo, creamos dos partículas superpuestas por fotograma: una semitransparente, blanca LineParticle, y una partícula de color brillante detrás de ella. Llamada
MakeExhaustFire () al final de PlayerShip.Update (), Inmediatamente antes de ajustar la velocidad del barco a cero.

Conclusión

Con todos estos efectos de partículas, Shape Blaster está empezando a verse muy bien. En la parte final de esta serie, agregaremos un efecto impresionante más: la cuadrícula de fondo combada.