Lag en Neon Vector Shooter for iOS Virtual Gamepads og Black Holes

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 delen legger vi til de virtuelle gamepadkontrollene og "Black hole" -fiender.

Oversikt

I serien så langt har vi satt opp det grunnleggende spillet for vår neon twin stick shooter, Shape Blaster. Deretter legger vi til to "virtuelle gamepads" på skjermen for å kontrollere skipet med.


Input er et must for hvilket som helst videospill, og iOS gir oss en interessant og tvetydig utfordring med multi-touch-inngang. Jeg vil vise deg en tilnærming, basert på konseptet av virtuelle gamepads, hvor vi simulerer maskinvare-gamepads ved å bruke bare berøring og litt kompleks logikk for å finne ut ting. Etter å ha lagt til virtuelle gamepads for multi-touch-inngang, vil vi også legge til svarte hull i spillet.

Virtual Gamepads

Berøringsbaserte kontroller på skjermen er den primære måten å legge inn på flertallet av iPhone- og iPad-baserte apper og spill. Faktisk tillater iOS bruk av et multi-touch-grensesnitt, som betyr å lese flere berøringspunkter samtidig. Skjønnheten til berøringsbaserte grensesnitt er at du kan definere grensesnittet for å være det du vil, enten det er en knapp, en virtuell kontrollpinne eller en skyvekontroll. Det vi skal implementere er et berøringsgrensesnitt jeg kaller "virtuelle gamepads".

EN gamepad beskriver vanligvis en standard, pluss-formet fysisk kontroll som ligner pluss-grensesnittet på et Game Boy-system eller PlayStation-kontrolleren (også kjent som retningsplugg eller D-pute). En gamepad tillater bevegelse i både opp- og nedaksen, og venstre og høyre akse. Resultatet er at du kan signalere åtte forskjellige retninger, med tillegg av "ingen retning". I Shape Blaster vil vårt gamepad-grensesnitt ikke være fysisk, men på skjermen, derav en virtuell gamepad.


En typisk fysisk gamepad; retningsplaten i dette tilfellet er plussformet.

Selv om det kun er fire innganger, er det åtte retninger (pluss nøytral) tilgjengelig.

For å ha en virtuell gamepad i spillet vårt, må vi gjenkjenne berøringsinngang når det skjer, og konvertere det til et skjema som spillet allerede forstår.

Den virtuelle gamepad implementert her virker i tre trinn:

  1. Bestem berøringstypen.
  2. Bestem om det er i området av en gamepad på skjermen.
  3. Emuler berøringen som en nøkkelpresse eller musebevegelse.

I hvert trinn vil vi bare fokusere på berøringen vi har, og holde styr på siste touch-hendelsen vi måtte sammenligne. Vi holder også oversikt over berørings-ID, som bestemmer hvilken finger som berører hvilken gamepad.

Skjermbildet under viser hvordan spillene vil vises på skjermen:

Skjermbilde av de endelige gamepads på plass.

Legge til Multi-Touch til Shape Blaster

I Nytte bibliotek, la oss se på arrangementsklassen vi primært vil benytte oss av. tTouchEvent encapsulates alt vi trenger for å håndtere berøringshendelser på et grunnnivå.

 klasse tTouchEvent public: enum EventType kTouchBegin, kTouchEnd, kTouchMove,; offentlig: EventType mEvent; tPoint2f mLocation; uint8_t mID; offentlig: tTouchEvent (const EventType og newEvent, const tPoint2f og newLocation, const uint8_t og newID): mEvent (newEvent), mLocation (newLocation), mID (newID) ;

De Eventtype lar oss definere hva slags hendelser vi tillater uten å bli for komplisert. mLocation vil være det faktiske berøringspunktet, og Mid vil være finger-ID, som starter ved null og legger til en for hver finger berørt på skjermen. Hvis vi definerer konstruktøren til bare å ta konst referanser, vil vi kunne ordne hendelsesklasser uten å eksplisitt opprette navngitte variabler for dem.

Vi skal bruke tTouchEvent utelukkende for å sende berøringshendelser fra operativsystemet til vår Input klasse. Vi vil også senere bruke den til å oppdatere den grafiske representasjonen av gamepads i VirtualGamepad klasse.

Inngangsklassen

Den originale XNA og C # versjonen av Input klassen kan håndtere mus, tastatur og faktiske fysiske gamepad innganger. Musen brukes til å brann på et vilkårlig punkt på skjermen fra en hvilken som helst posisjon; tastaturet kan brukes til både å flytte og skyte i bestemte retninger. Siden vi har valgt å etterligne den opprinnelige inngangen (for å være sann mot en "direkte port"), beholder vi mesteparten av den opprinnelige koden, ved hjelp av navnene tastatur og mus, selv om vi heller ikke har iOS-enheter.

Slik er vår Input klassen vil se ut. For hver enhet må vi beholde et "øyeblikks øyeblikksbilde" og "forrige øyeblikksbilde" slik at vi kan fortelle hva som er endret mellom den siste innhente hendelsen og den gjeldende inngangshendelsen. I vårt tilfelle, mMouseState og mKeyboardState er "nåværende øyeblikksbilde", og mLastMouseState og mLastKeyboardState representerer "forrige øyeblikksbilde".

 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 onTouch (const tTouchEvent & msg); venneklasse tSingleton; venneklasse VirtualGamepad; ; Input :: Input (): mMouseState (-1, -1), mLastMouseState (-1, -1), mIsAimingWithMouse (false), mLeftEngaged (255), mRightEngaged (255) mKeyboardState.resize (8); mLastKeyboardState.resize (8); mFreshKeyboardState.resize (8); for (size_t i = 0; i < 8; i++)  mKeyboardState[i] = false; mLastKeyboardState[i] = false; mFreshKeyboardState[i] = false;   tPoint2f Input::getMousePosition() const  return mMouseState; 

Oppdatering av inngang

På en PC er enhver hendelse vi får er "tydelig", noe som betyr at en musebevegelse er annerledes enn å trykke på brevet EN, og til og med brevet EN er forskjellig nok fra brevet S at vi kan fortelle det ikke er nøyaktig den samme hendelsen.

Med iOS, vi bare noensinne få berøringsinngangshendelser, og en berøring er ikke tydelig nok fra en annen for å fortelle om det er ment å være en musebevegelse eller et nøkkelpresse, eller til og med hvilken nøkkel den er. Alle hendelser ser nøyaktig ut på samme måte fra vårt synspunkt.

For å finne ut om denne tvetydigheten, introduserer vi to nye medlemmer, mFreshMouseState og mFreshKeyboardState. Deres formål er å aggregere, eller "fange alle", hendelsene i en bestemt ramme uten å endre de andre statlige variablene ellers. Når vi er fornøyd, har en ramme gått, vi kan oppdatere den nåværende tilstanden med de "friske" medlemmene ved å ringe Input :: oppdatering. Input :: oppdatering forteller også vår inngangsstatus for å gå videre.

 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; 

Siden vi gjør det en gang per ramme, ringer vi Input :: oppdatering () innenfra GameRoot :: onRedrawView ():

 // I GameRoot :: onRedrawView () Input :: getInstance () -> oppdatering ();

La oss nå se på hvordan vi berører inngang til enten en simulert mus eller tastatur. Først vil vi planlegge å ha to forskjellige rektangulære områder som representerer de virtuelle gamepads. Alt utenfor disse områdene vil vi vurdere "definitivt en mus event"; noe inni, vi vil vurdere "definitivt et tastaturhendelse."

Alt i de røde boksene vil vi kartlegge til vår simulerte tastaturinngang; alt annet vi vil behandle som musinngang.

La oss se på Input :: onTouch (), som får alle berøringshendelser. Vi tar et stort bilde, se på metoden, og noter områder Å GJØRE hvor mer spesifikk kode skal være:

 void Input :: onTouch (const tTouchEvent & msg) tPoint2f leftPoint = VirtualGamepad :: getInstance () -> mLeftPoint - tPoint2f (18, 18); tPoint2f rightPoint = VirtualGamepad :: getInstance () -> mRightPoint - tPoint2f (18, 18); tPoint2f intPoint ((int) msg.mLocation.x, (int) msg.mLocation.y); bool mouseDown = (msg.mEvent == tTouchEvent :: kTouchBegin) || (msg.mEvent == tTouchEvent :: kTouchMove); Hvis (! mouseDown) if (msg.mID == mLeftEngaged) // TODO: Sett alle bevegelsestastene som "tast opp" ellers hvis (msg.mID == mRightEngaged) // TODO: Sett alle avfyringstaster som "tast opp" hvis (mouseDown && tRectf (leftPoint, 164, 164) .contains (intPoint)) mLeftEngased = msg.mID; // TODO: Sett alle bevegelsestastene som "tast opp" // TODO: Bestem hvilke bevegelsestaster som skal settes hvis (mouseDown && tRectf (rightPoint, 164, 164) .contains (intPoint)) mRightEngaged = msg.mID; // TODO: Sett alle skyte nøkler som "tast opp" // TODO: Bestem hvilke avbrutt nøkler som skal settes hvis (! TRectf (leftPoint, 164, 164) .contains (intPoint) &&! TRectf (rightPoint, 164, 164) .contains (intPoint)) // Hvis vi gjør det her, var det definitivt et "mus event" mFreshMouseState = tPoint2f ((int32_t) msg.mLocation.x, (int32_t) msg.mLocation.y); 

Koden er enkel nok, men det skjer noen kraftig logikk som jeg vil påpeke:

  1. Vi bestemmer hvor venstre og høyre gamepads skal være på skjermen, slik at vi kan se om vi er i dem når vi berører eller slipper. Disse lagres i leftPoint og rightPoint lokale variabler.
  2. Vi bestemmer mousedown tilstand: Hvis vi "trykker" med en finger, må vi vite om det er innenfor leftPointer rett eller rightPointer rett, og i så fall ta til rette for å oppdatere fersk angi tastaturet. Hvis det ikke er i hverken rekt, antar vi at det er en museventil i stedet og oppdaterer fersk stat for mus.
  3. Endelig holder vi oversikt over berørings-IDene (eller finger-IDene) mens de trykkes på; hvis vi oppdager en finger som løfter av overflaten, og den er knyttet til en aktiv gamepad, vil vi nullstille det simulerte tastaturet for nevnte gamepad tilsvarende.

Nå som vi ser det store bildet, la oss bore oss litt lenger.

Fylle inn hullene

Når en finger løftes av overflaten av iPhone eller iPad, kontrollerer vi om det er en finger vi vet er på en gamepad, og i så fall nullstiller vi alle de "simulerte tastene" for den gamepad:

 hvis (! mouseDown) if (msg.mID == mLeftEngaged) mFreshKeyboardState [kA] = false; mFreshKeyboardState [kD] = false; mFreshKeyboardState [kW] = false; mFreshKeyboardState [kS] = false;  annet hvis (msg.mID == mRightEngaged) mFreshKeyboardState [kUp] = false; mFreshKeyboardState [kDown] = false; mFreshKeyboardState [kLeft] = false; mFreshKeyboardState [kRight] = false; 

Situasjonen er litt annerledes når det er en berøring som starter på overflaten eller beveger seg; vi sjekker for å se om kontakten er innenfor enten gamepad. Siden koden for begge gamepads er lik, ser vi bare på den venstre gamepaden (som omhandler bevegelse).

Når vi får en berøringshendelse, fjerner vi tastaturtilstanden helt for den aktuelle gamepad, og sjekker i vårt rette område for å finne ut hvilken tast eller knapp som skal trykkes. Så selv om vi har totalt åtte retninger (pluss nøytral), vil vi bare sjekke fire rektangler: en for opp, en for ned, en til venstre og en til høyre.

De ni områdene av interesse i vår gamepad.
 hvis (mouseDown && tRectf (leftPoint, 164, 164) .contains (intPoint)) mLeftEngert = msg.mID; mFreshKeyboardState [kA] = false; mFreshKeyboardState [kD] = false; mFreshKeyboardState [kW] = false; mFreshKeyboardState [kS] = false; hvis (tRectf (leftPoint, 72, 164) .contains (intPoint)) mFreshKeyboardState [kA] = true; mFreshKeyboardState [kD] = false;  ellers hvis (tRectf (leftPoint + tPoint2f (128, 0), 72, 164) .contains (intPoint)) mFreshKeyboardState [kA] = false; mFreshKeyboardState [kD] = true;  hvis (tRectf (leftPoint, 164, 72) .contains (intPoint)) mFreshKeyboardState [kW] = true; mFreshKeyboardState [kS] = false;  ellers hvis (tRectf (leftPoint + tPoint2f (0, 128), 164, 72) .contains (intPoint)) mFreshKeyboardState [kW] = false; mFreshKeyboardState [kS] = true; 

Viser grafikk for Virtual Gamepad

Hvis du kjører spillet nå, har du virtuell gamepad-støtte, men du vil faktisk ikke kunne se hvor de virtuelle gamepadene starter eller slutter.

Det er her VirtualGamepad klassen kommer til spill. De VirtualGamepadHovedformålet er å tegne gamepads på skjermen. Måten vi vil vise gamepad på, er hvordan andre spill pleier å gjøre det hvis de har gamepads: som en større "base" sirkel, og en mindre "kontrollpinne" sirkel vi kan flytte. Dette ligner en arkad joystick fra toppen ned, og lettere å tegne enn noen andre alternativer.

Først legg merke til at bildefiler vpad_top.png og vpad_bot.png har blitt lagt til i prosjektet. La oss endre Kunst klasse for å laste dem:

 klasse kunst: offentlig tsingleton beskyttet: ... tTexture * mVPadBottom; tTexture * mVPadTop; ... public: ... tTexture * getVPadBottom () const; tTexture * getVPadTop () const; ... venneklasse tSingleton; ; Art :: Art () ... mVPadTop = ny tTexture (tSurface ("vpad_top.png")); mVPadBottom = ny tTexture (tSurface ("vpad_bot.png"));  tTexture * Art :: getVPadBottom () const return mVPadBottom;  tTexture * Art :: getVPadTop () const return mVPadTop; 

De VirtualGamepad klassen vil tegne begge gamepads på skjermen, og beholde Stat informasjon i medlemmene mLeftStick og mRightStick på hvor å tegne "kontrollpinner" av gamepads.

Jeg har valgt noen litt vilkårlig posisjoner for gamepads, som initialiseres i mLeftPoint og mRightPoint medlemmer - beregningene plasserer dem på omtrent 3,75% i fra venstre eller høyre kant av skjermen, og ca 13% i fra bunnen av skjermen. Jeg baserte disse målene på et kommersielt spill med lignende virtuelle gamepads, men forskjellige spill.

 klasse VirtualGamepad: offentlig tSingleton offentlig: enum tilstand kCenter = 0x00, kTop = 0x01, kBottom = 0x02, kLeft = 0x04, kRight = 0x08, kTopLeft = 0x05, kTopRight = 0x09, kBottomLeft = 0x06, kBottomRight = 0x0a,; beskyttet: tPoint2f mLeftPoint; tPoint2f mRightPoint; int mLeftStick; int mRightStick; beskyttet: VirtualGamepad (); ugyldig DrawStickAtPoint (tSpriteBatch * spriteBatch, const tPoint2f og punkt, statstilstand); ugyldig UpdateBasedOnKeys (); offentlig: ugyldig tegning (tSpriteBatch * spriteBatch); ugyldig oppdatering (const tTouchEvent & msg); venneklasse tSingleton; venn klasse Input; ; VirtualGamepad :: VirtualGamepad (): mLeftStick (kCenter), mRightStick (kCenter) mLeftPoint = tPoint2f (int (3.0f / 80.0f * 800.0f), 600 - int (21.0f / 160.0f * 600.0f) - 128); mRightPoint = tPoint2f (800 - int (3.0f / 80.0f * 800.0f) - 128, 600 - int (21.0f / 160.0f * 600.0f) - 128); 

Som tidligere nevnt, mLeftStick og mRightStick er bitmasker, og deres bruk er å bestemme hvor å tegne den indre sirkelen av gamepad. Vi beregner bitmask i metoden VirtualGamepad :: UpdateBasedOnKeys ().

Denne metoden kalles umiddelbart etter Input :: onTouch, slik at vi kan lese de "friske" statsmedlemmene og vet at de er oppdaterte:

 void VirtualGamepad :: UpdateBasedOnKeys () Input * inp = Input :: getInstance (); mLeftStick = kCenter; hvis (inp-> mFreshKeyboardState [Input :: kA]) mLeftStick | = kLeft;  annet hvis (inp-> mFreshKeyboardState [Input :: kD]) mLeftStick | = kRight;  hvis (inp-> mFreshKeyboardState [Input :: kW]) mLeftStick | = kTop;  annet hvis (inp-> mFreshKeyboardState [Input :: kS]) mLeftStick | = kBottom;  mRightStick = kCenter; hvis (inp-> mFreshKeyboardState [Input :: kLeft]) mRightStick | = kLeft;  annet hvis (inp-> mFreshKeyboardState [Input :: kRight]) mRightStick | = kRight;  hvis (inp-> mFreshKeyboardState [Input :: kUp]) mRightStick | = kTop;  annet hvis (inp-> mFreshKeyboardState [Input :: kDown]) mRightStick | = kBottom; 

For å tegne en individuell gamepad, ringer vi VirtualGamepad :: DrawStickAtPoint (); denne metoden kjenner ikke eller bryr seg om hvilken gamepad du tegner, den vet bare hvor du vil ha den tegnet og staten skal tegne den. Fordi vi har brukt bitmasker og beregnet på forhånd, blir vår metode mindre og lettere å lese:

 void VirtualGamepad :: DrawStickAtPoint (tSpriteBatch * spriteBatch, const tPoint2f og punkt, statstilstand) tPoint2f offset = tPoint2f (18, 18); spriteBatch-> draw (10, Art :: getInstance () -> getVPadBottom (), punkt, tOptional()); bytte (tilstand) case kCenter: offset + = tPoint2f (0, 0); gå i stykker; tilfelle kTopLeft: offset + = tPoint2f (-13, -13); gå i stykker; tilfelle kTop: offset + = tPoint2f (0, -18); gå i stykker; tilfelle kTopRight: offset + = tPoint2f (13, -13); gå i stykker; tilfelle kRight: offset + = tPoint2f (18, 0); gå i stykker; tilfelle kBottomRight: offset + = tPoint2f (13, 13); gå i stykker; tilfelle kBottom: offset + = tPoint2f (0, 18); gå i stykker; tilfelle kBottomLeft: offset + = tPoint2f (-13, 13); gå i stykker; tilfelle kLeft: offset + = tPoint2f (-18, 0); gå i stykker;  spriteBatch-> draw (11, Art :: getInstance () -> getVPadTop (), punkt + offset, tOptional()); 

Tegning av to gamepads blir mye lettere, da det bare er et kall til ovennevnte metode to ganger. La oss se på VirtualGamepad :: draw ():

 void VirtualGamepad :: tegne (tSpriteBatch * spriteBatch) DrawStickAtPoint (spriteBatch, mLeftPoint, (State) mLeftStick); DrawStickAtPoint (spriteBatch, mRightPoint, (State) mRightStick); 

Til slutt må vi faktisk tegne den virtuelle gamepad, så inn GameRoot :: onRedrawView (), legg til følgende linje:

 VirtualGamepad :: getInstance () -> draw (mSpriteBatch);

Det er det. Hvis du kjører spillet nå, bør du se de virtuelle gamepadene i full effekt. Når du berører inne i den venstre gamepad, bør du bevege deg rundt. Når du berører inne i den høyre gamepad, bør avfyringsretningen endres. Faktisk kan du bruke begge gamepads samtidig, og til og med flytte ved hjelp av venstre gamepad og berøre utenfor den høyre gamepad for å få musebevegelsen. Og når du gir slipp, stopper du med å flytte og (potensielt) slutte å skyte.

Sammendrag av Virtual Gamepad Teknikken

Vi har fullt implementert virtuell gamepad-støtte, og det fungerer, men det kan hende du finner det litt clunky eller vanskelig å bruke. Hvorfor er det saken? Det er her den virkelige utfordringen med berøringsbaserte kontroller på iOS kommer inn i tradisjonelle spill som ikke ble opprinnelig designet for dem.

Du er ikke alene, skjønt. Mange spill enten lider av disse problemene, og har overvinnet dem.

Her er noen ting jeg har observert med berøringsskjerminngang; Du kan ha noen lignende observasjoner selv:

For det første har spillkontrollere en annen følelse enn en flat berøringsskjerm; du vet hvor fingeren er på en ekte gamepad, og hvordan du holder fingrene fra å glide av. På en berøringsskjerm kan fingrene imidlertid svinge litt for langt ut av berøringssonen, slik at innspillet ditt kanskje ikke er korrekt gjenkjent, og du kan ikke innse at det er tilfelle før det er for sent.

For det andre har du kanskje også lagt merke til at når du spiller med berørings kontroller, blir hånden din skjult for visjonen din, slik at du kan komme til å bli rammet av en fiende under hånden din, som du ikke så begynte med!

Til slutt kan du oppdage at berøringsområdene er enklere å bruke på en iPad enn en iPhone eller omvendt. Så vi har problemer med en annen skjermstørrelse som påvirker vår "innmatingsarealstørrelse", noe som definitivt er noe vi ikke opplever så mye på en stasjonær datamaskin. (De fleste tastaturer og mus er av samme størrelse og fungerer på samme måte, eller kan justeres.)

Her er noen endringer du kan gjøre med inngangssystemet som er beskrevet i denne artikkelen:

  • Tegn gamepadens sentrale plassering der kontakten begynner; Dette gjør at spillerenes hånd kan skifte seg så lett uten påvirkning, og betyr at de kan røre hvor som helst på skjermen.
  • Gjør ditt "spillbare område" mindre, og flytt gamepadet av det spillbare området helt. Nå vil ikke fingrene dine hindre visningen din.
  • Lag separate, forskjellige brukergrensesnitt for iPhone og iPad. Dette vil tillate deg å tilpasse designet basert på enhetstypen, men det krever også at du har forskjellige enheter til å teste mot.
  • Lag fiender eller spilleren sendes litt langsommere. Dette kan potensielt la brukeren oppleve spillet lettere, men det kan også gjøre spillet enklere å vinne.
  • Ditch virtuelle gamepads helt og bruke en annen ordning. Du er selv ansvarlig!

Igjen er det opp til deg hva du vil gjøre og hvordan du vil gjøre det. På plussiden er det mange måter å gjøre berøringsinngang på. Den tøffe delen er å få det riktig og gjøre spillerne lykkelige.

Svarte hull

En av de mest interessante fiender i Geometry Wars er svart hull. La oss undersøke hvordan vi kan gjøre noe lignende i Shape Blaster. Vi vil lage grunnleggende funksjonalitet nå, og vi vil revidere fienden i neste veiledning for å legge til partikkeleffekter og partikkel-interaksjoner.

Et svart hull med omkretsende partikler

Grunnleggende funksjonalitet

De svarte hullene trekker inn spillerens skip, nærliggende fiender og (etter neste opplæring) partikler, men vil avvise kuler.

Det er mange mulige funksjoner vi kan bruke for tiltrekning eller frastøt. Det enkleste er å bruke konstant kraft, slik at det svarte hullet trekker med samme styrke uavhengig av objektets avstand. Et annet alternativ er å få kraften til å øke lineært, fra null til en viss maksimal avstand, til full styrke for gjenstander direkte på toppen av det svarte hullet. Hvis vi ønsker å modellere tyngdekraften mer realistisk, kan vi bruke det inverse firkantet av avstanden, noe som betyr at tyngdekraften er proporsjonal med 1 / (avstand ^ 2).

Vi vil faktisk bruke hver av disse tre funksjonene til å håndtere forskjellige objekter. Kulene vil bli avstøtet med en konstant kraft; Fiender og spillerens skip vil bli tiltrukket av en lineær kraft; og partiklene vil bruke en invers firkantfunksjon.

Vi lager en ny klasse for svarte hull. La oss starte med grunnleggende funksjonalitet:

 klasse BlackHole: offentlig enhet beskyttet: int mHitPoints; offentlig: BlackHole (const tVector2f & posisjon); ugyldig oppdatering (); void draw (tSpriteBatch * spriteBatch); void wasShot (); tomrom (); ; BlackHole :: BlackHole (const tVector2f & posisjon): mHitPoints (10) mImage = Art :: getInstance () -> getBlackHole (); mPosition = posisjon; mRadius = mImage-> getSurfaceSize (). bredde / 2.0f; mKind = kBlackHole;  void BlackHole :: wasShot () mHitPoints--; hvis (mHitPoints <= 0)  mIsExpired = true;   void BlackHole::kill()  mHitPoints = 0; wasShot();  void BlackHole::draw(tSpriteBatch* spriteBatch)  // make the size of the black hole pulsate float scale = 1.0f + 0.1f * sinf(tTimer::getTimeMS() * 10.0f / 1000.0f); spriteBatch->tegne (1, mImage, tPoint2f ((int32_t) mPosition.x, (int32_t) mPosition.y), tOptional(), mColor, mOrientation, getSize () / 2.0f, tVector2f (skala)); 

De svarte hullene tar ti skudd for å drepe. Vi justerer skalaen til spritet litt for å gjøre det pulserende. Hvis du bestemmer deg for at ødeleggelse av svarte hull bør også gi poeng, må du gjøre lignende tilpasninger til Svart hull klasse som vi gjorde med Fiende klasse.

Deretter får vi de sorte hullene til å bruke en kraft på andre enheter. Vi trenger en liten hjelpemetode fra vår EntityManager:

 std :: liste EntityManager :: getNearbyEntities (const tPoint2f & pos, float radius) std :: liste resultat; for (std :: liste:: iterator iter = mEntities.begin (); iter! = mEntities.end (); iter ++) if (* iter) hvis (pos.distanceSquared ((* iter) -> getPosition ()) < radius * radius)  result.push_back(*iter);    return result; 

Denne metoden kan gjøres mer effektiv ved å bruke en mer komplisert romlig partisjonering, men for antall enheter vi vil ha, er det greit som det er.

Nå kan vi få de svarte hullene til å bruke kraft i deres Blackhole :: oppdatering () metode:

 void BlackHole :: update () std :: list enheter = EntityManager :: getInstance () -> getNearbyEntities (mPosition, 250); for (std :: liste:: iterator iter = entities.begin (); iter! = entities.end (); iter ++) if ((iter) -> getKind () == kEnemy &&! ((Enemy *) (* iter)) -> getIsActive ()) // Gjør ingenting annet hvis ((* iter) -> getKind () == kBullet) tVector2f temp = ((* iter) -> getPosition () - mPosition); (* iter) -> setVelocity ((* iter) -> getVelocity () + temp.normalize () * 0.3f);  ellers tVector2f dPos = mPosition - (* iter) -> getPosition (); flytlengde = dPos.length (); (* iter) -> setVelocity ((* iter) -> getVelocity () + dPos.normalize () * tMath :: mix (2.0f, 0.0f, lengde / 250.0f)); 

Svarte hull påvirker bare enheter innenfor en valgt radius (250 piksler). Kuler innenfor denne radiusen har en konstant frastøtende kraft påført, mens alt annet har en lineær tiltrekkende kraft påført.

Vi må legge til kollisionshåndtering for sorte hull til EntityManager. Legg til en std :: liste for svarte hull som vi gjorde for de andre typer enheter, og legg til følgende kode i EntityManager :: handleCollisions ():

 // håndter kollisjoner med svarte hull for (std :: liste:: iterator i = mBlackHoles.begin (); jeg! = mBlackHoles.end (); jeg ++) for (std :: liste:: iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) if ((* j) -> getIsActive () && isColliding (* i, * j)) (* j) -> wasShot ();  for (std :: liste:: iterator j = mBullets.begin (); j! = mBullets.end (); j ++) hvis (isColliding (* i, * j)) (* j) -> setExpired (); (* I) -> wasShot ();  hvis (isColliding (PlayerShip :: getInstance (), * i)) KillPlayer (); gå i stykker; 

Til slutt åpner du EnemySpawner klasse og få det til å lage noen svarte hull. Jeg begrenset det maksimale antall sorte hull til to, og ga en på 600 sjanse for at et svart hull skulle gyte hver ramme.

 hvis (EntityManager :: getInstance () -> getBlackHoleCount () < 2 && int32_t(tMath::random() * mInverseBlackHoleChance) == 0)  EntityManager::getInstance()->legg til (ny BlackHole (GetSpawnPosition ())); 

Konklusjon

Vi har diskutert og lagt til virtuelle gamepads, og lagt til svarte hull ved hjelp av ulike kraftformler. Shape Blaster begynner å se ganske bra ut. I neste del legger vi til noen galte, over-the-top partikkeleffekter.

referanser

  • Foto kreditt: Wii controller av kazuma jp.