Faceți un shooter vector neon pentru iOS mai mult gameplay

În această serie de tutoriale, vă vom arăta cum să faceți un shooter twin-stick inspirat de Geometry Wars, cu grafică neon, efecte particulare nebune și muzică minunată, pentru iOS folosind C ++ și OpenGL ES 2.0. Până acum, am stabilit modul de bază; acum, vom adăuga inamici și un sistem de notare.

Prezentare generală

În această parte ne vom baza pe tutorialul anterior prin adăugarea de dușmani, detectarea coliziunilor și notarea.

Iată noile caracteristici în acțiune:


Avertizare: Tare!

Vom adăuga următoarele clase noi pentru a trata acest lucru:

  • Dusman
  • EnemySpawner: Responsabil pentru crearea de inamici si cresterea treptata a dificultatilor jocului.
  • PlayerStatus: Urmărește scorul jucătorului, scorul ridicat și viețile.

S-ar putea să fi observat că în videoclip există două tipuri de dușmani, dar există o singură clasă Enemy. Am putea obține subclase din Enemy pentru fiecare tip de inamic. Versiunea originală XNA a jocului nu a făcut-o, datorită următoarelor dezavantaje:

  • Ei adaugă un cod de boilerplate mai mult.
  • Ele pot spori complexitatea codului și pot face mai greu de înțeles. Starea și funcționalitatea unui obiect se extind pe întregul său lanț de moștenire.
  • Ele nu sunt foarte flexibile - nu puteți împărți bucăți de funcționalitate între diferitele ramuri ale arborelui de moștenire dacă această funcționalitate nu este în clasa de bază. De exemplu, luați în considerare luarea a două clase, Mamifer și Pasăre, care ambele derivă din Animal. Pasăre clasa are a A zbura() metodă. Apoi decideți să adăugați o Băţ clasa care derivă din Mamifer și poate zbura, de asemenea. Pentru a distribui această funcție folosind doar moștenire, va trebui să mutați A zbura() metoda pentru a Animal clasa în care nu aparține. În plus, nu puteți elimina metode din clasele derivate, așadar dacă ați făcut a Pinguin clasa care derivă din Pasăre, ar trebui să aibă și a A zbura() metodă.

Pentru acest tutorial, vom face parte din versiunea originală XNA și vom favoriza compoziția asupra moștenirii pentru implementarea diferitelor tipuri de dușmani. Vom face acest lucru prin crearea de diverse reutilizabile comportamente pe care le putem adăuga inamicilor. Putem apoi să amestecăm cu ușurință comportamentele potrivite atunci când creăm noi tipuri de dușmani. De exemplu, dacă am fi avut deja FollowPlayer comportament și a DodgeBullet comportament, am putea face un nou dușman care să facă ambele pur și simplu prin adăugarea ambelor comportamente.

postări asemănatoare
  • Introducere în programarea orientată pe obiecte pentru dezvoltarea jocurilor
  • O abordare pragmatică a compoziției entităților
  • Unitate: acum gândiți la componente

Inamici

Dușmanii vor avea câteva proprietăți suplimentare față de entități. Pentru a oferi jucătorului un timp pentru a reacționa, vom face ca inamicii să se estompeze treptat înainte de a deveni activi și periculoși.

Să codificăm structura de bază a Dusman clasă:

 clasă Enemy: entitate publică public: enum Behavior kFollow = 0, kMoveRandom,; protejate: std :: lista mBehaviors; float mRandomDirection; int mRandomState; int mPointValue; int mTimeUntilStart; protejat: void AddBehavior (Comportament b); void ApplyBehavior (); public: Enemy (imaginea tTexture *, const tVector2f & position); void update (); bool getIsActive (); int getPointValue (); static Enemy * createSeeker (const tVector2f & poziție); inamicul static * createWanderer (const tVector2f & position); void handleCollision (Enemy * altul); void wasShot (); bool followPlayer (accelerare flotantă); bool moveRandomly (); ; Enemy :: Enemy (imagine tTexture *, const tVector2f & position): mPointValue (1), mTimeUntilStart (60) mImage = imagine; mPoziția = poziție; mRadius = imagine-> 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()->(), width = 2.0 (), tMath :: clamp (mPosition.y, getSize (), height = 2.0f, GameRoot :: getInstance () -> getViewportSize ) .height / 2.0f)); mVelocitate * = 0,8f;  void Enemy :: wasShot () mIsExpired = true; PlayerStatus :: getInstance () -> addPoints (mPointValue); PlayerStatus :: getInstance () -> increaseMultiplier (); tSound * temp = sunet :: getInstance () -> getExplosion (); dacă (! temp-> estePlaying ()) temp-> play (0, 1); 

Acest cod va face inamicii să se estompeze pentru 60 de cadre și va permite viteza lor de a funcționa. Înmulțirea vitezei cu 0,8 aduce un efect de fricțiune. Dacă facem dușmanii să accelereze la o rată constantă, această frecare îi va face să se apropie de o viteză maximă. Simplitatea și netezirea acestui tip de frecare este plăcută, dar poate doriți să utilizați o formulă diferită în funcție de efectul dorit.

a fost impuscat() metoda va fi chemată când inamicul va fi împușcat. Mai mult vom adăuga mai târziu în serie.

Vrem ca diferite tipuri de dușmani să se comporte diferit; vom realiza acest lucru prin alocarea comportamente. Un comportament va folosi o funcție personalizată care rulează fiecare cadru pentru a controla inamicul.

Versiunea originală XNA a Shape Blaster a folosit o caracteristică specială a limbajului de la C # pentru a automatiza comportamentele. Fără a intra în prea multe detalii (din moment ce nu le vom folosi), rezultatul final a fost că timpul de execuție C # va numi metodele de comportament în fiecare cadru, fără a fi nevoit să spună explicit acest lucru.

Deoarece această caracteristică de limbaj nu există nici în C, nici în C ++, va trebui să chemăm în mod explicit comportamentele pe cont propriu. Deși acest lucru necesită un cod mai mic, beneficiul lateral este că vom ști exact când comportamentele noastre sunt actualizate și, astfel, ne oferă un control mai bun.

Cel mai simplu comportament va fi followPlayer () comportamentul prezentat mai jos:

 bool Enemy :: followPlayer (accelerare flotantă) if (! PlayerShip :: getInstance () -> getIsDead ()) tVector2f temp = (PlayerShip :: getInstance () -> getPosition () - mPozi); temp = temp * (accelerație / lungime de temperatură ()); mVelocity + = temp;  dacă (mVelocity! = tVector2f (0,0)) mOrientation = atan2f (mVelocity.y, mVelocity.x);  return true; 

Acest lucru face pur și simplu inamicul accelera spre jucător la o rată constantă. Frecarea pe care am adăugat-o mai devreme ne va asigura că în cele din urmă depășește viteza maximă (cinci pixeli pe cadru atunci când accelerația este o unitate, deoarece \ (0,8 \ ori 5 + 1 = 5 \).

Să adăugăm schela necesară pentru ca comportamentele să funcționeze. Vrăjitorii trebuie să-și păstreze comportamentele, așa că vom adăuga o variabilă la Dusman clasă:

 std :: Lista mBehaviors;

mBehaviors este a std :: Lista conținând toate comportamentele active. Fiecare cadru va trece prin toate comportamentele pe care inamicul le are și va apela funcția de comportament bazată pe tipul de comportament. Dacă se întoarce metoda de comportament fals, înseamnă că comportamentul sa terminat, deci ar trebui să îl eliminăm din listă.

Vom adăuga următoarele metode la clasa Enemy:

 void Enemy :: AddBehavior (Comportament b) mBehaviors.push_back (b);  void Enemy :: ApplyBehavior () std :: lista:: iterator iter, iterNext; iter = mBehaviors.begin (); iterNext = iter; în timp ce (iter! = mBehaviors.end ()) iterNext ++; bool rezultat = fals; comutator (* iter) caz kFollow: result = followPlayer (0.9f); pauză; cazul kMoveRandom: result = moveRandomly (); pauză;  dacă (! rezultat) mBehaviors.erase (iter);  iter = iterNext; 

Și o vom modifica Actualizați() metoda de a apela ApplyBehaviours ():

 dacă (mTimeUntilStart <= 0)  ApplyBehaviours(); 

Acum putem face o metodă statică de creat căutare inamici. Tot ce trebuie să facem este să alegem imaginea pe care o dorim și să o adăugăm followPlayer () comportament:

 Enemy * Enemy :: createSeeker (const tVector2f & position) Enemy * inamic = nou Enemy (Art :: getInstance () -> getSeeker (), position); enemy-> AddBehaviour (kFollow); inamic-> mPointValue = 2; intoarce inamicul; 

Pentru a face un inamic care se mișcă în mod aleatoriu, o să alegem o direcție și apoi să facem mici ajustări aleatorii în acea direcție. Cu toate acestea, dacă reglează direcția fiecărui cadru, mișcarea va fi nervoasă, așa că vom regla periodic numai direcția. Dacă vrăjmașul intră în marginea ecranului, îl vom alege să aleagă o nouă direcție aleatorie care se îndreaptă spre perete.

 bool Enemy :: mișcareRandom () if (mRandomState == 0) mRandomDirection + = tMath :: random () * 0.2f - 0.1f;  mVelocity + = 0,4f * tVector2f (cosf (mRandomDirection), sinf (mRandomDirection)); mOrientation - = 0.05f; tRectf limite = tRectf (0,0, GameRoot :: getInstance () -> getViewportSize ()); bounds.location.x - = -mImage-> getSurfaceSize () width / 2.0f-1.0f; == -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); dacă (! bounds.contains (tPoint2f ((int32_t) mPosition.x, (int32_t) mPosition.y))) tVector2f temp = tVector2f (GameRoot :: getInstance () -> getViewportSize ) -> getViewportSize () y) / 2.0f; temp - = mPoziție; mRandomDirection = atan2f (temp.y, temp.x) + tMath :: aleator () * tMath :: PI - tMath :: PI / 2.0f;  mRandomState = (mRandomState + 1)% 6; return true; 

Acum putem face o metodă de fabricare a fabricației rătăcitor dușmani, la fel ca și noi pentru căutător:

 Enemy * Enemy :: createWanderer (const tVector2f & position) Enemy * inamic = nou Enemy (Art :: getInstance () -> getWanderer (), position); inamic-> mRandomDirection = tMath :: random () * tMath :: PI * 2.0f; inamic-> mRandomState = 0; enemy-> AddBehaviour (kMoveRandom); intoarce inamicul; 

Detectarea coliziunii

Pentru detectarea coliziunilor, vom modela nava jucătorului, dușmanii și gloanțele ca cercuri. Detectarea coliziunii circulară este drăguță, deoarece este simplă, este rapidă și nu se schimbă atunci când obiectele se rotesc. Dacă vă amintiți, Entitate clasa are o rază și o poziție (poziția se referă la centrul entității) - aceasta este tot ceea ce avem nevoie pentru detectarea coliziunii circulare.

Testarea fiecărei entități împotriva tuturor celorlalte entități care ar putea să se ciocnească poate fi foarte lentă dacă aveți un număr mare de entități. Există multe tehnici pe care le puteți utiliza pentru a accelera detectarea coliziunii în fază largă, cum ar fi quadtrees, mătură și prune, și copaci BSP. Cu toate acestea, pentru moment, vom avea doar cateva zeci de entitati pe ecran la un moment dat, asa ca nu ne vom face griji cu privire la aceste tehnici mai complexe. Le putem adăuga mai târziu, dacă avem nevoie de ele.

În Shape Blaster, nu fiecare entitate se poate ciocni cu orice alt tip de entitate. Gloanțele și nava jucătorului se pot ciocni doar cu dușmanii. Dușmanii se pot ciocni și cu alți dușmani; acest lucru le va împiedica să se suprapună.

Pentru a face față acestor tipuri diferite de coliziuni, vom adăuga două liste noi la EntityManager pentru a urmări gloanțele și dușmanii. Ori de câte ori adăugăm o entitate la EntityManager, vom dori să-l adăugăm la lista corespunzătoare, așa că vom face o privare addEntity () metodă de a face acest lucru. Vom elimina, de asemenea, toate entitățile expirate din toate listele din fiecare cadru.

 std :: Lista mEnemies; std :: Lista mBullets; void EntityManager :: addEntity (entitate Entity *) mEntities.push_back (entitate); comutator (entitate-> getKind ()) Entitatea cazului: kBullet: mBullets.push_back (entitate (Bullet *)); pauză; Entitatea cazului :: kEnemy: mEnemies.push_back (Enemy *) entitate); pauză; prestabilit: pauză;  // // // în Update () pentru (std :: list:: iterator iter = mBullets.begin (); iter! = mBullets.end (); iter ++) dacă ((* iter) -> esteExpirată ()) delete * iter; * iter = NULL;  mBullets.remove (NULL); pentru (std :: lista:: iterator iter = mEnemies.begin (); iter! = mEnemies.end (); iter ++) dacă ((* iter) -> esteExpirată ()) delete * iter; * iter = NULL;  mEnemies.remove (NULL);

Înlocuiți apelurile la entity.add () în EntityManager.add () și EntityManager.update () cu apeluri către addEntity ().

Acum, să adăugăm o metodă care va determina dacă două entități se ciocnesc:

 bool EntityManager :: isColliding (Entitatea * a, Entita * b) raza float = a-> getRadius () + b-> getRadius (); retur! a-> esteExpirată () &&! b-> isExpired () && a-> getPosition () distanceSquared (b-> getPosition () < radius * radius; 

Pentru a determina dacă două cercuri se suprapun, verificați dacă distanța dintre ele este mai mică decât suma razei lor. Metoda noastră optimizează ușor acest lucru, verificând dacă pătratul distanței este mai mic decât pătratul sumei razei. Amintiți-vă că este puțin mai rapid să calculați distanța pătrată decât distanța reală.

Diferite lucruri se vor întâmpla în funcție de care două obiecte se ciocnesc. Dacă se ciocnesc doi dușmani, vrem să se împrăștie unul pe celălalt; dacă un glonte lovește un dușman, glonțul și dușmanul ar trebui să fie distruse; dacă jucătorul atinge un inamic, jucătorul ar trebui să moară și nivelul ar trebui să fie resetat.

Vom adăuga o handleCollision () metoda pentru a Dusman clasă să se ocupe de coliziuni între dușmani:

 void Enemy :: handleCollision (Enemy * altul) tVector2f d = mPoziție - alt-> mPoziție; mVelocitate + = 10,0f * d / (d.lengthSquared () + 1,0f); 

Această metodă va împinge inamicul actual de departe de celălalt inamic. Cu cât sunt mai aproape, cu atât mai greu va fi împins, deoarece magnitudinea lui (d / d.LengthSquared ()) este doar unul peste distanță.

Respingerea playerului

Apoi, avem nevoie de o metodă de manipulare a navei jucătorului care a fost ucisă. Când se întâmplă acest lucru, nava jucătorului va dispărea pentru un timp scurt înainte de respawning.

Începem prin adăugarea a doi noi membri PlayerShip:

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

La începutul anului PlayerShip :: actualizare (), adăugați următoarele:

 dacă (getIsDead ()) mFramesUntilRespawn--; 

Și ne override a desena() așa cum se arată:

 void PlayerShip :: trage (tSpriteBatch * spriteBatch) if (! getIsDead ()) Entitatea :: trage (spriteBatch); 

În cele din urmă, adăugăm a ucide() metoda de a PlayerShip:

 void PlayerShip :: kill () mFramesUntilRespawn = 60; 

Acum că toate piesele sunt în loc, vom adăuga o metodă la EntityManager care trece prin toate entitățile și verificări pentru coliziuni:

 void EntityManager :: handleCollisions () pentru (std :: lista:: iterator i = mEnemies.begin (); i! = mEnemies.end (); i ++) pentru (std :: lista:: iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) dacă (isColliding (* i, * j)) (* i) -> handleCollision (* j); (* J) -> handleCollision (* i);  manipulează coliziuni între gloanțe și dușmani pentru (std :: list:: iterator i = mEnemies.begin (); i! = mEnemies.end (); i ++) pentru (std :: lista:: iterator j = mBullets.begin (); j! = mBullets.end (); j ++) dacă (isColliding (* i, * j)) (* i) -> wasShot (); (* J) -> setExpired ();  // manipulează coliziuni între player și inamici pentru (std :: list:: iterator i = mEnemies.begin (); i! = mEnemies.end (); i ++) if ((i) -> getIsActive () && esteColliding (PlayerShip :: getInstance (), * i)) PlayerShip :: getInstance () -> kill (); pentru (std :: lista:: iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) (* j) -> fostShot ();  EnemySpawner :: getInstance () -> reset (); pauză; 

Apelați această metodă de la Actualizați() imediat după setare mIsUpdating la Adevărat.

Enemy Spawner

Ultimul lucru pe care trebuie să-l faceți este să faceți EnemySpawner clasa, care este responsabilă pentru crearea dușmanilor. Vrem ca jocul să înceapă ușor și să devină mai greu, așa că EnemySpawner va crea dușmani într-o rată tot mai mare cu progresul timpului. Când jucătorul moare, vom reseta EnemySpawner la dificultatea inițială.

 clasa EnemySpawner: public tSingleton protejat: float mInverseSpawnChance; protejate: tVector2f GetSpawnPosition (); protejate: EnemySpawner (); public: void update (); void reset (); clasa prietenului 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 ()));  dacă (int32_t (tMath :: random () * mInverseSpawnChance) == 0) EntityManager :: getInstance () -> adăugați (Enemy :: createWanderer (GetSpawnPosition ()));  dacă (mInverseSpawnChance> 30) mInverseSpawnChance - = 0.005f;  tVector2f EnemySpawner :: GetSpawnPosition () tVector2f pos; do pos = tVector2f (tMath :: random) () * GameRoot :: getInstance () -> getViewportSize () lățime, tMath :: random () * GameRoot :: getInstance () -> getViewportSize ().  în timp ce (pos.distanceSquared (PlayerShip :: getInstance () -> getPosition ()) < 250 * 250); return pos;  void EnemySpawner::reset()  mInverseSpawnChance = 90; 

Fiecare cadru, există unul în mInverseSpawnChance de a genera fiecare tip de inamic. Șansa de a da naștere unui inamic crește treptat până când atinge maximum unu la douăzeci. Dușmanii sunt întotdeauna creați la cel puțin 250 de pixeli distanță de player.

Fii atent cu privire la in timp ce buclă în GetSpawnPosition (). Acesta va funcționa eficient, atâta timp cât zona în care dușmanii pot da naștere este mai mare decât zona în care nu pot să se înfulece. Cu toate acestea, dacă faceți zona prea interzisă, veți obține o buclă infinită.

Apel EnemySpawner :: actualizare () din GameRoot :: onRedrawView () și sunați EnemySpawner :: resetare () când jucătorul este ucis.

Scor și trăiește

  • În Shape Blaster, începeți cu patru vieți și veți obține o viață suplimentară la fiecare 2.000 de puncte.
  • Veți primi puncte pentru a distruge inamicii, cu diferite tipuri de dușmani în valoare de valori diferite de puncte.
  • Fiecare inamic distrus mărește, de asemenea, multiplicatorul scorului dvs. cu unul.
  • Dacă nu ucizi vrăjmașii într-o perioadă scurtă de timp, multiplicatorul tău va fi resetat.
  • Numărul total de puncte primite de la fiecare inamic pe care îl distrugeți este numărul de puncte în valoare de inamic, înmulțit cu multiplicatorul actual.
  • Dacă îți pierzi toată viața, jocul se termină și începi un nou joc, cu resetarea scorului la zero.

Pentru a face față tuturor acestor lucruri, vom face o clasă statică numită PlayerStatus:

 PlayerStatus: public tSingleton protejat: static const float kMultiplierExpiryTime; static const int kMaxMultiplier; static const std :: șir kHighScoreFilename; float mMultiplierTimeLeft; int mLives; int mScore; int mHighScore; int mMultiplier; int mScoreForExtraLife; uint32_t mLastTime; protejate: int LoadHighScore (); void SaveHighScore (scor int); protejat: PlayerStatus (); public: 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; clasa prietenului 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 :: actualizare () if (mMultiplier> 1) mMultiplierTimeLeft - = float (tTimer :: getTimeMS () - mLastTime) / 1000.0f; dacă (mMultiplierTimeLeft <= 0)  mMultiplierTimeLeft = kMultiplierExpiryTime; resetMultiplier();   mLastTime = tTimer::getTimeMS();  void PlayerStatus::addPoints(int basePoints)  if (!PlayerShip::getInstance()->getIsDead ()) mScore + = puncte de bază * mMultiplier; în timp ce (mScore> = mScoreForExtraLife) mScoreForExtraLife + = 2000; mLives ++;  void PlayerStatus :: increaseMultiplier () if (! PlayerShip :: getInstance () -> getIsDead ()) mMultiplierTimeLeft = kMultiplierExpiryTime; dacă (mMultiplier < kMaxMultiplier)  mMultiplier++;    void PlayerStatus::resetMultiplier()  mMultiplier = 1;  void PlayerStatus::removeLife()  mLives--; 

Apel PlayerStatus :: actualizare () din GameRoot :: onRedrawView () când jocul nu este întrerupt.

Apoi, vrem să afișăm scorul, viața și multiplicatorul pe ecran. Pentru a face acest lucru va trebui să adăugați o tSpriteFont în Conţinut proiect și o variabilă corespunzătoare în Artă clasă, pe care o vom numi Font. Încărcați fontul în Artăconstructorul, așa cum am făcut cu texturile.

Notă: Fontul pe care îl folosim este de fapt o imagine, mai degrabă decât un fișier cu font TrueType. Fonturile bazate pe imagini au fost modul în care jocurile și consolele clasice arcade au imprimat text pe ecran și chiar și acum unele jocuri de generație curentă utilizează încă tehnica. Un beneficiu pe care îl câștigăm din acest lucru este că vom termina folosind aceleași tehnici pentru a desena text pe ecran pe măsură ce noi facem alte sprite.

Modificați sfârșitul GameRoot :: onRedrawView () unde cursorul este desenat, după cum se arată mai jos:

 char buf [80]; sprintf (buf, "Locuiește:% 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, "Scor:% d", PlayerStatus :: getInstance () -> getScore ()); DrawRightAlignedString (buf, 5); sprintf (buf, "Multiplicator:% d", PlayerStatus :: getInstance () -> getMultiplier ()); DrawRightAlignedString (buf, 35); mSpriteBatch-> tragere (0, Art :: getInstance () -> getPointer (), intrare :: getInstance () -> getMousePosition());

DrawRightAlignedString () este o metodă de ajutor pentru desenarea textului aliniat în partea dreaptă a ecranului. Adăugați-o la GameRoot prin adăugarea codului de mai jos:

 #define kScale 3.0f void GameRoot :: DrawRightAlignedString (const std :: șir și 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), tVector2f (kScale)); 

Acum viata, scorul si multiplicatorul ar trebui sa apara pe ecran. Cu toate acestea, trebuie să modificăm aceste valori ca răspuns la evenimentele de joc. Adăugați o proprietate numită mPointValue la Dusman clasă.

 int Enemy :: getPointValue () retur mPointValue; 

Setați valoarea punctului pentru diferiți dușmani la ceva ce vă este potrivit. Am făcut dușmanii rătăciți în valoare de un punct, iar dușmanii căutători în valoare de două puncte.

Apoi, adăugați următoarele două linii la Enemy :: wasShot () pentru a crește scorul și multiplicatorul jucătorului:

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

Apel PlayerStatus :: removeLife () în PlayerShip :: ucide (). Dacă jucătorul își pierde toată viața, sunați PlayerStatus :: resetare () pentru a-și restabili scorul și a trăit la începutul unui nou joc.

Scoruri mari

Să adăugăm abilitatea jocului de a urmări cel mai bun scor. Vrem ca acest scor să persiste în toate piesele, astfel încât să îl salvăm într-un fișier. O vom păstra foarte simplu și vom salva scorul mare ca număr unic de text simplu într-un fișier (acesta va fi în directorul "Aplicație de suport" a aplicației, care este un nume fantezist pentru directorul "preferințe").

Adăugați următoarele la PlayerStatus:

 const std :: String PlayerStatus :: kHighScoreFilename ("highscore.txt"); void CreatePathIfNonExistant2 (const std :: string & newPath) @autoreleasepool // Crearea căii dacă nu există eroare NSError *; [[NSFileManager defaultManager] createDirectoryAtPath: [NSString șirWithUTF8String: newPath.c_str ()] cuIntermediateDirectories: YES atribute: eroare zero: & eroare]; 

CreatePathIfNonExistant2 () este o funcție pe care am făcut-o care va crea un director pe dispozitivul iOS dacă nu există deja. Deoarece calea noastră de preferință nu va exista inițial, va trebui să o creăm pentru prima dată.

 std :: șirul GetExecutableName2 () return [[[[NSBundle mainBundle] infoDictionary] obiectForKey: @ "CFBundleExecutable"] UTF8String]; 

GetExecutableName2 () returnează numele executabilului. Vom folosi numele aplicației ca parte a căii de preferință. Vom folosi această funcție în loc să codificăm cu fermitate numele executabilului, astfel încât să putem doar să reutilizăm acest cod pentru alte aplicații neschimbate.

 std :: șirul GetPreferencePath2 (const std :: șir și fișier) std :: string rezultat = std :: șir ([[NSSearchPathForDirectoriesInDomains (NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex: 0] UTF8String]) + + "/"; CreatePathIfNonExistant2 (rezultat); rezultatul retur + fișier; 

GetPreferencePath2 () returnează numele versiunii complete a șirului de cale de preferință și creează calea dacă aceasta nu există deja.

 int PlayerStatus :: LoadHighScore () int scorul = 0; std :: string fstring; dacă [[[NSFileManager defaultManager] fileExistsAtPath: [NSString stringWithUTF8String: GetPreferencePath2 (kHighScoreFilename) .c_str ()]]) fstring = [NSString stringWithContentsOfFile: Codare NSStringWithUTF8String: GetPreferencePath2 (kHighScoreFilename) nil] UTF8String]; dacă (! fstring.empty ()) sscanf (fstring.c_str (), "% d", & scor);  scor de întoarcere;  void PlayerStatus :: SaveHighScore (scorul int) char buf [20]; sprintf (buf, "% d", scor); [[NSString stringWithUTF8String: buf] writeToFile: [NSString șirWithUTF8String: GetPreferencePath2 (kHighScoreFilename) .c_str ()] atomic: codificare YES: eroare NSUTF8StringEncoding: nil]; 

LoadHighScore () metoda verifică mai întâi existența fișierului cu scor mare și apoi returnează ceea ce este în fișier ca un număr întreg. Este puțin probabil ca scorul să fie invalid, cu excepția cazului în care utilizatorul nu poate schimba manual fișierele din iOS, dar dacă scorul ajunge la un număr, scorul va ajunge la zero.

Vrem să încărcăm scorul mare când începe jocul și să îl salvăm când jucătorul obține un scor mare. Vom modifica constructorul static și reset () metode în PlayerStatus să facă acest lucru. De asemenea, vom adăuga un membru de ajutor, mIsGameOver, pe care o vom folosi într-o clipă.

 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; 

Care se ocupă de urmărirea scorului ridicat. Acum trebuie să o afișăm. Vom adăuga următorul cod la GameRoot :: onRedrawView () în același SpriteBatch bloc unde este redactat celălalt text:

 dacă PlayerStatus :: getInstance () -> getIsGameOver ()) sprintf (buf, "Game Over \ nScrieți scorul:% d \ nDeval Scor:% d", PlayerStatus :: getInstance : 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)); 

Acest lucru va face să afișeze scorul dvs. și scorul ridicat pe joc peste, centrat pe ecran.

Ca o ajustare finală, vom mări timpul înainte ca nava să respawns în timpul jocului pentru a da jucătorului timp pentru a-și vedea scorul. Modifica PlayerShip :: ucide () prin setarea timpului respawn la 300 de cadre (cinci secunde) dacă jucătorul nu mai are viață.

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

Jocul este acum gata pentru a juca. Poate nu arata mult, dar are toate mecanismele de baza implementate. În tutorialele viitoare vom adăuga efecte de particule și o grilă de fundal pentru a-i condimenta. Dar chiar acum, să adăugăm rapid un sunet și o muzică mai interesantă.

Sunet și muzică

Redarea sunetului și a muzicii este destul de simplă în iOS. Mai întâi, să adăugăm efecte sonore și muzică la conținutul conductei.

În primul rând, facem o clasă de ajutor pentru stațiile de sunet. Rețineți că jocul este Managementul sunetului clasa este numit Sunet, ci pe noi Utilitatea bibliotecii sună clasa de sunet tSound.

 clasa Sunet: public tSingleton protejat: tSound * mMusic; std :: vector mExplosions; std :: vector mShots; std :: vector mSpawns; protejat: Sunet (); publică: tSound * getMusic () const; tSound * getExplosion () const; tSound * getShot () const; tSound * getSpawn () const; clasa prietenului tSingleton; ; Sunet :: Sunet () char buf [80]; mMusic = nou tSound ("music.mp3"); pentru (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));  

Deoarece avem mai multe variante ale fiecărui sunet, Explozie, Lovitură, și Icre proprietățile vor alege un sunete aleatoriu între variante.

Apel Sunetconstructorul din GameRoot :: onInitView (). Pentru a reda muzica, adăugați următoarea linie la sfârșitul paginii GameRoot :: onInitView ().

 Sunet :: getInstance () -> getMusic () -> redare (0, (uint32_t) -1);

Pentru a reda sunete, puteți apela pur și simplu Joaca() metoda pe Efect sonor