Lag en Neon Vector Shooter for iOS Første trinn

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..

I stedet for å stole på et eksisterende spillramme eller sprite-bibliotek, forsøker vi å programmere så nær maskinvaren (eller "bare metall") som vi muligens kan. Siden enheter som kjører iOS, kjøres på mindre maskinvare enn en stasjonær PC eller spillkonsoll, vil dette gjøre det mulig for oss å få så mye bang for pengene som mulig.

Relaterte innlegg
Disse opplæringsprogrammene er basert på Michael Hoffmans originale XNA-serie, som har blitt oversatt til andre plattformer:
  • Lag en Neon Vector Shooter i XNA
  • Lag en Neon Vector Shooter i jMonkeyEngine

Målet med disse opplæringsprogrammene er å gå over de nødvendige elementene som gjør at du kan lage ditt eget høykvalitets mobilspill for iOS, enten fra grunnen av eller basert på et eksisterende skrivebordsspill. Jeg oppfordrer deg til å laste ned og spille med koden, eller til og med å bruke den som grunnlag for dine egne prosjekter.

Vi vil dekke følgende emner i denne serien:

  1. Første trinn, introdusering av Utility-biblioteket, oppsett av grunnleggende spill, skape spillerens skip, lyd og musikk.
  2. Fullfør implementeringen av spillmekanikken ved å legge til fiender, håndtere kollisjonsdeteksjon og spore spillernes score og liv.
  3. Legg til en virtuell gamepad på skjermen, slik at vi kan kontrollere spillet ved hjelp av multi-touch-inngang.
  4. Legg til galte, over-the-top partikkel effekter.
  5. Legg til bakgrunnsrammen for krumning.

Her er hva vi får ved slutten av serien:


Advarsel: Høyt!

Og her er hva vi får ved slutten av denne første delen:


Advarsel: Høyt!

Musikken og lydeffekter du kan høre i disse videoene ble laget av RetroModular, og du kan lese om hvordan han gjorde det over på lydavsnittet.

De sprites er av Jacob Zinman-Jeanes, vår bosatt Tuts + designer.

Skriften vi skal bruke, er en bitmap-skrift (med andre ord, ikke en faktisk "skrifttype", men en bildefil), noe jeg har laget for denne opplæringen.

Alt illustrasjonen finnes i kildefilene.

La oss komme i gang.


Oversikt

Før vi dykker inn i spesifikasjonene til spillet, la oss snakke om verktøyet Bibliotek og program Bootstrap-kode jeg har gitt for å støtte utviklingen av spillet vårt.

Utility-biblioteket

Selv om vi primært skal bruke C ++ og OpenGL til å kode vårt spill, trenger vi noen ekstra verktøysklasser. Dette er alle klassene jeg har skrevet for å hjelpe utviklingen i andre prosjekter, så de er testet tid og brukbar for nye prosjekter som denne.

  • package.h: En bekvemmelighetsoverskrift brukes til å inkludere alle relevante overskrifter fra verktøybiblioteket. Vi vil inkludere det ved å si #include "Utility / package.h" uten å måtte inkludere noe annet.

Patterns

Vi vil utnytte noen eksisterende prøvde og sanne programmeringsmønstre som brukes i C ++ og andre språk.

  • tSingleton: Implementerer en singleton klasse ved hjelp av et "Meyers Singleton" mønster. Det er mal basert, og utvidbart, slik at vi kan abonnere all singleton-kode til en enkelt klasse.
  • tOptional: Dette er en funksjon fra C ++ 14 (kalt std :: valgfritt) som ikke er helt tilgjengelig i nåværende versjoner av C ++ ennå (vi er fortsatt på C + + 11). Det er også en funksjon tilgjengelig i XNA og C # (hvor den kalles ha nullverdier.) Det tillater oss å ha "valgfrie" parametere for metoder. Den brukes i tSpriteBatch klasse.

Vector Math

Siden vi ikke bruker et eksisterende spillramme, trenger vi noen klasser for å håndtere matematikken bak kulissene.

  • tMath: En statisk klasse-taht gir noen metoder utover det som er tilgjengelig i C ++, for eksempel konvertering fra grader til radianer eller avrundingstall til makter på to.
  • tVector: Et grunnleggende sett med Vector klasser, som gir 2-element, 3-element og 4-element varianter. Vi skriver også denne strukturen for poeng og farger.
  • tMatrix: To matriksdefinisjoner, en 2x2-variant (for rotasjonsoperasjoner) og et 4x4-alternativ (for projeksjonsmatrisen kreves for å få ting på skjermen),
  • tRect: En rektangelklasse som gir plassering, størrelse og en metode for å bestemme om poeng ligger i rektangler eller ikke.

OpenGL Wrapper Classes

Selv om OpenGL er en kraftig API, er den C-basert, og administrerende objekter kan være noe vanskelig å gjøre i praksis. Så, vi har en liten håndfull klasser for å administrere OpenGL-objektene for oss.

  • tSurface: Gir en måte å lage en bitmap basert på et bilde lastet fra programmets bunt.
  • tTexture: Wraps grensesnittet til OpenGLs tekstkommandoer, og belastninger tSurfaces inn i teksturer.
  • tShader: Wraps grensesnittet til OpenGLs skyggekompiler, noe som gjør det enkelt å kompilere shaders.
  • tProgram: Wraps grensesnittet til OpenGLs shader programgrensesnitt, som egentlig er kombinasjonen av to tShader klasser.

Game Support Classes

Disse klassene representerer det nærmeste vi får til å ha et "spillramme"; De gir noen konsepter på høyt nivå som ikke er typiske for OpenGL, men som er nyttige for spillutviklingsformål.

  • tViewport: Inneholder visningsportens status. Vi bruker dette primært til å håndtere endringer i enhetens orientering.
  • tAutosizeViewport: En klasse som styrer endringer i visningsporten. Den håndterer endringer i enhetsretningen direkte, og skalerer visningsporten slik at den passer på skjermen på enheten, slik at aspektforholdet forblir det samme - noe som betyr at ting ikke blir strukket eller knust.
  • tSpriteFont: Lar oss laste inn en "bitmap font" fra applikasjonspakken, og bruk den til å skrive tekst på skjermen.
  • tSpriteBatch: Inspirert av XNAs SpriteBatch klasse, skrev jeg denne klassen for å inkapslere det beste av det som trengs av spillet vårt. Det tillater oss å sortere sprites når de tegnes på en slik måte at vi får best mulig hastighetsgevinst på maskinvaren vi har. Vi bruker også den direkte til å skrive tekst på skjermen.

Diverse klasser

Et minimalt sett med klasser for å rulle ut ting.

  • tTimer: En systemtimer, som primært brukes til animasjoner.
  • tInputEvent: Definisjoner av grunnleggende klasser for å gi orienteringsendringer (vippe enheten), berøre hendelser og et "virtuelt tastatur" -hendelse for å etterligne en gamepad mer diskret.
  • tSound: En klasse dedikert til å laste inn og spille av lydeffekter og musikk.

Program Bootstrap

Vi trenger også det jeg kaller "Boostrap" -kode, det vil si kode som abstrakterer hvordan et program starter, eller "støveler opp".

Her er hva som er i Bootstrap:

  • AppDelegate: Denne klassen håndterer programlansering, samt suspender og gjenoppta hendelser for når brukeren trykker på Hjem-knappen.
  • ViewController: Denne klassen håndterer hendelser for enhet orientering, og lager vår OpenGL-visning
  • OpenGLView: Denne klassen initialiserer OpenGL, forteller enheten å oppdatere med 60 bilder per sekund, og håndterer berøringshendelser.

Oversikt over spillet

I denne opplæringen vil vi lage en tvillingpinne shooter; spilleren vil styre skipet ved hjelp av multi-touch kontroller på skjermen.

Vi bruker en rekke klasser for å oppnå dette:

  • Entity: Baseklassen for fiender, kuler og spillerens skip. Enheter kan bevege seg og bli tegnet.
  • Kule og PlayerShip.
  • EntityManager: Holder styr på alle enheter i spillet og utfører kollisjonsdeteksjon.
  • Input: Hjelper med å administrere innspill fra berøringsskjermen.
  • Kunst: Laster og inneholder referanser til teksturen som trengs for spillet.
  • Lyd: Laster og holder referanser til lydene og musikken.
  • MathUtil og utvidelser: Inneholder noen nyttige statiske metoder og
    utvidelsesmetoder.
  • GameRoot: Kontrollerer hovedløkken i spillet. Dette er vår hovedklasse.

Koden i denne opplæringen tar sikte på å være enkel og lett å forstå. Det vil ikke ha alle funksjoner designet for å støtte alle mulige behov; heller, det vil bare gjøre hva den trenger å gjøre. Å holde det enkelt gjør det lettere for deg å forstå konseptene, og deretter endre og utvide dem til ditt eget unike spill.


Enheter og spillerens skip

Åpne det eksisterende Xcode-prosjektet. GameRoot er vår søknad hovedklasse.

Vi starter med å lage en grunnklasse for våre spill enheter. Ta en titt på Entitetsklasse:

 klassenhet offentlig: enum Kind kDontCare = 0, kBullet, kEnemy, kBlackHole,; beskyttet: tTexture * mImage; tColor4f mColor; tPoint2f mPosition; tVector2f mVelocity; float mOrientation; flyte mRadius; bool mIsExpired; Vennlig mKind; offentlig: Entitet (); virtuell ~ Entitet (); tDimension2f getSize () const; virtuell ugyldig oppdatering () = 0; virtuell ugyldig tegning (tSpriteBatch * spriteBatch); tPoint2f getPosition () const; tVector2f getVelocity () const; void setVelocity (const tVector2f & nv); flyte getRadius () const; bool isExpired () const; Kind getKind () const; tomt settExpired (); ;

Alle våre enheter (fiender, kuler og spillerens skip) har noen grunnleggende egenskaper, for eksempel et bilde og en posisjon. mIsExpired vil bli brukt til å indikere at enheten har blitt ødelagt og bør fjernes fra noen lister med referanse til den.

Deretter lager vi en EntityManager å spore våre enheter og å oppdatere og tegne dem:

 klassen EntityManager: public tSingleton beskyttet: std :: liste mEntities; std :: liste mAddedEntities; std :: liste mBullets; bool mIsUpdating; beskyttet: EntityManager (); offentlig: int getCount () const; void add (Entity * entity); void addEntity (Entity * entity); ugyldig oppdatering (); void draw (tSpriteBatch * spriteBatch); bool isColliding (Entity * a, Entity * b); venneklasse tSingleton; ; void EntityManager :: legg til (Entity * entity) if (! mIsUpdating) addEntity (entity);  ellers mAddedEntities.push_back (enhet);  tomt EntityManager :: oppdatering () mIsUpdating = true; for (std :: liste:: iterator iter = mEntities.begin (); iter! = mEntities.end (); iter ++) (* iter) -> oppdatering (); hvis ((* iter) -> isExpired ()) * iter = NULL;  mIsUpdating = false; for (std :: liste:: iterator iter = mAddedEntities.begin (); iter! = mAddedEntities.end (); iter ++) addEntity (* iter);  mAddedEntities.clear (); mEntities.remove (NULL); for (std :: liste:: iterator iter = mBullets.begin (); iter! = mBullets.end (); iter ++) if ((* iter) -> isExpired ()) delete * iter; * iter = NULL;  mBullets.remove (NULL);  void EntityManager :: draw (tSpriteBatch * spriteBatch) for (std :: liste:: iterator iter = mEntities.begin (); iter! = mEntities.end (); iter ++) (* iter) -> draw (spriteBatch); 

Husk at hvis du endrer en liste mens den overhører det, får du et runtime unntak. Ovennevnte kode tar vare på dette ved å kjøre opp noen enheter lagt til under oppdatering i en egen liste, og legge dem til etter at det er ferdig med å oppdatere eksisterende enheter.

Gjør dem synlige

Vi må laste inn noen teksturer hvis vi ønsker å tegne noe, så vi skal lage en statisk klasse for å holde referanser til alle våre teksturer:

 klasse kunst: offentlig tsingleton beskyttet: tTexture * mPlayer; tTexture * mSeeker; tTexture * mWanderer; tTexture * mBullet; tTexture * mPointer; beskyttet: kunst (); offentlig: tTexture * getPlayer () const; tTexture * getSeeker () const; tTexture * getWanderer () const; tTexture * getBullet () const; tTexture * getPointer () const; venneklasse tSingleton; ; Art :: Art () mPlayer = new tTexture (tSurface ("player.png")); mSeeker = ny tTexture (tSurface ("seeker.png")); mWanderer = ny tTexture (tSurface ("wanderer.png")); mBullet = ny tTexture (tSurface ("bullet.png")); mPointer = ny tTexture (tSurface ("pointer.png")); 

Vi laster opp kunsten ved å ringe Art :: getInstance () i GameRoot :: onInitView (). Dette fører til Kunst singleton å bli konstruert og å ringe til konstruktøren, Art :: Art ().

Et antall klasser må også kjenne skjermdimensjonene, så vi har følgende medlemmer i GameRoot:

 tDimension2f mViewportSize; tSpriteBatch * mSpriteBatch; tAutosizeViewport * mViewport;

Og i GameRoot konstruktør, setter vi størrelsen:

 GameRoot :: GameRoot (): mViewportSize (800, 600), mSpriteBatch (NULL) 

Oppløsningen 800x600px er hva den originale XNA-baserte Shape Blaster brukte. Vi kan bruke en hvilken som helst oppløsning vi ønsker (som en nærmere en iPhone eller iPads spesifikke oppløsning), men vi holder fast ved den opprinnelige oppløsningen bare for å sikre at vårt spill samsvarer med utseendet på originalen.

Nå går vi over PlayerShip klasse:

 klasse PlayerShip: offentlig enhet, offentlig tSingleton protected: static const int kCooldownFrames; int mCooldowmRemaining; int mFramesUntilRespawn; beskyttet: PlayerShip (); offentlig: ugyldig oppdatering (); void draw (tSpriteBatch * spriteBatch); bool getIsDead (); tomrom (); venneklasse tSingleton; ; SpillerShip :: PlayerShip (): mCooldowmRemaining (0), mFramesUntilRespawn (0) mImage = Art :: getInstance () -> getPlayer (); mPosition = tPoint2f (GameRoot :: getInstance () -> getViewportSize (). x / 2, GameRoot :: getInstance () -> getViewportSize (). y / 2); mRadius = 10; 

Vi lagde PlayerShip en singleton, sette bildet sitt og plasserte det i midten av skjermen.

Til slutt, la oss legge til spillerens skip til EntityManager. Koden i GameRoot :: onInitView ser slik ut:

 // I GameRoot :: onInitView EntityManager :: getInstance () -> legg til (PlayerShip :: getInstance ()); ... glClearColor (0,0,0,1); glenable (GL_BLEND); glBlendFunc (GL_SRC_ALPHA, GL_ONE); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glHint (GL_GENERATE_MIPMAP_HINT, GL_DONT_CARE); glDisable (GL_DEPTH_TEST); glDisable (GL_CULL_FACE);

Vi tegner sprites med additiv blanding, som er en del av det som vil gi dem deres "neon" utseende. Vi vil heller ikke ha noen bluring eller blanding, så vi bruker GL_NEAREST for våre filtre. Vi trenger ikke eller bryr oss om dybdeprøving eller backside culling (det legger bare unødvendig overhead uansett), så vi slår den av.

Koden i GameRoot :: onRedrawView ser slik ut:

 // I GameRoot :: onRedrawView EntityManager :: getInstance () -> oppdatering (); EntityManager :: getInstance () -> draw (mSpriteBatch); mSpriteBatch-> draw (0, Art :: getInstance () -> getPointer (), Input :: getInstance () -> getMousePosition (), tOptional()); mViewport-> run (); glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); mSpriteBatch-> ende (); glFlush ();

Hvis du kjører spillet på dette tidspunktet, bør du se skipet ditt i midten av skjermen. Det svarer imidlertid ikke på innspill. La oss legge til litt innspilling til spillet neste.


Input

For bevegelse bruker vi et multi-touch-grensesnitt. Før vi går i full kraft med skjermspillene, får vi bare et grunnleggende touch-grensesnitt oppe.

I den originale Shape Blaster for Windows kan spilleren bevegelse gjøres med WASD-tastene på tastaturet. For sikte kunne de bruke piltastene eller musen. Dette er ment å etterligne Geometry Wars tvillingpinne kontroller: en analog pinne for bevegelse, en for sikte.

Siden Shape Blaster allerede bruker begrepet tastatur- og musebevegelse, vil den enkleste måten å legge til innspill ved å emulere tastatur- og musekommandoer gjennom berøring. Vi starter med musebevegelse, da både berøring og mus deler en lignende komponent: et punkt som inneholder X- og Y-koordinater.

Vi lager en statisk klasse for å holde oversikt over de ulike inngangsenhetene og å ta vare på å skifte mellom de forskjellige typer sikter:

 klasse Input: offentlig tSingleton beskyttet: tPoint2f mMouseState; tPoint2f mLastMouseState; tPoint2f mFreshMouseState; std :: vektor mKeyboardState; std :: vektor mLastKeyboardState; std :: vektor mFreshKeyboardState; bool mIsAimingWithMouse; uint8_t mLeftEngasjert; uint8_t mRightEngaged; offentlig: enum KeyType kUp = 0, kLeft, kDown, kRight, kW, kA, kS, kD,; beskyttet: tVector2f GetMouseAimDirection () const; beskyttet: Input (); offentlig: tPoint2f getMousePosition () const; ugyldig oppdatering (); // Kontrollerer om en nøkkel bare ble trykket ned bool wasKeyPressed (KeyType) const; tVector2f getMovementDirection () const; tVector2f getAimDirection () const; void onKeyboard (const tKeyboardEvent & msg); void onTouch (const tTouchEvent & msg); venneklasse tSingleton; ; void Input :: update () mLastKeyboardState = mKeyboardState; mLastMouseState = mMouseState; mKeyboardState = mFreshKeyboardState; mMouseState = mFreshMouseState; hvis (mKeyboardState [kLeft] || mKeyboardState [kRight] || mKeyboardState [kUp] || mKeyboardState [kDown]) mIsAimingWithMouse = false;  annet hvis (mMouseState! = mLastMouseState) mIsAimingWithMouse = true; 

Vi ringer Input :: oppdatering () i begynnelsen av GameRoot :: onRedrawView () for inngangsklassen til arbeid.

Som nevnt tidligere bruker vi tastatur Oppgi senere i serien for å redegjøre for bevegelse.

skyting

La oss nå gjøre skipet skyte.

Først trenger vi en klasse for kuler.

 klasse Bullet: offentlig enhet offentlig: Bullet (const tPoint2f & posisjon, const tVector2f & hastighet); ugyldig oppdatering (); ; Bullet :: Bullet (const tPoint2f og posisjon, const tVector2f og hastighet) mImage = Art :: getInstance () -> getBullet (); mPosition = posisjon; mVelocity = hastighet; mOrientation = atan2f (mVelocity.y, mVelocity.x); mRadius = 8; mKind = kBullet;  void Bullet :: update () if (mVelocity.lengthSquared ()> 0) mOrientation = atan2f (mVelocity.y, mVelocity.x);  mPosisjon + = mVelocity; hvis (! tRectf (0, 0, GameRoot :: getInstance () -> getViewportSize ()) inneholder (tPoint2f ((int32_t) mPosition.x, (int32_t) mPosition.y))) mIsExpired = true; 

Vi ønsker en kort nedkjølingsperiode mellom kuler, så vi får en konstant for det:

 const int PlayerShip :: kCooldownFrames = 6;

Vi legger også til følgende kode til PlayerShip :: Update ():

 tVector2f aim = Input :: getInstance () -> getAimDirection (); hvis (aim.lengthSquared ()> 0 && mCooldowmRemaining <= 0)  mCooldowmRemaining = kCooldownFrames; float aimAngle = atan2f(aim.y, aim.x); float cosA = cosf(aimAngle); float sinA = sinf(aimAngle); tMatrix2x2f aimMat(tVector2f(cosA, sinA), tVector2f(-sinA, cosA)); float randomSpread = tMath::random() * 0.08f + tMath::random() * 0.08f - 0.08f; tVector2f vel = 11.0f * (tVector2f(cosA, sinA) + tVector2f(randomSpread, randomSpread)); tVector2f offset = aimMat * tVector2f(35, -8); EntityManager::getInstance()->legg til (ny Bullet (mPosition + offset, vel)); offset = aimMat * tVector2f (35, 8); EntityManager :: getInstance () -> add (ny Bullet (mPosition + offset, vel)); tSound * curShot = Lyd :: getInstance () -> getShot (); hvis (! curShot-> isPlaying ()) curShot-> play (0, 1);  hvis (mCooldowmRemaining> 0) mCooldowmRemaining--; 

Denne koden oppretter to kuler som reiser parallelt med hverandre. Det legger til en liten tilfeldighet i retningen, noe som gjør at skuddene spredes ut litt som en maskinpistol. Vi legger til to tilfeldige tall sammen, fordi dette gjør at summen er mer sannsynlig å være sentrert (rundt null) og mindre sannsynlig å sende kuler langt unna. Vi bruker en todimensjonal matrise for å rotere den opprinnelige posisjonen til kulene i den retningen de reiser.

Vi brukte også to nye hjelpemetoder:

  • Extensions :: NextFloat (): Returnerer en tilfeldig flyt mellom minimum og maksimum verdi.
  • MathUtil :: FromPolar (): Oppretter en tVector2f fra en vinkel og størrelse.

Så la oss se hvordan de ser ut:

 // I Extensions float Extensions :: nextFloat (float minValue, float maxValue) return (float) tMath :: tilfeldig () * (maxValue - minValue) + minValue;  // I MathUtil tVector2f MathUtil :: fraPolar (flytvinkel, flytestørrelse) returstørrelse * tVector2f ((float) cosf (vinkel), (float) sinf (vinkel)); 

Tilpasset markør

Det er en ting vi skal gjøre nå, da vi har det første Input klasse: la oss tegne en egendefinert musemarkør for å gjøre det lettere å se hvor skipet sikter mot. I GameRoot.Draw, bare tegne Art mPointer på musens posisjon.

 mSpriteBatch-> draw (0, Art :: getInstance () -> getPointer (), Input :: getInstance () -> getMousePosition (), tOptional());

Konklusjon

Hvis du tester spillet nå, vil du kunne røre hvor som helst på skjermen for å sikte på kontinuerlig strøm av kuler, noe som er en god start.


Advarsel: Høyt!

I neste del, vil vi fullføre det opprinnelige spillet ved å legge til fiender og en poengsum.