Faire un tireur de vecteur Neon pour iOS Plus de gameplay

Dans cette série de didacticiels, je vais vous montrer comment créer un jeu de tir double-stick inspiré de Geometry Wars, avec des graphismes au néon, des effets de particules incroyables et une musique géniale, pour iOS avec C ++ et OpenGL ES 2.0. Jusqu'ici, nous avons mis en place le gameplay de base; maintenant, nous allons ajouter des ennemis et un système de score.

Vue d'ensemble

Dans cette partie, nous allons construire sur le précédent tutoriel en ajoutant des ennemis, une détection de collision et un score..

Voici les nouvelles fonctionnalités en action:


Avertissement: fort!

Nous allons ajouter les nouvelles classes suivantes pour gérer cela:

  • Ennemi
  • EnemySpawner: Responsable de la création d'ennemis et de l'augmentation progressive de la difficulté du jeu.
  • PlayerStatus: Suit le score du joueur, le meilleur score et la vie.

Vous avez peut-être remarqué qu'il y a deux types d'ennemis dans la vidéo, mais qu'il n'y a qu'une seule classe Enemy. Nous pourrions dériver des sous-classes de Enemy pour chaque type d’ennemi. La version XNA originale du jeu ne l’était pas, à cause des inconvénients suivants:

  • Ils ajoutent plus de code passe-partout.
  • Ils peuvent augmenter la complexité du code et le rendre plus difficile à comprendre. L'état et la fonctionnalité d'un objet sont répartis sur toute sa chaîne d'héritage.
  • Ils ne sont pas très flexibles. Vous ne pouvez pas partager des fonctionnalités entre différentes branches de l'arborescence d'héritage si cette fonctionnalité n'est pas dans la classe de base. Par exemple, envisager de faire deux classes, Mammifère et Oiseau, qui proviennent tous deux de Animal. le Oiseau la classe a Voler() méthode. Ensuite, vous décidez d’ajouter un Chauve souris classe qui dérive de Mammifère et peut aussi voler. Pour partager cette fonctionnalité en utilisant uniquement l'héritage, vous devez déplacer le Voler() méthode à la Animal classe où il n'appartient pas. De plus, vous ne pouvez pas supprimer les méthodes des classes dérivées, donc si vous faites un manchot classe qui dérive de Oiseau, il faudrait aussi avoir un Voler() méthode.

Pour ce tutoriel, nous nous associons à la version originale de XNA et privilégions la composition à l'héritage pour implémenter les différents types d'ennemis. Nous ferons cela en créant divers objets réutilisables. comportements que nous pouvons ajouter aux ennemis. Nous pouvons alors facilement mélanger et assortir les comportements lorsque nous créons de nouveaux types d'ennemis. Par exemple, si nous avions déjà un FollowPlayer comportement et un DodgeBullet comportement, nous pourrions faire un nouvel ennemi qui fait les deux simplement en ajoutant les deux comportements.

Articles Similaires
  • Introduction à la programmation orientée objet pour le développement de jeux
  • Une approche pragmatique de la composition des entités
  • Unité: maintenant, vous pensez aux composants

Ennemis

Les ennemis auront quelques propriétés supplémentaires sur les entités. Afin de donner au joueur le temps de réagir, nous ferons en sorte que les ennemis disparaissent progressivement avant de devenir actifs et dangereux..

Codons la structure de base du Ennemi classe:

 classe Enemy: public Entity public: enum Behavior kFollow = 0, kMoveRandom,; protected: std :: list Comportements; float mRandomDirection; int mRandomState; int mPointValue; int mTimeUntilStart; protected: void AddBehaviour (Behavior b); void ApplyBehaviours (); public: Enemy (tTexture * image, const tVector2f & position); void update (); bool getIsActive (); int getPointValue (); Enemy statique * createSeeker (const tVector2f & position); Enemy statique * createWanderer (const tVector2f & position); void handleCollision (Enemy * other); vide wasShot (); bool followPlayer (accélération du flottement); bool moveRandomly (); ; Enemy :: Enemy (tTexture * image, const tVector2f & position): mPointValue (1), mTimeUntilStart (60) mImage = image; mPosition = position; 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 () -> augmentationMultiplier (); tSound * temp = Sound :: getInstance () -> getExplosion (); if (! temp-> isPlaying ()) temp-> play (0, 1); 

Ce code fera fondre les ennemis pendant 60 images et permettra à leur vitesse de fonctionner. Multiplier la vitesse par 0,8 simule un effet de friction. Si nous faisons en sorte que les ennemis accélèrent à un rythme constant, ce frottement leur permettra de s’approcher en douceur d’une vitesse maximale. La simplicité et la douceur de ce type de frottement sont bonnes, mais vous pouvez utiliser une formule différente en fonction de l'effet souhaité..

le a été abattu() La méthode sera appelée lorsque l’ennemi se fera tirer dessus. Nous en ajouterons plus tard dans la série.

Nous voulons que différents types d’ennemis se comportent différemment; nous accomplirons cela en assignant comportements. Un comportement utilisera une fonction personnalisée qui exécute chaque image pour contrôler l'ennemi.

La version XNA d'origine de Shape Blaster utilisait une fonctionnalité de langage spéciale à partir de C # pour automatiser les comportements. Sans entrer trop dans les détails (car nous ne les utiliserons pas), le résultat final est que le runtime C # appelle les méthodes de comportement à chaque image sans avoir à le dire explicitement..

Étant donné que cette fonctionnalité de langage n'existe pas en C ou en C ++, nous devrons appeler explicitement les comportements nous-mêmes. Bien que cela nécessite un peu plus de code, l’avantage supplémentaire est que nous savons exactement quand nos comportements sont mis à jour et nous donne ainsi un contrôle plus précis..

Notre comportement le plus simple sera le suivrePlayer () comportement indiqué ci-dessous:

 bool Enemy :: followPlayer (accélération du flottant) if (! PlayerShip :: getInstance () -> getIsDead ()) tVector2f temp = (PlayerShip :: getInstance () -> getPosition () - mPosition); temp = temp * (accélération / temp.longueur ()); mVelocity + = temp;  if (mVelocity! = tVector2f (0,0)) mOrientation = atan2f (mVelocity.y, mVelocity.x);  return true; 

Cela fait simplement que l'ennemi accélère vers le joueur à un rythme constant. Le frottement que nous avons ajouté plus tôt garantira qu’il atteindra éventuellement une vitesse maximale (cinq pixels par image lorsque l’accélération est égale à une unité, car \ (0.8 \ times 5 + 1 = 5 \).

Ajoutons l'échafaudage nécessaire pour que les comportements fonctionnent. Les ennemis doivent stocker leurs comportements, nous allons donc ajouter une variable à la Ennemi classe:

 std :: list Comportements;

mBehaviors est un std :: list contenant tous les comportements actifs. Chaque image passe en revue tous les comportements de l’ennemi et appelle la fonction de comportement en fonction du type de comportement. Si la méthode de comportement retourne faux, cela signifie que le comportement est terminé, nous devons donc le supprimer de la liste.

Nous allons ajouter les méthodes suivantes à la classe Enemy:

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

Et nous modifierons le mettre à jour() méthode pour appeler ApplyBehaviours ():

 si (mTimeUntilStart <= 0)  ApplyBehaviours(); 

Maintenant, nous pouvons créer une méthode statique pour créer cherchant ennemis. Tout ce que nous avons à faire est de choisir l’image que nous voulons et d’ajouter le suivrePlayer () comportement:

 Enemy * Enemy :: createSeeker (const tVector2f & position) Enemy * ennemi = nouvel Enemy (Art :: getInstance () -> getSeeker (), position); ennemi-> AddBehaviour (kSuivant); ennemi-> mPointValue = 2; renvoyer l'ennemi; 

Pour créer un ennemi qui se déplace de manière aléatoire, nous lui demanderons de choisir une direction, puis de faire de petits ajustements aléatoires dans cette direction. Cependant, si nous ajustons la direction à chaque image, le mouvement sera saccadé. Nous ne modifierons donc la direction que périodiquement. Si l'ennemi se trouve au bord de l'écran, nous lui demanderons de choisir une nouvelle direction aléatoire qui pointe vers le mur..

 bool Enemy :: moveRandomly () if (mRandomState == 0) mRandomDirection + = tMath :: random () * 0.2f - 0.1f;  mVelocity + = 0.4f * tVector2f (cosf (mRandomDirection), sinf (mRandomDirection)); mOrientation - = 0,05f; tRectf bounds = tRectf (0,0, GameRoot :: getInstance () -> getViewportSize ()); bounds.location.x - = -mImage-> getSurfaceSize (). width / 2.0f - 1.0f; bounds.location.y - = -mImage-> getSurfaceSize (). height / 2.0f - 1.0f; bounds.size.width + = 2.0f * (-mImage-> getSurfaceSize (). width / 2.0f - 1.0f); bounds.size.height + = 2.0f * (-mImage-> getSurfaceSize (). height / 2.0f - 1.0f); if (! bounds.contains (tPoint2f ((int32_t) mPosition.x, (int32_t) mPosition.y))) tVector2f temp = tVector2f (GameRoot :: getInstance () -> getViewportSize (). x, GameRoot :: getInstance ( ) -> getViewportSize (). y) / 2.0f; temp - = mPosition; mRandomDirection = atan2f (temp.y, temp.x) + tMath :: random () * tMath :: PI - tMath :: PI / 2.0f;  mRandomState = (mRandomState + 1)% 6; retourne vrai; 

Nous pouvons maintenant faire une méthode d'usine pour créer errant ennemis, un peu comme nous l'avons fait pour le chercheur:

 Enemy * Enemy :: createWanderer (const tVector2f & position) Enemy * ennemi = nouvel Enemy (Art :: getInstance () -> getWanderer (), position); ennemi-> mRandomDirection = tMath :: random () * tMath :: PI * 2.0f; ennemi-> mRandomState = 0; ennemi-> AddBehaviour (kMoveRandom); renvoyer l'ennemi; 

Détection de collision

Pour la détection de collision, nous modéliserons le vaisseau du joueur, les ennemis et les balles sous forme de cercles. La détection de collision circulaire est agréable car simple, rapide et ne change pas lorsque les objets pivotent. Si vous vous en souvenez, le Entité la classe a un rayon et une position (la position correspond au centre de l'entité) - c'est tout ce dont nous avons besoin pour la détection de collision circulaire.

Le test de chaque entité par rapport à toutes les autres entités pouvant potentiellement entrer en collision peut être très lent si vous avez un grand nombre d'entités. Il existe de nombreuses techniques que vous pouvez utiliser pour accélérer la détection de collision de phase large, telles que les arbres à quatre arbres, les éléments de balayage et d'élagage et les arbres BSP. Cependant, pour l'instant, nous n'aurons que quelques douzaines d'entités à la fois, nous ne nous inquiétons donc pas de ces techniques plus complexes. Nous pouvons toujours les ajouter plus tard si nous en avons besoin.

Dans Shape Blaster, toutes les entités ne peuvent pas entrer en collision avec tous les autres types d’entités. Les balles et le vaisseau du joueur ne peuvent entrer en collision qu'avec des ennemis. Les ennemis peuvent également entrer en collision avec d'autres ennemis; cela les empêchera de se chevaucher.

Pour traiter ces différents types de collisions, nous allons ajouter deux nouvelles listes à la liste. EntityManager garder une trace des balles et des ennemis. Chaque fois que nous ajoutons une entité à la EntityManager, nous voudrons l'ajouter à la liste appropriée, nous allons donc faire un privé addEntity () méthode pour le faire. Nous veillerons également à supprimer toutes les entités expirées de toutes les listes de chaque cadre..

 std :: list mEnnemis; std :: list mBullets; void EntityManager :: addEntity (Entity * entity) mEntities.push_back (entity); switch (entity-> getKind ()) case Entity :: kBullet: mBullets.push_back (entité (Bullet *)); Pause; case Entity :: kEnemy: mEnemies.push_back ((Enemy *) entité); Pause; défaut: break;  //… // dans Update () pour (std :: list:: iterator iter = mBullets.begin (); iter! = mBullets.end (); iter ++) if ((* iter) -> isExpired ()) delete * iter; * iter = NULL;  mBullets.remove (NULL); pour (std :: list:: iterator iter = mEnemies.begin (); iter! = mEnemies.end (); iter ++) if ((* iter) -> isExpired ()) delete * iter; * iter = NULL;  mEnemies.remove (NULL);

Remplacer les appels à entity.add () dans EntityManager.add () et EntityManager.update () avec des appels à addEntity ().

Ajoutons maintenant une méthode qui déterminera si deux entités entrent en collision:

 bool EntityManager :: isColliding (Entity * a, Entity * b) rayon de flottement = a-> getRadius () + b-> getRadius (); return! a-> isExpired () &&! b-> isExpired () && a-> getPosition (). distanceSquared (b-> getPosition ()) < radius * radius; 

Pour déterminer si deux cercles se chevauchent, il suffit de vérifier si la distance qui les sépare est inférieure à la somme de leurs rayons. Notre méthode optimise légèrement ceci en vérifiant si le carré de la distance est inférieur au carré de la somme des rayons. Rappelez-vous qu'il est un peu plus rapide de calculer la distance au carré que la distance réelle.

Différentes choses vont se passer selon lequel deux objets entrent en collision. Si deux ennemis se rencontrent, nous voulons qu’ils se repoussent; si une balle frappe un ennemi, la balle et l'ennemi doivent tous deux être détruits; si le joueur touche un ennemi, le joueur doit mourir et le niveau doit être réinitialisé.

Nous allons ajouter un handleCollision () méthode à la Ennemi classe pour gérer les collisions entre ennemis:

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

Cette méthode éloignera l'ennemi actuel de l'autre ennemi. Plus ils sont proches, plus il sera poussé, car la magnitude de (d / d.LengthSquared ()) est juste un sur la distance.

Respawning le joueur

Ensuite, nous avons besoin d'une méthode pour gérer le vaisseau du joueur en train d'être tué. Lorsque cela se produit, le vaisseau du joueur disparaît pendant un court instant avant de réapparaître.

Nous commençons par ajouter deux nouveaux membres à PlayerShip:

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

Au tout début de PlayerShip :: update (), ajoutez ce qui suit:

 if (getIsDead ()) mFramesUntilRespawn--; 

Et nous annulons dessiner() comme montré:

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

Enfin, nous ajoutons un tuer() méthode pour PlayerShip:

 void PlayerShip :: kill () mFramesUntilRespawn = 60; 

Maintenant que toutes les pièces sont en place, nous allons ajouter une méthode à la EntityManager qui passe par toutes les entités et vérifie les collisions:

 EntityManager :: handleCollisions () pour (std :: list):: itérateur i = mEnemies.begin (); i! = mEnemies.end (); i ++) pour (std :: list:: itérateur j = mEnemies.begin (); j! = mEnemies.end (); j ++) if (isColliding (* i, * j)) (* i) -> handleCollision (* j); (* j) -> handleCollision (* i);  // gérer les collisions entre balles et ennemis pour (std :: list:: itérateur i = mEnemies.begin (); i! = mEnemies.end (); i ++) pour (std :: list:: itérateur j = mBullets.begin (); j! = mBullets.end (); j ++) if (isColliding (* i, * j)) (* i) -> wasShot (); (* j) -> setExpired ();  // gérer les collisions entre le joueur et les ennemis pour (std :: list:: itérateur i = mEnemies.begin (); i! = mEnemies.end (); i ++) if ((* i) -> getIsActive () && isColliding (PlayerShip :: getInstance (), * i)) PlayerShip :: getInstance () -> kill (); pour (std :: list:: itérateur j = mEnemies.begin (); j! = mEnemies.end (); j ++) (* j) -> wasShot ();  EnemySpawner :: getInstance () -> reset (); Pause; 

Appelez cette méthode de mettre à jour() immédiatement après le réglage MISE À JOUR à vrai.

Spawner Ennemi

La dernière chose à faire est de faire la EnemySpawner classe, qui est responsable de la création d'ennemis. Nous voulons que le jeu commence facilement et devienne plus difficile. EnemySpawner créera des ennemis à un rythme croissant au fil du temps. Lorsque le joueur meurt, nous réinitialisons le EnemySpawner à sa difficulté initiale.

 classe EnemySpawner: tSingleton publique protected: float mInverseSpawnChance; protected: tVector2f GetSpawnPosition (); protected: EnemySpawner (); public: void update (); void reset (); classe d'amis 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; 

Chaque image, il y en a une dans mInverseSpawnChance de générer chaque type d'ennemi. Les chances de générer un ennemi augmentent progressivement jusqu'à atteindre un maximum de un sur vingt. Les ennemis sont toujours créés à au moins 250 pixels du joueur..

Attention au tandis que boucle dans GetSpawnPosition (). Cela fonctionnera efficacement tant que la zone dans laquelle les ennemis peuvent apparaître est plus grande que la zone où ils ne peuvent pas apparaître. Cependant, si vous agrandissez trop la zone interdite, vous obtiendrez une boucle infinie..

Appel EnemySpawner :: update () de GameRoot :: onRedrawView () et appeler EnemySpawner :: reset () quand le joueur est tué.

Score et vies

  • Dans Shape Blaster, vous commencez par quatre vies et gagnez une vie supplémentaire tous les 2 000 points..
  • Vous recevez des points pour la destruction d'ennemis, différents types d'ennemis valant différentes quantités de points.
  • Chaque ennemi détruit augmente également votre multiplicateur de score d'un.
  • Si vous ne tuez aucun ennemi dans un court laps de temps, votre multiplicateur sera réinitialisé..
  • Le nombre total de points reçus de chaque ennemi que vous détruisez est le nombre de points qu'il vaut, multiplié par votre multiplicateur actuel..
  • Si vous perdez toutes vos vies, le jeu est terminé et vous démarrez un nouveau jeu avec votre score réinitialisé à zéro.

Pour gérer tout cela, nous allons créer une classe statique appelée PlayerStatus:

 class PlayerStatus: tSingleton public protected: statique constante float kMultiplierExpiryTime; static const int kMaxMultiplier; statique statique std :: string kHighScoreFilename; float mMultiplierTimeLeft; int mLives; int mScore; int mHighScore; int mMultiplier; int mScoreForExtraLife; uint32_t mLastTime; protected: int LoadHighScore (); vide SaveHighScore (int score); protected: PlayerStatus (); public: void reset (); void update (); vide addPoints (int basePoints); augmentation de videMultiplier (); void resetMultiplier (); vide removeLife (); int getLives () const; int getScore () const; int getHighScore () const; int getMultiplier () const; bool getIsGameOver () const; classe d'amis tSingleton; ; PlayerStatus :: PlayerStatus () mScore = 0; mHighScore = LoadHighScore (); réinitialiser(); mLastTime = tTimer :: getTimeMS ();  void PlayerStatus :: reset () if (mScore> mHighScore) mHighScore = mScore; SaveHighScore (mHighScore);  mScore = 0; mMultiplicateur = 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 :: augmentationMultiplier () if (! PlayerShip :: getInstance () -> getIsDead ()) mMultiplierTimeLeft = kMultiplierExpiryTime; si (m Multiplicateur < kMaxMultiplier)  mMultiplier++;    void PlayerStatus::resetMultiplier()  mMultiplier = 1;  void PlayerStatus::removeLife()  mLives--; 

Appel PlayerStatus :: update () de GameRoot :: onRedrawView () quand le jeu n'est pas en pause.

Ensuite, nous voulons afficher votre score, vos vies et votre multiplicateur à l'écran. Pour ce faire, nous devrons ajouter un tSpriteFont dans le Contenu projet et une variable correspondante dans le Art classe, que nous nommerons Police de caractère. Charger la police dans ArtLe constructeur de comme nous l'avons fait avec les textures.

Remarque: La police que nous utilisons est en fait une image plutôt que quelque chose comme un fichier de police TrueType. Les polices à base d’images étaient la façon dont les jeux d’arcade et les consoles classiques imprimaient du texte à l’écran. Même à présent, certains jeux de la génération actuelle utilisent encore cette technique. Un des avantages que nous en tirons est que nous finirons par utiliser les mêmes techniques pour dessiner du texte à l'écran que pour les autres sprites..

Modifier la fin de GameRoot :: onRedrawView () où le curseur est dessiné, comme indiqué ci-dessous:

 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, "Score:% d", PlayerStatus :: getInstance () -> getScore ()); DrawRightAlignedString (buf, 5); sprintf (buf, "Multiplier:% d", PlayerStatus :: getInstance () -> getMultiplier ()); DrawRightAlignedString (buf, 35); mSpriteBatch-> draw (0, Art :: getInstance () -> getPointer (), Input :: getInstance () -> getMousePosition (), tOptional());

DrawRightAlignedString () est une méthode d'assistance pour dessiner un texte aligné à droite de l'écran. Ajoutez-le à GameRoot en ajoutant le code ci-dessous:

 #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)); 

Maintenant, vos vies, vos scores et vos multiplicateurs devraient s’afficher à l’écran. Cependant, nous devons encore modifier ces valeurs en réponse aux événements du jeu. Ajouter une propriété appelée mPointValue au Ennemi classe.

 int Enemy :: getPointValue () return mPointValue; 

Définissez la valeur en points pour différents ennemis sur quelque chose qui vous semble approprié. J'ai fait les ennemis errants valant un point, et les ennemis en quête valant deux points.

Ensuite, ajoutez les deux lignes suivantes à Ennemi :: wasShot () pour augmenter le score et le multiplicateur du joueur:

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

Appel PlayerStatus :: removeLife () dans PlayerShip :: kill (). Si le joueur perd toute sa vie, appelez PlayerStatus :: reset () réinitialiser leur score et leur vie au début d'un nouveau jeu.

Scores élevés

Ajoutons la possibilité pour le jeu de suivre votre meilleur score. Nous voulons que ce score persiste d'une partie à l'autre, nous allons donc l'enregistrer dans un fichier. Nous allons garder les choses très simples et enregistrer le meilleur score sous forme de numéro en clair dans un fichier (ce sera dans le répertoire "Application Support" de l'application, qui est un nom de fantaisie pour le répertoire "préférences".)

Ajouter ce qui suit à PlayerStatus:

 const std :: string PlayerStatus :: kHighScoreFilename ("highscore.txt"); void CreatePathIfNonExistant2 (const std :: string & newPath) @autoreleasepool // Créez le chemin s'il n'existe pas. NSError * error; [[NSFileManager defaultManager] createDirectoryAtPath: [NSString stringWithUTF8String: newPath.c_str ()] avecIntermediateDirectories: attributs YES: erreur n °: & erreur]; 

CreatePathIfNonExistant2 () est une fonction que j'ai créée qui va créer un répertoire sur le périphérique iOS s'il n'existe pas déjà. Puisque notre chemin de préférence n'existera pas initialement, nous devrons le créer la première fois..

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

GetExecutableName2 () renvoie le nom de l'exécutable. Nous utiliserons le nom de l'application dans le chemin de préférence. Nous allons utiliser cette fonction au lieu de coder en dur le nom de l'exécutable, afin de pouvoir simplement réutiliser ce code pour d'autres applications sans le modifier..

 std :: string GetPreferencePath2 (const std :: string & fichier) std :: string result = std :: string ([[NSSearchPathForDirectoriesInDomains (NSApplicationSupportDirectory, NSUserDomainMask, YES) objectOtIndex: 0] UTF8String]) + "/". + "/"; CreatePathIfNonExistant2 (résultat); renvoyer le résultat + fichier; 

GetPreferencePath2 () renvoie le nom complet de la version de chaîne du chemin de préférence et crée le chemin s'il n'existe pas déjà.

 int PlayerStatus :: LoadHighScore () int score = 0; std :: string fstring; Fichier [[NStruitManager] nil] UTF8String]; if (! fstring.empty ()) sscanf (fstring.c_str (), "% d", & score);  retour score;  void PlayerStatus :: SaveHighScore (score int) char buf [20]; sprintf (buf, "% d", score); [[NSString stringWithUTF8String: buf] writeToFile: [NSString stringWithUTF8String: GetPreferencePath2 (kHighScoreFilename) .c_str ()] de manière atomique: codage YES: erreur NSUTF8StringEncoding: nil]; 

le LoadHighScore () La méthode vérifie d'abord que le fichier de score élevé existe, puis renvoie le contenu du fichier sous forme d'entier. Il est peu probable que le score soit invalide, à moins que l'utilisateur ne puisse généralement pas modifier manuellement les fichiers depuis iOS, mais s'il finit par être un nombre non numérique, le score finira par être nul..

Nous voulons charger le meilleur score au début de la partie et le sauvegarder lorsque le joueur obtient un nouveau meilleur score. Nous allons modifier le constructeur statique et réinitialiser() méthodes en PlayerStatus faire cela. Nous allons également ajouter un membre d'assistance, mIsGameOver, que nous utiliserons dans un instant.

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

Cela permet de suivre le meilleur score. Nous devons maintenant l'afficher. Nous ajouterons le code suivant à GameRoot :: onRedrawView () dans le même SpriteBatch bloc où l'autre texte est dessiné:

 if (PlayerStatus :: getInstance () -> getIsGameOver ()) sprintf (buf, "Fin de la partie \ nVotre score:% d \ nRésultat élevé:% 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)); 

Cela fera apparaître votre score et votre meilleur score en fin de partie, centrés à l'écran.

Pour un ajustement final, nous allons augmenter le temps qui s'écoule avant que le navire ne revienne au jeu afin de donner au joueur le temps de voir son score. Modifier PlayerShip :: kill () en réglant le temps de réapparition à 300 images (cinq secondes) si le joueur est à court de vies.

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

Le jeu est maintenant prêt à jouer. Cela peut ne pas sembler beaucoup, mais il a tous les mécanismes de base mis en œuvre. Dans les prochains tutoriels, nous ajouterons des effets de particules et une grille d’arrière-plan. Mais pour le moment, ajoutons rapidement du son et de la musique pour le rendre plus intéressant.

Son et musique

Jouer du son et de la musique est assez simple sous iOS. Premièrement, ajoutons nos effets sonores et notre musique au pipeline de contenu..

Tout d'abord, nous créons une classe d'assistance statique pour les sons. Notez que le jeu est Bonne gestion la classe s'appelle Du son, mais notre Bibliothèque d'utilitaires la classe sonore s'appelle tSon.

 classe Sound: tSingleton public protected: tSound * mMusic; std :: vector mExplosions; std :: vect