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..
En la serie hasta ahora, hemos configurado el juego y agregado controles de gamepad virtual. A continuación, añadiremos efectos de partículas..
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.
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.
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 elAdministrador 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 :: vectormList 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);
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.
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.
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);
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.
Ahora que tenemos efectos de partículas, revisemos los agujeros negros y hagamos que interactúen con 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:
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. 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.vel + = n
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)
.
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:
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. LlamadaMakeExhaustFire ()
al final de PlayerShip.Update ()
, Inmediatamente antes de ajustar la velocidad del barco a cero.
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.