I den här serien av handledningar visar jag dig hur man gör en Geometry Wars-inspirerad tvillingstickskytte med neongrafik, galen partikeleffekter och fantastisk musik, för iOS med C ++ och OpenGL ES 2.0. Hittills har vi satt upp den grundläggande gameplayen; nu lägger vi till fiender och ett poängsystem.
I denna del bygger vi på den tidigare handledningen genom att lägga till fiender, kollisionsdetektering och poäng.
Här är de nya funktionerna i åtgärd:
Vi lägger till följande nya klasser för att hantera detta:
Fiende
EnemySpawner
: Ansvarig för att skapa fiender och gradvis öka spelets svårigheter.PlayerStatus
: Spårar spelarens poäng, hög poäng och liv.Du kanske har märkt att det finns två typer av fiender i videon, men det finns bara en Enemy-klass. Vi kan härleda underklasser från Enemy för varje fiende typ. Den ursprungliga XNA-versionen av spelet gjorde inte på grund av följande nackdelar:
Däggdjur
och Fågel
, vilka båda härledas från Djur
. De Fågel
klassen har a Flyga()
metod. Då väljer du att lägga till en fladdermus
klass som härrör från Däggdjur
och kan också flyga. För att dela denna funktion med endast arv borde du behöva flytta Flyga()
metod till Djur
klass där den inte hör hemma. Dessutom kan du inte ta bort metoder från härledda klasser, så om du gjorde en Pingvin
klass som härrör från Fågel
, det skulle också behöva ha en Flyga()
metod.För den här handledningen kommer vi att lägga sidan med den ursprungliga XNA-versionen och gynna komposition över arv för att implementera olika typer av fiender. Vi kommer att göra detta genom att skapa olika återanvändbara beteenden som vi kan lägga till fiender. Vi kan då enkelt mixa och matcha beteenden när vi skapar nya typer av fiender. Till exempel, om vi redan hade en FollowPlayer
beteende och a DodgeBullet
beteende, vi kunde göra en ny fiende som gör det bara genom att lägga till båda beteenden.
Fiender kommer att ha några ytterligare egenskaper över enheter. För att ge spelaren lite tid att reagera, kommer vi att göra fiender gradvis blekna innan de blir aktiva och farliga.
Låt oss koda grundstrukturen för Fiende
klass:
klass fiende: offentlig enhet offentlig: enum beteende kFollow = 0, kMoveRandom,; skyddad: std :: listamBehaviors; float mRandomDirection; int mRandomState; int mPointValue; int mTimeUntilStart; skyddad: Annuller AddBehaviour (Beteende b); ogiltiga ApplyBehaviours (); allmänhet: Enemy (tTexture * image, const tVector2f & position); tomt uppdatering (); bool getIsActive (); int getPointValue (); statisk Enemy * createSeeker (const tVector2f & position); statisk Enemy * createWanderer (const tVector2f & position); tomt handtagKollision (Enemy * Other); void wasShot (); bool followPlayer (float acceleration); bool moveRandomly (); ; Enemy :: Enemy (tTexture * bild, const tVector2f & position): mPointValue (1), mTimeUntilStart (60) mImage = image; mPosition = position; mRadius = bild-> getSurfaceSize (). bredd / 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 (). bredd / 2.0f), tMath :: clamp (mPosition.y, getSize () .höjd / 2.0f, GameRoot :: getInstance () -> getViewportSize () .Höjd - getSize ) .höjd / 2.0f)); mVelocity * = 0,8f; void Enemy :: wasShot () mIsExpired = true; PlayerStatus :: getInstance () -> addPoints (mPointValue); PlayerStatus :: getInstance () -> increaseMultiplier (); tSound * temp = Ljud :: getInstance () -> getExplosion (); om (! temp-> isPlaying ()) temp-> play (0, 1);
Den här koden kommer att göra fiender blekna i 60 ramar och tillåter deras hastighet att fungera. Multiplicera hastigheten med 0,8 försvinner en friktionsliknande effekt. Om vi gör fiender accelererar i konstant takt, kommer denna friktion att få dem att smidigt nå en maximal hastighet. Enkelheten och smidigheten hos denna typ av friktion är bra, men du kanske vill använda en annan formel beroende på vilken effekt du vill ha.
De blev skjuten()
Metoden kommer att kallas när fienden blir skjuten. Vi lägger till mer till det senare i serien.
Vi vill att olika typer av fiender ska agera annorlunda. Vi kommer att uppnå detta genom att tilldela beteenden. Ett beteende kommer att använda någon anpassad funktion som kör varje ram för att styra fienden.
Den ursprungliga XNA-versionen av Shape Blaster använde en speciell språkfunktion från C # för att automatisera beteenden. Utan att gå in på för mycket detaljer (eftersom vi inte kommer att använda dem) var slutresultatet att C # runtime skulle kalla beteendemetoderna varje ram utan att uttryckligen säga det.
Eftersom denna språkfunktion inte existerar i C eller C ++, måste vi uttryckligen kalla själva beteenden. Trots att detta kräver lite mer kod är sidofördelen vi vet exakt när våra beteenden uppdateras och ger oss finare kontroll.
Vårt enklaste beteende kommer att vara followPlayer ()
beteende som visas nedan:
bool Enemy :: followPlayer (float acceleration) if (! PlayerShip :: getInstance () -> getIsDead ()) tVector2f temp = (PlayerShip :: getInstance () -> getPosition () - mPosition); temp = temp * (acceleration / temp.length ()); mVelocity + = temp; om (mVelocity! = tVector2f (0,0)) mOrientation = atan2f (mVelocity.y, mVelocity.x); returnera sant;
Detta gör att fienden snabbt accelererar mot spelaren i en konstant takt. Friktionen som vi lagt till tidigare kommer att se till att den så småningom toppar ut med en viss maxhastighet (fem pixlar per ram när accelerationen är en enhet, eftersom \ (0,8 \ gånger 5 + 1 = 5 \).
Låt oss lägga till byggnadsställningen som behövs för att göra beteenden fungerande. Fiender måste lagra sina beteenden, så vi lägger till en variabel till Fiende
klass:
std :: listanmBehaviors;
mBehaviors är a std :: listan
innehållande alla aktiva beteenden. Varje ram kommer vi att gå igenom alla beteenden fienden har och ringa beteendefunktionen baserat på beteendestypen. Om beteendemetoden återvänder falsk
, det betyder att beteendet har slutförts, så vi borde ta bort det från listan.
Vi lägger till följande metoder till Enemy-klassen:
Void Enemy :: AddBehaviour (Beteende b) mBehaviors.push_back (b); void Enemy :: ApplyBehaviours () std :: list:: iterator iter, iterNext; iter = mBehaviors.begin (); iterNext = iter; medan (iter! = mBehaviors.end ()) iterNext ++; bool result = false; switch (* iter) fall kFollow: result = followPlayer (0.9f); ha sönder; fallet kMoveRandom: result = moveRandomly (); ha sönder; om (! resultat) mBehaviors.erase (iter); iter = iterNext;
Och vi ändrar uppdatering()
metod att ringa ApplyBehaviours ()
:
om (mTimeUntilStart <= 0) ApplyBehaviours();
Nu kan vi skapa en statisk metod för att skapa sökande fiender. Allt vi behöver göra är att välja den bild vi vill ha och lägga till followPlayer ()
beteende:
Enemy * Enemy :: createSeeker (const tVector2f & position) Enemy * fiende = ny Enemy (Art :: getInstance () -> getSeeker (), position); enemy-> AddBehaviour (kFollow); fiende-> mPointValue = 2; återvända fiende;
För att göra en fiende som rör sig slumpmässigt, får vi den välja en riktning och sedan göra små slumpmässiga justeringar i den riktningen. Om vi justerar riktningen varje ram, blir rörelsen jitterig, så vi justerar just riktningen periodiskt. Om fienden går in i kanten av skärmen, får vi välja en ny slumpmässig riktning som pekar bort från väggen.
bool Enemy :: moveRandomly () om (mRandomState == 0) mRandomDirection + = tMath :: slumpmässig () * 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 (). bredd / 2.0f - 1.0f; bounds.location.y - = -mImage-> getSurfaceSize (). höjd / 2.0f - 1.0f; bounds.size.width + = 2.0f * (-mImage-> getSurfaceSize (). bredd / 2.0f - 1.0f); bounds.size.height + = 2.0f * (-mImage-> getSurfaceSize (). höjd / 2.0f - 1.0f); om (! 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 :: slumpmässig () * tMath :: PI - tMath :: PI / 2.0f; mRandomState = (mRandomState + 1)% 6; återvänd sant;
Vi kan nu göra en fabriksmetod för att skapa vandrande fiender, mycket som vi gjorde för sökanden:
Enemy * Enemy :: createWanderer (const tVector2f & position) Enemy * fiende = ny Enemy (Art :: getInstance () -> getWanderer (), position); fiende-> mRandomDirection = tMath :: slumpmässig () * tMath :: PI * 2.0f; fiende-> mRandomState = 0; enemy-> AddBehaviour (kMoveRandom); återvända fiende;
För kollisionsdetektering modellerar vi spelarens skepp, fienderna och kulorna som cirklar. Cirkulär kollisionsdetektering är bra eftersom det är enkelt, det är snabbt och det ändras inte när objekten roterar. Om du kommer ihåg, Entitet
klassen har en radie och en position (positionen hänvisar till enhetens centrum) -det är allt vi behöver för cirkulär kollisionsdetektering.
Att testa varje enhet mot alla andra enheter som potentiellt kan kollidera kan vara mycket långsam om du har ett stort antal enheter. Det finns många tekniker du kan använda för att påskynda bredfasskollisionsdetektering, som quadtrees, sweep and prune, och BSP-träd. Men för nu kommer vi bara ha några dussin enheter på skärmen åt gången, så vi kommer inte oroa oss för dessa mer komplexa tekniker. Vi kan alltid lägga till dem senare om vi behöver dem.
I Shape Blaster kan inte varje enhet kollidera med alla andra typer av enheter. Kulor och spelarens skepp kan endast kollidera med fiender. Fiender kan också kollidera med andra fiender; Detta kommer att förhindra att de överlappar varandra.
För att hantera dessa olika typer av kollisioner lägger vi till två nya listor till EntityManager
att hålla koll på kulor och fiender. När vi lägger till en enhet i EntityManager
, Vi vill lägga till den i den lämpliga listan, så vi gör en privat addEntity ()
metod för att göra det. Vi kommer också att vara säker på att ta bort alla utgått enheter från alla listor varje ram.
std :: listanmEnemies; std :: listan mBullets; void EntityManager :: addEntity (Entity * entity) mEntities.push_back (entity); switch (entity-> getKind ()) case Enhet :: kBullet: mBullets.push_back ((Bullet *) enhet); ha sönder; Case Entity :: kEnemy: mEnemies.push_back ((Enemy *) enhet); ha sönder; standard: break; // ... // i Uppdatering () för (std :: lista :: iterator iter = mBullets.begin (); iter! = mBullets.end (); iter ++) if ((* iter) -> isExpired ()) delete * iter; * iter = NULL; mBullets.remove (NULL); för (std :: listan :: iterator iter = mEnemies.begin (); iter! = mEnemies.end (); iter ++) if ((* iter) -> isExpired ()) delete * iter; * iter = NULL; mEnemies.remove (NULL);
Byt samtalen till entity.add ()
i EntityManager.add ()
och EntityManager.update ()
med samtal till addEntity ()
.
Låt oss nu lägga till en metod som avgör om två enheter kolliderar:
bool EntityManager :: isColliding (Entitet * a, Entitet * b) floatradie = a-> getRadius () + b-> getRadius (); returnera! a-> isExpired () &&! b-> isExpired () && a-> getPosition (). distanceSquared (b-> getPosition ()) < radius * radius;
För att bestämma om två cirklar överlappar varandra, kontrollera helt enkelt huruvida avståndet mellan dem är mindre än summan av deras radier. Vår metod optimerar detta något genom att kontrollera om kvadraten av avståndet är mindre än kvadraten av summan av radierna. Kom ihåg att det är lite snabbare att beräkna avståndet kvadrerat än det faktiska avståndet.
Olika saker kommer att hända beroende på som två objekt kolliderar. Om två fiender kolliderar, vill vi att de ska driva varandra, Om en kula träffar en fiende, ska kula och fiende båda bli förstörda; Om spelaren berör en fiende ska spelaren dö och nivån bör återställas.
Vi lägger till en handleCollision ()
metod till Fiende
klass för att hantera kollisioner mellan fiender:
void Enemy :: handleCollision (Enemy * other) tVector2f d = mPosition - other-> mPosition; mVelocity + = 10,0f * d / (d.lengthSquared () + 1.0f);
Denna metod kommer att driva den nuvarande fienden bort från den andra fienden. Ju närmare de är desto hårdare kommer det att drivas, eftersom storleken på (d / d.LengthSquared ())
är bara en över avståndet.
Därefter behöver vi en metod för att hantera spelarens skepp att bli dödad. När detta händer, kommer spelarens skepp att försvinna en kort stund innan de respekteras.
Vi börjar med att lägga till två nya medlemmar till PlayerShip
:
int mFramesUntilRespawn; bool PlayerShip :: getIsDead () returnera mFramesUntilRespawn> 0;
I början av PlayerShip :: uppdatering ()
, lägg till följande:
om (getIsDead ()) mFramesUntilRespawn--;
Och vi åsidosätter dra()
som visat:
void PlayerShip :: draw (tSpriteBatch * spriteBatch) if (! getIsDead ()) Entity :: draw (spriteBatch);
Slutligen lägger vi till en döda()
metod till PlayerShip
:
void PlayerShip :: kill () mFramesUntilRespawn = 60;
Nu när alla bitar är på plats lägger vi till en metod för EntityManager
som går igenom alla enheter och kontrollerar kollisioner:
void EntityManager :: handleCollisions () för (std :: list:: iterator i = mEnemies.begin (); jag! = mEnemies.end (); jag ++) för (std :: lista :: iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) om (isColliding (* i, * j)) (* i) -> handhavandeCollision (* j); (* J) -> handleCollision (* i); // hantera kollisioner mellan kulor och fiender för (std :: lista :: iterator i = mEnemies.begin (); jag! = mEnemies.end (); jag ++) för (std :: lista :: iterator j = mBullets.begin (); j! = mBullets.end (); j ++) om (isColliding (* i, * j)) (* i) -> wasShot (); (* J) -> setExpired (); // hantera kollisioner mellan spelare och fiender för (std :: list :: iterator i = mEnemies.begin (); jag! = mEnemies.end (); i ++) if ((* i) -> getIsActive () && isColliding (PlayerShip :: getInstance (), * i)) PlayerShip :: getInstance () -> kill (); för (std :: lista :: iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) (* j) -> wasShot (); EnemySpawner :: getInstance () -> återställ (); ha sönder;
Ring denna metod från uppdatering()
omedelbart efter inställningen mIsUpdating
till Sann
.
Det sista att göra är att göra EnemySpawner
klass, som är ansvarig för att skapa fiender. Vi vill att spelet ska börja lätt och bli svårare, så EnemySpawner
kommer att skapa fiender i en ökande takt som tiden fortskrider. När spelaren dör kommer vi att återställa EnemySpawner
till sin första svårighet.
klass EnemySpawner: public tSingletonskyddad: float mInverseSpawnChance; skyddad: tVector2f GetSpawnPosition (); skyddad: EnemySpawner (); allmänhet: tomt uppdatering (); void reset (); vän klass tSingleton ; ; void EnemySpawner :: uppdatering () if (! PlayerShip :: getInstance () -> getIsDead () && EntityManager :: getInstance () -> getCount () < 200) if (int32_t(tMath::random() * mInverseSpawnChance) == 0) EntityManager::getInstance()->lägg (Enemy :: createSeeker (GetSpawnPosition ())); om (int32_t (tMath :: random () * mInverseSpawnChance) == 0) EntityManager :: getInstance () -> add (Enemy :: createWanderer (GetSpawnPosition ())); om (mInverseSpawnChance> 30) mInverseSpawnChance - = 0.005f; tVector2f EnemySpawner :: GetSpawnPosition () tVector2f pos; gör pos = tVector2f (tMath :: random () * GameRoot :: getInstance () -> getViewportSize (). bredd, tMath :: slumpmässig () * GameRoot :: getInstance () -> getViewportSize (). medan (pos.distanceSquared (PlayerShip :: getInstance () -> getPosition ()) < 250 * 250); return pos; void EnemySpawner::reset() mInverseSpawnChance = 90;
Varje ram, det finns en in mInverseSpawnChance
att generera varje typ av fiende. Möjligheten att gyta en fiende ökar gradvis tills den når högst en på tjugo. Fiender skapas alltid minst 250 pixlar bort från spelaren.
Var försiktig med medan
loop in GetSpawnPosition ()
. Det kommer att fungera effektivt så länge som området där fiender kan gräva är större än det område där de inte kan kasta. Men om du gör det förbudade området för stort får du en oändlig slinga.
Ring upp EnemySpawner :: uppdatering ()
från GameRoot :: onRedrawView ()
och ringa EnemySpawner :: reset ()
när spelaren dödas.
För att hantera allt detta kommer vi att göra en statisk klass som heter PlayerStatus
:
klass PlayerStatus: public tSingletonprotected: static const float kMultiplierExpiryTime; statisk const int kMaxMultiplierare; statisk const std :: sträng kHighScoreFilename; float mMultiplierTimeLeft; int mlives; int mScore; int mHighScore; intmultiplikator; int mScoreForExtraLife; uint32_t mLastTime; skyddad: int LoadHighScore (); void SaveHighScore (int poäng); skyddad: PlayerStatus (); allmänhet: void reset (); tomt uppdatering (); annuller addPoints (int basPoäng); void increaseMultiplier (); void resetMultiplier (); void removeLife (); int getLives () const; int getScore () const; int getHighScore () const; int getMultiplier () const; bool getIsGameOver () const; vän klass tSingleton ; ; PlayerStatus :: PlayerStatus () mScore = 0; mHighScore = LoadHighScore (); återställa(); mLastTime = tTimer :: getTimeMS (); void PlayerStatus :: återställ () om (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; om (mMultiplierTimeLeft <= 0) mMultiplierTimeLeft = kMultiplierExpiryTime; resetMultiplier(); mLastTime = tTimer::getTimeMS(); void PlayerStatus::addPoints(int basePoints) if (!PlayerShip::getInstance()->getIsDead ()) mScore + = basPoäng * mMultiplier; medan (mScore> = mScoreForExtraLife) mScoreForExtraLife + = 2000; mLives ++; void PlayerStatus :: increaseMultiplier () if (! PlayerShip :: getInstance () -> getIsDead ()) mMultiplierTimeLeft = kMultiplierExpiryTime; om (mM multiplikator < kMaxMultiplier) mMultiplier++; void PlayerStatus::resetMultiplier() mMultiplier = 1; void PlayerStatus::removeLife() mLives--;
Ring upp PlayerStatus :: uppdatering ()
från GameRoot :: onRedrawView ()
när spelet inte är pausat.
Därefter vill vi visa din poäng, liv och multiplikator på skärmen. För att göra detta måste vi lägga till en tSpriteFont
i Innehåll
projekt och en motsvarande variabel i Konst
klass, som vi kommer att namnge Font
. Ladda in teckensnittet i Konst
konstruktör som vi gjorde med texturerna.
Notera: Stilsorten vi använder är faktiskt en bild snarare än något som en TrueType-fontfil. Bildbaserade teckensnitt var hur klassiska arkadspel och konsoler tryckte text på skärmen, och även nu använder vissa nuvarande generationsspel fortfarande tekniken. En fördel vi får av detta är att vi slutar använda samma tekniker för att rita text på skärmen som vi gör andra sprites.
Ändra slutet av GameRoot :: onRedrawView ()
där markören dras, som visas nedan:
char buf [80]; sprintf (buf, "Liv:% 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, "Poäng:% d", PlayerStatus :: getInstance () -> getScore ()); DrawRightAlignedString (buf, 5); sprintf (buf, "Multiplikator:% d", PlayerStatus :: getInstance () -> getMultiplier ()); DrawRightAlignedString (buf, 35); mSpriteBatch-> draw (0, Art :: getInstance () -> getPointer (), Input :: getInstance () -> getMousePosition (), tOptional());
DrawRightAlignedString ()
är en hjälpmetod för att rita textjusterad på höger sida av skärmen. Lägg till den till GameRoot
genom att lägga till koden nedan:
#define kScale 3.0f void GameRoot :: DrawRightAlignedString (const std :: sträng & 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));
Nu ska dina liv, poäng och multiplikator visas på skärmen. Men vi behöver ändå ändra dessa värden som svar på spelhändelser. Lägg till en egendom som heter mPointValue
till Fiende
klass.
int Enemy :: getPointValue () returnera mPointValue;
Ställ in poängvärdet för olika fiender till något du tycker är lämpligt. Jag har gjort de vandrande fienderna värda en poäng och de sökande fiender värda två poäng.
Lägg sedan till följande två rader till Fiende :: wasShot ()
för att öka spelarens poäng och multiplikator:
PlayerStatus :: getInstance () -> addPoints (mPointValue); PlayerStatus :: getInstance () -> increaseMultiplier ();
Ring upp PlayerStatus :: removeLife ()
i PlayerShip :: kill ()
. Om spelaren förlorar alla sina liv, ring PlayerStatus :: reset ()
att återställa sin poäng och bor i början av ett nytt spel.
Låt oss lägga till möjligheten för spelet att spåra dina bästa poäng. Vi vill att den här poängen ska fortsätta över spel så vi sparar det till en fil. Vi kommer att hålla det väldigt enkelt och spara högsta poängen som ett enda vanligt textnummer i en fil (detta kommer att finnas i App: s "Application Support" -katalog, vilket är ett fint namn för katalogen "Preferences".)
Lägg till följande till PlayerStatus
:
const std :: string PlayerStatus :: kHighScoreFilename ("highscore.txt"); void CreatePathIfNonExistant2 (const std :: string & newPath) @autoreleasepool // Skapa sökvägen om den inte finns NSError * error; [[NSFileManager defaultManager] createDirectoryAtPath: [NSString stringWithUTF8String: newPath.c_str ()] withIntermediateDirectories: JA attribut: null fel: & error];
CreatePathIfNonExistant2 ()
är en funktion som jag har skapat som kommer att skapa en katalog på iOS-enheten om den inte redan existerar. Eftersom vår preferensväg inte kommer att finnas i början måste vi skapa den första gången.
std :: sträng GetExecutableName2 () return [[[[NSBundle mainBundle] infoDictionary] objectForKey: @ "CFBundleExecutable"] UTF8String];
GetExecutableName2 ()
returnerar namnet på den körbara filen. Vi använder namnet på programmet som en del av preferensvägen. Vi använder den här funktionen istället för att koda namnet på den körbara, så att vi bara kan återanvända den här koden för andra program oförändrade.
std :: string GetPreferencePath2 (const std :: sträng och fil) std :: strängresultat = std :: sträng ([[NSSearchPathForDirectoriesInDomains (NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex: 0] UTF8String]) + "/" + GetExecutableName2 + "/"; CreatePathIfNonExistant2 (resultat); returresultat + fil;
GetPreferencePath2 ()
returnerar namnet på den fullständiga strängversionen av preferensvägen och skapar sökvägen om den inte existerar redan.
int PlayerStatus :: LoadHighScore () int poäng = 0; std :: sträng fstring; om [[NSFileManager defaultManager] fileExistsAtPath: [NSString stringWithUTF8String: GetPreferencePath2 (kHighScoreFilename) .c_str ()]]) fstring = [[NSString stringWithContentsOfFile: [NSString stringWithUTF8String: GetPreferencePath2 (kHighScoreFilename) .c_str ()] kodning: NSUTF8StringEncoding error: noll] UTF8String]; om (! fstring.empty ()) sscanf (fstring.c_str (), "% d" och scoring); returneringsresultat; void PlayerStatus :: SaveHighScore (int poäng) char buf [20]; sprintf (buf, "% d", poäng); [[NSString stringWithUTF8String: buf] writeToFile: [NSString stringWithUTF8String: GetPreferencePath2 (kHighScoreFilename) .c_str ()] atomiskt: JA kodning: NSUTF8StringEncoding error: nil];
De LoadHighScore ()
Metoden kontrollerar först att högresultatfilen existerar, och returnerar sedan vad som finns i filen som ett heltal. Det är osannolikt att poängen kommer att vara ogiltig såvida inte användaren i allmänhet inte kan ändra filer manuellt från inom IOS, men om det blir ett icke-nummer, kommer poängen bara att bli noll.
Vi vill ladda hög poäng när spelet startar och spara det när spelaren får ett nytt högt poäng. Vi modifierar den statiska konstruktören och återställa()
metoder i PlayerStatus
att göra så. Vi lägger också till en hjälparmedlem, mIsGameOver
, som vi ska använda på ett ögonblick.
bool PlayerStatus :: getIsGameOver () const return mLives == 0; PlayerStatus :: PlayerStatus () mScore = 0; mHighScore = LoadHighScore (); återställa(); mLastTime = tTimer :: getTimeMS (); void PlayerStatus :: återställ () om (mScore> mHighScore) mHighScore = mScore; SaveHighScore (mHighScore); mScore = 0; mMultiplier = 1; mlives = 4; mScoreForExtraLife = 2000; mMultiplierTimeLeft = 0;
Det tar hand om att spåra högsta poängen. Nu behöver vi visa den. Vi lägger till följande kod till GameRoot :: onRedrawView ()
i samma SpriteBatch
blockera där den andra texten är ritad:
if (PlayerStatus :: getInstance () -> getIsGameOver ()) sprintf (buf, "Spel över \ nDin poäng:% 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));
Detta kommer att göra det visa din poäng och hög poäng på spelet över, centrerad på skärmen.
Som en sista justering ökar vi tiden innan skeppet respekterar spelet över för att ge spelaren tid att se sin poäng. Ändra PlayerShip :: kill ()
genom att ställa in responstid till 300 bildrutor (fem sekunder) om spelaren är borta.
void PlayerShip :: kill () PlayerStatus :: getInstance () -> removeLife (); mFramesUntilRespawn = PlayerStatus :: getInstance () -> getIsGameOver ()? 300: 120;
Spelet är nu klart att spela. Det kanske inte ser ut som mycket, men det har alla grundläggande mekanismer implementerats. I framtida handledning kommer vi att lägga till partikeleffekter och ett bakgrundsnät för att krydda det. Men just nu, låt oss snabbt lägga till lite ljud och musik för att göra det mer intressant.
Att spela ljud och musik är ganska enkelt i IOS. Låt oss först lägga till våra ljudeffekter och musik till innehållsrörledningen.
Först gör vi en statisk hjälparklass för ljuden. Observera att spelet är Ljudhantering klass kallas Ljud
, men vår Verktygsbibliotekets ljudklass kallas tSound
.
klass Ljud: public tSingletonskyddad: tSound * mMusic; std :: vektor mExplosions; std :: vektor mShots; std :: vektor mSpawns; skyddad: Ljud (); allmänhet: tSound * getMusic () const; tSound * getExplosion () const; tSound * getShot () const; tSound * getSpawn () const; vän klass tSingleton ; ; Ljud :: Ljud () char buf [80]; mMusic = ny tSound ("music.mp3"); för (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));
Eftersom vi har flera varianter av varje ljud, är Explosion
, Skott
, och Rom
egenskaper kommer att välja ett ljud slumpmässigt bland varianterna.
Ring upp Ljud
konstruktör i GameRoot :: onInitView ()
. För att spela upp musiken lägger du till följande rad i slutet av GameRoot :: onInitView ()
.
Ljud :: getInstance () -> getMusic () -> spela (0, (uint32_t) -1);
För att spela in ljud kan du helt enkelt ringa spela()
metod på a Ljudeffekt
. Denna metod ger också en överbelastning som gör att du kan starta ljudet efter en viss tid och justera hur många gånger ljudet löser. Vi ska använda -1
för musiken som den motsvarar "looping forever."
För att utlösa ljudeffekten för fotografering, lägg till följande rad PlayerShip :: uppdatering ()
, inuti om
uttalande där kulorna skapas:
tSound * curShot = Ljud :: getInstance () -> getShot (); om (! curShot-> isPlaying ()) curShot-> play (0, 1);
På samma sätt utlösa en explosionsljudseffekt varje gång en fiende förstörs genom att lägga till följande Fiende :: wasShot ()
:
tSound * temp = Ljud :: getInstance () -> getExplosion (); om (! temp-> isPlaying ()) temp-> play (0, 1);
Du har nu ljud och musik i ditt spe