Lag en Neon Vector Shooter for iOS The Warping Grid

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 denne siste delen legger vi til bakgrunnsnettet som forklarer basert på spillet-handlingen.

Oversikt

I serien så langt har vi opprettet gameplay, virtuell gamepad og partikkeleffekter. I denne siste delen vil vi skape et dynamisk, krøllende bakgrunnsnett.


Advarsel: Høyt!

Som nevnt i forrige del vil du legge merke til en dramatisk nedgang i framerate hvis du fortsatt kjører koden i feilsøkingsmodus. Se den veiledningen for detaljer om hvordan du bytter til utgivelsesmodus for full kompilatoroptimalisering (og en raskere bygging).

The Warping Grid

En av de kuleste effektene i Geometry Wars er det klingende bakgrunnsnettet. Vi undersøker hvordan du lager en lignende effekt i Shape Blaster. Gitteret vil reagere på kuler, svarte hull og spilleren respekterer. Det er ikke vanskelig å lage og det ser fantastisk ut.

Vi lager nettverket ved hjelp av en vårsimulering. Ved hvert skjæringspunkt av nettverket legger vi en liten vekt og legger en fjær på hver side. Disse fjærene vil bare trekke og aldri skyve, som et gummibånd. For å holde rutenettet på plass, vil massene ved grensen til ruten bli forankret på plass. Nedenfor er et diagram over oppsettet.


Vi lager en klasse som heter Nett å skape denne effekten. Men før vi jobber på nettet selv, må vi lage to hjelperklasser: Vår og PointMass.

The PointMass Class

De PointMass klassen representerer massene som vi vil knytte fjærene til. Fjærer forbinder aldri direkte med andre fjærer; I stedet bruker de en kraft til massene de forbinder, noe som igjen kan strekke andre fjærer.

 klasse PointMass beskyttet: tVector3f mAcceleration; float mDamping; offentlig: tVector3f mPosition; tVector3f mVelocity; float mInverseMass; offentlig: PointMass (); PointMass (const tVector3f & posisjon, float invMass); ugyldig anvendelseForce (const tVector3f & force); void increaseDamping (float factor); ugyldig oppdatering (); ; PointMass :: PointMass (): mAcceleration (0,0,0), mDamping (0.98f), mPosition (0), mVelocity (0,0,0), mInverseMass (0)  PointMass :: PointMass (const tVector3f & position , float invMass): mAcceleration (0,0,0), mDamping (0.98f), mPosisjon (posisjon), mVelocity (0,0,0), mInverseMass (invMass)  tomt PointMass :: applyForce (const tVector3f & force) mAcceleration + = force * mInverseMass;  tomt PointMass :: increaseDamping (float factor) mDamping * = faktor;  tomt PointMass :: oppdatering () mVelocity + = mAcceleration; mPosisjon + = mVelocity; mAcceleration = tVector3f (0,0,0); hvis (mVelocity.lengthSquared () < 0.001f * 0.001f)  mVelocity = tVector3f(0,0,0);  mVelocity *= mDamping; mDamping = 0.98f; 

Det er noen interessante poeng om denne klassen. Først legg merke til at det lagrer omvendt av massen, 1 / masse. Dette er ofte en god ide i fysikk simuleringer fordi fysikk likninger pleier å bruke den inverse av massen oftere, og fordi det gir oss en enkel måte å representere uendelig tunge, faste objekter ved å sette den inverse massen til null.

For det andre inneholder klassen også a demping variabel. Dette brukes grovt som friksjon eller luftmotstand; det bremser gradvis massen ned. Dette bidrar til at gridet til slutt kommer til hvile og øker også stabiliteten til vårens simulering.

De PointMass :: oppdatering () Metoden gjør arbeidet med å flytte punktmassen hver ramme. Det begynner med å simulere Euler-integrasjon, som bare betyr at vi legger til akselerasjonen til hastigheten og deretter legger til den oppdaterte hastigheten til stillingen. Dette er forskjellig fra standard Euler-integrasjon der vi vil oppdatere hastigheten etter å ha oppdatert posisjonen.

Tips: Symplegisk Euler er bedre for vårsimuleringer fordi den sparer energi. Hvis du bruker vanlig Euler-integrasjon og lager fjærer uten demping, vil de strekke seg lenger og lenger hver sprette etter hvert som de får energi, til slutt bryter simuleringen din.

Etter å ha oppdatert hastigheten og posisjonen, kontrollerer vi om hastigheten er svært liten, og hvis så setter vi den til null. Dette kan være viktig for ytelse på grunn av arten av denormaliserte flytende punktnumre.

(Når flytende poeng blir svært små, bruker de en spesiell representasjon kalt a denormalisert nummer. Dette har fordelen av å tillate flyter å representere mindre tall, men det kommer til en pris. De fleste brikkesettene kan ikke bruke sine standard aritmetiske operasjoner på deormaliserte tall og i stedet må etterligne dem ved hjelp av en rekke trinn. Dette kan være flere hundre ganger tregere enn å utføre operasjoner på normaliserte flytende punktnumre. Siden vi multipliserer vår hastighet av vår dempingfaktor hver ramme, vil den etter hvert bli svært liten. Vi bryr oss egentlig ikke om slike små hastigheter, så vi stiller det til null.)

De PointMass :: increaseDamping () Metoden brukes til midlertidig å øke mengden demping. Vi vil bruke dette senere for visse effekter.

Vårklassen

En fjær knytter to punktmasser, og hvis den strekkes forbi dens naturlige lengde, gjelder en kraft som trekker massene sammen. Fjærer følger en modifisert versjon av Hooke's Law med demping:

\ [f = -kx - bv \]

  • \ (f \) er kraften som produseres av våren.
  • \ (k \) er vårens konstant, eller stivheten til våren.
  • \ (x \) er avstanden våren strekkes utover sin naturlige lengde.
  • \ (b \) er dempningsfaktoren.
  • \ (v \) er hastigheten.

Koden for Vår klassen er som følger:

 klasse våren (offentlig: PointMass * mEnd1; PointMass * mEnd2; flyte mTargetLength; float mStiffness; float mDamping; offentlig: Vår (PointMass * end1, PointMass * end2, flytestivhet, flytdemping); ugyldig oppdatering (); ; Vår: Spring (PointMass * end1, PointMass * end2, flytestivhet, flytdemping): mEnd1 (end1), mEnd2 (end2), mTargetLength (mEnd1-> mPosition.distance (mEnd2-> mPosition) * 0.95f), mStivhet (stivhet), mDamping (demping)  ugyldig vår :: oppdatering () tVector3f x = mEnd1-> mPosisjon - mEnd2-> mPosisjon; float lengde = x.length (); hvis (lengde> mTargetLength) x = (x / lengde) * (lengde - mTargetLength); tVector3f dv = mEnd2-> mVelocity - mEnd1-> mVelocity; tVector3f force = mStivhet * x - dv * mDamping; mEnd1-> applyForce (-force); mEnd2-> applyForce (kraft); 

Når vi lager en fjær, setter vi vårens naturlige lengde til bare litt mindre enn avstanden mellom de to endepunktene. Dette holder rutenettet stramt selv når du er i ro, og forbedrer utseendet noe.

De Spring :: oppdatering () Metoden kontrollerer først om våren strekkes utover sin naturlige lengde. Hvis den ikke strekkes, skjer ingenting. Hvis det er, bruker vi den modifiserte Hooke's Law til å finne kraften fra våren og bruke den til de to forbundne massene.

Opprette rutenettet

Nå som vi har de nødvendige nestede klassene, er vi klare til å lage nettverket. Vi begynner med å lage PointMass gjenstander ved hvert skjæringspunkt på rutenettet. Vi lager også noen fast anker PointMass gjenstander for å holde rutenettet på plass. Vi knytter sammen massene med fjærer.

 std :: vektor mSprings; PointMass * mPoints; Grid :: Grid (const tRectf & rect, const tVector2f og mellomrom) mScreenSize = tVector2f (GameRoot :: getInstance () -> getViewportSize (). Bredde, GameRoot :: getInstance () -> getViewportSize (). int numColumns = (int) ((float) rect.size.width / spacing.x) + 1; int numRows = (int) ((float) rect.size.height / spacing.y) + 1; mPoints = new PointMass [numColumns * numRows]; mCols = numColumns; mRows = numRows; PointMass * fixedPoints = new PointMass [numColumns * numRows]; int kolonne = 0, rad = 0; for (float y = rect.location.y; y <= rect.location.y + rect.size.height; y += spacing.y)  for (float x = rect.location.x; x <= rect.location.x + rect.size.width; x += spacing.x)  SetPointMass(mPoints, column, row, PointMass(tVector3f(x, y, 0), 1)); SetPointMass(fixedPoints, column, row, PointMass(tVector3f(x, y, 0), 0)); column++;  row++; column = 0;  // link the point masses with springs for (int y = 0; y < numRows; y++)  for (int x = 0; x < numColumns; x++)  if (x == 0 || y == 0 || x == numColumns - 1 || y == numRows - 1)  mSprings.push_back(Spring(GetPointMass(fixedPoints, x, y), GetPointMass(mPoints, x, y), 0.1f, 0.1f));  else if (x % 3 == 0 && y % 3 == 0)  mSprings.push_back( Spring(GetPointMass(fixedPoints, x, y), GetPointMass(mPoints, x, y), 0.002f, 0.02f));  if (x > 0) mSprings.push_back (Spring (GetPointMass (mPoints, x - 1, y), GetPointMass (mPoints, x, y), 0.28f, 0.06f));  hvis (y> 0) mSprings.push_back (Spring (GetPointMass (mPoints, x, y - 1), GetPointMass (mPoints, x, y), 0.28f, 0.06f)); 

Den første til sløyfe skaper både vanlige masser og faste masser ved hvert skjæringspunkt av rutenettet. Vi vil ikke faktisk bruke alle de faste massene, og de ubrukte massene vil rett og slett bli søppel samlet etter at konstruktøren ender. Vi kan optimalisere ved å unngå å skape unødvendige objekter, men siden rutenettet vanligvis bare opprettes en gang, vil det ikke gjøre mye forskjell.

I tillegg til å bruke ankerpunktsmasser rundt grensen til rutenettet, vil vi også bruke noen ankermasser inne i rutenettet. Disse vil bli brukt til å forsiktig trekke rutenettet tilbake til sin opprinnelige posisjon etter å ha blitt deformert.

Siden ankerpunktene ikke beveger seg, trenger de ikke å bli oppdatert hver ramme; vi kan bare koble dem opp til fjærene og glemme dem. Derfor har vi ikke en medlemsvariabel i Nett klasse for disse massene.

Det finnes en rekke verdier du kan finjustere i etableringen av nettet. De viktigste er stivhet og demping av fjærene. Stivheten og dempingen av grenseankrene og indre ankre settes uavhengig av hovedfjærene.) Høyere stivhetsverdier vil gjøre fjærene oscillere raskere, og høyere dempingsverdier vil føre til at fjærene senkes raskere.

Manipulerer rutenettet

For at nettverket skal bevege seg, må vi oppdatere det hver ramme. Dette er veldig enkelt som vi allerede gjorde alt hardt arbeid i PointMass og Vår klasser:

 void Grid :: update () for (size_t i = 0; i < mSprings.size(); i++)  mSprings[i].update();  for(int i = 0; i < mCols * mRows; i++)  mPoints[i].update();  

Nå vil vi legge til noen metoder som manipulerer rutenettet. Du kan legge til metoder for enhver form for manipulasjon du kan tenke på. Vi vil implementere tre typer manipulasjoner her: skyve en del av rutenettet i en gitt retning, skyve rutenettet utover fra et tidspunkt og trekke ruten inn mot et punkt. Alle tre vil påvirke rutenettet innenfor en gitt radius fra noen målpunkt. Nedenfor er noen bilder av disse manipulasjonene i gang:


Kuler avviser rutenettet utover.


Suger ruten innover.

Bølge opprettet ved å skyve rutenettet langs z-aksen.
 void Grid :: applyDirectedForce (const tVector3f & force, const tVector3f og posisjon, floatradius) for (int i = 0; i < mCols * mRows; i++)  if (position.distanceSquared(mPoints[i].mPosition) < radius * radius)  mPoints[i].applyForce(10.0f * force / (10 + position.distance(mPoints[i].mPosition)));    void Grid::applyImplosiveForce(float force, const tVector3f& position, float radius)  for (int i = 0; i < mCols * mRows; i++)  float dist2 = position.distanceSquared(mPoints[i].mPosition); if (dist2 < radius * radius)  mPoints[i].applyForce(10.0f * force * (position - mPoints[i].mPosition) / (100 + dist2)); mPoints[i].increaseDamping(0.6f);    void Grid::applyExplosiveForce(float force, const tVector3f& position, float radius)  for (int i = 0; i < mCols * mRows; i++)  float dist2 = position.distanceSquared(mPoints[i].mPosition); if (dist2 < radius * radius)  mPoints[i].applyForce(100 * force * (mPoints[i].mPosition - position) / (10000 + dist2)); mPoints[i].increaseDamping(0.6f);   

Vi bruker alle tre metodene i Shape Blaster til forskjellige effekter.

Gjenoppretter rutenettet

Vi tegner rutenettet ved å tegne linjesegmenter mellom hvert nabopunkt. Først legger vi til en utvidelsesmetode som tar en tSpriteBatch pekeren som en parameter som tillater oss å tegne linjesegmenter ved å ta en tekstur av en enkelt piksel og strekke den inn i en linje.

Åpne Kunst klasse og erklære en tekstur for piksel:

 klasse kunst: offentlig tsingleton; beskyttet: tTexture * mPixel; ... public: tTexture * getPixel () const; ...;

Du kan sette pikseltekstur på samme måte som vi setter opp de andre bildene, så vi legger til pixel.png (et 1x1px bilde med eneste piksel sett til hvitt) til prosjektet og last det inn i tTexture:

 mPixel = ny tTexture (tSurface ("pixel.png"));

La oss nå legge til følgende metode til utvidelser klasse:

 void Extensions :: drawLine (tSpriteBatch * spriteBatch, const tVector2f og start, const tVector2f & end, const tColor4f og farge, flyttykkelse) tVector2f delta = endstart; spriteBatch-> draw (0, Art :: getInstance () -> getPixel (), tPoint2f ((int32_t) start.x, (int32_t) start.y), tOptional(), farge, toAngle (delta), tPoint2f (0, 0), tVector2f (delta.length (), tykkelse)); 

Denne metoden strekker seg, roterer og toner pikseltekstur for å produsere linjen vi ønsker.

Deretter trenger vi en metode for å projisere 3D-gridpunktene på vår 2D-skjerm. Normalt kan dette gjøres ved hjelp av matriser, men her forvandler vi koordinatene manuelt i stedet.

Legg til følgende i Nett klasse:

 tVector2f Grid :: toVec2 (const tVector3f & v) float factor = (v.z + 2000.0f) * 0.0005f; returnere (tVector2f (v.x, v.y) - mScreenSize * 0.5f) * faktor + mScreenSize * 0,5f; 

Denne transformasjonen vil gi grid et perspektivvinkel hvor langt unna punkter vises nærmere sammen på skjermen. Nå kan vi tegne rutenettet ved å iterere gjennom rader og kolonner og tegningslinjer mellom dem:

 void Grid :: draw (tSpriteBatch * spriteBatch) int width = mCols; int høyde = mRows; tColor4f farge (0,12f, 0,12f, 0,55f, 0,33f); for (int y = 1; y < height; y++)  for (int x = 1; x < width; x++)  tVector2f left, up; tVector2f p = toVec2(GetPointMass(mPoints, x, y)->mPosition); hvis (x> 1) left = toVec2 (GetPointMass (mPoints, x - 1, y) -> mPosition); flyte tykkelse = (y% 3 == 1)? 3.0f: 1.0f; Extensions :: drawLine (spriteBatch, venstre, p, farge, tykkelse);  hvis (y> 1) opp = tilVec2 (GetPointMass (mPoints, x, y - 1) -> mPosition); flyte tykkelse = (x% 3 == 1)? 3.0f: 1.0f; Extensions :: drawLine (spriteBatch, opp, p, farge, tykkelse); 

I ovennevnte kode, p er vårt nåværende punkt på rutenettet, venstre er poenget direkte til venstre og opp er poenget rett over det. Vi trekker hver tredje linje tykkere både horisontalt og vertikalt for visuell effekt.

interpole

Vi kan optimalisere rutenettet ved å forbedre visuell kvalitet for et gitt antall fjærer uten å øke ytelseskostnaden betydelig. Vi skal gjøre to slike optimaliseringer.

Vi vil gjøre grid tettere ved å legge til linjesegmenter inne i eksisterende gridceller. Vi gjør det ved å tegne linjer fra midtpunktet til den ene siden av cellen til midtpunktet på motsatt side. Bildet under viser de nye interpolerte linjene i rødt.

Tegning av de interpolerte linjene er rett frem. Hvis du har to poeng, en og b, deres midtpunkt er (a + b) / 2. Så, for å tegne de interpolerte linjene, legger vi til følgende kode inne i til løkker av vår Grid :: draw () metode:

 hvis (x> 1 && y> 1) tVector2f upLeft = toVec2 (GetPointMass (mPoints, x - 1, y - 1) -> mPosition); Extensions :: drawLine (spriteBatch, 0.5f * (upLeft + opp), 0.5f * (venstre + p), farge, 1.0f); // vertikal linje Extensions :: drawLine (spriteBatch, 0.5f * (upLeft + left), 0.5f * (opp + p), farge, 1.0f); // horisontal linje 

Den andre forbedringen er å utføre interpolering på våre rettlinjesegmenter for å gjøre dem til jevnere kurver. I den originale XNA-versjonen av dette spillet var koden avhengig av XNAs Vector2.CatmullRom () metode som utfører Catmull-Rom interpolering. Du overfører metoden fire sekvensielle punkter på en buet linje, og den vil returnere poeng langs en jevn kurve mellom det andre og det tredje poenget du angav.

Siden denne algoritmen ikke eksisterer i enten C eller C + + standardbibliotek, må vi implementere det selv. Heldigvis er det en referanseimplementasjon tilgjengelig for bruk. Jeg har gitt en MathUtil :: catmullRom () metode basert på denne referanseimplementasjonen:

 float MathUtil :: catmullRom (const float value1, const float value2, const float value3, const float value4, float amount) // Ved hjelp av formel fra http://www.mvps.org/directx/articles/catmull/ float amountSquared = beløp * beløp; float amountCubed = amountSquared * mengde; returnere (flyt) (0,5f * (2,0f * verdi2 + (verdi3 - verdi1) * beløp + (2,0f * verdi1 - 5,0f * verdi2 + 4,0f * verdi3 - verdi4) * amountSquared + (3.0f * verdi2 - verdi1 - 3.0f * verdi3 + verdi4) * amountCubed));  tVector2f MathUtil :: catmullRom (const tVector2f og verdi1, const tVector2f og verdi2, const tVector2f og verdi3, const tVector2f og verdi4, flytebeløp) return tVector2f (MathUtil :: catmullRom (value1.x, value2.x, value3.x, value4.x , mengde), MathUtil :: catmullRom (value1.y, value2.y, value3.y, value4.y, amount)); 

Det femte argumentet til MathUtil :: catmullRom () er en vektningsfaktor som bestemmer hvilket punkt på den interpolerte kurven den returnerer. En vektningsfaktor på 0 eller 1 Vil henholdsvis returnere det andre eller tredje poenget du oppgav, og en vektningsfaktor på 0.5 vil returnere punktet på den interpolerte kurven halvveis mellom de to punktene. Ved å bevege vektvektoren gradvis fra null til en og tegne linjer mellom de returnerte punktene, kan vi produsere en perfekt jevn kurve. For å holde ytelseskostnadene lave, tar vi kun et enkelt interpolert punkt i betraktning, med en vektningsfaktor på 0.5. Vi erstatter deretter den opprinnelige rette linjen i rutenettet med to linjer som møtes ved det interpolerte punktet.

Diagrammet nedenfor viser effekten av denne interpolasjonen:


Siden linjesegmentene i nettet er allerede små, bruker det ikke mer enn ett interpolert punkt generelt en merkbar forskjell.

Ofte vil linjene i rutenettet være veldig rett og vil ikke kreve noen utjevning. Vi kan se etter dette og unngå å trekke to linjer i stedet for en: Vi kontrollerer om avstanden mellom det interpolerte punktet og midtpunktet til den rette linjen er større enn ett bildepunkt; hvis det er, antar vi at linjen er buet og vi tegner to linjesegmenter.

Modifikasjonen til vår Grid :: draw () Metoden for å legge til Catmull-Rom interpolering for de horisontale linjene er vist nedenfor.

 venstre = toVec2 (GetPointMass (mPoints, x - 1, y) -> mPosition); flyte tykkelse = (y% 3 == 1)? 3.0f: 1.0f; int clampedX = (int) tMath :: min (x + 1, bredde - 1); tVector2f mid = MathUtil :: catmullRom (toVec2 (GetPointMass (mPoints, x - 2, y) -> mPosisjon), venstre, p, tilVec2 (GetPointMass (mPoints, clampedX, y) -> mPosition), 0,5f); if (mid.distanceSquared ((left + p) / 2)> 1) Extensions :: drawLine (spriteBatch, venstre, midt, farge, tykkelse); Extensions :: drawLine (spriteBatch, midt, p, farge, tykkelse);  ellers Extensions :: drawLine (spriteBatch, venstre, p, farge, tykkelse); 

Bildet under viser effekten av utjevningen. En grønn prikk tegnes på hvert interpolert punkt for bedre å illustrere hvor linjene er glattet.

Bruke Gitteret i Shape Blaster

Nå er det på tide å bruke rutenettet i spillet vårt. Vi begynner med å erklære en offentlig, statisk Nett variabel i GameRoot og opprette rutenettet i GameRoot :: onInitView. Vi lager et rutenett med omtrent 600 poeng som det.

 const int maxGridPoints = 600; tVector2f gridSpacing = tVector2f ((float) sqrtf (mViewportSize.width * mViewportSize.height / maxGridPoints)); mGrid = nytt grid (tRectf (0,0, mViewportSize), gridSpacing);

Selv om den originale XNA-versjonen av spillet bruker 1.600 poeng (i stedet for 600), blir dette altfor mye å håndtere, selv om den kraftige maskinvaren i iPhone. Heldigvis forlot den opprinnelige koden mengden poeng som kan tilpasses, og ved omtrent 600 gridpunkter kan vi fremdeles gi dem og fortsatt opprettholde en optimal bildefrekvens.

Da kaller vi Grid :: oppdatering () og Grid :: draw () fra GameRoot :: onRedrawView () metode i GameRoot. Dette vil tillate oss å se nettet når vi kjører spillet. Imidlertid må vi fortsatt gjøre ulike spillobjekter interaksjon med rutenettet.

Kuler vil avvise nettverket. Vi har allerede laget en metode for å gjøre dette kalt Grid :: applyExplosiveForce (). Legg til følgende linje i Bullet :: oppdatering () metode.

 GameRoot :: getInstance () -> getGrid () -> applyExplosiveForce (0.5f * mVelocity.length (), mPosition, 80);

Dette vil gjøre kuler støte rutenettet proporsjonalt til deres hastighet. Det var ganske enkelt.

La oss nå jobbe på sorte hull. Legg til denne linjen til Blackhole :: oppdatering ():

 GameRoot :: getInstance () -> getGrid () -> applyImplosiveForce ((float) sinf (mSprayAngle / 2.0f) * 10 + 20, mPosition, 200);

Dette gjør det svarte hullet suge i rutenett med varierende mengde kraft. Vi har brukt på nytt mSprayAngle variabel, noe som vil føre til at kraften på rutenettet pulserer i synkronisering med vinkelen det sprays partikler (selv om det er halvparten av frekvensen som skyldes divisjonen med to). Kraften som kommer inn, vil variere sinusformet mellom 10 og 30.

Til slutt vil vi skape en shockwave i nettet når spillerens skip responderer etter døden. Vi vil gjøre det ved å trekke rutenettet langs z-aksen og deretter la kraften forplante seg og sprette gjennom fjærene. Igjen, dette krever bare en liten modifikasjon til PlayerShip :: oppdatering ().

 hvis (getIsDead ()) mFramesUntilRespawn--; hvis (mFramesUntilRespawn == 0) GameRoot :: getInstance () -> getGrid () -> applyDirectedForce (tVector3f (0, 0, 5000), tVector3f (mPosition.x, mPosition.y, 0), 50); 

Hva blir det neste?

Vi har grunnleggende spill og effekter implementert. Det er opp til deg å gjøre det til et komplett og polert spill med din egen smak. Prøv å legge til noen interessante nye mekanikere, noen kule nye effekter eller en unik historie. Hvis du ikke er sikker på hvor du skal begynne, er det noen forslag:

  • Tweak og tilpass berørskontrollene til dine personlige preferanser.
  • Legg til støtte for maskinvarekontrollere i iOS 7 via GameController Framework.
  • Profil og optimaliser alle de sakte delene av koden ved hjelp av Xcodes innebygde Instruments-verktøy.
  • Forsøk å legge til etterbehandlingseffekter som Bloom Shader-presentasjonen i den originale XNA-versjonen.
  • Opprett nye fiendtlige typer som slanger eller eksploderende fiender.
  • Lag nye våpentyper som å søke raketter eller en lynpistol.
  • Legg til en tittelskjerm og hovedmeny, og et høyt score bord.
  • Legg til noen powerups som skjold eller bomber.
  • Forsøk å finne powerups som er morsomme og hjelpe spillet ditt skiller seg ut.
  • Opprett flere nivåer. Hardere nivåer kan introdusere tøffere fiender og mer avanserte våpen og powerups.
  • Legg til miljøfarer som lasere.

Himmelen er grensen!