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 final, agregaremos la cuadrícula de fondo que se deforma según la acción del juego..
En la serie hasta ahora, hemos creado la jugabilidad, el gamepad virtual y los efectos de partículas. En esta parte final, crearemos una cuadrícula de fondo dinámica y combada..
Como se mencionó en la parte anterior, notará una caída dramática en la tasa de cuadros si todavía está ejecutando el código en el modo de depuración. Vea ese tutorial para obtener detalles sobre cómo cambiar al modo de lanzamiento para la optimización completa del compilador (y una compilación más rápida).
Uno de los efectos más geniales en Geometry Wars es la cuadrícula de fondo combada. Examinaremos cómo crear un efecto similar en Shape Blaster. La cuadrícula reaccionará a las balas, los agujeros negros y el reaparición del jugador. No es difícil de hacer y se ve increíble.
Haremos la grilla utilizando una simulación de resorte. En cada intersección de la rejilla, pondremos un pequeño peso y uniremos un resorte en cada lado. Estos resortes solo tirarán y nunca empujarán, como una banda de goma. Para mantener la cuadrícula en posición, las masas en el borde de la cuadrícula se anclarán en su lugar. A continuación se muestra un diagrama de la disposición..
Vamos a crear una clase llamada Cuadrícula
para crear este efecto. Sin embargo, antes de trabajar en la propia cuadrícula, necesitamos hacer dos clases auxiliares: Primavera
y PointMass
.
los PointMass
clase representa las masas a las que uniremos los manantiales. Los resortes nunca se conectan directamente a otros resortes; en cambio, aplican una fuerza a las masas que conectan, que a su vez pueden estirar otros resortes.
clase PointMass protected: tVector3f mAcceleration; float mDamping; public: tVector3f mPosition; tVector3f mVelocity; float mInverseMass; público: PointMass (); PointMass (const tVector3f & position, float invMass); void applyForce (const tVector3f & force); aumento de vacíoDamping (factor de flotación); actualización nula (); ; PointMass :: PointMass (): mAcceleration (0,0,0), mDamping (0.98f), mPosition (0), mVelocity (0,0,0), mInverseMass (0) PointMass :: PointMass (constVector3f y posición , float invMass): mAcceleration (0,0,0), mDamping (0.98f), mPosition (posición), mVelocity (0,0,0), mInverseMass (invMass) void PointMass :: applyForce (const tVector3f & force) mAcceleration + = force * mInverseMass; void PointMass :: IncreaseDamping (factor flotante) mDamping * = factor; void PointMass :: update () mVelocity + = mAcceleration; mPosición + = mVelocidad; mAcceleration = tVector3f (0,0,0); if (mVelocity.lengthSquared () < 0.001f * 0.001f) mVelocity = tVector3f(0,0,0); mVelocity *= mDamping; mDamping = 0.98f;
Hay algunos puntos interesantes sobre esta clase. Primero, note que almacena el inverso de la misa, 1 / masa
. A menudo, esto es una buena idea en las simulaciones de física porque las ecuaciones de la física tienden a usar la inversa de la masa con mayor frecuencia, y porque nos brinda una manera fácil de representar objetos inmóviles, pesados e infinitos al establecer la masa inversa en cero..
Segundo, la clase también contiene un mojadura variable. Esto se usa aproximadamente como fricción o resistencia del aire; gradualmente ralentiza la masa hacia abajo. Esto ayuda a que la cuadrícula se detenga y aumente la estabilidad de la simulación del resorte..
los PointMass :: update ()
El método hace el trabajo de mover la masa puntual de cada cuadro. Comienza haciendo una integración de Euler simpléctica, lo que significa que solo agregamos la aceleración a la velocidad y luego agregamos la velocidad actualizada a la posición. Esto difiere de la integración estándar de Euler en la que actualizaríamos la velocidad después de actualizar la posición.
Propina: Symplectic Euler es mejor para las simulaciones de primavera porque conserva la energía. Si utiliza la integración regular de Euler y crea resortes sin amortiguación, tenderán a estirarse más y más a medida que ganen energía, rompiendo su simulación..
Después de actualizar la velocidad y la posición, verificamos si la velocidad es muy pequeña y, de ser así, la ponemos a cero. Esto puede ser importante para el rendimiento debido a la naturaleza de los números de punto flotante desnormalizados.
(Cuando los números de punto flotante se vuelven muy pequeños, usan una representación especial llamada numero desnormalizado. Esto tiene la ventaja de permitir que los flotadores representen números más pequeños, pero tienen un precio. La mayoría de los conjuntos de chips no pueden usar sus operaciones aritméticas estándar en números desnormalizados y, en cambio, deben emularlos siguiendo una serie de pasos. Esto puede ser de decenas a cientos de veces más lento que realizar operaciones en números de punto flotante normalizados. Ya que multiplicamos nuestra velocidad por nuestro factor de amortiguación en cada cuadro, eventualmente se volverá muy pequeño. En realidad, no nos importan velocidades tan pequeñas, así que simplemente lo ponemos a cero.)
los PointMass :: IncreaseDamping ()
El método se utiliza para aumentar temporalmente la cantidad de amortiguación. Usaremos esto más adelante para ciertos efectos..
Un resorte conecta dos masas puntuales y, si se extiende más allá de su longitud natural, aplica una fuerza que une las masas. Los resortes siguen una versión modificada de la Ley de Hooke con amortiguación:
\ [f = −kx - bv \]
El código para el Primavera
La clase es la siguiente:
clase Spring public: PointMass * mEnd1; PointMass * mEnd2; float mTargetLength; flotar m rigidez; float mDamping; público: Spring (PointMass * end1, PointMass * end2, rigidez del flotador, amortiguación del flotador); actualización nula (); ; Spring :: Spring (PointMass * end1, PointMass * end2, rigidez del flotador, amortiguación del flotador): mEnd1 (end1), mEnd2 (end2), mTargetLength (mEnd1-> mPosition.distance (mEnd2-> mPosition) * 0.95f), mStiffness (rigidez), mDamping (atenuación) void Spring :: update () tVector3f x = mEnd1-> mPosition - mEnd2-> mPosition; longitud de flotación = x.length (); if (length> mTargetLength) x = (x / length) * (length - mTargetLength); tVector3f dv = mEnd2-> mVelocity - mEnd1-> mVelocity; tVector3f force = mStiffness * x - dv * mDamping; mEnd1-> applyForce (-force); mEnd2-> applyForce (force);
Cuando creamos un resorte, establecemos que la longitud natural del resorte sea ligeramente menor que la distancia entre los dos puntos finales. Esto mantiene la rejilla tensa incluso cuando está en reposo, y mejora un poco la apariencia..
los Spring :: update ()
El método primero verifica si el resorte se estira más allá de su longitud natural. Si no se estira, no pasa nada. Si es así, usamos la Ley de Hooke modificada para encontrar la fuerza del resorte y aplicarla a las dos masas conectadas.
Ahora que tenemos las clases anidadas necesarias, estamos listos para crear la cuadrícula. Comenzamos creando PointMass
Objetos en cada intersección de la cuadrícula. También creamos un ancla inamovible. PointMass
Objetos para mantener la rejilla en su lugar. Luego unimos las masas con manantiales..
std :: vectormSprings; PointMass * mPoints; Grid :: Grid (const tRectf & rect, const tVector2f & spacing) mScreenSize = tVector2f (GameRoot :: getInstance () -> getViewportSize (). Width, GameRoot :: getInstance () -> getViewportSize (). Height); int numColumns = (int) ((float) rect.size.width / spacing.x) + 1; int numRows = (int) ((float) rect.size.height / spacing.y) + 1; mPoints = new PointMass [numColumns * numRows]; mCols = numColumns; mRows = numRows; PointMass * fixedPoints = new PointMass [numColumns * numRows]; columna int = 0, fila = 0; para (float y = rect.location.y; y <= rect.location.y + rect.size.height; y += spacing.y) for (float x = rect.location.x; x <= rect.location.x + rect.size.width; x += spacing.x) SetPointMass(mPoints, column, row, PointMass(tVector3f(x, y, 0), 1)); SetPointMass(fixedPoints, column, row, PointMass(tVector3f(x, y, 0), 0)); column++; row++; column = 0; // link the point masses with springs for (int y = 0; y < numRows; y++) for (int x = 0; x < numColumns; x++) if (x == 0 || y == 0 || x == numColumns - 1 || y == numRows - 1) mSprings.push_back(Spring(GetPointMass(fixedPoints, x, y), GetPointMass(mPoints, x, y), 0.1f, 0.1f)); else if (x % 3 == 0 && y % 3 == 0) mSprings.push_back( Spring(GetPointMass(fixedPoints, x, y), GetPointMass(mPoints, x, y), 0.002f, 0.02f)); if (x > 0) mSprings.push_back (Spring (GetPointMass (mPoints, x - 1, y), GetPointMass (mPoints, x, y), 0.28f, 0.06f)); if (y> 0) mSprings.push_back (Spring (GetPointMass (mPoints, x, y - 1), GetPointMass (mPoints, x, y), 0.28f, 0.06f));
El primero para
el bucle crea masas regulares e inmóviles en cada intersección de la cuadrícula. En realidad no usaremos todas las masas inamovibles, y las masas no utilizadas simplemente serán recolectadas en algún momento después de que el constructor termine. Podríamos optimizar evitando la creación de objetos innecesarios, pero como la cuadrícula normalmente solo se crea una vez, no habrá mucha diferencia..
Además de usar masas de puntos de anclaje alrededor del borde de la cuadrícula, también usaremos algunas masas de anclaje dentro de la cuadrícula. Se utilizarán para ayudar a que la rejilla vuelva a su posición original muy suavemente después de deformarse..
Como los puntos de anclaje nunca se mueven, no es necesario actualizarlos cada fotograma; Simplemente podemos engancharlos a los resortes y olvidarlos. Por lo tanto, no tenemos una variable miembro en el Cuadrícula
clase para estas masas.
Hay una serie de valores que puede modificar en la creación de la cuadrícula. Los más importantes son la rigidez y la amortiguación de los resortes. (La rigidez y la amortiguación de los anclajes de borde y los anclajes interiores se establecen independientemente de los resortes principales). Los valores más altos de rigidez harán que los resortes oscilen más rápidamente, y los valores más altos de amortiguación harán que los resortes se desaceleren más pronto.
Para que la cuadrícula se mueva, debemos actualizarla cada cuadro. Esto es muy simple como ya hicimos todo el trabajo duro en el PointMass
y Primavera
clases:
void Grid :: update () for (size_t i = 0; i < mSprings.size(); i++) mSprings[i].update(); for(int i = 0; i < mCols * mRows; i++) mPoints[i].update();
Ahora agregaremos algunos métodos que manipulan la cuadrícula. Puedes agregar métodos para cualquier tipo de manipulación que puedas imaginar. Aquí implementaremos tres tipos de manipulaciones: empujar parte de la cuadrícula en una dirección determinada, empujar la cuadrícula hacia afuera desde algún punto y tirar de la cuadrícula hacia algún punto. Los tres afectarán a la cuadrícula dentro de un radio dado desde algún punto objetivo. A continuación se muestran algunas imágenes de estas manipulaciones en acción:
Balas que repelen la rejilla hacia afuera..
Chupando la rejilla hacia adentro..
Ola creada empujando la cuadrícula a lo largo del eje z.
void Grid :: applyDirectedForce (const tVector3f & force, const tVector3f & position, radio de flotación) for (int i = 0; i < mCols * mRows; i++) if (position.distanceSquared(mPoints[i].mPosition) < radius * radius) mPoints[i].applyForce(10.0f * force / (10 + position.distance(mPoints[i].mPosition))); void Grid::applyImplosiveForce(float force, const tVector3f& position, float radius) for (int i = 0; i < mCols * mRows; i++) float dist2 = position.distanceSquared(mPoints[i].mPosition); if (dist2 < radius * radius) mPoints[i].applyForce(10.0f * force * (position - mPoints[i].mPosition) / (100 + dist2)); mPoints[i].increaseDamping(0.6f); void Grid::applyExplosiveForce(float force, const tVector3f& position, float radius) for (int i = 0; i < mCols * mRows; i++) float dist2 = position.distanceSquared(mPoints[i].mPosition); if (dist2 < radius * radius) mPoints[i].applyForce(100 * force * (mPoints[i].mPosition - position) / (10000 + dist2)); mPoints[i].increaseDamping(0.6f);
Usaremos estos tres métodos en Shape Blaster para diferentes efectos..
Dibujaremos la cuadrícula dibujando segmentos de línea entre cada par de puntos adyacentes. Primero, agregaremos un método de extensión tomando una tSpriteBatch
puntero como parámetro que nos permite dibujar segmentos de línea tomando una textura de un solo píxel y estirándolo en una línea.
Abre el Art º
Clase y declara una textura para el píxel:
clase de arte: público tSingleton; protected: tTexture * mPixel;… public: tTexture * getPixel () const;…;
Puede configurar la textura de píxeles de la misma manera que configuramos las otras imágenes, así que agregaremos pixel.png
(una imagen de 1x1px con el único píxel establecido en blanco) para el proyecto y cargarla en el tTextura
:
mPixel = new tTexture (tSurface ("pixel.png"));
Ahora vamos a agregar el siguiente método a la Extensiones
clase:
void Extensions :: drawLine (tSpriteBatch * spriteBatch, const tVector2f & start, const tVector2f & end, const tColor4f & color, flotante grosor) tVector2f delta = end - start; spriteBatch-> draw (0, Art :: getInstance () -> getPixel (), tPoint2f ((int32_t) start.x, (int32_t) start.y), tOptional(), color, toAngle (delta), tPoint2f (0, 0), tVector2f (delta.length (), espesor));
Este método estira, rota y tiñe la textura de píxeles para producir la línea que deseamos.
A continuación, necesitamos un método para proyectar los puntos de cuadrícula 3D en nuestra pantalla 2D. Normalmente, esto se puede hacer utilizando matrices, pero aquí transformaremos las coordenadas manualmente..
Agregue lo siguiente a la Cuadrícula
clase:
tVector2f Grid :: toVec2 (const tVector3f & v) factor flotante = (v.z + 2000.0f) * 0.0005f; return (tVector2f (v.x, v.y) - mScreenSize * 0.5f) * factor + mScreenSize * 0.5f;
Esta transformación le dará a la cuadrícula una vista en perspectiva donde los puntos lejanos aparecen más cerca en la pantalla. Ahora podemos dibujar la cuadrícula iterando a través de las filas y columnas y dibujando líneas entre ellas:
void Grid :: draw (tSpriteBatch * spriteBatch) int width = mCols; altura int = mRows; Color tColor4f (0.12f, 0.12f, 0.55f, 0.33f); para (int y = 1; y < height; y++) for (int x = 1; x < width; x++) tVector2f left, up; tVector2f p = toVec2(GetPointMass(mPoints, x, y)->mPosición); if (x> 1) left = toVec2 (GetPointMass (mPoints, x - 1, y) -> mPosition); espesor del flotador = (y% 3 == 1)? 3.0f: 1.0f; Extensiones :: drawLine (spriteBatch, left, p, color, grosor); if (y> 1) up = toVec2 (GetPointMass (mPoints, x, y - 1) -> mPosition); grosor del flotador = (x% 3 == 1)? 3.0f: 1.0f; Extensiones :: drawLine (spriteBatch, up, p, color, grosor);
En el codigo anterior, pag
Es nuestro punto actual en la grilla., izquierda
Es el punto directamente a su izquierda y arriba
Es el punto directamente encima de él. Dibujamos cada tercera línea más gruesa tanto horizontal como verticalmente para obtener un efecto visual.
Podemos optimizar la cuadrícula mejorando la calidad visual para un número determinado de resortes sin aumentar significativamente el costo de rendimiento. Vamos a hacer dos optimizaciones de este tipo..
Haremos que la cuadrícula sea más densa agregando segmentos de línea dentro de las celdas de cuadrícula existentes. Lo hacemos dibujando líneas desde el punto medio de un lado de la celda hasta el punto medio del lado opuesto. La imagen de abajo muestra las nuevas líneas interpoladas en rojo..
Dibujar las líneas interpoladas es sencillo. Si tienes dos puntos, una
y segundo
, su punto medio es (a + b) / 2
. Entonces, para dibujar las líneas interpoladas, agregamos el siguiente código dentro de para
bucles de nuestro Cuadrícula :: dibujar ()
método:
if (x> 1 && y> 1) tVector2f upLeft = toVec2 (GetPointMass (mPoints, x - 1, y - 1) -> mPosition); Extensiones :: drawLine (spriteBatch, 0.5f * (upLeft + up), 0.5f * (left + p), color, 1.0f); // Extensiones de línea vertical :: drawLine (spriteBatch, 0.5f * (upLeft + left), 0.5f * (up + p), color, 1.0f); // linea horizontal
La segunda mejora es realizar la interpolación en nuestros segmentos de línea recta para convertirlos en curvas más suaves. En la versión XNA original de este juego, el código se basó en XNA Vector2.CatmullRom ()
Método que realiza la interpolación Catmull-Rom. Pase el método cuatro puntos secuenciales en una línea curva, y devolverá puntos a lo largo de una curva suave entre el segundo y el tercer punto que proporcionó.
Dado que este algoritmo no existe en la biblioteca estándar de C o C ++, tendremos que implementarlo nosotros mismos. Afortunadamente, hay una implementación de referencia disponible para usar. He proporcionado un MathUtil :: catmullRom ()
Método basado en esta implementación de referencia:
float MathUtil :: catmullRom (const float value1, const float value2, const float value3, const float value4, float amount) // Usando la fórmula de http://www.mvps.org/directx/articles/catmull/ float amountSquared = cantidad * cantidad; float amountCubed = amountSquared * amount; return (float) (0.5f * (2.0f * value2 + (value3 - value1) * cantidad + (2.0f * value1 - 5.0f * value2 + 4.0f * value3 - value4) * amountSquared + (3.0f * value2 - value1 - 3.0f * value3 + value4) * amountCubed)); tVector2f MathUtil :: catmullRom (const tVector2f & value1, const tVector2f & value2, const tVector2f & value3, const tVector2f & value4, cantidad flotante) return tVector2f (MathUtil :: catmullRom (value1.x, value2.x, value3.x, value4.x , cantidad), MathUtil :: catmullRom (valor1.y, valor2.y, valor3.y, valor4.y, cantidad));
El quinto argumento para MathUtil :: catmullRom ()
es un factor de ponderación que determina qué punto de la curva interpolada retorna. Un factor de ponderación de 0
o 1
devolverá respectivamente el segundo o tercer punto que proporcionó, y un factor de ponderación de 0.5
devolverá el punto en la curva interpolada a medio camino entre los dos puntos. Moviendo gradualmente el factor de ponderación de cero a uno y dibujando líneas entre los puntos devueltos, podemos producir una curva perfectamente suave. Sin embargo, para mantener el costo de rendimiento bajo, solo tomaremos en consideración un único punto interpolado, con un factor de ponderación de 0.5
. Luego reemplazamos la línea recta original en la cuadrícula con dos líneas que se encuentran en el punto interpolado.
El siguiente diagrama muestra el efecto de esta interpolación:
Dado que los segmentos de línea en la cuadrícula ya son pequeños, el uso de más de un punto interpolado generalmente no hace una diferencia notable.
A menudo, las líneas en nuestra cuadrícula serán muy rectas y no requerirán ningún suavizado. Podemos verificar esto y evitar tener que dibujar dos líneas en lugar de una: comprobamos si la distancia entre el punto interpolado y el punto medio de la línea recta es mayor que un píxel; si es así, asumimos que la línea es curva y dibujamos dos segmentos de línea.
La modificación a nuestra Cuadrícula :: dibujar ()
el método para agregar la interpolación Catmull-Rom para las líneas horizontales se muestra a continuación.
left = toVec2 (GetPointMass (mPoints, x - 1, y) -> mPosition); espesor del flotador = (y% 3 == 1)? 3.0f: 1.0f; int clampedX = (int) tMath :: min (x + 1, ancho - 1); tVector2f mid = MathUtil :: catmullRom (toVec2 (GetPointMass (mPoints, x - 2, y) -> mPosition), izquierda, p, toVec2 (GetPointMass (mPoints, clampedX, y) -> mPosition), 0.5f); if (mid.distanceSquared ((left + p) / 2)> 1) Extensions :: drawLine (spriteBatch, left, mid, color, grosor); Extensiones :: drawLine (spriteBatch, mid, p, color, grosor); else Extensions :: drawLine (spriteBatch, left, p, color, grosor);
La imagen de abajo muestra los efectos del suavizado. Se dibuja un punto verde en cada punto interpolado para ilustrar mejor dónde se suavizan las líneas.
Ahora es el momento de usar la cuadrícula en nuestro juego. Comenzamos por declarar un público, estático. Cuadrícula
variable en GameRoot
y creando la grilla en el GameRoot :: onInitView
. Crearemos una grilla con aproximadamente 600 puntos como tal..
const int maxGridPoints = 600; tVector2f gridSpacing = tVector2f ((float) sqrtf (mViewportSize.width * mViewportSize.height / maxGridPoints)); mGrid = new Grid (tRectf (0,0, mViewportSize), gridSpacing);
Aunque la versión XNA original del juego usa 1,600 puntos (en lugar de 600), esto se vuelve demasiado difícil de manejar incluso por el potente hardware del iPhone. Afortunadamente, el código original dejó la cantidad de puntos personalizables, y en aproximadamente 600 puntos de cuadrícula, aún podemos renderizarlos y mantener una velocidad de cuadros óptima.
Entonces llamamos Grid :: update ()
y Cuadrícula :: dibujar ()
desde el GameRoot :: onRedrawView ()
método en GameRoot
. Esto nos permitirá ver la cuadrícula cuando ejecutemos el juego. Sin embargo, todavía tenemos que hacer que varios objetos del juego interactúen con la cuadrícula..
Las balas repelerán la rejilla. Ya hicimos un método para hacerlo llamado. Grid :: applyExplosiveForce ()
. Agregue la siguiente línea a la Bullet :: update ()
método.
GameRoot :: getInstance () -> getGrid () -> applyExplosiveForce (0.5f * mVelocity.length (), mPosition, 80);
Esto hará que las balas repelan la cuadrícula proporcionalmente a su velocidad. Eso fue bastante facil.
Ahora trabajemos en agujeros negros. Añade esta línea a BlackHole :: update ()
:
GameRoot :: getInstance () -> getGrid () -> applyImplosiveForce ((float) sinf (mSprayAngle / 2.0f) * 10 + 20, mPosition, 200);
Esto hace que el agujero negro aspire la rejilla con una cantidad variable de fuerza. Hemos reutilizado el mSprayAngle
variable, lo que hará que la fuerza en la cuadrícula vibre en sincronía con el ángulo en que rocía las partículas (aunque a la mitad de la frecuencia debida a la división en dos). La fuerza pasada variará sinusoidalmente entre 10
y 30
.
Finalmente, crearemos una onda de choque en la cuadrícula cuando el barco del jugador reaparezca después de la muerte. Lo haremos tirando de la rejilla a lo largo del eje z y luego permitiendo que la fuerza se propague y rebote a través de los resortes. De nuevo, esto solo requiere una pequeña modificación de PlayerShip :: update ()
.
if (getIsDead ()) mFramesUntilRespawn--; if (mFramesUntilRespawn == 0) GameRoot :: getInstance () -> getGrid () -> applyDirectedForce (tVector3f (0, 0, 5000), tVector3f (mPosition.x, mPosition.y, 0), 50);
Tenemos el juego básico y los efectos implementados. Depende de usted convertirlo en un juego completo y pulido con su propio sabor. Intente agregar algunas nuevas mecánicas interesantes, algunos efectos nuevos geniales o una historia única. En caso de que no esté seguro de por dónde empezar, aquí hay algunas sugerencias:
El cielo es el límite!