Haz un Neon Vector Shooter para iOS Gamepads virtuales y agujeros negros

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, agregaremos los controles del gamepad virtual y los enemigos del "agujero negro".

Visión general

En la serie hasta ahora, hemos configurado el modo de juego básico para nuestro juego de disparos de neón con doble varilla, Shape Blaster. A continuación, agregaremos dos "gamepads virtuales" en pantalla para controlar el envío con.


La entrada es una necesidad para cualquier videojuego, y iOS nos ofrece un desafío interesante y ambiguo con la entrada multitáctil. Te mostraré un enfoque basado en el concepto de gamepads virtuales, donde simularemos gamepads de hardware utilizando solo el toque y un poco de lógica compleja para resolver las cosas. Después de agregar los gamepads virtuales para entrada multitáctil, también agregaremos agujeros negros al juego.

Gamepads virtuales

Los controles táctiles en pantalla son el principal medio de entrada para la mayoría de las aplicaciones y juegos basados ​​en iPhone y iPad. De hecho, iOS permite el uso de una interfaz multitáctil, lo que significa la lectura de varios puntos de contacto al mismo tiempo. La belleza de las interfaces táctiles es que puede definir que la interfaz sea lo que desee, ya sea con un solo botón, una palanca de control virtual o un control deslizante. Lo que implementaremos es una interfaz táctil que llamaré "gamepads virtuales".

UNA gamepad por lo general, describe un control físico estándar en forma de más, similar a la interfaz más en un sistema Game Boy o controlador de PlayStation (también conocido como pad de dirección o D-pad). Un gamepad permite el movimiento tanto en el eje hacia arriba y hacia abajo, como en el eje izquierdo y derecho. El resultado es que puede señalar ocho direcciones distintas, con la adición de "sin dirección". En Shape Blaster, nuestra interfaz de gamepad no será física, sino en pantalla, por lo tanto, una virtual gamepad.


Un gamepad físico típico; La almohadilla direccional en este caso tiene forma de plus..

Aunque solo hay cuatro entradas, hay ocho direcciones (más neutral) disponibles.

Para tener un gamepad virtual en nuestro juego, debemos reconocer las entradas táctiles cuando esto sucede, y convertirlo en una forma que el juego ya entienda..

El gamepad virtual implementado aquí funciona en tres pasos:

  1. Determine el tipo de toque.
  2. Determine si está en el área de un gamepad en pantalla.
  3. Emular el toque como una pulsación de tecla o el movimiento del ratón..

En cada paso nos centraremos únicamente en el toque que tenemos, y realizaremos un seguimiento del último evento de toque que tuvimos que comparar. También haremos un seguimiento de la identifición de toque, que determina qué dedo está tocando qué gamepad.

La siguiente captura de pantalla muestra cómo aparecerán los gamepads en la pantalla:

Captura de pantalla de los gamepads finales en posición..

Añadiendo Multi-Touch a Shape Blaster

En el Utilidad biblioteca, veamos la clase de evento que usaremos principalmente. toTouchEvent Encapsula todo lo que necesitamos para manejar eventos táctiles a un nivel básico..

 class tTouchEvent public: enum EventType kTouchBegin, kTouchEnd, kTouchMove,; public: EventType mEvent; tPoint2f mLocation; uint8_t mID; public: tTouchEvent (const EventType & newEvent, const tPoint2f & newLocation, const uint8_t & newID): mEvent (newEvent), mLocation (newLocation), mID (newID) ;

los Tipo de evento nos permite definir el tipo de eventos que permitiremos sin complicarse demasiado. ubicación será el punto de contacto real, y medio será el ID del dedo, que comienza en cero y agrega uno para cada dedo que se toca en la pantalla. Si definimos el constructor para tomar solo const referencias, podremos instanciar clases de eventos sin tener que crear explícitamente variables nombradas para ellos.

Nosotros usaremos toTouchEvent exclusivamente para enviar eventos táctiles desde el sistema operativo a nuestro Entrada clase. También lo usaremos más adelante para actualizar la representación gráfica de los gamepads en el VirtualGamepad clase.

La clase de entrada

La versión original de XNA y C # de la Entrada la clase puede manejar el mouse, el teclado y las entradas físicas reales del gamepad. El mouse se usa para disparar en un punto arbitrario en la pantalla desde cualquier posición; El teclado se puede utilizar para mover y disparar en determinadas direcciones. Como hemos elegido emular la entrada original (para mantenernos fieles a un "puerto directo"), mantendremos la mayoría del código original de la misma manera, usando los nombres teclado y ratón, aunque no tengamos ni en dispositivos iOS.

Así es como nuestro Entrada clase se verá Para cada dispositivo, deberemos mantener una "instantánea actual" y una "instantánea anterior" para poder saber qué ha cambiado entre el último evento de entrada y el evento de entrada actual. En nuestro caso, mMouseState y mKeyboardState son la "instantánea actual", y mLastMouseState y mLastKeyboardState Representa la "instantánea anterior".

 Clase de entrada: público tSingleton protegido: tPoint2f mMouseState; tPoint2f mLastMouseState; tPoint2f mFreshMouseState; std :: vector mKeyboardState; std :: vector mLastKeyboardState; std :: vector mFreshKeyboardState; bool mIsAimingWithMouse; uint8_t mLeftEngaged; uint8_t mRightEngaged; public: enum KeyType kUp = 0, kLeft, kDown, kRight, kW, kA, kS, kD,; protegido: tVector2f GetMouseAimDirection () const; protegido: Entrada (); public: tPoint2f getMousePosition () const; actualización nula (); // Comprueba si una tecla se acaba de presionar bool wasKeyPressed (KeyType) const; tVector2f getMovementDirection () const; tVector2f getAimDirection () const; void onTouch (const tTouchEvent & msg); amigo clase tSingleton; amigo clase VirtualGamepad; ; Entrada :: Entrada (): mMouseState (-1, -1), mLastMouseState (-1, -1), mIsAimingWithMouse (false), mLeftEngaged (255), mRightEngaged (255) mKeyboardState.resize (8); mLastKeyboardState.resize (8); mFreshKeyboardState.resize (8); para (size_t i = 0; i < 8; i++)  mKeyboardState[i] = false; mLastKeyboardState[i] = false; mFreshKeyboardState[i] = false;   tPoint2f Input::getMousePosition() const  return mMouseState; 

Actualización de entrada

En una PC, cualquier evento que obtengamos es "distinto", lo que significa que un movimiento del mouse es diferente a empujar la letra UNA, e incluso la carta UNA es lo suficientemente diferente de la letra S que podemos decir que no es exactamente el mismo evento.

Con iOS, nosotros solo alguna vez obtenga eventos de entrada táctil, y un toque no es lo suficientemente distinto de otro para que podamos saber si se trata de un movimiento del mouse o de una pulsación de tecla, o incluso de qué tecla es. Todos los eventos se ven exactamente iguales desde nuestro punto de vista.

Para ayudar a resolver esta ambigüedad, presentaremos dos nuevos miembros, mFreshMouseState y mFreshKeyboardState. Su propósito es agregar, o "capturar todos", los eventos en un marco particular, sin modificar las otras variables de estado de otra manera. Una vez que estemos satisfechos de que haya pasado un marco, podemos actualizar el estado actual con los miembros "nuevos" llamando Entrada :: actualizar. Entrada :: actualizar También le dice a nuestro estado de entrada para avanzar.

 void Input :: update () mLastKeyboardState = mKeyboardState; mLastMouseState = mMouseState; mKeyboardState = mFreshKeyboardState; mMouseState = mFreshMouseState; if (mKeyboardState [kLeft] || mKeyboardState [kRight] || mKeyboardState [kUp] || mKeyboardState [kDown]) mIsAimingWithMouse = false;  else if (mMouseState! = mLastMouseState) mIsAimingWithMouse = true; 

Ya que lo haremos una vez por cuadro, llamaremos Entrada :: actualizar () desde dentro GameRoot :: onRedrawView ():

 // In GameRoot :: onRedrawView () Input :: getInstance () -> update ();

Ahora veamos cómo convertimos la entrada táctil en un mouse o teclado simulado. Primero, planearemos tener dos áreas rectangulares diferentes que representen los gamepads virtuales. Cualquier cosa fuera de estas áreas consideraremos "definitivamente un evento de mouse"; Cualquier cosa dentro, consideraremos "definitivamente un evento de teclado".

Cualquier cosa dentro de los cuadros rojos se asignará a nuestra entrada de teclado simulada; Cualquier otra cosa que trataremos como entrada de ratón.

Miremos a Entrada :: onTouch (), que recibe todos los eventos táctiles. Tomaremos un panorama general del método y solo anotaremos áreas QUE HACER Donde código más específico debe ser:

 void Input :: onTouch (const tTouchEvent & msg) tPoint2f leftPoint = VirtualGamepad :: getInstance () -> mLeftPoint - tPoint2f (18, 18); tPoint2f rightPoint = VirtualGamepad :: getInstance () -> mRightPoint - tPoint2f (18, 18); tPoint2f intPoint ((int) msg.mLocation.x, (int) msg.mLocation.y); bool mouseDown = (msg.mEvent == tTouchEvent :: kTouchBegin) || (msg.mEvent == tTouchEvent :: kTouchMove); if (! mouseDown) if (msg.mID == mLeftEngaged) // TODO: configura todas las teclas de movimiento como "key up" else if (msg.mID == mRightEngaged) // TODO: configura todas las teclas de encendido como "key up" if (mouseDown && tRectf (leftPoint, 164, 164) .contains (intPoint)) mLeftEngaged = msg.mID; // TODO: configura todas las teclas de movimiento como "tecla arriba" // TODO: determina qué teclas de movimiento configurar if (mouseDown && tRectf (rightPoint, 164, 164) .contains (intPoint)) mRightEngaged = msg.mID; // TODO: configura todas las teclas de disparo como "key up" // TODO: determina qué teclas de disparo configurar if (! TRectf (leftPoint, 164, 164) .contains (intPoint) &&! TRectf (rightPoint, 164, 164) .contains (intPoint)) // Si lo hacemos aquí, touch fue definitivamente un "evento del mouse" mFreshMouseState = tPoint2f ((int32_t) msg.mLocation.x, (int32_t) msg.mLocation.y); 

El código es bastante simple, pero está ocurriendo una lógica poderosa que señalaré:

  1. Determinamos dónde estarán en pantalla los gamepads izquierdo y derecho, de modo que podamos ver si estamos en ellos cuando tocamos o soltamos. Estos se almacenan en el leftPoint y rightPoint variables locales.
  2. Determinamos la ratón hacia abajo Estado: si estamos "presionando" con un dedo, necesitamos saber si está dentro leftPointes rect o rightPoint's rect, y si es así, tomar medidas para actualizar el Fresco Estado para el teclado. Si no está en ninguno de los dos rectos, asumiremos que es un evento de mouse y actualizaremos Fresco estado para el ratón.
  3. Finalmente, hacemos un seguimiento de las identificaciones táctiles (o identificaciones de los dedos) a medida que se presionan; Si detectamos que un dedo se levanta de la superficie y está asociado con un gamepad activo, restableceremos el teclado simulado para dicho gamepad..

Ahora que vemos el panorama general, vamos a profundizar un poco más.

Rellenando los huecos

Cuando un dedo se levanta de la superficie del iPhone o iPad, verificamos si es un dedo que sabemos que está en un gamepad y, de ser así, restablecemos todas las "teclas simuladas" para ese gamepad:

 if (! mouseDown) if (msg.mID == mLeftEngaged) mFreshKeyboardState [kA] = falso; mFreshKeyboardState [kD] = falso; mFreshKeyboardState [kW] = falso; mFreshKeyboardState [kS] = falso;  else if (msg.mID == mRightEngaged) mFreshKeyboardState [kUp] = falso; mFreshKeyboardState [kDown] = false; mFreshKeyboardState [kLeft] = false; mFreshKeyboardState [kRight] = falso; 

La situación es algo diferente cuando hay un toque que comienza en la superficie o se mueve; Verificamos si el toque está dentro de cualquier gamepad. Dado que el código para ambos gamepads es similar, solo echaremos un vistazo al gamepad izquierdo (que trata del movimiento).

Siempre que tengamos un evento táctil, borraremos completamente el estado del teclado para ese gamepad en particular y verificaremos dentro de nuestra área correcta para determinar qué tecla o teclas presionar. Entonces, aunque tenemos un total de ocho direcciones (más neutral), solo marcaremos cuatro rectángulos: uno para arriba, uno para abajo, uno para izquierda y otro para derecha.

Las nueve áreas de interés en nuestro gamepad..
 if (mouseDown && tRectf (leftPoint, 164, 164) .contains (intPoint)) mLeftEngaged = msg.mID; mFreshKeyboardState [kA] = falso; mFreshKeyboardState [kD] = falso; mFreshKeyboardState [kW] = falso; mFreshKeyboardState [kS] = falso; if (tRectf (leftPoint, 72, 164) .contains (intPoint)) mFreshKeyboardState [kA] = true; mFreshKeyboardState [kD] = falso;  else if (tRectf (leftPoint + tPoint2f (128, 0), 72, 164) .contains (intPoint)) mFreshKeyboardState [kA] = falso; mFreshKeyboardState [kD] = true;  if (tRectf (leftPoint, 164, 72) .contains (intPoint)) mFreshKeyboardState [kW] = true; mFreshKeyboardState [kS] = falso;  else if (tRectf (leftPoint + tPoint2f (0, 128), 164, 72) .contains (intPoint)) mFreshKeyboardState [kW] = false; mFreshKeyboardState [kS] = true; 

Visualización de gráficos para el Gamepad virtual

Si ejecuta el juego ahora, tendrá soporte para el gamepad virtual, pero en realidad no podrá ver dónde comienzan o terminan los gamepads virtuales..

Aquí es donde el VirtualGamepad La clase entra en juego. los VirtualGamepadEl propósito principal de 's es dibujar los gamepads en la pantalla. La forma en que mostraremos el gamepad será la forma en que otros juegos tienden a hacerlo si tienen gamepads: como un círculo "base" más grande, y un círculo más pequeño de "palanca de control" que podemos mover. Esto parece similar a un joystick de arcade desde arriba hacia abajo, y más fácil de dibujar que algunas otras alternativas..

Primero, note que los archivos de imagen. vpad_top.png y vpad_bot.png Se han añadido al proyecto. Vamos a modificar el Art º clase para cargarlos:

 clase de arte: público tSingleton protegido: ... tTexture * mVPadBottom; tTexture * mVPadTop;… public:… tTexture * getVPadBottom () const; tTexture * getVPadTop () const;… amigo clase tSingleton; ; Art :: Art () … mVPadTop = new tTexture (tSurface ("vpad_top.png")); mVPadBottom = new tTexture (tSurface ("vpad_bot.png"));  tTexture * Art :: getVPadBottom () const return mVPadBottom;  tTexture * Art :: getVPadTop () const return mVPadTop; 

los VirtualGamepad La clase dibujará ambos gamepads en la pantalla y mantendrá Estado informacion en los miembros mLeftStick y mRightStick sobre dónde dibujar los "sticks de control" de los gamepads.

He elegido algunas posiciones ligeramente arbitrarias para los gamepads, que se inicializan en el mLeftPoint y mRightPoint miembros: los cálculos los colocan en aproximadamente el 3.75% desde el borde izquierdo o derecho de la pantalla, y aproximadamente el 13% desde la parte inferior de la pantalla. Basé estas medidas en un juego comercial con gamepads virtuales similares pero jugabilidad diferente.

 clase VirtualGamepad: tSingleton público público: enumeración Estado kCenter = 0x00, kTop = 0x01, kBottom = 0x02, kLeft = 0x04, kRight = 0x08, kTopLeft = 0x05, kzopcc. protegido: tPoint2f mLeftPoint; tPoint2f mRightPoint; int mLeftStick; int mRightStick; protegido: VirtualGamepad (); void DrawStickAtPoint (tSpriteBatch * spriteBatch, const tPoint2f & point, estado del estado); void UpdateBasedOnKeys (); public: void draw (tSpriteBatch * spriteBatch); actualización nula (const tTouchEvent & msg); amigo clase tSingleton; Entrada de clase de amigo; ; VirtualGamepad :: VirtualGamepad (): mLeftStick (kCenter), mRightStick (kCenter) mLeftPoint = tPoint2f (int (3.0f / 80.0f * 800.0f), 600 - int (21.0f / 160.0f * 600.0f) - 128); mRightPoint = tPoint2f (800 - int (3.0f / 80.0f * 800.0f) - 128, 600 - int (21.0f / 160.0f * 600.0f) - 128); 

Como se menciono antes, mLeftStick y mRightStick son máscaras de bits, y su uso es para determinar dónde dibujar el círculo interior del gamepad. Calcularemos la máscara de bits en el método. VirtualGamepad :: UpdateBasedOnKeys ().

Este método se llama inmediatamente después Entrada :: onTouch, para que podamos leer a los miembros estatales "nuevos" y saber que están actualizados:

 void VirtualGamepad :: UpdateBasedOnKeys () Input * inp = Input :: getInstance (); mLeftStick = kCenter; if (inp-> mFreshKeyboardState [Input :: kA]) mLeftStick | = kLeft;  else if (inp-> mFreshKeyboardState [Input :: kD]) mLeftStick | = kRight;  if (inp-> mFreshKeyboardState [Input :: kW]) mLeftStick | = kTop;  else if (inp-> mFreshKeyboardState [Input :: kS]) mLeftStick | = kBottom;  mRightStick = kCenter; if (inp-> mFreshKeyboardState [Input :: kLeft]) mRightStick | = kLeft;  else if (inp-> mFreshKeyboardState [Input :: kRight]) mRightStick | = kRight;  if (inp-> mFreshKeyboardState [Input :: kUp]) mRightStick | = kTop;  else if (inp-> mFreshKeyboardState [Input :: kDown]) mRightStick | = kBottom; 

Para dibujar un gamepad individual, llamamos VirtualGamepad :: DrawStickAtPoint (); Este método no sabe ni le importa qué gamepad está dibujando, solo sabe dónde quiere dibujarlo y el estado para hacerlo. Debido a que usamos máscaras de bits y calculamos de antemano, nuestro método se vuelve más pequeño y más fácil leer:

 void VirtualGamepad :: DrawStickAtPoint (tSpriteBatch * spriteBatch, const tPoint2f & point, estado del estado) tPoint2f offset = tPoint2f (18, 18); spriteBatch-> draw (10, Art :: getInstance () -> getVPadBottom (), point, tOptional()); switch (estado) caso kCenter: offset + = tPoint2f (0, 0); descanso; case kTopLeft: offset + = tPoint2f (-13, -13); descanso; case kTop: offset + = tPoint2f (0, -18); descanso; case kTopRight: offset + = tPoint2f (13, -13); descanso; case kRight: offset + = tPoint2f (18, 0); descanso; case kBottomRight: offset + = tPoint2f (13, 13); descanso; case kBottom: offset + = tPoint2f (0, 18); descanso; case kBottomLeft: offset + = tPoint2f (-13, 13); descanso; caso kLeft: offset + = tPoint2f (-18, 0); descanso;  spriteBatch-> draw (11, Art :: getInstance () -> getVPadTop (), point + offset, tOptional()); 

Dibujar dos gamepads es mucho más fácil, ya que es solo una llamada al método anterior dos veces. Miremos a VirtualGamepad :: draw ():

 void VirtualGamepad :: draw (tSpriteBatch * spriteBatch) DrawStickAtPoint (spriteBatch, mLeftPoint, (State) mLeftStick); DrawStickAtPoint (spriteBatch, mRightPoint, (State) mRightStick); 

Finalmente, necesitamos dibujar el gamepad virtual, así que en GameRoot :: onRedrawView (), añadir la siguiente línea:

 VirtualGamepad :: getInstance () -> draw (mSpriteBatch);

Eso es. Si ejecutas el juego ahora, deberías ver los gamepads virtuales en plena vigencia. Cuando tocas dentro del gamepad izquierdo, debes moverte. Cuando tocas dentro del gamepad correcto, tu dirección de disparo debería cambiar. De hecho, puedes usar ambos mandos de juego a la vez, e incluso moverte usando el mando de juego izquierdo y tocar fuera del mando de juego derecho para mover el mouse. Y cuando sueltas, dejas de moverte y (potencialmente) dejas de disparar..

Resumen de la técnica de gamepad virtual

Hemos implementado completamente el soporte para gamepad virtual, y funciona, pero puede resultarle un poco torpe o difícil de usar. ¿Por qué es ese el caso? Aquí es donde el verdadero desafío de los controles táctiles en iOS viene con los juegos tradicionales que inicialmente no fueron diseñados para ellos..

Aunque no estás solo. Muchos juegos sufren de estos problemas y los han superado..

Aquí hay algunas cosas que he observado con la entrada de pantalla táctil; usted podría tener algunas observaciones similares a ti mismo:

Primero, los controladores de juego tienen una sensación diferente a una pantalla táctil plana; sabes dónde está tu dedo en un gamepad real y cómo evitar que tus dedos se deslicen. Sin embargo, en una pantalla táctil, sus dedos pueden quedar un poco alejados de la zona táctil, por lo que es posible que su entrada no se reconozca correctamente, y es posible que no se dé cuenta de que es así hasta que sea demasiado tarde..

Segundo, es posible que también hayas notado, cuando juegas con los controles táctiles, que tu mano oscurece tu visión, por lo que la nave puede ser golpeada por un enemigo debajo de tu mano que no viste para empezar.!

Finalmente, puede encontrar que las áreas táctiles son más fáciles de usar en un iPad que en un iPhone o viceversa. Por lo tanto, tenemos problemas con un tamaño de pantalla diferente que afecta nuestro "tamaño del área de entrada", que definitivamente es algo que no experimentamos tanto en una computadora de escritorio. (La mayoría de los teclados y ratones son del mismo tamaño y actúan de la misma manera, o se pueden ajustar).

Aquí hay algunos cambios que podría realizar en el sistema de entrada descrito en este artículo:

  • Dibuja la ubicación central de tu gamepad donde comienza tu toque; esto permite que la mano del jugador se mueva ligeramente sin impacto, y significa que pueden tocar en cualquier parte de la pantalla.
  • Haz que tu "área de juego" sea más pequeña y mueve el gamepad fuera del área de juego por completo. Ahora tus dedos no obstruirán tu vista..
  • Haga interfaces de usuario separadas y distintas para iPhone y iPad. Esto le permitirá ajustar el diseño según el tipo de dispositivo, pero también requiere que tenga diferentes dispositivos para probar.
  • Haz que los enemigos o el jugador se desplacen un poco más lento. Esto le permite al usuario experimentar el juego más fácilmente, pero también hace que su juego sea más fácil de ganar.
  • Deshazte de los gamepads virtuales y usa otro esquema. Estás a cargo, después de todo!

Una vez más, depende de ti lo que quieras hacer y cómo quieres hacerlo. En el lado positivo, hay muchas maneras de hacer la entrada táctil. La parte difícil es hacerlo bien y hacer felices a tus jugadores..

Agujeros negros

Uno de los enemigos más interesantes en Geometry Wars es el calabozo. Examinemos cómo podemos hacer algo similar en Shape Blaster. Crearemos la funcionalidad básica ahora, y volveremos a visitar al enemigo en el siguiente tutorial para agregar efectos de partículas e interacciones de partículas..

Un agujero negro con partículas en órbita.

Funcionalidad básica

Los agujeros negros tirarán de la nave del jugador, los enemigos cercanos y (después del siguiente tutorial) las partículas, pero repelerán las balas..

Hay muchas funciones posibles que podemos usar para la atracción o la repulsión. Lo más simple es usar una fuerza constante para que el agujero negro tire con la misma fuerza independientemente de la distancia del objeto. Otra opción es hacer que la fuerza aumente linealmente, desde cero a cierta distancia máxima, hasta fuerza total para objetos directamente sobre el agujero negro. Si quisiéramos modelar la gravedad de manera más realista, podemos usar el cuadrado inverso de la distancia, lo que significa que la fuerza de la gravedad es proporcional a 1 / (distancia ^ 2).

En realidad, usaremos cada una de estas tres funciones para manejar diferentes objetos. Las balas serán repelidas con una fuerza constante; Los enemigos y la nave del jugador serán atraídos con una fuerza lineal; y las partículas usarán una función cuadrada inversa.

Haremos una nueva clase para agujeros negros. Comencemos con la funcionalidad básica:

 clase BlackHole: entidad pública protected: int mHitPoints; público: BlackHole (const tVector2f & position); actualización nula (); empate vacío (tSpriteBatch * spriteBatch); void wasShot (); matanza del vacío (); ; BlackHole :: BlackHole (const tVector2f & position): mHitPoints (10) mImage = Art :: getInstance () -> getBlackHole (); mPosición = posición; mRadius = mImage-> getSurfaceSize (). width / 2.0f; mKind = kBlackHole;  void BlackHole :: wasShot () mHitPoints--; si (mHitPoints <= 0)  mIsExpired = true;   void BlackHole::kill()  mHitPoints = 0; wasShot();  void BlackHole::draw(tSpriteBatch* spriteBatch)  // make the size of the black hole pulsate float scale = 1.0f + 0.1f * sinf(tTimer::getTimeMS() * 10.0f / 1000.0f); spriteBatch->draw (1, mImage, tPoint2f ((int32_t) mPosition.x, (int32_t) mPosition.y), tOptional(), mColor, mOrientation, getSize () / 2.0f, tVector2f (escala)); 

Los agujeros negros toman diez tiros para matar. Ajustamos la escala del sprite ligeramente para hacer que pulse. Si decide que la destrucción de los agujeros negros también debe otorgar puntos, debe realizar ajustes similares a los Agujero negro clase como lo hicimos con el Enemigo clase.

A continuación, haremos que los agujeros negros en realidad apliquen una fuerza sobre otras entidades. Necesitaremos un pequeño método de ayuda de nuestra EntityManager:

 std :: list EntityManager :: getNearbyEntities (const tPoint2f & pos, float radius) std :: list resultado; para (std :: list:: iterator iter = mEntities.begin (); iter! = mEntities.end (); iter ++) if (* iter) if (pos.distanceSquared ((* iter) -> getPosition ()) < radius * radius)  result.push_back(*iter);    return result; 

Este método podría hacerse más eficiente utilizando un esquema de partición espacial más complicado, pero para la cantidad de entidades que tendremos, está bien como está.

Ahora podemos hacer que los agujeros negros apliquen fuerza en sus BlackHole :: update () método:

 void BlackHole :: update () std :: list entities = EntityManager :: getInstance () -> getNearbyEntities (mPosition, 250); para (std :: list:: iterator iter = entities.begin (); iter! = entidades.end (); iter ++) if ((* iter) -> getKind () == kEnemy &&! ((Enemy *) (* iter)) -> getIsActive ()) // No hacer nada else if ((* iter) -> getKind () == kBullet) tVector2f temp = ((* iter) -> getPosition () - mPosition); (* iter) -> setVelocity ((* iter) -> getVelocity () + temp.normalize () * 0.3f);  else tVector2f dPos = mPosition - (* iter) -> getPosition (); longitud de flotación = dPos.length (); (* iter) -> setVelocity ((* iter) -> getVelocity () + dPos.normalize () * tMath :: mix (2.0f, 0.0f, length / 250.0f)); 

Los agujeros negros solo afectan a las entidades dentro de un radio elegido (250 píxeles). Las balas dentro de este radio tienen una fuerza de repulsión constante aplicada, mientras que todo lo demás tiene una fuerza atractiva lineal aplicada.

Tendremos que agregar manejo de colisiones para agujeros negros a la EntityManager. Añadir un std :: list para los agujeros negros, como hicimos con los otros tipos de entidades, y agregue el siguiente código en EntityManager :: handleCollisions ():

 // manejar colisiones con agujeros negros para (std :: list:: iterador i = mBlackHoles.begin (); i! = mBlackHoles.end (); i ++) para (std :: list:: iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) if ((* j) -> getIsActive () && isColliding (* i, * j)) (* j) -> wasShot ();  para (std :: list:: iterador j = mBullets.begin (); j! = mBullets.end (); j ++) if (isColliding (* i, * j)) (* j) -> setExpired (); (* i) -> wasShot ();  if (isColliding (PlayerShip :: getInstance (), * i)) KillPlayer (); descanso; 

Finalmente, abre el EnemySpawner Clase y haz que creen algunos agujeros negros. Limité el número máximo de agujeros negros a dos, y di una posibilidad entre 600 de que se formara un agujero negro en cada cuadro.

 if (EntityManager :: getInstance () -> getBlackHoleCount () < 2 && int32_t(tMath::random() * mInverseBlackHoleChance) == 0)  EntityManager::getInstance()->add (nuevo BlackHole (GetSpawnPosition ())); 

Conclusión

Hemos discutido y agregado gamepads virtuales, y agregado agujeros negros usando varias fórmulas de fuerza. Shape Blaster está empezando a verse bastante bien. En la siguiente parte, agregaremos algunos efectos de partículas locas y exageradas..

Referencias

  • Crédito de la foto: controlador de Wii por kazuma jp.