In deze serie tutorials laat ik je zien hoe je een Geometry Wars-geïnspireerde twin-stick shooter maakt met neon graphics, gekke partikeleffecten en geweldige muziek, voor iOS met C ++ en OpenGL ES 2.0. Tot nu toe hebben we de basisgameplay ingesteld; nu voegen we vijanden en een scoresysteem toe.
In dit deel bouwen we verder op de vorige zelfstudie door vijanden toe te voegen, botsingsdetectie en scoren.
Dit zijn de nieuwe functies in actie:
We zullen de volgende nieuwe klassen toevoegen om dit te behandelen:
Vijand
EnemySpawner
: Verantwoordelijk voor het maken van vijanden en het geleidelijk vergroten van de moeilijkheidsgraad van de game.PlayerStatus
: Volgt de score van de speler, hoge score en levens.Je hebt misschien gemerkt dat er twee soorten vijanden in de video zitten, maar er is maar één vijandklasse. We zouden subclasses van vijand voor elk vijandtype kunnen afleiden. De originele XNA-versie van het spel niet, vanwege de volgende nadelen:
Zoogdier
en Vogel
, waaruit beide voortkomen Dier
. De Vogel
klasse heeft een Vlieg()
methode. Dan besluit u om een toe te voegen Knuppel
klasse die is afgeleid van Zoogdier
en kan ook vliegen. Als u deze functionaliteit alleen via overerving wilt delen, moet u de Vlieg()
methode om de Dier
klas waar het niet thuishoort. Bovendien kunt u methoden niet verwijderen uit afgeleide klassen, dus als u een pinguïn
klasse die is afgeleid van Vogel
, het zou ook een moeten hebben Vlieg()
methode.Voor deze tutorial zullen we instemmen met de originele XNA-versie en de voorkeur geven aan compositie via overerving voor het implementeren van de verschillende soorten vijanden. We zullen dit doen door verschillende herbruikbare te maken gedragingen die we aan vijanden kunnen toevoegen. We kunnen dan gemakkelijk gedrag combineren en matchen wanneer we nieuwe typen vijanden maken. Als we bijvoorbeeld al een FollowPlayer
gedrag en a DodgeBullet
gedrag, kunnen we een nieuwe vijand maken die beide doet door beide gedragingen toe te voegen.
Vijanden hebben een paar extra eigenschappen ten opzichte van entiteiten. Om de speler de tijd te geven om te reageren, zullen we vijanden geleidelijk doen vervagen voordat ze actief en gevaarlijk worden.
Laten we de basisstructuur van de code coderen Vijand
klasse:
class Enemy: public Entity public: enum Behavior kFollow = 0, kMoveRandom,; beschermd: std :: lijstmBehaviors; zweven mRandomDirection; int mRandomState; int mPointValue; int mTimeUntilStart; protected: void AddBehaviour (gedrag b); void ApplyBehaviours (); public: Enemy (tTexture * image, const tVector2f & position); ongeldige update (); bool getIsActive (); int getPointValue (); static Enemy * createSeeker (const tVector2f & position); static Enemy * createWanderer (const tVector2f & position); ongeldige handleCollision (Enemy * andere); void wasShot (); bool followPlayer (vlotteracceleratie); bool moveRandomly (); ; Enemy :: Enemy (tTexture * image, const tVector2f & position): mPointValue (1), mTimeUntilStart (60) mImage = image; mPosition = positie; mRadius = image-> getSurfaceSize (). width / 2.0f; mColor = tColor4f (0,0,0,0); mKind = kEnemy; void Enemy :: update () if (mTimeUntilStart <= 0) ApplyBehaviours(); else mTimeUntilStart--; mColor = tColor4f(1,1,1,1) * (1.0f - (float)mTimeUntilStart / 60.0f); mPosition += mVelocity; mPosition = tVector2f(tMath::clamp(mPosition.x, getSize().width / 2.0f, GameRoot::getInstance()->getViewportSize (). width - getSize (). width / 2.0f), tMath :: clamp (mPosition.y, getSize (). height / 2.0f, GameRoot :: getInstance () -> getViewportSize (). height - getSize ( ) .height / 2.0f)); mVelocity * = 0.8f; void Enemy :: wasShot () mIsExpired = true; PlayerStatus :: getInstance () -> addPoints (mPointValue); PlayerStatus :: getInstance () -> increaseMultiplier (); tSound * temp = Sound :: getInstance () -> getExplosion (); if (! temp-> isPlaying ()) temp-> play (0, 1);
Deze code zorgt ervoor dat vijanden 60 frames vervagen en hun snelheid laten werken. Het vermenigvuldigen van de snelheid met 0,8 maakt een wrijvingsachtig effect. Als we vijanden met een constante snelheid laten accelereren, zorgt deze wrijving ervoor dat ze soepel een maximale snelheid naderen. De eenvoud en soepelheid van dit soort wrijving is leuk, maar je wilt misschien een andere formule gebruiken, afhankelijk van het effect dat je wilt.
De was doodgeschoten()
methode wordt aangeroepen wanneer de vijand wordt neergeschoten. We zullen er later in de serie meer aan toevoegen.
We willen dat verschillende soorten vijanden zich anders gedragen; we zullen dit bereiken door toe te wijzen gedragingen. Een gedrag gebruikt een aangepaste functie die elk frame uitvoert om de vijand te besturen.
De originele XNA-versie van Shape Blaster heeft een speciale taalfunctie van C # gebruikt om het gedrag te automatiseren. Zonder in te gaan op te veel details (omdat we ze niet zullen gebruiken), was het eindresultaat dat de C # runtime de gedragsmethoden elk frame zou noemen zonder dit expliciet te hoeven zeggen.
Aangezien deze taalfunctie niet bestaat in C of C ++, moeten we het gedrag expliciet zelf noemen. Hoewel dit een beetje meer code vereist, is het voordeel dat we precies weten wanneer ons gedrag wordt bijgewerkt en ons daardoor een fijnere controle geeft.
Ons eenvoudigste gedrag is de followPlayer ()
gedrag hieronder weergegeven:
bool Enemy :: followPlayer (float acceleration) if (! PlayerShip :: getInstance () -> getIsDead ()) tVector2f temp = (PlayerShip :: getInstance () -> getPosition () - mPosition); temp = temp * (versnelling / temp.length ()); mVelocity + = temp; if (mViersterkte! = tVector2f (0,0)) mOrientation = atan2f (mVelocity.y, mVelocity.x); return true;
Dit zorgt er simpelweg voor dat de vijand in een constant tempo naar de speler accelereert. De wrijving die we eerder hebben toegevoegd, zorgt ervoor dat deze uiteindelijk een maximum snelheid haalt (vijf pixels per frame wanneer versnelling één eenheid is, sinds \ (0.8 \ keer 5 + 1 = 5 \).
Laten we de steigers toevoegen die nodig zijn om gedrag te laten werken. Vijanden moeten hun gedrag opslaan, dus we zullen een variabele toevoegen aan de Vijand
klasse:
std :: listmBehaviors;
mHehaviors is een std :: list
met alle actieve gedragingen. Elk frame zullen we door alle gedragingen van de vijand heen gaan en de gedragsfunctie op basis van het gedragstype noemen. Als de gedragsmethode terugkeert vals
, het betekent dat het gedrag is voltooid, dus we moeten het van de lijst verwijderen.
We voegen de volgende methoden toe aan de klasse 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); breken; case kMoveRandom: result = moveRandomly (); breken; if (! result) mBehaviors.erase (iter); iter = iterNext;
En we zullen de bijwerken()
methode om te bellen ApplyBehaviours ()
:
if (mTimeUntilStart <= 0) ApplyBehaviours();
Nu kunnen we een statische methode maken om te maken op zoek naar vijanden. Het enige dat we moeten doen is de gewenste afbeelding kiezen en de afbeelding toevoegen followPlayer ()
gedrag:
Enemy * Enemy :: createSeeker (const tVector2f & position) Enemy * enemy = new Enemy (Art :: getInstance () -> getSeeker (), position); enemy-> AddBehaviour (kFollow); vijand-> mPointValue = 2; keer vijand terug;
Om een vijand te maken die willekeurig beweegt, laten we hem een richting kiezen en dan kleine willekeurige aanpassingen in die richting maken. Als we echter de richting van elk frame aanpassen, wordt de beweging schokkerig, dus we passen de richting alleen periodiek aan. Als de vijand tegen de rand van het scherm aanloopt, laten we hem een nieuwe willekeurige richting kiezen die van de muur af wijst.
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; geef waar terug;
We kunnen nu een fabrieksmethode maken om te creëren omzwerving vijanden, net zoals we deden voor de zoeker:
Enemy * Enemy :: createWanderer (const tVector2f & position) Enemy * enemy = new Enemy (Art :: getInstance () -> getWanderer (), position); vijand-> mRandomDirection = tMath :: random () * tMath :: PI * 2.0f; vijand-> mRandomState = 0; enemy-> AddBehaviour (kMoveRandom); keer vijand terug;
Voor botsingdetectie modelleren we het schip van de speler, de vijanden en de kogels als cirkels. Circulaire detectie van botsingen is leuk omdat het eenvoudig is, het is snel en het verandert niet wanneer de objecten draaien. Als je je dat herinnert, de Entiteit
klasse heeft een straal en een positie (de positie verwijst naar het midden van de entiteit) - dit is alles wat we nodig hebben voor detectie van cirkelvormige botsingen.
Het testen van elke entiteit tegen alle andere entiteiten die mogelijk zouden kunnen botsen, kan erg traag zijn als u een groot aantal entiteiten heeft. Er zijn veel technieken die u kunt gebruiken om breedveldige botsingsdetectie te versnellen, zoals quadtrees, sweep en snoeien, en BSP-bomen. Voorlopig zullen we echter slechts een paar dozijn entiteiten tegelijk op het scherm hebben, dus we zullen ons geen zorgen maken over deze meer complexe technieken. We kunnen ze altijd later toevoegen als we ze nodig hebben.
In Shape Blaster kan niet elke entiteit botsen met elk ander type entiteit. Kogels en het schip van de speler kunnen alleen met vijanden botsen. Vijanden kunnen ook botsen met andere vijanden; dit zal voorkomen dat ze elkaar overlappen.
Om met deze verschillende soorten botsingen om te gaan, zullen we twee nieuwe lijsten toevoegen aan de EntityManager
om kogels en vijanden te volgen. Telkens wanneer we een entiteit toevoegen aan de EntityManager
, we willen het toevoegen aan de juiste lijst, dus we zullen een privé maken addEntity ()
methode om dit te doen. We zullen ook zeker zijn dat alle verlopen entiteiten uit alle lijsten van elk frame worden verwijderd.
std :: listmEnemies; std :: list mBullets; void EntityManager :: addEntity (Entity * entity) mEntities.push_back (entity); switch (entity-> getKind ()) case Entity :: kBullet: mBullets.push_back ((Bullet *) entity); breken; case Entiteit: kEnemy: mEnemies.push_back ((Enemy *) entiteit); breken; standaard: pauze; // ... // in Update () voor (std :: lijst :: iterator iter = mBullets.begin (); iter! = mBullets.end (); iter ++) if ((* iter) -> is Expired ()) delete * iter; * iter = NULL; mBullets.remove (NULL); voor (std :: list :: iterator iter = mEnemies.begin (); iter! = mEnemies.end (); iter ++) if ((* iter) -> is Expired ()) delete * iter; * iter = NULL; mEnemies.remove (NULL);
Vervang de oproepen door entity.add ()
in EntityManager.add ()
en EntityManager.update ()
met oproepen naar addEntity ()
.
Laten we nu een methode toevoegen die bepaalt of twee entiteiten botsen:
bool EntityManager :: isColliding (Entity * a, Entity * b) float radius = a-> getRadius () + b-> getRadius (); return! a-> is Expired () &&! b-> is Expired () && a-> getPosition (). distanceSquared (b-> getPosition ()) < radius * radius;
Om te bepalen of twee cirkels elkaar overlappen, controleert u eenvoudigweg of de afstand tussen de cirkels kleiner is dan de som van hun stralen. Onze methode optimaliseert dit enigszins door te controleren of het kwadraat van de afstand kleiner is dan het kwadraat van de som van de radii. Onthoud dat het een beetje sneller is om de afstand in het kwadraat te berekenen dan de werkelijke afstand.
Verschillende dingen zullen afhankelijk van gebeuren welke twee objecten botsen. Als twee vijanden tegen elkaar botsen, willen we dat ze elkaar weg duwen; als een kogel een vijand raakt, moeten de kogel en de vijand allebei worden vernietigd; als de speler een vijand aanraakt, moet de speler sterven en moet het niveau worden gereset.
We voegen een toe handleCollision ()
methode om de Vijand
klasse om botsingen tussen vijanden aan te pakken:
void Enemy :: handleCollision (Enemy * other) tVector2f d = mPosition - other-> mPosition; mVelocity + = 10.0f * d / (d.lengthSquared () + 1.0f);
Deze methode duwt de huidige vijand weg van de andere vijand. Hoe dichterbij ze zijn, hoe moeilijker het wordt geduwd, omdat de omvang van (d / d.LengthSquared ())
is slechts één over de afstand.
Vervolgens hebben we een methode nodig om het schip van de speler te laten doden. Wanneer dit gebeurt, verdwijnt het schip van de speler voor een korte tijd voordat het opnieuw gaat duiken.
We beginnen met het toevoegen van twee nieuwe leden aan PlayerShip
:
int mFramesUntil Respawn; bool PlayerShip :: getIsDead () return mFramesUntilRespawn> 0;
Aan het begin van PlayerShip :: update ()
, voeg het volgende toe:
if (getIsDead ()) mFramesUntilRespawn--;
En we negeren trek()
zoals getoond:
void PlayerShip :: draw (tSpriteBatch * spriteBatch) if (! getIsDead ()) Entity :: draw (spriteBatch);
Ten slotte voegen we een toe doden()
methode om PlayerShip
:
void PlayerShip :: kill () mFramesUntilRespawn = 60;
Nu alle stukken op hun plaats zitten, zullen we een methode toevoegen aan de EntityManager
dat gaat door alle entiteiten en controleert op botsingen:
void EntityManager :: handleCollisions () for (std :: list:: iterator i = mEnemies.begin (); i! = mEnemies.end (); i ++) for (std :: lijst :: iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) if (isColliding (* i, * j)) (* i) -> handleCollision (* j); (* J) -> handleCollision (* i); // omgaan met botsingen tussen kogels en vijanden voor (std :: lijst :: iterator i = mEnemies.begin (); i! = mEnemies.end (); i ++) for (std :: lijst :: iterator j = mBullets.begin (); j! = mBullets.end (); j ++) if (isColliding (* i, * j)) (* i) -> wasShot (); (* J) -> setExpired (); // omgaan met botsingen tussen de speler en vijanden voor (std :: lijst :: iterator i = mEnemies.begin (); i! = mEnemies.end (); i ++) if ((* i) -> getIsActive () && isColliding (PlayerShip :: getInstance (), * i)) PlayerShip :: getInstance () -> kill (); voor (std :: lijst :: iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) (* j) -> wasShot (); EnemySpawner :: getInstance () -> reset (); breken;
Noem deze methode van bijwerken()
onmiddellijk na het instellen mIsUpdating
naar waar
.
Het laatste ding om te doen is het maken van de EnemySpawner
klas, die verantwoordelijk is voor het maken van vijanden. We willen dat het spel gemakkelijk begint en harder wordt, dus het EnemySpawner
zal vijanden in toenemende mate creëren naarmate de tijd vordert. Wanneer de speler overlijdt, stellen we de EnemySpawner
tot zijn aanvankelijke moeilijkheid.
klasse EnemySpawner: openbare tSingletonprotected: float mInverseSpawnChance; beschermd: tVector2f GetSpawnPosition (); beschermd: EnemySpawner (); public: void update (); void reset (); vriend klas tSingleton ; ; void EnemySpawner :: update () if (! PlayerShip :: getInstance () -> getIsDead () && EntityManager :: getInstance () -> getCount () < 200) if (int32_t(tMath::random() * mInverseSpawnChance) == 0) EntityManager::getInstance()->toe te voegen (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;
Elk frame, er is er een in mInverseSpawnChance
van het genereren van elk type vijand. De kans om een vijand te spawnen neemt geleidelijk toe tot een maximum van één op twintig. Vijanden worden altijd op ten minste 250 pixels van de speler verwijderd.
Wees voorzichtig met de terwijl
loop in GetSpawnPosition ()
. Het zal efficiënt werken zolang het gebied waarin vijanden kunnen spawnen groter is dan het gebied waar ze niet kunnen spawnen. Als je echter het verboden gebied te groot maakt, krijg je een oneindige lus.
telefoontje EnemySpawner :: update ()
van GameRoot :: onRedrawView ()
en bel EnemySpawner :: reset ()
wanneer de speler wordt gedood.
Om dit allemaal aan te pakken, maken we een statische klasse genaamd PlayerStatus
:
class PlayerStatus: public tSingletonprotected: static const float kMultiplierExpiryTime; static const int kMaxMultiplier; static const std :: string kHighScoreFilename; zweven mMultiplierTimeLeft; int mLives; int mScore; int mHighScore; int mMultiplier; int mScoreForExtraLife; uint32_t mLastTime; beschermd: int LoadHighScore (); void SaveHighScore (int score); beschermd: PlayerStatus (); public: void reset (); ongeldige update (); void addPoints (int basePoints); void increase increaseMultiplier (); void reset Multiplier (); void removeLife (); int getLives () const; int getScore () const; int getHighScore () const; int getMultiplier () const; bool getIsGameOver () const; vriend klas 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--;
telefoontje PlayerStatus :: update ()
van GameRoot :: onRedrawView ()
wanneer het spel niet is gepauzeerd.
Vervolgens willen we je score, lives en multiplier op het scherm weergeven. Om dit te doen, moeten we een toevoegen tSpriteFont
in de Inhoud
project en een bijbehorende variabele in de Kunst
klas, die we zullen noemen doopvont
. Laad het lettertype in Kunst
's constructor zoals we deden met de texturen.
Notitie: Het lettertype dat we gebruiken is eigenlijk een afbeelding in plaats van zoiets als een TrueType-lettertypebestand. Beeldgebaseerde lettertypen waren de manier waarop klassieke arcadespellen en consoles tekst op het scherm drukten, en zelfs nu nog gebruiken sommige games van de huidige generatie de techniek nog steeds. Een voordeel dat we hieruit halen, is dat we uiteindelijk dezelfde technieken gebruiken om tekst op het scherm te tekenen als bij andere sprites.
Wijzig het einde van GameRoot :: onRedrawView ()
waar de cursor wordt getekend, zoals hieronder weergegeven:
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 ()
is een hulpmethode voor het tekenen van tekst die aan de rechterkant van het scherm is uitgelijnd. Voeg het toe aan GameRoot
door de onderstaande code toe te voegen:
#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));
Nu moeten je levens, score en multiplier op het scherm worden weergegeven. We moeten deze waarden echter nog steeds aanpassen in reactie op game-evenementen. Voeg een eigenschap toe met de naam mPointValue
naar de Vijand
klasse.
int Enemy :: getPointValue () return mPointValue;
Stel de puntwaarde voor verschillende vijanden in op iets waarvan u denkt dat het geschikt is. Ik heb de dwalende vijanden een punt waard gemaakt en de zoekende vijanden twee punten waard.
Voeg vervolgens de volgende twee regels toe aan Enemy :: wasShot ()
om de score en multiplier van de speler te verhogen:
PlayerStatus :: getInstance () -> addPoints (mPointValue); PlayerStatus :: getInstance () -> increaseMultiplier ();
telefoontje PlayerStatus :: removeLife ()
in PlayerShip :: kill ()
. Als de speler zijn hele leven verliest, bel dan PlayerStatus :: reset ()
om hun score opnieuw in te stellen en leeft aan het begin van een nieuw spel.
Laten we de mogelijkheid voor het spel toevoegen om je beste score bij te houden. We willen dat deze score blijft behouden voor alle spelen, dus we bewaren het in een bestand. We zullen het heel eenvoudig houden en de hoogste score opslaan als één enkel tekstnummer in een bestand (dit zal in de "Application Support" -directory van de app staan, wat een mooie naam is voor de map "preferences".)
Voeg het volgende toe aan PlayerStatus
:
const std :: string PlayerStatus :: kHighScoreFilename ("highscore.txt"); void CreatePathIfNonExistant2 (const std :: string & newPath) @autoreleasepool // Maak het pad als het niet bestaat NSError * error; [[NSFileManager defaultManager] createDirectoryAtPath: [NSString stringWithUTF8String: newPath.c_str ()] withIntermediateDirectories: YES attributes: nil error: & error];
CreatePathIfNonExistant2 ()
is een functie die ik heb gemaakt en die een map op het iOS-apparaat maakt als deze nog niet bestaat. Omdat ons voorkeurspad in eerste instantie niet bestaat, moeten we het de eerste keer maken.
std :: string GetExecutableName2 () return [[[NSBundle mainBundle] infoDictionary] objectForKey: @ "CFBundleExecutable"] UTF8String];
GetExecutableName2 ()
geeft de naam van het uitvoerbare bestand terug. We gebruiken de naam van de applicatie als onderdeel van het voorkeurspad. We zullen deze functie gebruiken in plaats van de naam van het uitvoerbare bestand hardcoderen, zodat we deze code voor andere toepassingen gewoon kunnen blijven gebruiken.
std :: string GetPreferencePath2 (const std :: string & file) std :: string result = std :: string ([[NSSearchPathForDirectoriesInDomains (NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex: 0] UTF8String]) + "/" + GetExecutableName2 () + "/"; CreatePathIfNonExistant2 (resultaat); resultaat teruggeven + bestand;
GetPreferencePath2 ()
retourneert de volledige tekenreeksversienaam van het voorkeurspad en maakt het pad als dit nog niet bestaat.
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 ()] encoding: NSUTF8StringEncoding error: nul] UTF8String]; if (! fstring.empty ()) sscanf (fstring.c_str (), "% d", & score); terugkeer score; ongeldig PlayerStatus :: SaveHighScore (int score) char buf [20]; sprintf (buf, "% d", score); [[NSString stringWithUTF8String: buf] writeToFile: [NSString stringWithUTF8String: GetPreferencePath2 (kHighScoreFilename) .c_str ()] atomically: YES encoding: NSUTF8StringEncoding error: nil];
De LoadHighScore ()
methode controleert eerst of het bestand met de hoogste score bestaat en retourneert de inhoud van het bestand als een geheel getal. Het is onwaarschijnlijk dat de score ongeldig is, tenzij de gebruiker over het algemeen niet in staat is om bestanden handmatig te wijzigen vanuit iOS, maar als de score uiteindelijk een niet-nummer is, wordt de score uiteindelijk nul.
We willen de hoogste score laden wanneer het spel opstart en opslaan wanneer de speler een nieuwe hoge score krijgt. We zullen de statische constructor en wijzigen reset ()
methoden in PlayerStatus
om dit te doen. We voegen ook een helper-lid toe, mIsGameOver
, die we in een moment zullen gebruiken.
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;
Dat zorgt voor het volgen van de hoge score. Nu moeten we het weergeven. We zullen de volgende code toevoegen aan GameRoot :: onRedrawView ()
in hetzelfde SpriteBatch
blok waar de andere tekst is getekend:
if (PlayerStatus :: getInstance () -> getIsGameOver ()) sprintf (buf, "Game Over \ nUw 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));
Dit zorgt ervoor dat uw score en hoge score op het spel worden weergegeven, gecentreerd op het scherm.
Als laatste aanpassing verhogen we de tijd voordat het schip opnieuw wordt uitgespeeld om de speler de tijd te geven om zijn score te zien. Wijzigen PlayerShip :: kill ()
door de respawn-tijd in te stellen op 300 frames (vijf seconden) als de speler geen levens meer heeft.
void PlayerShip :: kill () PlayerStatus :: getInstance () -> removeLife (); mFramesUntilRespawn = PlayerStatus :: getInstance () -> getIsGameOver ()? 300: 120;
Het spel is nu klaar om te spelen. Het ziet er misschien niet zo uit, maar het heeft alle basismechanismen geïmplementeerd. In toekomstige zelfstudies zullen we deeltjeseffecten en een achtergrondraster toevoegen om het op te fleuren. Maar laten we nu snel wat geluid en muziek toevoegen om het interessanter te maken.
Geluid en muziek afspelen is vrij eenvoudig in iOS. Laten we eerst onze geluidseffecten en muziek toevoegen aan de inhoudspijplijn.
Eerst maken we een statische helperklasse voor de geluiden. Merk op dat het spel is Geluidsbeheer klasse wordt genoemd Geluid
, maar onze Utility bibliotheek geluidsklasse wordt genoemd tSound
.
class Sound: public tSingletonbeschermd: tGeluid * mMusic; std :: vector mExplosions; std :: vector mShots; std :: vector mSpawns; beschermd: Geluid (); public: tSound * getMusic () const; tSound * getExplosion () const; tSound * getShot () const; tSound * getSpawn () const; vriend klas tSingleton ; ; Geluid :: Geluid () char buf [80]; mMusic = new tSound ("music.mp3"); voor (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(n