Erstellen Sie einen Neon Vector Shooter für iOS Mehr Spielspaß

In dieser Serie von Tutorials zeige ich Ihnen, wie Sie einen von Geometry Wars inspirierten Doppelstock-Shooter mit Neon-Grafik, verrückten Partikeleffekten und beeindruckender Musik für iOS mit C ++ und OpenGL ES 2.0 erstellen. Bisher haben wir das grundlegende Gameplay eingerichtet. Jetzt fügen wir Feinde und ein Punktesystem hinzu.

Überblick

In diesem Teil bauen wir auf dem vorherigen Tutorial auf, indem wir Feinde, Kollisionserkennung und Treffer hinzufügen.

Hier sind die neuen Funktionen in Aktion:


Warnung: Laut!

Wir werden die folgenden neuen Klassen hinzufügen, um dies zu handhaben:

  • Feind
  • EnemySpawner: Verantwortlich dafür, Feinde zu schaffen und den Schwierigkeitsgrad des Spiels schrittweise zu erhöhen.
  • PlayerStatus: Erfasst die Punktzahl, die Höchstpunktzahl und das Leben des Spielers.

Sie haben vielleicht bemerkt, dass es zwei Arten von Feinden in dem Video gibt, aber es gibt nur eine Feindklasse. Wir könnten für jeden Feindtyp Unterklassen von Feind ableiten. Die ursprüngliche XNA-Version des Spiels war aufgrund der folgenden Nachteile nicht verfügbar:

  • Sie fügen mehr Boilerplate-Code hinzu.
  • Sie können die Komplexität des Codes erhöhen und das Verständnis erschweren. Der Zustand und die Funktionalität eines Objekts wird über die gesamte Vererbungskette verteilt.
  • Sie sind nicht sehr flexibel. Sie können keine Funktionalitätsteile zwischen verschiedenen Zweigen des Vererbungsbaums freigeben, wenn diese Funktionalität nicht in der Basisklasse enthalten ist. Sie können beispielsweise zwei Klassen erstellen, Säugetier und Vogel, die beide von ableiten Tier. Das Vogel Klasse hat eine Fliegen() Methode. Dann entscheiden Sie sich, eine Fledermaus Klasse, die von abgeleitet ist Säugetier und kann auch fliegen. Um diese Funktionalität nur mit Vererbung zu teilen, müssen Sie das verschieben Fliegen() Methode zum Tier Klasse, wo es nicht hingehört. Außerdem können Sie keine Methoden aus abgeleiteten Klassen entfernen, wenn Sie also eine erstellt haben Pinguin Klasse, von der abgeleitet wurde Vogel, es müsste auch eine haben Fliegen() Methode.

In diesem Tutorial werden wir uns neben der ursprünglichen XNA-Version befinden und die Komposition der Vererbung vorziehen, um die verschiedenen Arten von Feinden zu implementieren. Wir werden dies tun, indem wir verschiedene wiederverwendbare erstellen Verhalten dass wir Feinden hinzufügen können. Wir können dann auf einfache Weise Verhalten mischen und anpassen, wenn wir neue Arten von Feinden erstellen. Zum Beispiel, wenn wir schon eine hatten FollowPlayer Verhalten und a DodgeBullet Verhalten, könnten wir einen neuen Feind machen, der beides durch einfaches Hinzufügen beider Verhaltensweisen tut.

zusammenhängende Posts
  • Einführung in die objektorientierte Programmierung für die Spielentwicklung
  • Ein pragmatischer Ansatz für die Zusammensetzung der Entität
  • Unity: Jetzt denkst du mit Komponenten

Feinde

Gegner haben einige zusätzliche Eigenschaften gegenüber Entitäten. Um dem Spieler etwas Zeit zum Reagieren zu geben, lassen wir die Feinde allmählich einblenden, bevor sie aktiv und gefährlich werden.

Lassen Sie uns die Grundstruktur des programmieren Feind Klasse:

 Klasse Feind: public Entity public: enum Verhalten kFollow = 0, kMoveRandom,; protected: std :: list mBehaviors; float mRandomDirection; int mRandomState; int mPointValue; int mTimeUntilStart; protected: void AddBehaviour (Behavior b); void ApplyBehaviours (); public: Feind (tTexture * image, const tVector2f & position); void update (); bool getIsActive (); int getPointValue (); statischer Feind * createSeeker (const tVector2f & position); statischer Feind * createWanderer (const tVector2f & position); void handleCollision (Enemy * other); void wasShot (); bool followPlayer (Floatbeschleunigung); bool moveRandomly (); ; Feind :: Feind (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 () -> erhöhtMultiplier (); tSound * temp = Sound :: getInstance () -> getExplosion (); if (! temp-> isPlaying ()) temp-> play (0, 1); 

Dieser Code lässt die Feinde 60 Bilder lang einblenden und ihre Geschwindigkeit funktioniert. Die Multiplikation der Geschwindigkeit mit 0,8 führt zu einem reibungsartigen Effekt. Wenn wir Gegner dazu bringen, mit konstanter Geschwindigkeit zu beschleunigen, wird diese Reibung dazu führen, dass sie sich sanft einer Höchstgeschwindigkeit annähern. Die Einfachheit und Glätte dieser Art von Reibung ist schön, aber je nach gewünschtem Effekt können Sie eine andere Formel verwenden.

Das wurde erschossen() Methode wird aufgerufen, wenn der Feind erschossen wird. Wir werden später in der Serie mehr hinzufügen.

Wir wollen, dass sich verschiedene Arten von Feinden anders verhalten; wir werden dies durch Zuweisung erreichen Verhalten. Ein Verhalten verwendet eine benutzerdefinierte Funktion, die jeden Frame zur Kontrolle des Feindes ausführt.

Die ursprüngliche XNA-Version von Shape Blaster verwendete ein spezielles Sprachfeature von C #, um das Verhalten zu automatisieren. Ohne auf zu viele Details einzugehen (da wir sie nicht verwenden werden), war das Endergebnis, dass die C # -Laufzeit die Verhaltensmethoden jeden Frame aufruft, ohne dass dies ausdrücklich gesagt werden muss.

Da dieses Sprachfeature weder in C noch in C ++ existiert, müssen wir die Verhalten selbst explizit aufrufen. Obwohl dies etwas mehr Code erfordert, wissen wir als Nächstes genau, wann unsere Verhaltensweisen aktualisiert werden, und gibt uns somit eine genauere Kontrolle.

Unser einfachstes Verhalten wird das sein followPlayer () Verhalten unten gezeigt:

 bool Enemy :: followPlayer (Float-Beschleunigung) if (! PlayerShip :: getInstance () -> getIsDead ()) tVector2f temp = (PlayerShip :: getInstance () -> getPosition () - mPosition); Temp = Temp * (Beschleunigung / Temp.Länge ()); mVelocity + = Temp;  if (mVelocity! = tVector2f (0,0)) mOrientation = atan2f (mVelocity.y, mVelocity.x);  return true; 

Dadurch beschleunigt der Feind mit konstanter Geschwindigkeit auf den Spieler zu. Die zuvor hinzugefügte Reibung stellt sicher, dass sie mit einiger Geschwindigkeit maximal wird (fünf Pixel pro Bild, wenn die Beschleunigung eine Einheit ist, da \ (0,8 \ mal 5 + 1 = 5 \).

Fügen wir das Gerüst hinzu, das zum Verhalten benötigt wird. Feinde müssen ihr Verhalten speichern, also fügen wir eine Variable hinzu Feind Klasse:

 std :: list mBehaviors;

mBehaviors ist ein std :: list alle aktiven Verhaltensweisen enthalten. Bei jedem Frame gehen wir alle Verhaltensweisen durch, die der Feind hat, und rufen die Verhaltensfunktion basierend auf dem Verhaltenstyp auf. Wenn die Verhaltensmethode zurückkehrt falsch, Dies bedeutet, dass das Verhalten abgeschlossen ist. Daher sollten wir es aus der Liste entfernen.

Wir werden der Enemy-Klasse die folgenden Methoden hinzufügen:

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

Und wir werden das ändern aktualisieren() Methode aufzurufen ApplyBehaviours ():

 if (mTimeUntilStart <= 0)  ApplyBehaviours(); 

Jetzt können wir eine statische Methode erstellen suchend Feinde Wir müssen nur das gewünschte Bild auswählen und das Bild hinzufügen followPlayer () Verhalten:

 Feind * Feind :: createSeeker (const tVector2f & position) Feind * Feind = neuer Feind (Art :: getInstance () -> getSeeker (), Position); Feind-> AddBehaviour (kFollow); Feind-> mPointValue = 2; Feind zurückkehren; 

Damit sich ein Feind zufällig bewegt, müssen wir eine Richtung wählen und dann kleine zufällige Anpassungen in dieser Richtung vornehmen. Wenn wir jedoch die Richtung bei jedem Bild anpassen, wird die Bewegung unruhig, so dass wir die Richtung nur periodisch anpassen. Wenn der Feind auf den Bildschirmrand trifft, wird er eine neue zufällige Richtung auswählen, die von der Wand weg zeigt.

 bool Enemy :: moveRandomly () if (mRandomState == 0) mRandomDirection + = tMath :: random () * 0.2f - 0.1f;  mVelocity + = 0,4f * tVector2f (cosf (mRandomDirection), sinf (mRandomDirection)); Orientierung - = 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; wahr zurückgeben; 

Wir können jetzt eine Factory-Methode zum Erstellen erstellen wandern Feinde, ähnlich wie wir es für den Sucher getan haben:

 Feind * Feind :: createWanderer (const tVector2f & position) Feind * Feind = neuer Feind (Art :: getInstance () -> getWanderer (), Position); feind-> mRandomDirection = tMath :: random () * tMath :: PI * 2.0f; Feind-> mRandomState = 0; Feind-> AddBehaviour (kMoveRandom); Feind zurückkehren; 

Kollisionserkennung

Für die Kollisionserkennung modellieren wir das Schiff des Spielers, die Feinde und die Kugeln als Kreise. Die Erkennung von kreisförmigen Kollisionen ist einfach, weil sie einfach und schnell ist und sich nicht ändert, wenn sich die Objekte drehen. Wenn Sie sich erinnern, die Entität Klasse hat einen Radius und eine Position (die Position bezieht sich auf das Zentrum der Entität) - dies ist alles, was wir für die Erkennung von kreisförmigen Kollisionen benötigen.

Das Testen jeder Entität gegen alle anderen Entitäten, die möglicherweise kollidieren könnten, kann bei einer großen Anzahl von Entitäten sehr langsam sein. Es gibt viele Techniken, mit denen Sie die Kollisionserkennung für breite Phasen beschleunigen können, wie Quadtrees, Sweep und Prune sowie BSP-Bäume. Momentan werden jedoch nur ein paar Dutzend Objekte auf dem Bildschirm angezeigt, sodass wir uns nicht um diese komplexeren Techniken kümmern müssen. Wir können sie später jederzeit hinzufügen, wenn wir sie brauchen.

In Shape Blaster kann nicht jede Entität mit jedem anderen Entitätstyp kollidieren. Kugeln und das Schiff des Spielers können nur mit Feinden kollidieren. Feinde können auch mit anderen Feinden kollidieren. Dies verhindert, dass sie sich überlappen.

Um mit diesen unterschiedlichen Arten von Kollisionen fertig zu werden, fügen wir der Liste zwei neue Listen hinzu EntityManager um Kugeln und Feinde im Auge zu behalten. Wann immer wir eine Entität zum hinzufügen EntityManager, Wir wollen es der entsprechenden Liste hinzufügen, also machen wir eine private addEntity () Methode, um dies zu tun. Wir werden auch sicher sein, dass alle abgelaufenen Objekte aus allen Listen in jedem Frame entfernt werden.

 std :: list mEnemies; std :: list mBullets; void EntityManager :: addEntity (Entity * entity) mEntities.push_back (entity); switch (entity-> getKind ()) case Entity :: kBullet: mBullets.push_back ((Bullet *) - Entität); brechen; case Entity :: kEnemy: mEnemies.push_back ((Enemy *) Entität); brechen; Standard: Pause;  //… // in Update () für (std :: list):: iterator iter = mBullets.begin (); iter! = mBullets.end (); iter ++) if ((* iter) -> isExpired ()) delete * iter; * iter = NULL;  mBullets.remove (NULL); für (std :: list:: iterator iter = mEnemies.begin (); iter! = mEnemies.end (); iter ++) if ((* iter) -> isExpired ()) delete * iter; * iter = NULL;  mEnemies.remove (NULL);

Ersetzen Sie die Anrufe an entity.add () im EntityManager.add () und EntityManager.update () mit anrufen zu addEntity ().

Fügen wir nun eine Methode hinzu, die bestimmt, ob zwei Entitäten kollidieren:

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

Um festzustellen, ob sich zwei Kreise überlappen, prüfen Sie einfach, ob der Abstand zwischen ihnen kleiner als die Summe ihrer Radien ist. Unsere Methode optimiert dies geringfügig, indem geprüft wird, ob das Quadrat der Entfernung kleiner als das Quadrat der Summe der Radien ist. Denken Sie daran, dass die Entfernung im Quadrat etwas schneller berechnet werden kann als die tatsächliche Entfernung.

Abhängig davon werden verschiedene Dinge passieren welche zwei Objekte kollidieren. Wenn zwei Feinde zusammenstoßen, wollen wir, dass sie sich gegenseitig wegschieben. Wenn eine Kugel einen Feind trifft, sollten sowohl die Kugel als auch der Feind zerstört werden. Wenn der Spieler einen Feind berührt, sollte der Spieler sterben und das Level zurückgesetzt werden.

Wir werden ein hinzufügen handleCollision () Methode zum Feind Klasse, um Kollisionen zwischen Feinden zu handhaben:

 void Enemy :: handleCollision (Enemy * other) tVector2f d = mPosition - other-> mPosition; mVelocity + = 10.0f * d / (d.Längenquadrat () + 1,0f); 

Diese Methode drückt den aktuellen Feind vom anderen Feind weg. Je näher sie sind, desto schwieriger wird es geschoben, weil die Größenordnung von (d / d.LengthSquared ()) ist nur einer über die Entfernung.

Erneuern Sie den Spieler

Als nächstes brauchen wir eine Methode, um das Schiff des Spielers zu erledigen. In diesem Fall verschwindet das Schiff des Spielers für kurze Zeit, bevor es erneut startet.

Wir fangen damit an, zwei neue Mitglieder hinzuzufügen SpielerSchiff:

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

Ganz am Anfang von PlayerShip :: Update (), Folgendes hinzufügen:

 if (getIsDead ()) mFramesUntilRespawn--; 

Und wir überschreiben zeichnen() wie gezeigt:

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

Zum Schluss fügen wir ein töten() Methode zu SpielerSchiff:

 void PlayerShip :: kill () mFramesUntilRespawn = 60; 

Nun, da alle Teile vorhanden sind, fügen wir dem eine Methode hinzu EntityManager das geht durch alle Entitäten und prüft auf Kollisionen:

 void EntityManager :: handleCollisions () für (std :: list):: Iterator i = mEnemies.begin (); i! = mEnemies.end (); i ++) für (std :: list:: Iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) if (isColliding (* i, * j)) (* i) -> handleCollision (* j); (* j) -> handleCollision (* i);  // behandelt Kollisionen zwischen Kugeln und Feinden für (std :: list:: Iterator i = mEnemies.begin (); i! = mEnemies.end (); i ++) für (std :: list:: Iterator j = mBullets.begin (); j! = mBullets.end (); j ++) if (isColliding (* i, * j)) (* i) -> wasShot (); (* j) -> setExpired ();  // behandelt Kollisionen zwischen dem Spieler und Feinden für (std :: list:: Iterator i = mEnemies.begin (); i! = mEnemies.end (); i ++) if ((* i) -> getIsActive () && isColliding (PlayerShip :: getInstance (), * i)) PlayerShip :: getInstance () -> kill (); für (std :: list:: Iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) (* j) -> wasShot ();  EnemySpawner :: getInstance () -> reset (); brechen; 

Rufen Sie diese Methode von auf aktualisieren() unmittelbar nach der Einstellung mIsUpdating zu wahr.

Feindlicher Spawner

Das Letzte, was Sie tun müssen, ist das EnemySpawner Klasse, die für die Schaffung von Feinden verantwortlich ist. Wir möchten, dass das Spiel einfach beginnt und härter wird, so der EnemySpawner wird im Laufe der Zeit immer schneller Feinde schaffen. Wenn der Spieler stirbt, setzen wir das zurück EnemySpawner zu seiner anfänglichen Schwierigkeit.

 Klasse EnemySpawner: public tSingleton protected: float mInverseSpawnChance; protected: tVector2f GetSpawnPosition (); protected: EnemySpawner (); public: void update (); Rücksetzen aufheben (); Freundenklasse 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; 

Jeder Frame enthält eine Eins mInverseSpawnChance jede Art von Feind zu erzeugen. Die Chance, einen Feind zu spawnen, steigt allmählich, bis er maximal ein Zwanzigstel erreicht. Gegner werden immer mindestens 250 Pixel vom Player entfernt erstellt.

Seien Sie vorsichtig mit dem während einhängen GetSpawnPosition (). Es funktioniert effizient, solange der Bereich, in dem Feinde spawn können, größer ist als der Bereich, in dem sie nicht laichen können. Wenn Sie jedoch den verbotenen Bereich zu groß machen, erhalten Sie eine Endlosschleife.

Anruf EnemySpawner :: update () von GameRoot :: onRedrawView () und Ruf an EnemySpawner :: reset () wenn der Spieler getötet wird.

Score und Leben

  • Bei Shape Blaster beginnen Sie mit vier Leben und erhalten alle 2.000 Punkte ein zusätzliches Leben.
  • Sie erhalten Punkte für die Zerstörung von Feinden, wobei verschiedene Arten von Gegnern unterschiedliche Punktzahlen wert sind.
  • Jeder zerstörte Gegner erhöht außerdem den Punktemultiplikator um eins.
  • Wenn Sie innerhalb kurzer Zeit keine Gegner töten, wird Ihr Multiplikator zurückgesetzt.
  • Die Gesamtanzahl an Punkten, die von jedem Gegner, den Sie zerstören, erhalten wird, ist die Anzahl der Punkte, die der Gegner wert ist, multipliziert mit Ihrem aktuellen Multiplikator.
  • Wenn Sie alle Ihre Leben verlieren, ist das Spiel vorbei und Sie beginnen ein neues Spiel, wobei Ihre Punktzahl auf Null zurückgesetzt wird.

Um all dies zu bewältigen, erstellen wir eine statische Klasse PlayerStatus:

 Klasse PlayerStatus: public tSingleton protected: static const float kMultiplierExpiryTime; statische const int kMaxMultiplier; static const std :: string kHighScoreFilename; float mMultiplierTimeLeft; int mlives; int mScore; int mHighScore; int mMultiplier; int mScoreForExtraLife; uint32_t mLastTime; protected: int LoadHighScore (); void SaveHighScore (int score); protected: PlayerStatus (); public: void reset (); void update (); void addPoints (int basePoints); void ZunahmeMultiplier (); void resetMultiplier (); void removeLife (); int getLives () const; int getScore () const; int getHighScore () const; int getMultiplier () const; bool getIsGameOver () const; Freundenklasse tSingleton; ; PlayerStatus :: PlayerStatus () mScore = 0; mHighScore = LoadHighScore (); reset (); mLastTime = tTimer :: getTimeMS ();  void PlayerStatus :: reset () if (mScore> mHighScore) mHighScore = mScore; SaveHighScore (mHighScore);  mScore = 0; mM Multiplikator = 1; mLives = 4; mScoreForExtraLife = 2000; mMultiplierTimeLeft = 0;  void PlayerStatus :: update () if (mMultiplier> 1) mMultiplierTimeLeft - = float (tTimer :: getTimeMS () - mLastTime) / 1000.0f; if (mMultiplierTimeLeft <= 0)  mMultiplierTimeLeft = kMultiplierExpiryTime; resetMultiplier();   mLastTime = tTimer::getTimeMS();  void PlayerStatus::addPoints(int basePoints)  if (!PlayerShip::getInstance()->getIsDead ()) mScore + = basePoints * mM Multiplikator; while (mScore> = mScoreForExtraLife) mScoreForExtraLife + = 2000; mLives ++;  void PlayerStatus :: gainMultiplier () if (! PlayerShip :: getInstance () -> getIsDead ()) mMultiplierTimeLeft = kMultiplierExpiryTime; if (mM Vervielfacher < kMaxMultiplier)  mMultiplier++;    void PlayerStatus::resetMultiplier()  mMultiplier = 1;  void PlayerStatus::removeLife()  mLives--; 

Anruf PlayerStatus :: Update () von GameRoot :: onRedrawView () wenn das Spiel nicht pausiert ist.

Als Nächstes möchten wir Ihre Punktzahl, Ihr Leben und Ihren Multiplikator auf dem Bildschirm anzeigen. Dazu müssen wir eine hinzufügen tSpriteFont in dem Inhalt Projekt und eine entsprechende Variable in der Kunst Klasse, die wir nennen werden Schriftart. Laden Sie die Schrift in KunstKonstruktor wie wir es mit den Texturen gemacht haben.

Hinweis: Die Schriftart, die wir verwenden, ist eigentlich ein Bild und nicht etwa eine TrueType-Schriftartdatei. Bei bildbasierten Schriftarten wurde bei klassischen Arcade-Spielen und -Konsolen Text auf dem Bildschirm gedruckt, und selbst heute verwenden einige Spiele der aktuellen Generation noch immer diese Technik. Ein Vorteil, den wir daraus ziehen, besteht darin, dass wir dieselben Techniken zum Zeichnen von Text auf dem Bildschirm verwenden wie andere Sprites.

Ändern Sie das Ende von GameRoot :: onRedrawView () Wo der Cursor gezeichnet wird, wie unten gezeigt:

 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 (), Eingabe :: getInstance () -> getMousePosition (), tOptional());

DrawRightAlignedString () ist eine Hilfsmethode zum Zeichnen von Text, der auf der rechten Seite des Bildschirms ausgerichtet ist. Fügen Sie es zu hinzu GameRoot indem Sie den folgenden Code hinzufügen:

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

Jetzt sollten Ihre Leben, Punkte und Multiplikatoren auf dem Bildschirm angezeigt werden. Diese Werte müssen jedoch noch in Reaktion auf Spielereignisse geändert werden. Fügen Sie eine Eigenschaft hinzu mPointValue zum Feind Klasse.

 int Enemy :: getPointValue () return mPointValue; 

Stellen Sie den Punktwert für verschiedene Feinde auf einen Wert, der Ihrer Meinung nach angemessen ist. Ich habe die wandernden Feinde um einen Punkt verdient und die suchenden Feinde um zwei Punkte.

Fügen Sie als nächstes die folgenden zwei Zeilen hinzu Feind :: wasShot () um die Punktzahl und den Multiplikator des Spielers zu erhöhen:

 PlayerStatus :: getInstance () -> addPoints (mPointValue); PlayerStatus :: getInstance () -> erhöhtMultiplier ();

Anruf PlayerStatus :: removeLife () im PlayerShip :: kill (). Wenn der Spieler sein ganzes Leben verliert, rufen Sie an PlayerStatus :: reset () ihre Punktzahl und Leben am Anfang eines neuen Spiels zurücksetzen.

Highscores

Fügen wir dem Spiel die Möglichkeit hinzu, Ihre beste Punktzahl zu ermitteln. Wir möchten, dass diese Partitur über alle Spiele hinweg bestehen bleibt, also speichern wir sie in einer Datei. Wir werden es sehr einfach halten und die hohe Punktzahl als eine einzige Klartextnummer in einer Datei speichern (dies wird im "Application Support" -Verzeichnis der App sein, ein ausgefallener Name für das "Preferences" -Verzeichnis).

Fügen Sie Folgendes zu hinzu PlayerStatus:

 const std :: string PlayerStatus :: kHighScoreFilename ("highscore.txt"); void CreatePathIfNonExistant2 (const std :: string & newPath) @autoreleasepool // Erzeuge den Pfad, falls er nicht existiert NSError * error; [[NSFileManager defaultManager] createDirectoryAtPath: [NSString stringWithUTF8String: newPath.c_str ()] withIntermediateDirectories: YES-Attribute: nil error: & error]; 

CreatePathIfNonExistant2 () Ich habe eine Funktion erstellt, die ein Verzeichnis auf dem iOS-Gerät erstellt, wenn es noch nicht vorhanden ist. Da unser Präferenzpfad anfangs nicht vorhanden ist, müssen wir ihn beim ersten Mal erstellen.

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

GetExecutableName2 () gibt den Namen der ausführbaren Datei zurück. Wir werden den Namen der Anwendung als Teil des Präferenzpfads verwenden. Wir werden diese Funktion verwenden, anstatt den Namen der ausführbaren Datei fest zu codieren, sodass wir diesen Code einfach für andere Anwendungen unverändert verwenden können.

 std :: string GetPreferencePath2 (const std :: string & file) std :: string ergebnis = std :: string ([[NSSearchPathForDirectoriesInDomains (NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex: 0] UTF8String] + "+) + "/"; CreatePathIfNonExistant2 (Ergebnis); Ergebnis + Datei zurückgeben; 

GetPreferencePath2 () Gibt den vollständigen String-Versionsnamen des Präferenzpfads zurück und erstellt den Pfad, falls noch nicht vorhanden.

 int PlayerStatus :: LoadHighScore () int score = 0; std :: string fstring; if ([[NSFileManager defaultManager] fileExistsAtPath: [DurchlaufenAusgehenAktPath2 (kHighScoreFilename) .c_str ()]); null] UTF8String]; if (! fstring.empty ()) sscanf (fstring.c_str (), "% d" & Score);  Ergebnis zurückgeben;  void PlayerStatus :: SaveHighScore (int score) char buf [20]; sprintf (buf, "% d", score); [[NSString stringWithUTF8String: buf] writeToFile: [NSString stringWithUTF8String: GetPreferencePath2 (kHighScoreFilename) .c_str ()] atomisch: YES-Codierung: NSUTF8StringEncoding-Fehler: nil]; 

Das LoadHighScore () Die Methode prüft zuerst, ob die Datei mit der höchsten Punktzahl vorhanden ist, und gibt dann den Inhalt der Datei als Ganzzahl zurück. Es ist unwahrscheinlich, dass die Punktzahl ungültig ist, es sei denn, der Benutzer kann Dateien normalerweise nicht manuell in iOS ändern. Wenn die Punktzahl jedoch eine Nicht-Zahl ist, wird die Punktzahl gleich Null.

Wir möchten den Highscore laden, wenn das Spiel beginnt, und speichern, wenn der Spieler einen neuen Highscore erhält. Wir modifizieren den statischen Konstruktor und zurücksetzen () Methoden in PlayerStatus zu tun Wir werden auch ein Helfermitglied hinzufügen, mIsGameOver, das werden wir gleich nutzen.

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

Das sorgt dafür, dass die hohe Punktzahl verfolgt wird. Jetzt müssen wir es anzeigen. Wir werden den folgenden Code hinzufügen GameRoot :: onRedrawView () im gleichen SpriteBatch Block, wo der andere Text gezeichnet wird:

 if (PlayerStatus :: getInstance () -> getIsGameOver ()) sprintf (buf, "Game Over \ nIhr Score:% 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)); 

Dadurch wird Ihre Punktzahl und Höchstpunktzahl auf dem Spielende angezeigt, zentriert auf dem Bildschirm.

Als letzte Anpassung erhöhen wir die Zeit, bevor das Schiff beim Spielende erneut erscheint, um dem Spieler Zeit zu geben, seine Punktzahl zu sehen. Ändern PlayerShip :: kill () Setzen Sie die Respawn-Zeit auf 300 Frames (fünf Sekunden), wenn der Spieler keine Leben mehr hat.

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

Das Spiel ist jetzt spielbereit. Es sieht vielleicht nicht nach viel aus, hat aber alle grundlegenden Mechanismen implementiert. In zukünftigen Tutorials werden Partikeleffekte und ein Hintergrundraster hinzugefügt, um es aufzufrischen. Aber jetzt, lasst uns schnell etwas Sound und Musik hinzufügen, um es interessanter zu machen.

Sound und Musik

Das Abspielen von Ton und Musik ist unter iOS relativ einfach. Lassen Sie uns zunächst unsere Soundeffekte und Musik zur Inhaltspipeline hinzufügen.

Zuerst erstellen wir eine statische Hilfsklasse für die Sounds. Beachten Sie, dass das Spiel ist Sound-Management Klasse wird aufgerufen Klingen, aber unser Dienstbibliothek Tonklasse heißt tSound.

 Klasse Sound: public tSingleton protected: tSound * mMusic; std :: ve