Gör en Neon Vector Shooter för iOS virtuella spelkuddar och svarta hål

I den här serien av handledningar visar jag dig hur man gör en Geometry Wars-inspirerad tvillingstickskytte med neongrafik, galen partikeleffekter och fantastisk musik, för iOS med C ++ och OpenGL ES 2.0. I den här delen lägger vi till de virtuella gamepadkontrollerna och "Black hole" -fienderna.

Översikt

I serien hittills har vi satt upp den grundläggande speluppspelningen för vår neon twin stick shooter, Shape Blaster. Därefter lägger vi till två skärm "virtuella gamepads" för att styra skeppet med.


Ingång är ett måste för vilket videospel som helst, och iOS ger oss en intressant och tvetydig utmaning med multi-touch-inmatning. Jag ska visa dig ett tillvägagångssätt, baserat på begreppet virtuella gamepads, där vi simulerar hårdvaruspel genom att bara använda touch och lite komplicerad logik för att räkna ut saker. Efter att ha lagt till virtuella gamepads för multi-touch-ingång, lägger vi också till svarta hål i spelet.

Virtuella spelportar

På skärmen är pekbaserade kontroller det primära sättet att mata in för de flesta iPhone- och iPad-baserade appar och spel. I själva verket tillåter iOS användningen av ett multi-touch-gränssnitt, vilket innebär att man läser flera touchpunkter samtidigt. Skönheten hos touch-baserade gränssnitt är att du kan definiera gränssnittet för att vara vad du vill, oavsett om det är en knapp, en virtuell kontrollpinne eller en glidstyrning. Vad vi ska genomföra är ett touch-gränssnitt som jag kallar "virtuella gamepads".

en gamepad beskriver typiskt en standard, plusformad fysisk kontroll som liknar plusgränssnittet på ett Game Boy-system eller PlayStation-kontrollenhet (även känd som en riktningsplatta eller D-pad). En gamepad tillåter rörelse både i upp och ner axel och vänster och höger axel. Resultatet är att du kan signalera åtta olika riktningar, med tillägg av "ingen riktning". I Shape Blaster kommer vårt gamepad-gränssnitt inte att vara fysiskt, men på skärmen, alltså en virtuell gamepad.


En typisk fysisk gamepad; riktningsplattan i detta fall är plusformad.

Även om det bara finns fyra ingångar, finns åtta riktningar (plus neutrala) tillgängliga.

För att ha en virtuell gamepad i vårt spel måste vi känna igen inmatningsinmatning när det händer och konvertera det till en form som spelet förstår.

Den virtuella gamepad som implementeras här fungerar i tre steg:

  1. Bestäm beräkningstypen.
  2. Bestäm om det är i närheten av en skärmspelare.
  3. Emulera beröringen som en nyckel- eller musrörelse.

I varje steg kommer vi att fokusera enbart på den beröring vi har och hålla reda på den senaste touch händelsen vi var tvungna att jämföra. Vi kommer också hålla reda på touch-ID, som bestämmer vilket finger som rör vilken spelport.

Skärmbilden nedan visar hur spelprogrammen kommer att visas på skärmen:

Skärmdump av de sista spelportarna i position.

Lägga till Multi-Touch till Shape Blaster

I Verktyg bibliotek, låt oss titta på händelseklassen som vi huvudsakligen kommer att använda oss av. tTouchEvent inkapslar allt vi behöver för att hantera beröringshändelser på en grundläggande nivå.

 klass tTouchEvent public: enum EventType kTouchBegin, kTouchEnd, kTouchMove,; allmänhet: EventType mEvent; tPoint2f mLocation; uint8_t mID; allmän: tTouchEvent (const EventType & newEvent, const tPoint2f & newLocation, const uint8_t & newID): mEvent (newEvent), mLocation (newLocation), mID (newID) ;

De Event typ tillåter oss att definiera vilken typ av händelser vi tillåter utan att bli alltför komplicerade. mLocation kommer att vara den faktiska beröringspunkten, och mitten kommer att vara finger-id, som börjar vid noll och lägger till ett för varje finger som berörs på skärmen. Om vi ​​definierar konstruktören att bara ta const referenser, vi kommer att kunna inställa händelseklasser utan att uttryckligen skapa namngivna variabler för dem.

Vi ska använda tTouchEvent uteslutande för att skicka beröringshändelser från operativsystemet till vår Inmatning klass. Vi använder det också senare för att uppdatera grafikföreställningen för gamepadsna i VirtualGamepad klass.

Ingångsklassen

Den ursprungliga XNA och C # versionen av Inmatning klassen kan hantera mus, tangentbord och faktiska fysiska gamepad ingångar. Musen används för att skjuta på en godtycklig punkt på skärmen från vilken position som helst; tangentbordet kan användas för att både flytta och skjuta i givna riktningar. Eftersom vi har valt att emulera den ursprungliga inmatningen (för att vara sann mot en "direkt port"), behåller vi det mesta av den ursprungliga koden med namnen tangentbord och mus, även om vi varken har på iOS-enheter.

Så här är vår Inmatning klassen kommer att se ut. För varje enhet behöver vi behålla en "aktuell ögonblicksbild" och "tidigare ögonblicksbild" så att vi kan berätta vad som ändrats mellan den senaste inmatningshändelsen och den aktuella inmatningshändelsen. I vårat fall, mMouseState och mKeyboardState är den "aktuella ögonblicksbilden" och mLastMouseState och mLastKeyboardState representera "föregående ögonblicksbild".

 klass Input: public tSingleton protected: tPoint2f mMouseState; tPoint2f mLastMouseState; tPoint2f mFreshMouseState; std :: vektor mKeyboardState; std :: vektor mLastKeyboardState; std :: vektor mFreshKeyboardState; bool mIsAimingWithMouse; uint8_t mLeftEngaged; uint8_t mRightEngaged; allmänhet: enum KeyType kUp = 0, kLeft, kDown, kRight, kW, kA, kS, kD,; skyddad: tVector2f GetMouseAimDirection () const; skyddad: Input (); allmänhet: tPoint2f getMousePosition () const; tomt uppdatering (); // Kontrollerar om en knapp bara trycktes ner bool wasKeyPressed (KeyType) const; tVector2f getMovementDirection () const; tVector2f getAimDirection () const; void onTouch (const tTouchEvent & msg); vän klass tSingleton; vänklass VirtualGamepad; ; Input :: Input (): mMouseState (-1, -1), mLastMouseState (-1, -1), mIsAimingWithMouse (false), mLeftEngaged (255), mRightEngaged (255) mKeyboardState.resize (8); mLastKeyboardState.resize (8); mFreshKeyboardState.resize (8); för (size_t i = 0; i < 8; i++)  mKeyboardState[i] = false; mLastKeyboardState[i] = false; mFreshKeyboardState[i] = false;   tPoint2f Input::getMousePosition() const  return mMouseState; 

Uppdatering av inmatning

På en dator är varje händelse vi får är "distinkt", vilket innebär att en musrörelse är annorlunda än att trycka på brevet en, och även brevet en är annorlunda än brevet S som vi kan berätta är det inte exakt samma händelse.

Med IOS, vi bara någonsin få beröra ingångshändelser, och en knapptryckning är inte tillräckligt tydlig från en annan för att vi ska kunna berätta om det är meningen att det är en musrörelse eller en knapptryckning eller ens vilken nyckel det är. Alla händelser ser exakt ut ur vår synvinkel.

För att få reda på denna tvetydighet introducerar vi två nya medlemmar, mFreshMouseState och mFreshKeyboardState. Deras syfte är att aggregera eller "fånga alla" händelserna i en viss ram utan att ändra de andra tillståndsvariablerna på annat sätt. När vi väl är nöjda har en ram gått, vi kan uppdatera det aktuella tillståndet med de "fria" medlemmarna genom att ringa Input :: uppdatering. Input :: uppdatering berättar också vårt ingående tillstånd att avancera.

 void Input :: update () mLastKeyboardState = mKeyboardState; mLastMouseState = mMouseState; mKeyboardState = mFreshKeyboardState; mMouseState = mFreshMouseState; om (mKeyboardState [kLeft] || mKeyboardState [kRight] || mKeyboardState [kUp] || mKeyboardState [kDown]) mIsAimingWithMouse = false;  annars om (mMouseState! = mLastMouseState) mIsAimingWithMouse = true; 

Eftersom vi gör det en gång per ram, vi ringer Input :: update () inifrån GameRoot :: onRedrawView ():

 // I GameRoot :: onRedrawView () Ingång :: getInstance () -> uppdatering ();

Låt oss nu titta på hur vi vänder på inmatning till antingen en simulerad mus eller tangentbord. Först ska vi planera att ha två olika rektangulära områden som representerar de virtuella gamepaderna. Något utanför dessa områden kommer vi att överväga "definitivt en mushändelse"; Allting inne, vi kommer att överväga "definitivt ett tangentbord händelse."

Något i de röda rutorna kommer vi att kartlägga till vår simulerade tangentbordsinmatning; allt annat vi ska behandla som musinsats.

Låt oss titta på Input :: onTouch (), som får alla beröringshändelser. Vi tar en stor bild titta på metoden och notera bara områden ATT GÖRA där mer specifik kod ska vara:

 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); om (! mouseDown) if (msg.mID == mLeftEngaged) // TODO: Ställ in alla rörelsekoder som "tangent upp" annars om (msg.mID == mRightEngaged) // TODO: Ställ in alla skjutnycklar som "key up" om (mouseDown && tRectf (leftPoint, 164, 164) .contains (intPoint)) mLeftEngaged = msg.mID; // TODO: Ställ in alla rörelseknappar som "key up" // TODO: Bestäm vilka rörelsestangenter som ska ställas in om (mouseDown && tRectf (rightPoint, 164, 164) .contains (intPoint)) mRightEngaged = msg.mID; // TODO: Ställ in alla avbränningsnycklar som "key up" // TODO: Bestäm vilka brännnycklar som ska ställas in om (! TRectf (leftPoint, 164, 164) .contains (intPoint) &&! TRectf (rightPoint, 164, 164) .contains (intPoint)) // Om vi ​​gör det här, var det definitivt en "mushändelse" mFreshMouseState = tPoint2f ((int32_t) msg.mLocation.x, (int32_t) msg.mLocation.y); 

Koden är enkel nog, men det finns en del kraftfull logik som jag påpekar:

  1. Vi bestämmer var vänster och höger gamepads kommer att vara på skärmen, så att vi kan se om vi är i dem när vi rör ner eller släpper. Dessa lagras i leftPoint och rightPoint lokala variabler.
  2. Vi bestämmer mousedown state: om vi "trycker" med ett finger måste vi veta om det är inom leftPoints rekt eller rightPoints rekt, och vidta åtgärder för att uppdatera färsk Ange för tangentbordet. Om det inte finns någon rekt, antar vi att det är en mushändelse istället och uppdatera färsk staten för mus.
  3. Slutligen håller vi reda på berörings-ID-erna (eller finger-ID-numren) när de trycks in. om vi upptäcker ett finger som lyfter av ytan och det är associerat med en aktiv gamepad, återställer vi det simulerade tangentbordet för nämnda gamepad i enlighet därmed.

Nu när vi ser den stora bilden, låt oss borra ner lite längre.

Fylla i luckorna

När ett finger lyfts av ytan på iPhone eller iPad, kontrollerar vi om det är ett finger vi vet är på en gamepad och, om så, återställer vi alla "simulerade nycklar" för den gamepad:

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

Situationen är något annorlunda när det är en touch som börjar på ytan eller rör sig; Vi kontrollerar för att se om kontakten ligger inom antingen gamepad. Eftersom koden för båda spelportarna är likartad, tar vi bara en titt på den vänstra gamepaden (som handlar om rörelse).

När vi får en touch-händelse, rensar vi tangentbordstillståndet helt för den specifika spelporten och kontrollerar inom oss det rätta området för att bestämma vilken knapp eller tangenter som ska tryckas. Så även om vi har totalt åtta riktningar (plus neutral), kontrollerar vi bara fyra rektanglar: en för upp, en för ner, en för vänster och en för höger.

De nio intressanta områdena i vår gamepad.
 om (mouseDown && tRectf (leftPoint, 164, 164) .contains (intPoint)) mLeftEngaged = msg.mID; mFreshKeyboardState [kA] = false; mFreshKeyboardState [kD] = false; mFreshKeyboardState [kW] = false; mFreshKeyboardState [kS] = false; om (tRectf (leftPoint, 72, 164) .contains (intPoint)) mFreshKeyboardState [kA] = true; mFreshKeyboardState [kD] = false;  annars om (tRectf (leftPoint + tPoint2f (128, 0), 72, 164) .contains (intPoint)) mFreshKeyboardState [kA] = false; mFreshKeyboardState [kD] = true;  om (tRectf (leftPoint, 164, 72) .contains (intPoint)) mFreshKeyboardState [kW] = true; mFreshKeyboardState [kS] = false;  annars om (tRectf (leftPoint + tPoint2f (0, 128), 164, 72) .contains (intPoint)) mFreshKeyboardState [kW] = false; mFreshKeyboardState [kS] = true; 

Visar grafik för Virtual Gamepad

Om du kör spelet nu har du virtuellt gamepad-stöd, men du kommer inte att kunna se var de virtuella gamepaderna startar eller slutar.

Det är här VirtualGamepad klassen kommer i spel. De VirtualGamepadHuvudsyftet är att rita gamepaderna på skärmen. Sättet som vi kommer att visa gamepad kommer att vara hur andra spel tenderar att göra det om de har gamepads: som en större "bas" cirkel och en mindre "control stick" cirkel kan vi flytta. Det här ligner en arkad joystick från toppen och lättare att rita än några andra alternativ.

Först märker du att bildfilerna vpad_top.png och vpad_bot.png har lagts till i projektet. Låt oss ändra Konst klass för att ladda dem:

 klass Art: public tSingleton skyddad: ... tTexture * mVPadBottom; tTexture * mVPadTop; ... public: ... tTexture * getVPadBottom () const; tTexture * getVPadTop () const; ... vänklass 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 kommer att dra båda gamepads på skärmen och behåll stat information i medlemmarna mLeftStick och mRightStick på var man ska rita "styrspinnarna" på gamepadsna.

Jag har valt några lite godtyckliga positioner för gamepadsna, som initialiseras i mLeftPoint och mRightPoint medlemmar-beräkningarna placerar dem på ungefär 3,75% i från vänster eller höger kant på skärmen och cirka 13% i från undersidan av skärmen. Jag baserade dessa mätningar på ett kommersiellt spel med liknande virtuella gamepads men olika spel.

 klass VirtualGamepad: public tSingleton public: enum State kCenter = 0x00, kTop = 0x01, kBottom = 0x02, kLeft = 0x04, kRight = 0x08, kTopLeft = 0x05, kTopRight = 0x09, kBottomLeft = 0x06, kBottomRight = 0x0a,; skyddad: tPoint2f mLeftPoint; tPoint2f mRightPoint; int mLeftStick; int mRightStick; skyddad: VirtualGamepad (); tomt DrawStickAtPoint (tSpriteBatch * spriteBatch, const tPoint2f & punkt, Statligt tillstånd); void UpdateBasedOnKeys (); allmänhet: tomtdragning (tSpriteBatch * spriteBatch); tomt uppdatering (const tTouchEvent & msg); vän klass tSingleton; vän klass 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 tidigare nämnt, mLeftStick och mRightStick är bitmaskar, och deras användning är att bestämma vart man ska rita gamepadens inre cirkel. Vi beräknar bitmask i metoden VirtualGamepad :: UpdateBasedOnKeys ().

Denna metod kallas omedelbart efter Input :: onTouch, så att vi kan läsa de "fräscha" statsmedlemmarna och vet att de är aktuella:

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

För att rita en individuell gamepad ringer vi VirtualGamepad :: DrawStickAtPoint (); den här metoden känner inte till eller bryr sig om vilket spel som du ritar, det vet bara vart du vill ha det ritat och staten ska rita in. Eftersom vi har använt bitmasker och beräknats före tid blir vår metod mindre och lättare att läsa:

 void VirtualGamepad :: DrawStickAtPoint (tSpriteBatch * spriteBatch, const tPoint2f och punkt, Statligt tillstånd) tPoint2f offset = tPoint2f (18, 18); spriteBatch-> draw (10, Art :: getInstance () -> getVPadBottom (), punkt, tOptional()); switch (state) fall kCenter: offset + = tPoint2f (0, 0); ha sönder; fallet kTopLeft: offset + = tPoint2f (-13, -13); ha sönder; fallet kTop: offset + = tPoint2f (0, -18); ha sönder; fallet kTopRight: offset + = tPoint2f (13, -13); ha sönder; fallet kRight: offset + = tPoint2f (18, 0); ha sönder; fallet kBottomRight: offset + = tPoint2f (13, 13); ha sönder; fallet kBottom: offset + = tPoint2f (0, 18); ha sönder; fallet kBottomLeft: offset + = tPoint2f (-13, 13); ha sönder; fall kLeft: offset + = tPoint2f (-18, 0); ha sönder;  spriteBatch-> draw (11, Art :: getInstance () -> getVPadTop (), punkt + offset, tOptional()); 

Att rita två gamepads blir mycket lättare eftersom det bara är ett samtal till ovanstående metod två gånger. Låt oss titta på VirtualGamepad :: draw ():

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

Slutligen måste vi faktiskt rita den virtuella spelporten, så in GameRoot :: onRedrawView (), lägg till följande rad:

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

Det är allt. Om du kör spelet nu borde du se de virtuella gamepaderna i full effekt. När du rör inuti vänster gamepad borde du flytta runt. När du rör inuti rätt gamepad bör din skjutriktning ändras. Faktum är att du kan använda båda gamepadarna samtidigt, och även flytta med vänster gamepad och trycka utanför den externa spelporten för att få musrörelsen. Och när du släpper, slutar du flytta och (eventuellt) sluta fotografera.

Sammanfattning av Virtual Gamepad Technique

Vi har fullt implementerat virtuellt gamepad-stöd, och det fungerar, men du kanske tycker det är lite clunky eller svårt att använda. Varför är det så? Det är här den verkliga utmaningen av touch-baserade kontroller på IOS kommer med traditionella spel som inte ursprungligen utformades för dem.

Du är dock inte ensam. Många spel antingen lider av dessa problem och har övervunnit dem.

Här är några saker jag har observerat med pekskärmsinmatning; du kanske har några liknande observationer själv:

Först har spelkontrollen en annan känsla än en platt pekskärm; du vet var ditt finger är på en riktig gamepad och hur man håller fingrarna från att glida av sig. Men på en pekskärm kan dina fingrar drifta lite för långt borta från beröringszonen, så din inmatning kanske inte är korrekt igenkänd och du kanske inte inser att det är fallet förrän det är för sent.

För det andra kan du också ha märkt när du spelar med pekontroller, att din hand döljer din vision, så att du skickar kan drabbas av en fiende under din hand som du inte såg att börja med!

Slutligen kan du upptäcka att beröringsytorna är enklare att använda på en iPad än en iPhone eller vice versa. Så vi har problem med en annan skärmstorlek som påverkar vår "inmatningsarealstorlek", vilket definitivt är något vi inte upplever så mycket på en stationär dator. (De flesta tangentbord och möss är lika stora och fungerar på samma sätt eller kan justeras.)

Här är några ändringar som du kan göra för ingångssystemet som beskrivs i den här artikeln:

  • Rita din gamepads centrala plats där din beröring börjar. Detta gör det möjligt för spelarens hand att skifta någonsin så lite utan påverkan, och innebär att de kan röra var som helst på skärmen.
  • Gör ditt "spelbara område" mindre och flytta gamepadet från det spelbara området helt. Nu kommer dina fingrar inte att hindra din åsikt.
  • Skapa separata, tydliga användargränssnitt för iPhone och iPad. Detta gör att du kan anpassa designen baserat på enhetstypen, men det kräver också att du har olika enheter att testa mot.
  • Gör fiender eller spelaren skicka lite långsammare. Det här låter användaren uppleva spelet lättare, men det kan också göra ditt spel lättare att vinna.
  • Ditch virtuella gamepads helt och hållet och använd ett annat system. Du är trots allt ansvarig!

Återigen är det upp till dig vad du vill göra och hur du vill göra det. På plussidan finns det många sätt att göra pekinmatning. Den svåra delen är att få det rätt och göra dina spelare lyckliga.

Svarta hål

En av de mest intressanta fienderna i Geometry Wars är svart hål. Låt oss undersöka hur vi kan göra något liknande i Shape Blaster. Vi kommer att skapa den grundläggande funktionaliteten nu, och vi kommer att återkomma fienden i nästa handledning för att lägga till partikel effekter och partikel interaktioner.

Ett svart hål med kretsande partiklar

Grundfunktionalitet

De svarta hålen kommer att dra in spelarens skepp, närliggande fiender, och (efter nästa handledning) partiklar, men kommer att avvärja kulor.

Det finns många möjliga funktioner vi kan använda för attraktion eller avstängning. Det enklaste är att använda konstant kraft så att det svarta hålet drar med samma styrka, oavsett objektets avstånd. Ett annat alternativ är att öka kraften linjärt, från noll vid ett visst maximalt avstånd, till full styrka för objekt direkt ovanpå det svarta hålet. Om vi ​​skulle vilja modellera gravitationen mer realistiskt kan vi använda det inversa kvadratet av avståndet, vilket innebär att tyngdkraften är proportionell mot 1 / (avstånd ^ 2).

Vi använder faktiskt alla dessa tre funktioner för att hantera olika objekt. Kulorna kommer att avstötas med en konstant kraft; Fienderna och spelarens skepp kommer att lockas med en linjär kraft; och partiklarna kommer att använda en invers kvadratfunktion.

Vi gör en ny klass för svarta hål. Låt oss börja med grundläggande funktionalitet:

 klass BlackHole: offentlig enhet skyddad: int mHitPoints; allmänhet: BlackHole (const tVector2f & position); tomt uppdatering (); void draw (tSpriteBatch * spriteBatch); void wasShot (); tomrumsdöd (); ; BlackHole :: BlackHole (const tVector2f & position): mHitPoints (10) mImage = Art :: getInstance () -> getBlackHole (); mPosition = position; mRadius = mImage-> getSurfaceSize (). bredd / 2.0f; mKind = kBlackHole;  void BlackHole :: wasShot () mHitPoints--; om (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->rita (1, mImage, tPoint2f ((int32_t) mPosition.x, (int32_t) mPosition.y), tOptional(), mColor, mOrientation, getSize () / 2.0f, tVector2f (skala)); 

De svarta hålen tar tio skott för att döda. Vi justerar spritens skala en aning för att den ska pulseras. Om du bestämmer dig för att förstöra svarta hål bör du också ge poäng, du måste göra liknande anpassningar till Svart hål klass som vi gjorde med Fiende klass.

Därefter gör vi att de svarta hålen faktiskt tillämpar en kraft på andra enheter. Vi behöver en liten hjälpmetod från vår EntityManager:

 std :: listan EntityManager :: getNearbyEntities (const tPoint2f & pos, floatradie) std :: lista resultat; för (std :: listan:: iterator iter = mEntities.begin (); iter! = mEntities.end (); iter ++) om (* iter) om (pos.distanceSquared ((* iter) -> getPosition ()) < radius * radius)  result.push_back(*iter);    return result; 

Denna metod kan effektiviseras genom att använda ett mer komplicerat rumsligt partitioneringsschema, men för antalet enheter vi kommer att ha, är det bra som det är.

Nu kan vi göra de svarta hålen applicera kraft i deras Blackhole :: uppdatering () metod:

 void BlackHole :: uppdatering () std :: list enheter = EntityManager :: getInstance () -> getNearbyEntities (mPosition, 250); för (std :: listan:: iterator iter = entities.begin (); iter! = entities.end (); iter ++) if ((iter) -> getKind () == kEnemy &&! ((Enemy *) (* iter)) -> getIsActive ()) // Gör ingenting annat om ((* iter) -> getKind () == kBullet) tVector2f temp = ((* iter) -> getPosition () - mPosition); (* iter) -> setVelocity ((* iter) -> getVelocity () + temp.normalize () * 0.3f);  annars tVector2f dPos = mPosition - (* iter) -> getPosition (); floatlängd = dPos.length (); (* iter) -> setVelocity ((* iter) -> getVelocity () + dPos.normalize () * tMath :: mix (2.0f, 0.0f, längd / 250.0f)); 

Svarta hål påverkar endast enheter inom en vald radie (250 pixlar). Kulor inom denna radie har en konstant repulsiv kraft, medan allt annat har en linjär attraktiv kraft som appliceras.

Vi måste lägga till kollisionshantering för svarta hål till EntityManager. Lägg till en std :: listan för svarta hål som vi gjorde för de andra typerna av enheter, och lägg till följande kod i EntityManager :: handleCollisions ():

 // hantera kollisioner med svarta hål för (std :: lista:: iterator i = mBlackHoles.begin (); jag! = mBlackHoles.end (); jag ++) för (std :: lista:: iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) if ((* j) -> getIsActive () && isColliding (* i, * j)) (* j) -> wasShot ();  för (std :: lista:: iterator j = mBullets.begin (); j! = mBullets.end (); j ++) om (isColliding (* i, * j)) (* j) -> setExpired (); (* I) -> wasShot ();  om (isColliding (PlayerShip :: getInstance (), * i)) KillPlayer (); ha sönder; 

Slutligen öppna EnemySpawner klass och få det att skapa några svarta hål. Jag begränsade det maximala antalet svarta hål till två och gav en i 600 chans att ett svart hål skulle gyta varje ram.

 om (EntityManager :: getInstance () -> getBlackHoleCount () < 2 && int32_t(tMath::random() * mInverseBlackHoleChance) == 0)  EntityManager::getInstance()->lägg till (ny BlackHole (GetSpawnPosition ())); 

Slutsats

Vi har diskuterat och lagt till virtuella gamepads och lagt till svarta hål med olika kraftformler. Shape Blaster börjar se ganska bra ut. I nästa del lägger vi till några galna, över-the-top partikel effekter.

referenser

  • Foto kredit: Wii controller av kazuma jp.