I denne serien av opplæringsprogrammer viser jeg deg hvordan du lager en Geometry Wars-inspirert tvillingpokerskytespill, med neongrafik, galne partikkeleffekter og fantastisk musikk, for iOS med C ++ og OpenGL ES 2.0. Så langt har vi satt opp det grunnleggende gameplayet; nå legger vi til fiender og et scoring system.
I denne delen vil vi bygge videre på den forrige opplæringen ved å legge til fiender, kollisjonsdeteksjon og scoring.
Her er de nye funksjonene i aksjon:
Vi vil legge til følgende nye klasser for å håndtere dette:
Fiende
EnemySpawner
: Ansvarlig for å skape fiender og gradvis øke spillets vanskeligheter.PlayerStatus
: Sporer spillernes poengsum, høy poengsum og liv.Du har kanskje lagt merke til at det finnes to typer fiender i videoen, men det er bare én fiendtlig klasse. Vi kunne utlede underklasser fra Enemy for hver fiendtlig type. Den originale XNA-versjonen av spillet gjorde det ikke på grunn av følgende ulemper:
Pattedyr
og Fugl
, som begge kommer fra Dyr
. De Fugl
klassen har a Fly()
metode. Da bestemmer du å legge til en Flaggermus
klasse som kommer fra Pattedyr
og kan også fly. For å dele denne funksjonaliteten bare ved bruk av arv, må du flytte Fly()
metode til Dyr
klassen der den ikke tilhører. I tillegg kan du ikke fjerne metoder fra avledede klasser, så hvis du gjorde en Pingvin
klasse som er avledet fra Fugl
, det ville også måtte ha a Fly()
metode.For denne opplæringen vil vi side med den originale XNA-versjonen og favorisere komposisjon over arv for å implementere de forskjellige typer fiender. Vi vil gjøre dette ved å lage ulike gjenbrukbare atferd at vi kan legge til fiender. Vi kan så enkelt blande og matche atferd når vi lager nye typer fiender. For eksempel, hvis vi allerede hadde en FollowPlayer
oppførsel og a DodgeBullet
atferd, vi kunne lage en ny fiende som gjør begge bare ved å legge til begge deler.
Fiender vil ha noen ekstra egenskaper over enheter. For å gi spilleren litt tid til å reagere, vil vi få fiender til å forsvinne gradvis før de blir aktive og farlige.
La oss kode den grunnleggende strukturen til Fiende
klasse:
klasse Enemy: offentlig enhet public: enum Behavior kFollow = 0, kMoveRandom,; beskyttet: std :: listemBehaviors; float mRandomDirection; int mRandomState; int mPointValue; int mTimeUntilStart; beskyttet: ugyldig AddBehaviour (Behavior b); ugyldig ApplyBehaviours (); offentlig: Enemy (tTexture * image, const tVector2f & posisjon); ugyldig oppdatering (); bool getIsActive (); int getPointValue (); statisk Enemy * createSeeker (const tVector2f & posisjon); statisk Enemy * createWanderer (const tVector2f & posisjon); void handleCollision (Enemy * andre); void wasShot (); bool followPlayer (float akselerasjon); bool moveRandomly (); ; Enemy :: Enemy (tTexture * bilde, const tVector2f & posisjon): mPointValue (1), mTimeUntilStart (60) mImage = image; mPosition = posisjon; mRadius = image-> getSurfaceSize (). bredde / 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 (). bredde / getSize () .bredde / 2.0f), tMath :: klemme (mPosition.y, getSize (). høyde / 2.0f, GameRoot :: getInstance () -> getViewportSize (). høyde - getSize ) .height / 2.0f)); mVelocity * = 0.8f; void Enemy :: wasShot () mIsExpired = true; PlayerStatus :: getInstance () -> addPoints (mPointValue); PlayerStatus :: getInstance () -> increaseMultiplier (); tSound * temp = Lyd :: getInstance () -> getExplosion (); hvis (! temp-> isPlaying ()) temp-> play (0, 1);
Denne koden vil gjøre fiender fade inn i 60 rammer og vil tillate at hastigheten deres fungerer. Multiplikasjon av hastigheten med 0,8 gir en friksjonslignende effekt. Hvis vi får fiender til å akselerere i konstant hastighet, vil denne friksjonen føre til at de jevnt nærmer seg en maksimal hastighet. Enkelheten og glattheten til denne typen friksjon er fin, men du vil kanskje bruke en annen formel, avhengig av hvilken effekt du vil ha.
De ble skutt()
Metoden vil bli kalt når fienden blir skutt. Vi legger til mer på det senere i serien.
Vi ønsker forskjellige typer fiender å oppføre seg annerledes; Vi vil oppnå dette ved å tildele atferd. En oppførsel vil bruke noen tilpasset funksjon som kjører hver ramme for å kontrollere fienden.
Den originale XNA-versjonen av Shape Blaster brukte en spesialspråksfunksjon fra C # for å automatisere atferdene. Uten å gå inn i for mye detalj (siden vi ikke bruker dem), var sluttresultatet at C # -kjøretiden ville kalle oppførselsmetodene hver ramme uten å måtte si det eksplisitt.
Siden denne språkfunksjonen ikke finnes i enten C eller C ++, må vi eksplisitt kalle oppføringene selv. Selv om dette krever litt mer kode, er det en fordel at vi vet nøyaktig når vår oppførsel er oppdatert og dermed gir oss finere kontroll.
Vår enkleste oppførsel vil være followPlayer ()
oppførsel som er vist nedenfor:
bool Enemy :: followPlayer (float acceleration) if (! PlayerShip :: getInstance () -> getIsDead ()) tVector2f temp = (PlayerShip :: getInstance () -> getPosition () - mPosition); temp = temp * (akselerasjon / temp.length ()); mVelocity + = temp; hvis (mVelocity! = tVector2f (0,0)) mOrientation = atan2f (mVelocity.y, mVelocity.x); returnere sann;
Dette gjør bare fienden akselerere mot spilleren med en konstant hastighet. Friksjonen vi la til tidligere, sørger for at den til slutt øker med litt maks hastighet (fem piksler per ramme når akselerasjonen er en enhet, siden \ (0,8 \ ganger 5 + 1 = 5 \).
La oss legge til stillasene som trengs for å gjøre atferdene fungerer. Fiender må lagre deres oppførsel, så vi vil legge til en variabel til Fiende
klasse:
std :: listemBehaviors;
mBehaviors er a std :: liste
som inneholder alle aktive oppføringer. Hver ramme vil vi gjennomgå alle oppføringene fienden har, og ring oppføringsfunksjonen basert på oppførselstypen. Hvis oppførselsmetoden returnerer falsk
, Det betyr at oppførselen er fullført, så vi bør fjerne den fra listen.
Vi legger til følgende metoder for Fiendtlig-klassen:
void Enemy :: AddBehaviour (Oppførsel b) mBehaviors.push_back (b); void Enemy :: ApplyBehaviours () std :: list:: iterator iter, iterNext; iter = mBehaviors.begin (); iterNext = iter; mens (iter! = mBehaviors.end ()) iterNext ++; bool resultat = false; bytt (* iter) case kFollow: result = followPlayer (0.9f); gå i stykker; tilfelle kMoveRandom: result = moveRandomly (); gå i stykker; hvis (! resultat) mBehaviors.erase (iter); iter = iterNext;
Og vi vil endre Oppdater()
metode for å ringe ApplyBehaviours ()
:
hvis (mTimeUntilStart <= 0) ApplyBehaviours();
Nå kan vi lage en statisk metode for å skape søker fiender. Alt vi trenger å gjøre er å velge bildet vi ønsker og legge til followPlayer ()
oppførsel:
Enemy * Enemy :: createSeeker (const tVector2f & posisjon) Enemy * enemy = new Enemy (Art :: getInstance () -> getSeeker (), posisjon); fiende> AddBehaviour (kFollow); fiende-> mPointValue = 2; returnere fiende;
For å lage en fiende som beveger seg tilfeldig, vil vi få den til å velge en retning og deretter lage små tilfeldige justeringer i den retningen. Men hvis vi justerer retningen hver ramme, vil bevegelsen være jitteraktig, så vi justerer kun retningen med jevne mellomrom. Hvis fienden løper inn i kanten av skjermen, vil vi få den til å velge en ny tilfeldig retning som peker bort fra veggen.
bool Enemy :: moveRandomly () hvis (mRandomState == 0) mRandomDirection + = tMath :: tilfeldig () * 0.2f - 0.1f; mVelocity + = 0.4f * tVector2f (cosf (mRandomDirection), sinf (mRandomDirection)); mOrientasjon - = 0,05f; tRectf bounds = tRectf (0,0, GameRoot :: getInstance () -> getViewportSize ()); bounds.location.x - = -mImage-> getSurfaceSize (). bredde / 2.0f - 1.0f; bounds.location.y - = -mImage-> getSurfaceSize (). høyde / 2.0f - 1.0f; bounds.size.width + = 2.0f * (-mImage-> getSurfaceSize (). bredde / 2.0f - 1.0f); bounds.size.height + = 2.0f * (-mImage-> getSurfaceSize (). høyde / 2.0f - 1.0f); hvis (! 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 :: tilfeldig () * tMath :: PI - tMath :: PI / 2.0f; mRandomState = (mRandomState + 1)% 6; returnere sant;
Vi kan nå lage en fabrikkmetode for å lage vandrende fiender, mye som vi gjorde for søkeren:
Enemy * Enemy :: createWanderer (const tVector2f & posisjon) Enemy * enemy = new Enemy (Art :: getInstance () -> getWanderer (), posisjon); fiende-> mRandomDirection = tMath :: tilfeldig () * tMath :: PI * 2.0f; fiende-> mRandomState = 0; fiende> AddBehaviour (kMoveRandom); returnere fiende;
For kollisjonsdeteksjon modellerer vi spillerens skip, fiender og kuler som sirkler. Sirkulær kollisjonsdeteksjon er fin fordi det er enkelt, det er raskt, og det endres ikke når objektene roterer. Hvis du husker, Entity
klassen har en radius og en posisjon (posisjonen refererer til sentrum av enheten) -det er alt vi trenger for sirkulær kollisjon gjenkjenning.
Testing av hver enhet mot alle andre enheter som potensielt kan kollidere kan være veldig sakte hvis du har et stort antall enheter. Det er mange teknikker du kan bruke til å øke hastigheten på bråkfasekollisjonsdeteksjon, som quadtrees, feie og beskjære og BSP-trær. Men for nå vil vi bare ha et par dusin enheter på skjermen av gangen, så vi vil ikke bekymre deg for disse mer komplekse teknikkene. Vi kan alltid legge dem til senere hvis vi trenger dem.
I Shape Blaster kan ikke alle enheter kollidere med alle andre typer enheter. Kuler og spillerens skip kan bare kollidere med fiender. Fiender kan også kollidere med andre fiender; Dette vil forhindre at de overlapper.
For å håndtere disse forskjellige typer kollisjoner, legger vi til to nye lister til EntityManager
å holde styr på kuler og fiender. Når vi legger til en enhet i EntityManager
, Vi vil legge det til den aktuelle listen, så vi skal lage en privat addEntity ()
metode for å gjøre det. Vi vil også være sikker på å fjerne utgåtte enheter fra alle listene hver ramme.
std :: listemEnemies; std :: liste mBullets; void EntityManager :: addEntity (Entity * entity) mEntities.push_back (enhet); bytte (entity-> getKind ()) case Entity :: kBullet: mBullets.push_back ((Bullet *) enhet); gå i stykker; case Entity :: kEnemy: mEnemies.push_back ((Enemy *) enhet); gå i stykker; standard: break; // ... // i Update () for (std :: list :: iterator iter = mBullets.begin (); iter! = mBullets.end (); iter ++) if ((* iter) -> isExpired ()) delete * iter; * iter = NULL; mBullets.remove (NULL); for (std :: liste :: iterator iter = mEnemies.begin (); iter! = mEnemies.end (); iter ++) if ((* iter) -> isExpired ()) delete * iter; * iter = NULL; mEnemies.remove (NULL);
Bytt anrop til entity.add ()
i EntityManager.add ()
og EntityManager.update ()
med anrop til addEntity ()
.
La oss nå legge til en metode som vil avgjøre om to enheter kolliderer:
bool EntityManager :: isColliding (Entitet * a, Entitet * b) float radius = a-> getRadius () + b-> getRadius (); returnere! a-> isExpired () &&! b-> isExpired () && a-> getPosition (). distanceSquared (b-> getPosition ()) < radius * radius;
For å finne ut om to sirkler overlapper, må du bare sjekke om avstanden mellom dem er mindre enn summen av deres radius. Metoden optimaliserer dette litt ved å sjekke om kvadratet av avstanden er mindre enn kvadratet av summen av radien. Husk at det er litt raskere å beregne avstanden som er kvadret enn den faktiske avstanden.
Ulike ting vil skje avhengig av hvilken to gjenstander kolliderer. Hvis to fiender kolliderer, vil vi at de skal presse hverandre unna; Hvis en kule treffer en fiende, skal både kula og fiende bli ødelagt; Hvis spilleren berører en fiende, skal spilleren dø og nivået skal nullstilles.
Vi legger til en handleCollision ()
metode til Fiende
klasse for å håndtere sammenstøt mellom fiender:
void Enemy :: handleCollision (Enemy * andre) tVector2f d = mPosition - other-> mPosition; mVelocity + = 10.0f * d / (d.lengthSquared () + 1.0f);
Denne metoden vil presse den nåværende fienden bort fra den andre fienden. Jo nærmere de er, jo vanskeligere vil det bli presset, fordi størrelsen på (d / d.LengthSquared ())
er bare en over avstanden.
Deretter trenger vi en metode for å håndtere spillerens skip som blir drept. Når dette skjer, forsvinner spillers skip i en kort tid før de respekteres.
Vi starter med å legge to nye medlemmer til PlayerShip
:
int mFramesUntilRespawn; bool PlayerShip :: getIsDead () return mFramesUntilRespawn> 0;
I begynnelsen av PlayerShip :: oppdatering ()
, legg til følgende:
hvis (getIsDead ()) mFramesUntilRespawn--;
Og vi tilsidesetter tegne()
som vist:
void PlayerShip :: draw (tSpriteBatch * spriteBatch) if (! getIsDead ()) Entity :: draw (spriteBatch);
Til slutt legger vi til en drepe()
metode til PlayerShip
:
void PlayerShip :: kill () mFramesUntilRespawn = 60;
Nå som alle stykkene er på plass, legger vi til en metode for EntityManager
som går gjennom alle enhetene og sjekker for kollisjoner:
void EntityManager :: handleCollisions () for (std :: list:: iterator i = mEnemies.begin (); jeg! = mEnemies.end (); jeg ++) for (std :: liste :: iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) hvis (isColliding (* i, * j)) (* i) -> handleCollision (* j); (* J) -> handleCollision (* i); // håndter kollisjoner mellom kuler og fiender for (std :: liste :: iterator i = mEnemies.begin (); jeg! = mEnemies.end (); jeg ++) for (std :: liste :: iterator j = mBullets.begin (); j! = mBullets.end (); j ++) hvis (isColliding (* i, * j)) (* i) -> wasShot (); (* J) -> setExpired (); // håndter kollisjoner mellom spilleren og fiender for (std :: list :: iterator i = mEnemies.begin (); jeg! = mEnemies.end (); i ++) if ((* i) -> getIsActive () && isColliding (PlayerShip :: getInstance (), * i)) SpillerShip :: getInstance () -> kill (); for (std :: liste :: iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) (* j) -> wasShot (); EnemySpawner :: getInstance () -> reset (); gå i stykker;
Ring denne metoden fra Oppdater()
umiddelbart etter innstillingen mIsUpdating
til ekte
.
Den siste tingen å gjøre er å lage EnemySpawner
klassen, som er ansvarlig for å skape fiender. Vi vil at spillet skal begynne enkelt og bli vanskeligere, så EnemySpawner
vil skape fiender i økende grad etter hvert som tiden går. Når spilleren dør, vil vi nullstille EnemySpawner
til den første vanskeligheten.
klassen EnemySpawner: public tSingletonbeskyttet: float mInverseSpawnChance; beskyttet: tVector2f GetSpawnPosition (); beskyttet: EnemySpawner (); offentlig: ugyldig oppdatering (); void reset (); venneklasse tSingleton ; ; void EnemySpawner :: update () if (! PlayerShip :: getInstance () -> getIsDead () && EntityManager :: getInstance () -> getCount () < 200) if (int32_t(tMath::random() * mInverseSpawnChance) == 0) EntityManager::getInstance()->legg (fiende :: createSeeker (GetSpawnPosition ())); hvis (int32_t (tMath :: tilfeldig () * mInverseSpawnChance) == 0) EntityManager :: getInstance () -> add (Enemy :: createWanderer (GetSpawnPosition ())); hvis (mInverseSpawnChance> 30) mInverseSpawnChance - = 0.005f; tVector2f EnemySpawner :: GetSpawnPosition () tVector2f pos; gjør pos = tVector2f (tMath :: random () * GameRoot :: getInstance () -> getViewportSize (). bredde, tMath :: tilfeldig () * GameRoot :: getInstance () -> getViewportSize (). mens (pos.distanceSquared (PlayerShip :: getInstance () -> getPosition ()) < 250 * 250); return pos; void EnemySpawner::reset() mInverseSpawnChance = 90;
Hver ramme, det er en i mInverseSpawnChance
å generere hver type fiende. Sjansen for å gyte en fiende øker gradvis til den når maksimalt en på tjue. Fiender er alltid opprettet minst 250 piksler unna spilleren.
Vær forsiktig med samtidig som
sløyfe inn GetSpawnPosition ()
. Det vil fungere effektivt så lenge området der fiender kan gyte er større enn området der de ikke kan gyte. Men hvis du gjør det forbudte området for stort, vil du få en uendelig sløyfe.
Anrop EnemySpawner :: oppdatering ()
fra GameRoot :: onRedrawView ()
og ring EnemySpawner :: reset ()
når spilleren blir drept.
For å håndtere alt dette, vil vi lage en statisk klasse kalt PlayerStatus
:
klasse PlayerStatus: public tSingletonbeskyttet: statisk const float kMultiplierExpiryTime; statisk const int kMaxMultiplier; statisk const std :: streng kHighScoreFilename; float mMultiplierTimeLeft; int mlives; int mScore; int mHighScore; int mMultiplier; int mScoreForExtraLife; uint32_t mLastTime; beskyttet: int LoadHighScore (); void SaveHighScore (int score); beskyttet: PlayerStatus (); offentlig: ugyldig tilbakestilling (); ugyldig oppdatering (); 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; venneklasse tSingleton ; ; PlayerStatus :: PlayerStatus () mScore = 0; mHighScore = LoadHighScore (); tilbakestille(); 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; hvis (mMultiplierTimeLeft <= 0) mMultiplierTimeLeft = kMultiplierExpiryTime; resetMultiplier(); mLastTime = tTimer::getTimeMS(); void PlayerStatus::addPoints(int basePoints) if (!PlayerShip::getInstance()->getIsDead ()) mScore + = basePoints * mMultiplier; mens (mScore> = mScoreForExtraLife) mScoreForExtraLife + = 2000; mLives ++; void PlayerStatus :: increaseMultiplier () if (! PlayerShip :: getInstance () -> getIsDead ()) mMultiplierTimeLeft = kMultiplierExpiryTime; hvis (mMultiplier < kMaxMultiplier) mMultiplier++; void PlayerStatus::resetMultiplier() mMultiplier = 1; void PlayerStatus::removeLife() mLives--;
Anrop PlayerStatus :: oppdatering ()
fra GameRoot :: onRedrawView ()
når spillet ikke er pauset.
Deretter ønsker vi å vise poeng, liv og multiplikator på skjermen. For å gjøre dette må vi legge til en tSpriteFont
i Innhold
prosjekt og en tilsvarende variabel i Kunst
klassen, som vi vil nevne Font
. Legg inn skrifttypen i Kunst
Konstruktør som vi gjorde med teksturer.
Merk: Skriften vi bruker er faktisk et bilde i stedet for noe som en TrueType-skriftfil. Bildebaserte fonter var hvordan klassiske arkadespill og konsoller trykt tekst på skjermen, og selv nå bruker noen nåværende generasjons spill fortsatt teknikken. En fordel vi får fra dette er at vi ender med å bruke samme teknikker for å tegne tekst på skjermen som vi gjør andre sprites.
Endre slutten av GameRoot :: onRedrawView ()
hvor markøren er trukket, som vist nedenfor:
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, "Score:% 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 ()
er en hjelpemetode for å tegne tekstjustert på høyre side av skjermen. Legg det til GameRoot
ved å legge til koden nedenfor:
#define kScale 3.0f void GameRoot :: DrawRightAlignedString (const std :: streng og 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));
Nå skal dine liv, poeng og multiplikator vises på skjermen. Imidlertid må vi stadig endre disse verdiene som svar på spillbegivenheter. Legg til en eiendom som heter mPointValue
til Fiende
klasse.
int Enemy :: getPointValue () return mPointValue;
Sett poengverdien for forskjellige fiender til noe du føler er passende. Jeg har gjort de vandrende fiender verdt et poeng, og de søker fiender verdt to poeng.
Deretter legger du til følgende to linjer til Enemy :: wasShot ()
for å øke spillerens poengsum og multiplikator:
PlayerStatus :: getInstance () -> addPoints (mPointValue); PlayerStatus :: getInstance () -> increaseMultiplier ();
Anrop PlayerStatus :: removeLife ()
i PlayerShip :: drepe ()
. Hvis spilleren mister hele livet, ring PlayerStatus :: reset ()
å tilbakestille sin poengsum og lever i starten av et nytt spill.
La oss legge til evnen til spillet til å spore din beste poengsum. Vi vil at denne poengsummen skal fortsette over spillene, så vi lagrer den til en fil. Vi vil holde det veldig enkelt og lagre høy poengsum som et enkelt rentekstnummer i en fil (dette kommer til å være i App's "Application Support" -katalog, som er et fancy navn for "preferanser" -katalogen.)
Legg til følgende til PlayerStatus
:
const std :: string PlayerStatus :: kHighScoreFilename ("highscore.txt"); void CreatePathIfNonExistant2 (const std :: string & newPath) @autoreleasepool // Opprett banen hvis den ikke eksisterer NSError * feil; [[NSFileManager defaultManager] createDirectoryAtPath: [NSString stringWithUTF8String: newPath.c_str ()] withIntermediateDirectories: YES attributter: null feil: & feil];
CreatePathIfNonExistant2 ()
er en funksjon jeg har laget som vil opprette en katalog på iOS-enheten hvis den ikke allerede eksisterer. Siden vår preferansebane ikke eksisterer i utgangspunktet, må vi opprette den første gangen.
std :: string GetExecutableName2 () return [[[[NSBundle mainBundle] infoDictionary] objectForKey: @ "CFBundleExecutable"] UTF8String];
GetExecutableName2 ()
Returnerer navnet på den kjørbare. Vi bruker navnet på programmet som en del av preferansebanen. Vi bruker denne funksjonen i stedet for hardkoding navnet på den kjørbare, slik at vi bare kan bruke denne koden til andre programmer uendret.
std :: string GetPreferencePath2 (const std :: streng og fil) std :: string result = std :: string ([[NSSearchPathForDirectoriesInDomains (NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex: 0] UTF8String]) "" "" GetExecutableName2 " + "/"; CreatePathIfNonExistant2 (resultat); returresultat + fil;
GetPreferencePath2 ()
returnerer navnet på den fullstendige strengversjonen av preferansebanen, og lager banen hvis den ikke allerede eksisterer.
int PlayerStatus :: LoadHighScore () int score = 0; std :: streng fstring; hvis [[NSFileManager defaultManager] fileExistsAtPath: [NSString stringWithUTF8String: GetPreferencePath2 (kHighScoreFilename) .c_str ()]]) fstring = [[NSString stringWithContentsOfFile: [NSString stringWithUTF8String: GetPreferencePath2 (kHighScoreFilename) .c_str ()] koding: NSUTF8StringEncoding feil: null] UTF8String]; hvis (! fstring.empty ()) sscanf (fstring.c_str (), "% d", og score); returpoeng; void PlayerStatus :: SaveHighScore (int score) char buf [20]; sprintf (buf, "% d", score); [[NSString stringWithUTF8String: buf] writeToFile: [NSString stringWithUTF8String: GetPreferencePath2 (kHighScoreFilename) .c_str ()] atomisk: JA koding: NSUTF8StringEncoding error: null];
De LoadHighScore ()
Metoden kontrollerer først at high score-filen eksisterer, og returnerer deretter det som er i filen som et heltall. Det er usannsynlig at poenget vil være ugyldig, med mindre brukeren vanligvis ikke kan endre filer manuelt fra iOS, men hvis poenget blir et ikke-nummer, vil poenget bare ende opp med å være null.
Vi ønsker å laste høy poengsum når spillet starter, og lagre det når spilleren får en ny høy poengsum. Vi vil endre den statiske konstruktøren og tilbakestille()
metoder i PlayerStatus
å gjøre slik. Vi vil også legge til et hjelpermedlem, mIsGameOver
, som vi skal bruke på et øyeblikk.
bool PlayerStatus :: getIsGameOver () const return mLives == 0; PlayerStatus :: PlayerStatus () mScore = 0; mHighScore = LoadHighScore (); tilbakestille(); mLastTime = tTimer :: getTimeMS (); void PlayerStatus :: reset () if (mScore> mHighScore) mHighScore = mScore; SaveHighScore (mHighScore); mScore = 0; mMultiplier = 1; mlives = 4; mScoreForExtraLife = 2000; mMultiplierTimeLeft = 0;
Det tar seg av å spore høy poengsum. Nå må vi vise den. Vi legger til følgende kode til GameRoot :: onRedrawView ()
i samme SpriteBatch
blokkere hvor den andre teksten er tegnet:
if (PlayerStatus :: getInstance () -> getIsGameOver ()) sprintf (buf, "Spill over \ nDin poeng:% 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));
Dette vil gjøre det vise poengsummen og høy poengsum på spillet over, sentrert på skjermen.
Som en endelig justering øker vi tiden før skipet responwns på spillet over for å gi spilleren tid til å se sin score. endre PlayerShip :: drepe ()
ved å sette responstiden til 300 bilder (fem sekunder) hvis spilleren er ute av livet.
void PlayerShip :: kill () PlayerStatus :: getInstance () -> removeLife (); mFramesUntilRespawn = PlayerStatus :: getInstance () -> getIsGameOver ()? 300: 120;
Spillet er nå klar til å spille. Det kan ikke se ut som mye, men det har alle grunnleggende mekanikkene implementert. I fremtidige opplæringsprogrammer vil vi legge til partikkeleffekter og et bakgrunnsnett for å krydre det opp. Men akkurat nå, la oss raskt legge til litt lyd og musikk for å gjøre det mer interessant.
Å spille lyd og musikk er ganske enkelt i IOS. Først legger vi til lydeffekter og musikk til innholdsrørledningen.
Først lager vi en statisk hjelpeklasse for lydene. Legg merke til at spillet er Lydhåndtering klassen heter Lyd
, men vår Utility biblioteket lydklassen kalles tSound
.
klasse lyd: offentlig tSingletonbeskyttet: tSound * mMusic; std :: vektor mExplosions; std :: vektor mShots; std :: vektor mSpawns; beskyttet: lyd (); offentlig: tSound * getMusic () const; tSound * getExplosion () const; tSound * getShot () const; tSound * getSpawn () const; venneklasse tSingleton ; ; Lyd :: Lyd () char buf [80]; mMusic = ny tSound ("music.mp3"); for (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));
Siden vi har flere varianter av hver lyd, vil Eksplosjon
, Skudd
, og gyte
egenskaper vil velge en lyd tilfeldig blant varianter.
Anrop Lyd
Konstruktør i GameRoot :: onInitView ()
. For å spille av musikken, legg til følgende linje på slutten av GameRoot :: onInitView ()
.
Lyd :: getInstance () -> getMusic () -> spill (0, (uint32_t) -1);
For å spille lyder inn, kan du bare ringe spille()
metode på en Lyd effekt
. Denne metoden gir også en overbelastning som lar deg starte lyden etter en viss tidsperiode, og justere antall ganger lyden slår. Vi skal bruke -1
for musikken som det tilsvarer "looping forever."
For å utløse lydeffekten for opptak, legg til følgende linje inn PlayerShip :: oppdatering ()
, inne i hvis
uttalelse hvor kulene er opprettet:
tSound * curShot = Lyd :: getInstance () -> getShot (); hvis (! curShot-> isPlaying ()) curShot-> play (0, 1);
Likeledes utløse en eksplosjonslyd effekt hver gang en fiende blir ødelagt ved å legge til følgende Enemy :: wasShot ()
:
tSound * temp = Lyd :: getInstance () -> getExplosion (); hvis (! temp-> isPlaying ()) temp-> play (0, 1);
Du har nå lyd og musikk i spillet ditt. Lett, er det ikke?
Det bryter opp den grunnleggende spillmekanikken. I neste opplæring legger vi til en virtuell gam