Crea uno sparatutto al neon per iOS più gameplay

In questa serie di tutorial, ti mostrerò come creare uno sparatutto twin-stick ispirato a Geometry Wars, con grafica al neon, effetti pazzeschi e musica fantastica, per iOS che utilizza C ++ e OpenGL ES 2.0. Finora, abbiamo impostato il gameplay di base; ora, aggiungeremo nemici e un sistema di punteggio.

Panoramica

In questa parte svilupperemo il tutorial precedente aggiungendo nemici, rilevamento collisioni e punteggio.

Ecco le nuove funzionalità in azione:


Avvertenza: forte!

Aggiungeremo le seguenti nuove classi per gestire questo:

  • Nemico
  • EnemySpawner: Responsabile della creazione di nemici e graduale aumento della difficoltà del gioco.
  • PlayerStatus: Tiene traccia del punteggio, del punteggio elevato e delle vite del giocatore.

Potresti aver notato che ci sono due tipi di nemici nel video, ma c'è solo una classe nemica. Potremmo derivare sottoclassi dal nemico per ogni tipo di nemico. La versione originale XNA del gioco non ha, a causa dei seguenti inconvenienti:

  • Aggiungono più codice standard.
  • Possono aumentare la complessità del codice e renderlo più difficile da capire. Lo stato e la funzionalità di un oggetto si diffondono su tutta la sua catena ereditaria.
  • Non sono molto flessibili: non è possibile condividere parti di funzionalità tra diversi rami dell'albero di ereditarietà se tale funzionalità non si trova nella classe base. Ad esempio, considera la possibilità di creare due classi, Mammifero e Uccello, da cui entrambi derivano Animale. Il Uccello la classe ha un Volare() metodo. Quindi decidi di aggiungere un pipistrello classe che deriva da Mammifero e può anche volare. Per condividere questa funzionalità usando solo l'ereditarietà dovresti spostare il file Volare() metodo per il Animale classe in cui non appartiene. Inoltre, non è possibile rimuovere i metodi dalle classi derivate, quindi se si è fatto un Pinguino classe derivata da Uccello, dovrebbe anche avere un Volare() metodo.

Per questo tutorial, ci schiereremo con la versione originale di XNA e favoriremo la composizione sull'ereditarietà per implementare i diversi tipi di nemici. Lo faremo creando vari riutilizzabili comportamenti che possiamo aggiungere ai nemici. Possiamo quindi facilmente combinare i comportamenti quando creiamo nuovi tipi di nemici. Ad esempio, se abbiamo già avuto un FollowPlayer comportamento e a DodgeBullet comportamento, potremmo fare un nuovo nemico che fa entrambi semplicemente aggiungendo entrambi i comportamenti.

Post correlati
  • Introduzione alla programmazione orientata agli oggetti per lo sviluppo di giochi
  • Un approccio pragmatico alla composizione delle entità
  • Unità: ora stai pensando con i componenti

Nemici

I nemici avranno alcune proprietà aggiuntive rispetto alle entità. Per dare al giocatore un po 'di tempo per reagire, faremo svanire gradualmente i nemici prima che diventino attivi e pericolosi.

Cerchiamo di codificare la struttura di base del Nemico classe:

 class Enemy: public Entity public: enum Behavior kFollow = 0, kMoveRandom,; protetto: std :: list mBehaviors; float mRandomDirection; int mRandomState; int mPointValue; int mTimeUntilStart; protected: void AddBehaviour (Behavior b); void ApplyBehaviours (); public: Enemy (immagine tTexture *, const tVector2f & position); void update (); bool getIsActive (); int getPointValue (); Enemy statico * createSeeker (const tVector2f e position); Enemy statico * createWanderer (const tVector2f & position); void handleCollision (Enemy * other); void wasShot (); bool followPlayer (accelerazione float); bool moveRandomly (); ; Enemy :: Enemy (immagine tTexture *, const tVector2f e posizione): mPointValue (1), mTimeUntilStart (60) mImage = image; mPosition = posizione; 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 ( ) altezza / 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); 

Questo codice farà sbiadire i nemici per 60 fotogrammi e permetterà alla loro velocità di funzionare. Moltiplicare la velocità di 0.8 falsi un effetto simile ad attrito. Se facciamo accelerare i nemici a una velocità costante, questa attrito li farà avvicinare facilmente alla massima velocità. La semplicità e la levigatezza di questo tipo di attrito sono buone, ma potresti voler usare una formula diversa a seconda dell'effetto desiderato.

Il fu sparato() il metodo verrà chiamato quando il nemico viene colpito. Aggiungeremo altro in seguito nella serie.

Vogliamo che diversi tipi di nemici si comportino diversamente; lo faremo assegnando comportamenti. Un comportamento utilizzerà alcune funzioni personalizzate che eseguono ciascun fotogramma per controllare il nemico.

La versione XNA originale di Shape Blaster utilizzava una caratteristica del linguaggio speciale da C # per automatizzare i comportamenti. Senza entrare troppo nel dettaglio (dal momento che non li useremo), il risultato finale era che il runtime C # chiamava i metodi di comportamento ogni frame senza doverlo dire esplicitamente.

Poiché questa caratteristica del linguaggio non esiste in C o C ++, dovremo chiamare esplicitamente i comportamenti da soli. Anche se questo richiede un po 'più di codice, il beneficio collaterale è che sapremo esattamente quando i nostri comportamenti saranno aggiornati e quindi ci darà un controllo più preciso.

Il nostro comportamento più semplice sarà il followPlayer () comportamento mostrato di seguito:

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

Questo fa semplicemente accelerare il nemico verso il giocatore a un ritmo costante. L'attrito che abbiamo aggiunto prima farà sì che alla fine si superi alla massima velocità (cinque pixel per frame quando l'accelerazione è di una unità, dal momento che \ (0.8 \ times 5 + 1 = 5 \).

Aggiungiamo l'impalcatura necessaria per far funzionare i comportamenti. I nemici devono memorizzare i loro comportamenti, quindi aggiungeremo una variabile al Nemico classe:

 std :: list mBehaviors;

L'equivoco è un std :: list contenente tutti i comportamenti attivi. Ogni frame passerà attraverso tutti i comportamenti del nemico e chiamerà la funzione di comportamento in base al tipo di comportamento. Se il metodo comportamentale ritorna falso, significa che il comportamento è stato completato, quindi dovremmo rimuoverlo dall'elenco.

Aggiungeremo i seguenti metodi alla classe Enemy:

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

E modificheremo il aggiornare() metodo da chiamare ApplyBehaviours ():

 if (mTimeUntilStart <= 0)  ApplyBehaviours(); 

Ora possiamo creare un metodo statico da creare cerca nemici. Tutto quello che dobbiamo fare è scegliere l'immagine che vogliamo e aggiungere il followPlayer () comportamento:

 Enemy * Enemy :: createSeeker (const tVector2f e position) Enemy * enemy = new Enemy (Art :: getInstance () -> getSeeker (), posizione); dal nemico> AddBehaviour (kFollow); enemy-> mPointValue = 2; restituire il nemico; 

Per rendere un nemico che si muove in modo casuale, lo faremo scegliere una direzione e quindi apportare piccole regolazioni casuali in quella direzione. Tuttavia, se regoliamo la direzione ogni fotogramma, il movimento sarà nervoso, quindi regoleremo la direzione solo periodicamente. Se il nemico corre sul bordo dello schermo, lo faremo scegliere una nuova direzione casuale che punta lontano dal muro.

 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; ritorna vero; 

Ora possiamo creare un metodo factory per la creazione vagare nemici, proprio come abbiamo fatto per il ricercatore:

 Enemy * Enemy :: createWanderer (const tVector2f e position) Enemy * enemy = new Enemy (Art :: getInstance () -> getWanderer (), posizione); enemy-> mRandomDirection = tMath :: random () * tMath :: PI * 2.0f; nemico-> mRandomState = 0; dal nemico> AddBehaviour (kMoveRandom); restituire il nemico; 

Rilevamento collisione

Per il rilevamento delle collisioni, modelleremo la nave del giocatore, i nemici e i proiettili come cerchi. Il rilevamento delle collisioni circolari è bello perché è semplice, è veloce e non cambia quando gli oggetti ruotano. Se ricordi, il Entità la classe ha un raggio e una posizione (la posizione si riferisce al centro dell'entità) - questo è tutto ciò di cui abbiamo bisogno per il rilevamento di collisioni circolari.

Testare ciascuna entità contro tutte le altre entità che potrebbero potenzialmente collidere può essere molto lenta se si dispone di un numero elevato di entità. Esistono molte tecniche che è possibile utilizzare per accelerare il rilevamento di collisioni di fase ampia, come quadrifori, sweep e prune e alberi BSP. Tuttavia, per ora, avremo solo poche decine di entità sullo schermo alla volta, quindi non ci preoccuperemo di queste tecniche più complesse. Possiamo sempre aggiungerli più tardi se ne abbiamo bisogno.

In Shape Blaster, non tutte le entità possono entrare in collisione con ogni altro tipo di entità. I proiettili e la nave del giocatore possono scontrarsi solo con i nemici. I nemici possono anche scontrarsi con altri nemici; ciò impedirà loro di sovrapporsi.

Per gestire questi diversi tipi di collisioni, aggiungeremo due nuovi elenchi al EntityManager per tenere traccia di proiettili e nemici. Ogni volta che aggiungiamo un'entità al EntityManager, vorremmo aggiungerlo all'elenco appropriato, quindi faremo un privato addEntity () metodo per farlo. Saremo anche sicuri di rimuovere tutte le entità scadute da tutti gli elenchi di ogni frame.

 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à); rompere; caso Entity :: kEnemy: mEnemies.push_back ((Enemy *) entità); rompere; default: break;  // ... // in Update () per (std :: list:: iterator iter = mBullets.begin (); iter! = mBullets.end (); iter ++) if ((* iter) -> isExpired ()) delete * iter; * iter = NULL;  mBullets.remove (NULL); per (std :: list:: iterator iter = mEnemies.begin (); iter! = mEnemies.end (); iter ++) if ((* iter) -> isExpired ()) delete * iter; * iter = NULL;  mEnemies.remove (NULL);

Sostituisci le chiamate a entity.add () nel EntityManager.add () e EntityManager.update () con chiamate a addEntity ().

Ora aggiungiamo un metodo che determinerà se due entità si scontrano:

 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; 

Per determinare se due cerchi si sovrappongono, è sufficiente verificare se la distanza tra loro è inferiore alla somma dei loro raggi. Il nostro metodo ottimizza questo leggermente controllando se il quadrato della distanza è inferiore al quadrato della somma dei raggi. Ricorda che è un po 'più veloce calcolare la distanza al quadrato della distanza effettiva.

A seconda delle cose accadranno cose diverse quale due oggetti si scontrano. Se due nemici si scontrano, vogliamo che si spingano a vicenda; se un proiettile colpisce un nemico, entrambi i proiettili e il nemico dovrebbero essere distrutti; se il giocatore tocca un nemico, il giocatore dovrebbe morire e il livello dovrebbe azzerarsi.

Aggiungeremo un handleCollision () metodo per il Nemico classe per gestire le collisioni tra i nemici:

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

Questo metodo spingerà il nemico attuale lontano dall'altro nemico. Più vicini sono, più difficile sarà spinto, perché la grandezza di (d / d.LengthSquared ()) è solo uno sulla distanza.

Ripetizione del giocatore

Quindi, abbiamo bisogno di un metodo per gestire la nave del giocatore che viene uccisa. Quando ciò accade, la nave del giocatore sparirà per un breve periodo prima di rinascere.

Iniziamo aggiungendo due nuovi membri a PlayerShip:

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

All'inizio di PlayerShip :: update (), aggiungere il seguente:

 if (getIsDead ()) mFramesUntilRespawn--; 

E abbiamo l'override disegnare() come mostrato:

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

Infine, aggiungiamo a uccidere() metodo a PlayerShip:

 void PlayerShip :: kill () mFramesUntilRespawn = 60; 

Ora che tutti i pezzi sono a posto, aggiungeremo un metodo al EntityManager che passa attraverso tutte le entità e controlla le collisioni:

 void EntityManager :: handleCollisions () for (std :: list:: iterator i = mEnemies.begin (); i! = mEnemies.end (); i ++) for (std :: list:: iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) if (isColliding (* i, * j)) (* i) -> handleCollision (* j); (* J) -> handleCollision (* i);  // gestisce le collisioni tra proiettili e nemici per (std :: list:: iterator i = mEnemies.begin (); i! = mEnemies.end (); i ++) for (std :: list:: iterator j = mBullets.begin (); j! = mBullets.end (); j ++) if (isColliding (* i, * j)) (* i) -> wasShot (); (* J) -> setExpired ();  // gestisce le collisioni tra il giocatore e i nemici per (std :: list:: iterator i = mEnemies.begin (); i! = mEnemies.end (); i ++) if ((* i) -> getIsActive () && isColliding (PlayerShip :: getInstance (), * i)) PlayerShip :: getInstance () -> kill (); per (std :: lista:: iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) (* j) -> wasShot ();  EnemySpawner :: getInstance () -> reset (); rompere; 

Chiama questo metodo da aggiornare() immediatamente dopo l'impostazione mIsUpdating a vero.

Spawner nemico

L'ultima cosa da fare è fare il EnemySpawner classe, che è responsabile per la creazione di nemici. Vogliamo che il gioco inizi facilmente e diventi più difficile, quindi il gioco EnemySpawner creerà i nemici ad un ritmo crescente con il passare del tempo. Quando il giocatore muore, ripristineremo il EnemySpawner alla sua difficoltà iniziale.

 class EnemySpawner: public tSingleton protected: float mInverseSpawnChance; protected: tVector2f GetSpawnPosition (); protected: EnemySpawner (); pubblico: void update (); void reset (); classe di amici tSingleton; ; void EnemySpawner :: update () if (! PlayerShip :: getInstance () -> getIsDead () && EntityManager :: getInstance () -> getCount () < 200)  if (int32_t(tMath::random() * mInverseSpawnChance) == 0)  EntityManager::getInstance()->aggiungere (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; 

Ogni frame, ce n'è uno in mInverseSpawnChance di generare ogni tipo di nemico. La possibilità di generare un nemico aumenta gradualmente fino a raggiungere un massimo di uno su venti. I nemici vengono sempre creati ad almeno 250 pixel di distanza dal giocatore.

Stai attento a mentre loop in GetSpawnPosition (). Funzionerà in modo efficiente finché l'area in cui i nemici possono spawnare è più grande dell'area in cui non possono spawnare. Tuttavia, se rendi l'area proibita troppo grande, otterrai un ciclo infinito.

Chiamata EnemySpawner :: update () a partire dal GameRoot :: onRedrawView () e chiama EnemySpawner :: reset () quando il giocatore viene ucciso.

Punteggio e vita

  • In Shape Blaster, inizi con quattro vite e otterrai una vita aggiuntiva ogni 2.000 punti.
  • Ottieni punti per distruggere i nemici, con diversi tipi di nemici che valgono diversi punti.
  • Ogni nemico distrutto aumenta anche il moltiplicatore del punteggio di uno.
  • Se non uccidi nessun nemico entro un breve lasso di tempo, il tuo moltiplicatore verrà resettato.
  • La quantità totale di punti ricevuti da ciascun nemico che distruggi è il numero di punti che il nemico vale, moltiplicato per il moltiplicatore corrente.
  • Se perdi tutte le vite, il gioco è finito e inizi una nuova partita con il tuo punteggio azzerato.

Per gestire tutto questo, faremo una classe statica chiamata PlayerStatus:

 class 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; protected: int LoadHighScore (); void SaveHighScore (int score); protected: PlayerStatus (); pubblico: void reset (); void update (); 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; classe di amici tSingleton; ; PlayerStatus :: PlayerStatus () mScore = 0; mHighScore = LoadHighScore (); reset(); 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; if (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; if (mMultiplier < kMaxMultiplier)  mMultiplier++;    void PlayerStatus::resetMultiplier()  mMultiplier = 1;  void PlayerStatus::removeLife()  mLives--; 

Chiamata PlayerStatus :: update () a partire dal GameRoot :: onRedrawView () quando il gioco non è in pausa.

Successivamente, vogliamo visualizzare il tuo punteggio, le vite e il moltiplicatore sullo schermo. Per fare ciò dovremo aggiungere un tSpriteFont nel Soddisfare progetto e una variabile corrispondente nel Arte classe, che nomineremo Font. Carica il carattere Arteil costruttore come abbiamo fatto con le trame.

Nota: Il carattere che stiamo usando è in realtà un'immagine piuttosto che qualcosa come un file di font TrueType. I font basati sull'immagine erano il modo in cui i classici giochi arcade e le console stampavano il testo sullo schermo, e anche ora alcuni giochi della generazione attuale usano ancora la tecnica. Un beneficio che ne ricaviamo è che finiremo per utilizzare le stesse tecniche per disegnare il testo sullo schermo mentre facciamo altri sprite.

Modifica la fine di GameRoot :: onRedrawView () dove viene disegnato il cursore, come mostrato di seguito:

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

DrawRightAlignedString () è un metodo di supporto per disegnare il testo allineato sul lato destro dello schermo. Aggiungilo a GameRoot aggiungendo il codice qui sotto:

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

Ora le tue vite, punteggio e moltiplicatore dovrebbero essere visualizzati sullo schermo. Tuttavia, dobbiamo ancora modificare questi valori in risposta agli eventi di gioco. Aggiungi una proprietà chiamata mPointValue al Nemico classe.

 int Enemy :: getPointValue () return mPointValue; 

Imposta il valore in punti per diversi nemici su qualcosa che ritieni appropriato. Ho reso i nemici erranti del valore di un punto, e i nemici in cerca degno di due punti.

Quindi, aggiungere le seguenti due righe a Enemy :: wasShot () per aumentare il punteggio e il moltiplicatore del giocatore:

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

Chiamata PlayerStatus :: removeLife () nel PlayerShip :: kill (). Se il giocatore perde tutte le loro vite, chiama PlayerStatus :: reset () per resettare il proprio punteggio e vivere all'inizio di una nuova partita.

Miglior punteggio

Aggiungiamo la possibilità per il gioco di tracciare il tuo punteggio migliore. Vogliamo che questo punteggio persista attraverso le riproduzioni, quindi lo salveremo in un file. Lo terremo molto semplice e conserveremo il punteggio più alto come un singolo numero di testo semplice in un file (questo sarà nella directory "Application Support" dell'app, che è un nome di fantasia per la directory "preferences").

Aggiungi il seguente a PlayerStatus:

 const std :: string PlayerStatus :: kHighScoreFilename ("highscore.txt"); void CreatePathIfNonExistant2 (const std :: string & newPath) @autoreleasepool // Crea il percorso se non esiste Errore NSError *; [[NSFileManager defaultManager] createDirectoryAtPath: [NSString stringWithUTF8String: newPath.c_str ()] withIntermediateDirectories: YES attributes: nil error: & error]; 

CreatePathIfNonExistant2 () è una funzione che ho creato che creerà una directory sul dispositivo iOS se non esiste già. Dal momento che il nostro percorso di preferenza non esisterà inizialmente, dovremo crearlo la prima volta.

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

GetExecutableName2 () restituisce il nome dell'eseguibile. Useremo il nome dell'applicazione come parte del percorso delle preferenze. Useremo questa funzione invece di codificare il nome dell'eseguibile, in modo che possiamo riutilizzare questo codice per altre applicazioni invariate.

 std :: string GetPreferencePath2 (const std :: string e file) std :: string result = std :: string ([[NSSearchPathForDirectoriesInDomains (NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex: 0] UTF8String]) + "/" + GetExecutableName2 () + "/"; CreatePathIfNonExistant2 (risultato); return result + file; 

GetPreferencePath2 () restituisce il nome completo della versione della stringa del percorso di preferenza e crea il percorso se non esiste già.

 int PlayerStatus :: LoadHighScore () int score = 0; std :: string fstring; if ([[NSFileManager defaultManager] fileExistsAtPath: [NSString stringWithUTF8String: GetPreferencePath2 (kHighScoreFilename) .c_str ()]]) fstring = [[NSString stringWithContentsOfFile: [NSString stringWithUTF8String: GetPreferencePath2 (kHighScoreFilename) .c_str ()] codifica: NSUTF8StringEncoding errore: nil] UTF8String]; if (! fstring.empty ()) sscanf (fstring.c_str (), "% d", & score);  punteggio di ritorno;  void PlayerStatus :: SaveHighScore (int score) char buf [20]; sprintf (buf, "% d", punteggio); [[NSString stringWithUTF8String: buf] writeToFile: [NSString stringWithUTF8String: GetPreferencePath2 (kHighScoreFilename) .c_str ()] atomicamente: YES encoding: NSUTF8StringEncoding error: nil]; 

Il LoadHighScore () il metodo controlla prima che esista il file con il punteggio più alto e poi restituisce ciò che è nel file come numero intero. È improbabile che il punteggio non sia valido a meno che l'utente non sia generalmente in grado di cambiare i file manualmente da iOS, ma se il punteggio finisce con il non numero, il punteggio finirà per essere zero.

Vogliamo caricare il punteggio più alto all'avvio del gioco e salvarlo quando il giocatore ottiene un nuovo punteggio elevato. Modificheremo il costruttore statico e reset() metodi in PlayerStatus fare così. Aggiungeremo anche un membro dell'assistente, mIsGameOver, che useremo in un momento.

 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; mMultiplier = 1; mLives = 4; mScoreForExtraLife = 2000; mMultiplierTimeLeft = 0; 

Questo si occupa di tracciare il punteggio più alto. Ora dobbiamo mostrarlo. Aggiungeremo il seguente codice a GameRoot :: onRedrawView () nello stesso SpriteBatch blocca dove viene disegnato l'altro testo:

 if (PlayerStatus :: getInstance () -> getIsGameOver ()) sprintf (buf, "Game Over \ nIl tuo punteggio:% d \ nAlto punteggio:% 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)); 

Questo farà visualizzare il tuo punteggio e il punteggio più alto al termine del gioco, centrato sullo schermo.

Come aggiustamento finale, aumenteremo il tempo prima che la nave si rigenerino al termine del gioco per dare al giocatore il tempo di vedere il proprio punteggio. Modificare PlayerShip :: kill () impostando il tempo di respawn a 300 fotogrammi (cinque secondi) se il giocatore ha esaurito le vite.

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

Il gioco è ora pronto per giocare. Potrebbe non sembrare molto, ma ha implementato tutte le meccaniche di base. Nelle future esercitazioni aggiungeremo effetti particellari e una griglia di sfondo per renderla più piccante. Ma in questo momento, aggiungiamo rapidamente suoni e musica per renderlo più interessante.

Suono e musica

Riproduzione di suoni e musica è abbastanza semplice in iOS. Per prima cosa, aggiungiamo i nostri effetti sonori e musica alla pipeline dei contenuti.

Per prima cosa, creiamo una classe helper statica per i suoni. Si noti che il gioco è Gestione del suono la classe è chiamata Suono, ma nostro Libreria di utilità viene chiamata la classe sonora tSound.

 classe Suono: pubblico tSingleton protected: tSound * mMusic; std :: vector mExplosions; std :: vector mShots; std :: vector mSpawns; protected: Sound (); public: tSound * getMusic () const; tSound * getExplosion () const; tSound * getShot () const; tSound * getSpawn () const; classe di amici tSingleton; ; Sound :: Sound () char buf [80]; mMusic = new tSound ("music.mp3"); per (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))