Haz un Neon Vector Shooter para iOS más juego

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. Hasta ahora, hemos configurado la jugabilidad básica; Ahora, vamos a añadir enemigos y un sistema de puntuación..

Visión general

En esta parte, nos basaremos en el tutorial anterior agregando enemigos, detección de colisiones y anotando.

Aquí están las nuevas características en acción:


Advertencia: Alto!

Agregaremos las siguientes nuevas clases para manejar esto:

  • Enemigo
  • EnemySpawner: Responsable de crear enemigos y aumentar gradualmente la dificultad del juego.
  • Estado de jugador: Sigue la puntuación del jugador, la puntuación más alta y las vidas.

Puede que hayas notado que hay dos tipos de enemigos en el video, pero solo hay una clase de Enemigo. Podríamos derivar subclases del enemigo para cada tipo de enemigo. La versión XNA original del juego no lo hizo, debido a los siguientes inconvenientes:

  • Añaden más código repetitivo..
  • Pueden aumentar la complejidad del código y hacer que sea más difícil de entender. El estado y la funcionalidad de un objeto se extienden a lo largo de toda su cadena de herencia..
  • No son muy flexibles: no puede compartir piezas de funcionalidad entre diferentes ramas del árbol de herencia si esa funcionalidad no está en la clase base. Por ejemplo, considere hacer dos clases, Mamífero y Pájaro, que ambos derivan de Animal. los Pájaro clase tiene un Volar() método. Entonces decides agregar un Murciélago clase que se deriva de Mamífero y también puede volar. Para compartir esta funcionalidad usando solo la herencia, tendría que mover la Volar() método para el Animal Clase a la que no pertenece. Además, no puede eliminar métodos de clases derivadas, por lo que si realizó una Pingüino clase que deriva de Pájaro, también tendría que tener un Volar() método.

Para este tutorial, nos alinearemos con la versión XNA original y favoreceremos la composición sobre la herencia para implementar los diferentes tipos de enemigos. Haremos esto creando varios reutilizables. comportamientos que podemos añadir a los enemigos. Entonces podemos mezclar y combinar comportamientos fácilmente cuando creamos nuevos tipos de enemigos. Por ejemplo, si ya tuviéramos un FollowPlayer comportamiento y un DodgeBullet comportamiento, podríamos crear un nuevo enemigo que haga ambos simplemente agregando ambos comportamientos.

Artículos Relacionados
  • Introducción a la programación orientada a objetos para el desarrollo de juegos
  • Un enfoque pragmático de la composición de la entidad
  • Unidad: Ahora estás pensando con componentes

Enemigos

Los enemigos tendrán algunas propiedades adicionales sobre las entidades. Para que el jugador tenga tiempo de reaccionar, haremos que los enemigos desaparezcan gradualmente antes de que se vuelvan activos y peligrosos..

Vamos a codificar la estructura básica de la Enemigo clase:

 clase Enemy: public Entity public: enum Behavior kFollow = 0, kMoveRandom,; protegido: std :: list mBehaviors; float mRandomDirection; int mRandomState; int mPointValue; int mTimeUntilStart; protegido: void AddBehaviour (Comportamiento b); void ApplyBehaviours (); public: Enemy (tTexture * image, const tVector2f & position); actualización nula (); bool getIsActive (); int getPointValue (); static Enemy * createSeeker (const tVector2f & position); static Enemy * createWanderer (const tVector2f & position); void handleCollision (Enemy * other); void wasShot (); bool followPlayer (aceleración de flotación); bool moveRandomly (); ; Enemy :: Enemy (tTexture * image, const tVector2f & position): mPointValue (1), mTimeUntilStart (60) mImage = image; mPosición = posición; mRadius = image-> getSurfaceSize (). width / 2.0f; mColor = tColor4f (0,0,0,0); mKind = kEnemy;  void Enemy :: update () if (mTimeUntilStart <= 0)  ApplyBehaviours();  else  mTimeUntilStart--; mColor = tColor4f(1,1,1,1) * (1.0f - (float)mTimeUntilStart / 60.0f);  mPosition += mVelocity; mPosition = tVector2f(tMath::clamp(mPosition.x, getSize().width / 2.0f, GameRoot::getInstance()->getViewportSize (). width - getSize (). width / 2.0f), tMath :: clamp (mPosition.y, getSize (). height / 2.0f, GameRoot :: getInstance () -> getViewportSize (). height - getSize ( ) .height / 2.0f)); mVelocity * = 0.8f;  void Enemy :: wasShot () mIsExpired = true; PlayerStatus :: getInstance () -> addPoints (mPointValue); PlayerStatus :: getInstance () -> IncreaseMultiplier (); tSound * temp = Sound :: getInstance () -> getExplosion (); if (! temp-> isPlaying ()) temp-> play (0, 1); 

Este código hará que los enemigos se desvanezcan durante 60 cuadros y permitirá que funcione su velocidad. Multiplicando la velocidad por 0.8 se falsifica un efecto similar a la fricción. Si hacemos que los enemigos aceleren a una velocidad constante, esta fricción hará que se acerquen suavemente a una velocidad máxima. La simplicidad y la suavidad de este tipo de fricción son agradables, pero es posible que desee utilizar una fórmula diferente según el efecto que desee..

los fue disparado() Método será llamado cuando el enemigo recibe un disparo. Lo añadiremos más adelante en la serie..

Queremos que los diferentes tipos de enemigos se comporten de manera diferente; lo lograremos asignando comportamientos. Un comportamiento usará alguna función personalizada que ejecuta cada cuadro para controlar al enemigo.

La versión XNA original de Shape Blaster usaba una función de lenguaje especial de C # para automatizar los comportamientos. Sin entrar en demasiados detalles (ya que no los usaremos), el resultado final fue que el tiempo de ejecución de C # llamaría a los métodos de comportamiento en cada cuadro sin tener que decirlo explícitamente..

Como esta función de lenguaje no existe en C o C ++, tendremos que llamar a los comportamientos explícitamente nosotros mismos. Aunque esto requiere un poco más de código, el beneficio adicional es que sabremos exactamente cuándo se actualizan nuestros comportamientos y, por lo tanto, nos da un control más preciso..

Nuestro comportamiento más simple será el followPlayer () comportamiento que se muestra a continuación:

 bool Enemy :: followPlayer (aceleración flotante) if (! PlayerShip :: getInstance () -> getIsDead ()) tVector2f temp = (PlayerShip :: getInstance () -> getPosition () - mPosition); temp = temp * (aceleración / temp.length ()); mVelocity + = temp;  if (mVelocity! = tVector2f (0,0)) mOrientation = atan2f (mVelocity.y, mVelocity.x);  devuelve true; 

Esto simplemente hace que el enemigo acelere hacia el jugador a una velocidad constante. La fricción que agregamos anteriormente asegurará que finalmente se complete a una velocidad máxima (cinco píxeles por cuadro cuando la aceleración es una unidad, ya que \ (0.8 \ veces 5 + 1 = 5 \).

Agreguemos los andamios necesarios para que los comportamientos funcionen. Los enemigos necesitan almacenar sus comportamientos, por lo que agregaremos una variable a la Enemigo clase:

 std :: list mBehaviors;

mBehaviors es un std :: list Contiene todos los comportamientos activos. En cada cuadro, veremos todos los comportamientos que tiene el enemigo y llamaremos la función de comportamiento según el tipo de comportamiento. Si el método de comportamiento vuelve falso, significa que el comportamiento se ha completado, por lo que deberíamos eliminarlo de la lista.

Agregaremos los siguientes métodos a la clase de enemigo:

 void Enemy :: AddBehaviour (Comportamiento b) mBehaviors.push_back (b);  void Enemy :: ApplyBehaviours () std :: list:: iterator iter, iterNext; iter = mBehaviors.begin (); iterNext = iter; while (iter! = mBehaviors.end ()) iterNext ++; resultado bool = falso; switch (* iter) case kFollow: result = followPlayer (0.9f); descanso; caso kMoveRandom: resultado = moveRandomly (); descanso;  if (! result) mBehaviors.erase (iter);  iter = iterNext; 

Y modificaremos la actualizar() método para llamar AplicarBehaviours ():

 if (mTimeUntilStart <= 0)  ApplyBehaviours(); 

Ahora podemos hacer un método estático para crear. buscando enemigos. Todo lo que tenemos que hacer es seleccionar la imagen que queremos y agregar la followPlayer () comportamiento:

 Enemy * Enemy :: createSeeker (const tVector2f & position) Enemy * enemigo = nuevo Enemy (Art :: getInstance () -> getSeeker (), position); enemigo-> AddBehaviour (kFollow); enemigo-> mPointValue = 2; enemigo de vuelta 

Para hacer que un enemigo se mueva al azar, haremos que elija una dirección y luego hagamos pequeños ajustes al azar en esa dirección. Sin embargo, si ajustamos la dirección en cada fotograma, el movimiento será inestable, por lo que solo ajustaremos la dirección periódicamente. Si el enemigo se topa con el borde de la pantalla, haremos que elija una nueva dirección aleatoria que apunte lejos de la pared..

 bool Enemy :: moveRandomly () if (mRandomState == 0) mRandomDirection + = tMath :: random () * 0.2f - 0.1f;  mVelocity + = 0.4f * tVector2f (cosf (mRandomDirection), sinf (mRandomDirection)); Orientación - = 0.05f; límites tRectf = tRectf (0,0, GameRoot :: getInstance () -> getViewportSize ()); lines.location.x - = -mImage-> getSurfaceSize (). width / 2.0f - 1.0f; fronteras.ubicación.y - = -mImagen-> getSurfaceSize (). height / 2.0f - 1.0f; límites.size.width + = 2.0f * (-mImage-> getSurfaceSize (). width / 2.0f - 1.0f); límites.size.height + = 2.0f * (-mImage-> getSurfaceSize (). height / 2.0f - 1.0f); if (! bound.contains (tPoint2f ((int32_t) mPosition.x, (int32_t) mPosition.y))) tVector2f temp = tVector2f (GameRoot :: getInstance () -> getViewportSize (). x, GameRoot :: getInstance ( ) -> getViewportSize (). y) / 2.0f; temp - = mPosición; mRandomDirection = atan2f (temp.y, temp.x) + tMath :: random () * tMath :: PI - tMath :: PI / 2.0f;  mRandomState = (mRandomState + 1)% 6; devuelve verdadero 

Ahora podemos hacer un método de fábrica para crear errante Los enemigos, al igual que hicimos para el buscador:

 Enemy * Enemy :: createWanderer (const tVector2f & position) Enemy * enemy = new Enemy (Art :: getInstance () -> getWanderer (), position); enemigo-> mRandomDirection = tMath :: random () * tMath :: PI * 2.0f; enemigo-> mRandomState = 0; enemigo-> AddBehaviour (kMoveRandom); enemigo de vuelta 

Detección de colisiones

Para la detección de colisiones, modelaremos la nave del jugador, los enemigos y las balas como círculos. La detección de colisiones circulares es buena porque es simple, rápida y no cambia cuando los objetos giran. Si recuerdas, el Entidad la clase tiene un radio y una posición (la posición se refiere al centro de la entidad); esto es todo lo que necesitamos para la detección circular de colisiones.

Probar cada entidad contra todas las demás entidades que potencialmente podrían colisionar puede ser muy lento si tiene una gran cantidad de entidades. Existen muchas técnicas que puede utilizar para acelerar la detección de colisiones de fase amplia, como quadtrees, barrido y podado, y árboles BSP. Sin embargo, por ahora, solo tendremos unas pocas docenas de entidades en pantalla a la vez, por lo que no nos preocuparemos por estas técnicas más complejas. Siempre podemos agregarlos más tarde si los necesitamos..

En Shape Blaster, no todas las entidades pueden colisionar con cualquier otro tipo de entidad. Las balas y la nave del jugador solo pueden chocar con enemigos. Los enemigos también pueden chocar con otros enemigos; esto evitará que se superpongan.

Para lidiar con estos diferentes tipos de colisiones, agregaremos dos nuevas listas a la EntityManager Para realizar un seguimiento de las balas y los enemigos. Cada vez que agregamos una entidad a la EntityManager, Queremos agregarlo a la lista correspondiente, por lo que haremos un privado addEntity () Método para hacerlo. También nos aseguraremos de eliminar cualquier entidad caducada de todas las listas de cada fotograma..

 std :: list mEnemies; std :: list mBullets; void EntityManager :: addEntity (Entidad * entidad) mEntities.push_back (entidad); switch (entity-> getKind ()) case Entity :: kBullet: mBullets.push_back ((Bullet *), entidad); descanso; entidad de caso :: kEnemy: mEnemies.push_back ((Enemy *) entidad); descanso; por defecto: break;  //… // en Update () para (std :: list:: iterator iter = mBullets.begin (); iter! = mBullets.end (); iter ++) if ((* iter) -> isExpired ()) delete * iter; * iter = NULL;  mBullets.remove (NULL); para (std :: list:: iterator iter = mEnemies.begin (); iter! = mEnemies.end (); iter ++) if ((* iter) -> isExpired ()) delete * iter; * iter = NULL;  mEnemies.remove (NULL);

Reemplace las llamadas a entidad.add () en EntityManager.add () y EntityManager.update () con llamadas a addEntity ().

Ahora agreguemos un método que determinará si dos entidades están chocando:

 bool EntityManager :: isColliding (Entidad * a, Entidad * b) radio flotante = a-> getRadius () + b-> getRadius (); volver! a-> isExpired () &&! b-> isExpired () && a-> getPosition (). distanceSquared (b-> getPosition ()) < radius * radius; 

Para determinar si dos círculos se superponen, simplemente verifique si la distancia entre ellos es menor que la suma de sus radios. Nuestro método optimiza esto ligeramente al verificar si el cuadrado de la distancia es menor que el cuadrado de la suma de los radios. Recuerde que es un poco más rápido calcular la distancia al cuadrado que la distancia real.

Diferentes cosas sucederán dependiendo de cual Dos objetos chocan. Si dos enemigos chocan, queremos que se empujen unos a otros; si una bala golpea a un enemigo, la bala y el enemigo deben ser destruidos; Si el jugador toca a un enemigo, el jugador debería morir y el nivel debería restablecerse..

Añadiremos un manejarColision () método para el Enemigo clase para manejar las colisiones entre enemigos:

 void Enemy :: handleCollision (Enemy * other) tVector2f d = mPosition - other-> mPosition; mVelocity + = 10.0f * d / (d.lengthSquared () + 1.0f); 

Este método empujará al enemigo actual lejos del otro enemigo. Cuanto más cerca estén, más será empujado, porque la magnitud de (d / d.LengthSquared ()) es solo uno sobre la distancia.

Reapareciendo el jugador

A continuación, necesitamos un método para manejar la matanza de la nave del jugador. Cuando esto sucede, la nave del jugador desaparecerá por un corto tiempo antes de reaparecer.

Comenzamos agregando dos nuevos miembros a JugadorEnvío:

 int mFramesUntilRespawn; bool PlayerShip :: getIsDead () return mFramesUntilRespawn> 0; 

Al principio de PlayerShip :: update (), agregue lo siguiente:

 if (getIsDead ()) mFramesUntilRespawn--; 

Y anulamos dibujar() como se muestra:

 void PlayerShip :: draw (tSpriteBatch * spriteBatch) if (! getIsDead ()) Entity :: draw (spriteBatch); 

Finalmente, agregamos un matar() método para JugadorEnvío:

 void PlayerShip :: kill () mFramesUntilRespawn = 60; 

Ahora que todas las piezas están en su lugar, agregaremos un método a la EntityManager que pasa por todas las entidades y verifica colisiones:

 void EntityManager :: handleCollisions () for (std :: list:: iterator i = mEnemies.begin (); i! = mEnemies.end (); i ++) para (std :: list:: iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) if (isColliding (* i, * j)) (* i) -> handleCollision (* j); (* j) -> handleCollision (* i);  // manejar colisiones entre balas y enemigos para (std :: list:: iterator i = mEnemies.begin (); i! = mEnemies.end (); i ++) para (std :: list:: iterador j = mBullets.begin (); j! = mBullets.end (); j ++) if (isColliding (* i, * j)) (* i) -> wasShot (); (* j) -> setExpired ();  // manejar las colisiones entre el jugador y los enemigos para (std :: list:: iterator i = mEnemies.begin (); i! = mEnemies.end (); i ++) if ((* i) -> getIsActive () && isColliding (PlayerShip :: getInstance (), * i)) PlayerShip :: getInstance () -> kill (); para (std :: list:: iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) (* j) -> wasShot ();  EnemySpawner :: getInstance () -> reset (); descanso; 

Llame a este método desde actualizar() inmediatamente después de configurar mIsUpdating a cierto.

Engendrador enemigo

Lo último que hay que hacer es hacer el EnemySpawner Clase, que se encarga de crear enemigos. Queremos que el juego comience fácil y se vuelva más difícil, por lo que EnemySpawner Creará enemigos a un ritmo cada vez mayor a medida que avanza el tiempo. Cuando el jugador muere, restableceremos la EnemySpawner a su dificultad inicial.

 clase EnemySpawner: público tSingleton protegido: float mInverseSpawnChance; protegido: tVector2f GetSpawnPosition (); protegido: EnemySpawner (); public: void update (); restablecer el vacío (); amigo clase tSingleton; ; void EnemySpawner :: update () if (! PlayerShip :: getInstance () -> getIsDead () && EntityManager :: getInstance () -> getCount () < 200)  if (int32_t(tMath::random() * mInverseSpawnChance) == 0)  EntityManager::getInstance()->add (Enemy :: createSeeker (GetSpawnPosition ()));  if (int32_t (tMath :: random () * mInverseSpawnChance) == 0) EntityManager :: getInstance () -> add (Enemy :: createWanderer (GetSpawnPosition ()));  if (mInverseSpawnChance> 30) mInverseSpawnChance - = 0.005f;  tVector2f EnemySpawner :: GetSpawnPosition () tVector2f pos; do pos = tVector2f (tMath :: random () * GameRoot :: getInstance () -> getViewportSize (). width, tMath :: random () * GameRoot :: getInstance () -> getViewportSize (). height);  while (pos.distanceSquared (PlayerShip :: getInstance () -> getPosition ()) < 250 * 250); return pos;  void EnemySpawner::reset()  mInverseSpawnChance = 90; 

Cada cuadro, hay uno en mInverseSpawnChance De generar cada tipo de enemigo. La probabilidad de engendrar a un enemigo aumenta gradualmente hasta alcanzar un máximo de uno en veinte. Los enemigos siempre se crean al menos a 250 píxeles del jugador..

Ten cuidado con el mientras incluyete GetSpawnPosition (). Funcionará de manera eficiente siempre y cuando el área en la que los enemigos puedan engendrar sea más grande que el área donde no puedan engendrar. Sin embargo, si hace que el área prohibida sea demasiado grande, obtendrá un bucle infinito.

Llamada EnemySpawner :: update () desde GameRoot :: onRedrawView () y llama EnemySpawner :: reset () cuando el jugador muere.

Score and Lives

  • En Shape Blaster, comienzas con cuatro vidas y obtendrás una vida adicional cada 2,000 puntos..
  • Recibes puntos por destruir enemigos, con diferentes tipos de enemigos que valen diferentes cantidades de puntos.
  • Cada enemigo destruido también aumenta tu puntaje multiplicador en uno..
  • Si no matas a ningún enemigo en un corto período de tiempo, tu multiplicador se reiniciará..
  • La cantidad total de puntos recibidos de cada enemigo que destruyas es el número de puntos que vale el enemigo, multiplicado por tu multiplicador actual.
  • Si pierdes todas tus vidas, el juego termina y comienzas un juego nuevo con tu puntuación restablecida a cero.

Para manejar todo esto, haremos una clase estática llamada Estado de jugador:

 clase PlayerStatus: public tSingleton protected: static const float kMultiplierExpiryTime; static const int kMaxMultiplier; static const std :: string kHighScoreFilename; float mMultiplierTimeLeft; int mLives; int mScore; int mHighScore; int mMultiplier; int mScoreForExtraLife; uint32_t mLastTime; protegido: int LoadHighScore (); void SaveHighScore (puntuación int); protegido: PlayerStatus (); public: void reset (); actualización nula (); void addPoints (int basePoints); void IncreaseMultiplier (); void resetMultiplier (); void removeLife (); int getLives () const; int getScore () const; int getHighScore () const; int getMultiplier () const; bool getIsGameOver () const; amigo clase tSingleton; ; PlayerStatus :: PlayerStatus () mScore = 0; mHighScore = LoadHighScore (); Reiniciar(); mLastTime = tTimer :: getTimeMS ();  void PlayerStatus :: reset () if (mScore> mHighScore) mHighScore = mScore; SaveHighScore (mHighScore);  mScore = 0; mMultiplier = 1; mLives = 4; mScoreForExtraLife = 2000; mMultiplierTimeLeft = 0;  void PlayerStatus :: update () if (mMultiplier> 1) mMultiplierTimeLeft - = float (tTimer :: getTimeMS () - mLastTime) / 1000.0f; si (mMultiplierTimeLeft <= 0)  mMultiplierTimeLeft = kMultiplierExpiryTime; resetMultiplier();   mLastTime = tTimer::getTimeMS();  void PlayerStatus::addPoints(int basePoints)  if (!PlayerShip::getInstance()->getIsDead ()) mScore + = basePoints * mMultiplier; while (mScore> = mScoreForExtraLife) mScoreForExtraLife + = 2000; mLives ++;  void PlayerStatus :: IncreaseMultiplier () if (! PlayerShip :: getInstance () -> getIsDead ()) mMultiplierTimeLeft = kMultiplierExpiryTime; si (mMultiplier < kMaxMultiplier)  mMultiplier++;    void PlayerStatus::resetMultiplier()  mMultiplier = 1;  void PlayerStatus::removeLife()  mLives--; 

Llamada PlayerStatus :: update () desde GameRoot :: onRedrawView () cuando el juego no se detiene.

A continuación, queremos mostrar su puntaje, vidas y multiplicador en la pantalla. Para ello tendremos que añadir un tSpriteFont en el Contenido proyecto y una variable correspondiente en el Art º clase, que nombraremos Fuente. Cargar la fuente en Art ºConstructor como hicimos con las texturas..

Nota: La fuente que estamos usando es en realidad una imagen en lugar de algo como un archivo de fuente TrueType. Las fuentes basadas en imágenes eran la forma en que los juegos y consolas clásicos de arcade imprimían el texto en la pantalla, e incluso ahora algunos juegos de la generación actual todavía utilizan la técnica. Un beneficio que obtenemos de esto es que terminaremos usando las mismas técnicas para dibujar texto en la pantalla como lo hacemos con otros sprites..

Modificar el final de GameRoot :: onRedrawView () donde se dibuja el cursor, como se muestra abajo:

 char buf [80]; sprintf (buf, "Lives:% d", PlayerStatus :: getInstance () -> getLives ()); mSpriteBatch-> drawString (1, Art :: getInstance () -> getFont (), buf, tPoint2f (5,5), tColor4f (1,1,1,1), 0, tPoint2f (0,0), tVector2f ( kScale)); sprintf (buf, "Puntuación:% d", PlayerStatus :: getInstance () -> getScore ()); DrawRightAlignedString (buf, 5); sprintf (buf, "Multiplicador:% d", PlayerStatus :: getInstance () -> getMultiplier ()); DrawRightAlignedString (buf, 35); mSpriteBatch-> draw (0, Art :: getInstance () -> getPointer (), Input :: getInstance () -> getMousePosition (), tOptional());

DrawRightAlignedString () es un método auxiliar para dibujar texto alineado en el lado derecho de la pantalla. Agregarlo a GameRoot añadiendo el siguiente código:

 # define kScale 3.0f void GameRoot :: DrawRightAlignedString (const std :: string & str, int32_t y) int32_t textWidth = int32_t (Art :: getInstance () -> getFont (). getTextSize (str) .width * kScale); mSpriteBatch-> drawString (1, Art :: getInstance () -> getFont (), str, tPoint2f (mViewportSize.width - textWidth - 5, y), tColor4f (1,1,1,1), 0, tPoint2f (0 , 0), tVector2f (kScale)); 

Ahora sus vidas, puntaje y multiplicador deberían aparecer en pantalla. Sin embargo, todavía necesitamos modificar estos valores en respuesta a los eventos del juego. Añadir una propiedad llamada mPointValue al Enemigo clase.

 int Enemy :: getPointValue () return mPointValue; 

Establezca el valor de puntos para diferentes enemigos a algo que considere apropiado. He hecho que los enemigos errantes valgan un punto, y los enemigos que buscan valen dos puntos.

A continuación, agregue las siguientes dos líneas a Enemigo :: wasShot () Para aumentar el puntaje y el multiplicador del jugador:

 PlayerStatus :: getInstance () -> addPoints (mPointValue); PlayerStatus :: getInstance () -> IncreaseMultiplier ();

Llamada PlayerStatus :: removeLife () en PlayerShip :: kill (). Si el jugador pierde todas sus vidas, llame PlayerStatus :: reset () para restablecer su puntuación y vidas al comienzo de un nuevo juego.

Puntuaciones altas

Agreguemos la habilidad del juego para rastrear tu mejor puntaje. Queremos que esta puntuación persista en todas las jugadas, así que la guardaremos en un archivo. Lo mantendremos realmente simple y guardaremos la puntuación más alta como un solo número de texto sin formato en un archivo (esto estará en el directorio "Application Support" de la aplicación, que es un nombre elegante para el directorio "preferences").

Agregue lo siguiente a Estado de jugador:

 const std :: string PlayerStatus :: kHighScoreFilename ("highscore.txt"); void CreatePathIfNonExistant2 (const std :: string & newPath) @autoreleasepool // Cree la ruta si no existe error NSError *; [[NSFileManager defaultManager] createDirectoryAtPath: [NSString stringWithUTF8String: newPath.c_str ()] withIntermediateDirectories: YES atributos: nil error: & error]; 

CreatePathIfNonExistant2 () es una función que he creado que creará un directorio en el dispositivo iOS si aún no existe. Dado que nuestra ruta de preferencia no existirá inicialmente, tendremos que crearla la primera vez.

 std :: string GetExecutableName2 () return [[[[NSBundle mainBundle] infoDictionary] objectForKey: @ "CFBundleExecutable"] UTF8String]; 

GetExecutableName2 () Devuelve el nombre del ejecutable. Usaremos el nombre de la aplicación como parte de la ruta de preferencia. Usaremos esta función en lugar de codificar el nombre del ejecutable para que podamos reutilizar este código para otras aplicaciones sin cambios..

 std :: string GetPreferencePath2 (const std :: string & file) std :: string result = std :: string ([[[NSSearchPathForDirectoriesInDomains (NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex: 0] UTF8String]) + / / Flexible + "/"; CreatePathIfNonExistant2 (resultado); devolver el resultado + archivo; 

GetPreferencePath2 () devuelve el nombre completo de la versión de la cadena de la ruta de preferencia y crea la ruta si aún no existe.

 int PlayerStatus :: LoadHighScore () int score = 0; std :: string fstring; if ([[El archivo del archivo es un archivo de correo electrónico.] [Cadena de control en el dominio de la escuela.] []. nil] UTF8String]; if (! fstring.empty ()) sscanf (fstring.c_str (), "% d", y puntuación);  puntuación de retorno;  void PlayerStatus :: SaveHighScore (puntaje int) char buf [20]; sprintf (buf, "% d", puntuación); [[NSString stringWithUTF8String: buf] writeToFile: [NSString stringWithUTF8String: GetPreferencePath2 (kHighScoreFilename) .c_str ()] atómicamente: SÍ codificación: NSUTF8StringEncoding error: nil]; 

los LoadHighScore () el método primero verifica que el archivo de puntuación alta exista y luego devuelve lo que hay en el archivo como un entero. Es poco probable que el puntaje no sea válido a menos que el usuario generalmente no pueda cambiar los archivos manualmente desde iOS, pero si el puntaje no es un número, el puntaje será cero..

Queremos cargar la puntuación más alta cuando se inicia el juego, y guardarla cuando el jugador obtenga una nueva puntuación más alta. Modificaremos el constructor estático y Reiniciar() métodos en Estado de jugador para hacerlo También añadiremos un miembro auxiliar., mIsGameOver, que usaremos en un momento.

 bool PlayerStatus :: getIsGameOver () const return mLives == 0;  PlayerStatus :: PlayerStatus () mScore = 0; mHighScore = LoadHighScore (); Reiniciar(); mLastTime = tTimer :: getTimeMS ();  void PlayerStatus :: reset () if (mScore> mHighScore) mHighScore = mScore; SaveHighScore (mHighScore);  mScore = 0; mMultiplier = 1; mLives = 4; mScoreForExtraLife = 2000; mMultiplierTimeLeft = 0; 

Eso se encarga de rastrear la puntuación más alta. Ahora tenemos que mostrarlo. Agregaremos el siguiente código a GameRoot :: onRedrawView () en el mismo SpriteBatch Bloque donde se dibuja el otro texto:

 if (PlayerStatus :: getInstance () -> getIsGameOver ()) sprintf (buf, "Game Over \ nSu puntuación:% d \ nHigh Score:% d", PlayerStatus :: getInstance () -> getScore (), PlayerStatus: : getInstance () -> getHighScore ()); tDimension2f textSize = Art :: getInstance () -> getFont (). getTextSize (buf); mSpriteBatch-> drawString (1, Art :: getInstance () -> getFont (), buf, (mViewportSize - textSize) / 2, tColor4f (1,1,1,1), 0, tPoint2f (0,0), tVector2f (kScale)); 

Esto hará que muestre tu puntuación y tu puntuación más alta en el juego, centrado en la pantalla.

Como ajuste final, aumentaremos el tiempo antes de que la nave reaparezca en el juego para darle tiempo al jugador para ver su puntuación. Modificar PlayerShip :: kill () estableciendo el tiempo de reaparición en 300 cuadros (cinco segundos) si el jugador está fuera de vida.

 void PlayerShip :: kill () PlayerStatus :: getInstance () -> removeLife (); mFramesUntilRespawn = PlayerStatus :: getInstance () -> getIsGameOver ()? 300: 120; 

El juego ya está listo para jugar. Puede que no parezca mucho, pero tiene implementados todos los mecanismos básicos. En futuros tutoriales agregaremos efectos de partículas y una cuadrícula de fondo para darle sabor. Pero en este momento, agreguemos rápidamente algo de sonido y música para hacerlo más interesante..

Sonido y musica

Reproducir sonido y música es bastante simple en iOS. Primero, agreguemos nuestros efectos de sonido y música al canal de contenido.

Primero, hacemos una clase de ayuda estática para los sonidos. Tenga en cuenta que el juego es Manejo de sonido clase se llama Sonar, pero nuestro Utilidad de la biblioteca se llama clase de sonido sonido.

 Clase de sonido: público tSingleton protegido: tSound * mMusic; std :: vector mExplosiones; std :: vector mShots; std :: vector mSpawns; protegido: Sonido (); public: tSound * getMusic () const; tSound * getExplosion () const; tSound * getShot () const; tSound * getSpawn () const; amigo clase tSingleton; ; Sonido :: Sonido () char buf [80]; mMusic = new tSound ("music.mp3"); para (int i = 1; i <= 8; i++)  sprintf(buf, "explosion-0%d.wav", i); mExplosions.push_back(new tSound(buf)); if (i <= 4)  sprintf(buf, "shoot-0%d.wav", i); mShots.push_back(new tSound(buf));  sprintf(buf, "spawn-0%d.wav", i); mSpawns.push_back(new tSound(buf));  

Como tenemos múltiples variaciones de ca