Maak een Neon Vector Shooter voor iOS virtuele gamepads en zwarte gaten

In deze serie tutorials laat ik je zien hoe je een Geometry Wars-geïnspireerde twin-stick shooter maakt met neon graphics, gekke partikeleffecten en geweldige muziek, voor iOS met C ++ en OpenGL ES 2.0. In dit deel voegen we de virtuele gamepad-besturingselementen en de 'zwart gat'-vijanden toe.

Overzicht

In de serie tot nu toe hebben we de basisgameplay ingesteld voor onze neon twin-stick shooter, Shape Blaster. Vervolgens voegen we twee virtuele gamepads op het scherm toe om het schip mee te besturen.


Input is een must voor elk videogame en iOS biedt ons een interessante en dubbelzinnige uitdaging met multi-touch-invoer. Ik zal je één aanpak laten zien, gebaseerd op het concept van virtuele gamepads, waar we hardware gamepads zullen simuleren door alleen aanraking en een beetje ingewikkelde logica te gebruiken om dingen uit te zoeken. Na het toevoegen van de virtuele gamepads voor multi-touch-invoer, voegen we ook zwarte gaten toe aan het spel.

Virtuele gamepads

Schermbediening op het scherm is het belangrijkste invoermiddel voor de meeste iPhone- en iPad-gebaseerde apps en games. In feite maakt iOS het gebruik van een multi-touch-interface mogelijk, wat betekent dat er verschillende aanraakpunten tegelijkertijd worden gelezen. Het mooie van op aanraking gebaseerde interfaces is dat u de interface kunt definiëren zoals u wilt, of het nu gaat om een ​​knop, een virtuele bedieningsstick of een schuifregelaar. Wat we zullen implementeren is een aanraakinterface die ik 'virtuele gamepads' zal noemen.

EEN gamepad beschrijft typisch een standaard, plus-vormige fysieke controle zoals de plus-interface op een Game Boy-systeem of PlayStation-controller (ook bekend als een richtingspad of een D-pad). Een gamepad maakt beweging mogelijk in zowel de opwaartse als neerwaartse as en de linker en rechter as. Het resultaat is dat je acht verschillende richtingen kunt signaleren, met de toevoeging "geen richting". In Shape Blaster is onze gamepad-interface niet fysiek, maar op het scherm, dus een virtueel gamepad.


Een typische fysieke gamepad; de directionele pad is in dit geval plus-vormig.

Hoewel er slechts vier ingangen zijn, zijn er acht richtingen (plus neutraal) beschikbaar.

Om een ​​virtuele gamepad in onze game te hebben, moeten we touch-invoer herkennen wanneer dit gebeurt en deze omzetten naar een vorm die de game al begrijpt.

De virtuele gamepad die hier wordt geïmplementeerd, werkt in drie stappen:

  1. Bepaal het aanraaktype.
  2. Bepaal of het zich in de buurt van een gamepad op het scherm bevindt.
  3. Emuleer de aanraking als een toetsdruk of muisbeweging.

Bij elke stap richten we ons uitsluitend op de aanraking die we hebben en houden we de laatste aanraakgebeurtenis bij die we moesten vergelijken. We houden ook het tik op ID, welke bepaalt welke vinger welke gamepad aanraakt.

De onderstaande schermafbeelding laat zien hoe de gamepads op het scherm verschijnen:

Screenshot van de laatste gamepads in positie.

Multi-Touch to Shape Blaster toevoegen

In de Nut bibliotheek, laten we naar de gebeurtenisklasse kijken waar we in de eerste plaats gebruik van zullen maken. tTouchEvent kapselt alles in wat we nodig hebben om op basisniveau touch events aan te kunnen.

 class tTouchEvent public: enum EventType kTouchBegin, kTouchEnd, kTouchMove,; publiek: EventType mEvent; tPoint2f mLocation; uint8_t mID; public: tTouchEvent (const EventType & newEvent, const tPoint2f & newLocation, const uint8_t & newID): mEvent (newEvent), mLocation (newLocation), mID (newID) ;

De EventType stelt ons in staat om het soort evenementen te definiëren dat we zullen toestaan ​​zonder al te ingewikkeld te worden. mLocation zal het daadwerkelijke aanrakingspunt zijn, en mID is de vinger-ID, die begint bij nul en er één wordt toegevoegd voor elke vinger die op het scherm wordt aangeraakt. Als we de constructor definiëren om alleen te nemen const Referenties, we kunnen eventklassen instantiëren zonder expliciet benoemde variabelen voor hen te hoeven maken.

We zullen gebruiken tTouchEvent uitsluitend om aanraakgebeurtenissen vanuit het besturingssysteem naar ons te verzenden Invoer klasse. We zullen het later ook gebruiken om de grafische weergave van de gamepads in de VirtualGamepad klasse.

De invoerklasse

De originele XNA- en C # -versie van de Invoer klasse kan overweg met muis-, toetsenbord- en daadwerkelijke fysieke gamepad-ingangen. De muis wordt gebruikt om vanuit elke positie op een willekeurig punt op het scherm te schieten; het toetsenbord kan worden gebruikt om zowel in een bepaalde richting te bewegen als te schieten. Omdat we ervoor hebben gekozen om de originele invoer te emuleren (om trouw te blijven aan een "directe poort"), behouden we het grootste deel van de originele code hetzelfde, met behulp van de namen toetsenbord en muis, ook al hebben we geen van beide op iOS-apparaten.

Dit is hoe onze Invoer de klas zal er uitzien. Voor elk apparaat moeten we een 'huidige momentopname' en een 'vorige snapshot' behouden, zodat we kunnen zien wat er is gewijzigd tussen de laatste invoergebeurtenis en de huidige invoergebeurtenis. In ons geval, mMouseState en mKeyboardState zijn de "huidige snapshot", en mLastMouseState en mLastKeyboardState vertegenwoordigen de "vorige momentopname".

 klasse Input: public tSingleton beschermd: tPoint2f mMouseState; tPoint2f mLastMouseState; tPoint2f mFreshMouseState; std :: vector mKeyboardState; std :: vector mLastKeyboardState; std :: vector mFreshKeyboardState; bool mIsAimingWithMouse; uint8_t mLeftEngaged; uint8_t mRightEngaged; public: enum KeyType kUp = 0, kLeft, kDown, kRight, kW, kA, kS, kD,; beschermd: tVector2f GetMouseAimDirection () const; beveiligd: Input (); publiek: tPoint2f getMousePosition () const; ongeldige update (); // Controleert of een sleutel zojuist is ingedrukt bool wasKeyPressed (KeyType) const; tVector2f getMovementDirection () const; tVector2f getAimDirection () const; void onTouch (const tTouchEvent & msg); vriend klas tSingleton; vriendenklasse VirtualGamepad; ; Invoer :: 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; 

Input bijwerken

Op een pc is elke gebeurtenis 'duidelijk', wat betekent dat een muisbeweging anders is dan de letter indrukken EEN, en zelfs de brief EEN is anders genoeg dan de letter S dat we kunnen zeggen dat het dat niet is precies dezelfde gebeurtenis.

Met iOS, wij alleen ooit krijg invoerinvoergebeurtenissen en één aanraking is niet duidelijk genoeg van elkaar om te zeggen of het een muisbeweging of een toetsdruk moet zijn, of zelfs welke toets het is. Alle evenementen zien er vanuit ons oogpunt hetzelfde uit.

Om deze ambiguïteit te helpen ontdekken, introduceren we twee nieuwe leden, mFreshMouseState en mFreshKeyboardState. Hun doel is om de gebeurtenissen in een bepaald frame te verzamelen, of "te vangen", zonder de andere toestandsvariabelen anders aan te passen. Zodra we tevreden zijn dat een frame is verstreken, kunnen we de huidige status met de "nieuwe" leden bijwerken door te bellen Input ::-update. Input ::-update vertelt ook onze invoerstatus om verder te gaan.

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

Omdat we het eenmaal per frame doen, bellen we Input :: update () van binnenuit GameRoot :: onRedrawView ():

 // In GameRoot :: onRedrawView () Input :: getInstance () -> update ();

Laten we nu eens kijken naar hoe we de invoer veranderen in een gesimuleerde muis of een toetsenbord. Ten eerste zullen we van plan zijn om twee verschillende rechthoekige gebieden te hebben die de virtuele gamepads representeren. Alles buiten deze gebieden beschouwen we als "zeker een muisevenement"; alles binnenin, zullen we "zeker een toetsenbordevenement" overwegen.

Alles in de rode vakken zullen we toewijzen aan onze gesimuleerde toetsenbordinvoer; alles wat we zullen behandelen als muisinvoer.

Laten we eens kijken Input :: OnTouch (), die alle aanraakgebeurtenissen krijgt. We nemen een grote foto van de methode en noteren alleen gebieden TE DOEN waar meer specifieke code zou moeten zijn:

 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); if (! mouseDown) if (msg.mID == mLeftEngaged) // TODO: stel alle verplaatsingstoetsen in als "key up" else if (msg.mID == mRightEngaged) // TODO: stel alle ontsteeksleutels in als "sleutel omhoog" als (mouseDown && tRectf (leftPoint, 164, 164) .contains (intPoint)) mLeftEngaged = msg.mID; // TODO: stel alle verplaatsingstoetsen in als "key up" // TODO: Bepaal welke verplaatsingstoetsen moeten worden ingesteld if (mouseDown && tRectf (rightPoint, 164, 164). Bevat (intPoint)) mRightEngaged = msg.mID; // TODO: Stel alle ontsleuteltoetsen in als "sleutel omhoog" // TODO: Bepaal welke ontbrandingssleutels moeten worden ingesteld als (! TRectf (leftPoint, 164, 164) .contains (intPoint) &&! TRectf (rightPoint, 164, 164) .contains (intPoint)) // Als we hier komen, is touch absoluut een "muisgebeurtenis" mFreshMouseState = tPoint2f ((int32_t) msg.mLocation.x, (int32_t) msg.mLocation.y); 

De code is eenvoudig genoeg, maar er is een aantal krachtige logica die ik zal aangeven:

  1. We bepalen waar de linker- en rechter gamepads op het scherm verschijnen, zodat we kunnen zien of we erin zitten wanneer we aanraken of loslaten. Deze worden opgeslagen in de leftPoint en rightPoint lokale variabelen.
  2. Wij bepalen de mouseDown staat: als we met een vinger "drukken", moeten we weten of het van binnen is leftPoint's rect of rightPointrect, en zo ja actie ondernemen om de vers staat voor het toetsenbord. Als het in geen van beide rect is, nemen we aan dat het in plaats daarvan een muisgebeurtenis is en werkt het vers staat voor de muis.
  3. Ten slotte houden we de aanraak-ID's (of vinger-ID's) bij terwijl ze worden ingedrukt; als we een vinger detecteren die van het oppervlak afkomt en deze is gekoppeld aan een actieve gamepad, dan zullen we het gesimuleerde toetsenbord voor dat gamepad overeenkomstig resetten.

Nu we het grote plaatje zien, laten we een beetje verder gaan.

De gaten opvullen

Wanneer een vinger van het oppervlak van de iPhone of iPad wordt getild, controleren we of het een vinger is waarvan we weten dat deze zich op een gamepad bevindt en zo ja, we resetten alle "gesimuleerde sleutels" voor die gamepad:

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

De situatie is enigszins anders wanneer er een aanraking op het oppervlak begint of beweegt; we controleren om te zien of de aanraking zich binnen een gamepad bevindt. Aangezien de code voor beide gamepads vergelijkbaar is, kijken we alleen naar de linker gamepad (die zich bezighoudt met beweging).

Telkens wanneer we een aanraakgebeurtenis krijgen, maken we de toetsenbordstatus volledig leeg voor dat gamepad en controleren we binnen ons rect gebied om te bepalen met welke toets of toetsen. Dus hoewel we in totaal acht richtingen hebben (plus neutraal), controleren we alleen vier rechthoeken: één voor omhoog, één voor omlaag, één voor links en één voor rechts.

De negen interessegebieden in onze gamepad.
 if (mouseDown && tRectf (leftPoint, 164, 164). bevat (intPoint)) mLeftEngaged = msg.mID; mFreshKeyboardState [kA] = false; mFreshKeyboardState [kD] = false; mFreshKeyboardState [kW] = false; mFreshKeyboardState [kS] = false; if (tRectf (leftPoint, 72, 164). bevat (intPoint)) mFreshKeyboardState [kA] = true; mFreshKeyboardState [kD] = false;  else if (tRectf (leftPoint + tPoint2f (128, 0), 72, 164) .contains (intPoint)) mFreshKeyboardState [kA] = false; mFreshKeyboardState [kD] = true;  if (tRectf (leftPoint, 164, 72). bevat (intPoint)) mFreshKeyboardState [kW] = true; mFreshKeyboardState [kS] = false;  else if (tRectf (leftPoint + tPoint2f (0, 128), 164, 72). bevat (intPoint)) mFreshKeyboardState [kW] = false; mFreshKeyboardState [kS] = true; 

Afbeeldingen weergeven voor het virtuele gamepad

Als je het spel nu uitvoert, heb je virtuele gamepad-ondersteuning, maar je zult niet echt kunnen zien waar de virtuele gamepads beginnen of eindigen.

Dit is waar de VirtualGamepad klasse komt om de hoek kijken. De VirtualGamepadHet belangrijkste doel is om de gamepads op het scherm te tekenen. De manier waarop we de gamepad zullen weergeven, is zoals andere spellen dat doen als ze gamepads hebben: als een grotere "basis" -cirkel en een kleinere "control stick" -cirkel die we kunnen verplaatsen. Dit lijkt op een arcadejoystick van boven naar beneden en is eenvoudiger te tekenen dan sommige andere alternatieven.

Merk allereerst op dat de afbeeldingbestanden vpad_top.png en vpad_bot.png zijn toegevoegd aan het project. Laten we het aanpassen Kunst klasse om ze te laden:

 klasse Art: public tSingleton protected: ... tTexture * mVPadBottom; tTexture * mVPadTop; ... public: ... tTexture * getVPadBottom () const; tTexture * getVPadTop () const; ... vriendenklasse tSingleton; ; Art :: Art () ... mVPadTop = new tTexture (tSurface ("vpad_top.png")); mVPadBottom = new tTexture (tSurface ("vpad_bot.png"));  tTexture * Art :: getVPadBottom () const return mVPadBottom;  tTexture * Art :: getVPadTop () const return mVPadTop; 

De VirtualGamepad klasse zal beide gamepads op het scherm tekenen en behouden Staat informatie in de leden mLeftStick en mRightStick op waar de "bedieningsknoppen" van de gamepads te tekenen.

Ik heb enkele enigszins willekeurige posities gekozen voor de gamepads, die zijn geïnitialiseerd in de mLeftPoint en mRightPoint leden: de berekeningen plaatsen ze op ongeveer 3,75% vanaf de linker- of rechterkant van het scherm en ongeveer 13% vanaf de onderkant van het scherm. Ik heb deze metingen gebaseerd op een commercieel spel met vergelijkbare virtuele gamepads maar met verschillende gameplay.

 class VirtualGamepad: public tSingleton public: enum State kCenter = 0x00, kTop = 0x01, kBottom = 0x02, kLeft = 0x04, kRight = 0x08, kTopLeft = 0x05, kTopRight = 0x09, kBottomLeft = 0x06, kBottomRight = 0x0a,; beschermd: tPoint2f mLeftPoint; tPoint2f mRightPoint; int mLeftStick; int mRightStick; beschermd: VirtualGamepad (); ongeldig DrawStickAtPoint (tSpriteBatch * spriteBatch, const tPoint2f & point, State state); void UpdateBasedOnKeys (); public: void draw (tSpriteBatch * spriteBatch); ongeldige update (const tTouchEvent & msg); vriend klas tSingleton; vriendklasse 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); 

Zoals eerder vermeld, mLeftStick en mRightStick zijn bitmaskers en het gebruik ervan is om te bepalen waar de binnenste cirkel van de gamepad moet worden getrokken. We zullen het bitmasker in de methode berekenen VirtualGamepad :: UpdateBasedOnKeys ().

Deze methode wordt onmiddellijk daarna genoemd Input :: OnTouch, zodat we de "nieuwe" staatsleden kunnen lezen en weten dat ze up-to-date zijn:

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

Om een ​​individuele gamepad te tekenen, bellen we VirtualGamepad :: DrawStickAtPoint (); deze methode weet niet of maakt niet uit welke gamepad je aan het tekenen bent, het weet alleen waar je het getekend wilt hebben en de staat om het in te tekenen. Omdat we bitmasks hebben gebruikt en vooraf berekend, wordt onze methode kleiner en gemakkelijker om lezen:

 void VirtualGamepad :: DrawStickAtPoint (tSpriteBatch * spriteBatch, const tPoint2f & point, State state) tPoint2f offset = tPoint2f (18, 18); spriteBatch-> draw (10, Art :: getInstance () -> getVPadBottom (), point, tOptioneel()); switch (state) case kCenter: offset + = tPoint2f (0, 0); breken; case kTopLeft: offset + = tPoint2f (-13, -13); breken; case kTop: offset + = tPoint2f (0, -18); breken; case kTopRight: offset + = tPoint2f (13, -13); breken; case kRight: offset + = tPoint2f (18, 0); breken; case kBottomRight: offset + = tPoint2f (13, 13); breken; case kBottom: offset + = tPoint2f (0, 18); breken; case kBottomLeft: offset + = tPoint2f (-13, 13); breken; case kLeft: offset + = tPoint2f (-18, 0); breken;  spriteBatch-> draw (11, Art :: getInstance () -> getVPadTop (), punt + offset, tOptional()); 

Het tekenen van twee gamepads wordt veel eenvoudiger omdat het slechts een keer bellen is naar de bovenstaande methode. Laten we eens kijken VirtualGamepad :: draw ():

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

Ten slotte moeten we eigenlijk de virtuele gamepad tekenen, dus in GameRoot :: onRedrawView (), voeg de volgende regel toe:

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

Dat is het. Als je het spel nu uitvoert, zou je de virtuele gamepads volledig moeten zien werken. Wanneer u in de linker gamepad aanraakt, moet u zich verplaatsen. Wanneer u in de juiste gamepad aanraakt, moet uw richting veranderen. Je kunt zelfs beide gamepads tegelijk gebruiken en zelfs bewegen met de linker gamepad en buiten het juiste gamepad raken om muisbewegingen te krijgen. En als je loslaat, stop je met bewegen en (mogelijk) stop je met fotograferen.

Samenvatting van de Virtual Gamepad-techniek

We hebben de virtuele gamepad-ondersteuning volledig geïmplementeerd en het werkt, maar u vindt het misschien een beetje onhandig of moeilijk te gebruiken. Waarom is dat zo? Dit is waar de echte uitdaging van aanraakgebaseerde besturingselementen op iOS wordt geleverd met traditionele games die oorspronkelijk niet voor hen waren ontworpen.

Je bent echter niet de enige. Veel games hebben last van deze problemen en hebben deze overwonnen.

Hier zijn een paar dingen die ik heb waargenomen met invoer via het aanraakscherm; je zou zelf een aantal vergelijkbare observaties kunnen hebben:

Ten eerste hebben gamecontrollers een ander gevoel dan een plat touchscreen; u weet waar uw vinger zich op een echte gamepad bevindt en hoe u voorkomt dat uw vingers wegglippen. Op een touchscreen kunnen uw vingers echter iets te ver uit de aanraakzone drijven, waardoor uw invoer mogelijk niet correct wordt herkend en u zich misschien niet realiseert dat dit het geval is totdat het te laat is.

Ten tweede heb je misschien ook gemerkt dat tijdens het spelen met touch controls, je hand je zicht verduistert, dus je schip kan geraakt worden door een vijand onder je hand die je niet zag om mee te beginnen!

Ten slotte zult u merken dat de aanraakgebieden gemakkelijker te gebruiken zijn op een iPad dan op een iPhone of omgekeerd. We hebben dus problemen met een andere schermgrootte die van invloed is op onze 'invoergebiedsgrootte', iets dat we zeker niet zo vaak op een desktopcomputer ervaren. (De meeste toetsenborden en muizen hebben dezelfde afmetingen en werken op dezelfde manier, of kunnen worden aangepast.)

Hier zijn enkele wijzigingen die u zou kunnen aanbrengen in het invoersysteem dat in dit artikel wordt beschreven:

  • Teken de centrale locatie van je gamepad waar je aanraking begint; hierdoor kan de hand van de speler enigszins schuiven zonder impact, en kunnen ze overal op het scherm raken.
  • Maak je "speelbare gebied" kleiner en verplaats het gamepad volledig van het speelbare gebied. Nu zullen je vingers je zicht niet belemmeren.
  • Maak afzonderlijke, afzonderlijke gebruikersinterfaces voor iPhone en iPad. Hiermee kunt u het ontwerp aanpassen op basis van het apparaattype, maar moet u ook verschillende apparaten hebben om te testen.
  • Maak vijanden of de speler vaart iets langzamer. Hierdoor kan de gebruiker het spel mogelijk gemakkelijker ervaren, maar het maakt je spel mogelijk ook gemakkelijker om te winnen.
  • Verdorren virtuele gamepads helemaal en gebruik een ander schema. Jij hebt tenslotte de leiding!

Nogmaals, het is aan jou wat je wilt doen en hoe je het wilt doen. Aan de positieve kant zijn er veel manieren om aanraakinvoer uit te voeren. Het moeilijke is om het goed te doen en je spelers gelukkig te maken.

Zwarte gaten

Een van de meest interessante vijanden in Geometry Wars is de zwart gat. Laten we eens kijken hoe we iets vergelijkbaars kunnen maken in Shape Blaster. We zullen nu de basisfunctionaliteit maken en we zullen de vijand in de volgende tutorial opnieuw bezoeken om deeltjeseffecten en deeltjesinteracties toe te voegen.

Een zwart gat met deeltjes in een baan om de aarde

Basisfunctionaliteit

De zwarte gaten zullen het schip van de speler, nabije vijanden en (na de volgende tutorial) deeltjes trekken, maar zullen kogels afstoten.

Er zijn veel mogelijke functies die we kunnen gebruiken voor aantrekking of afstoting. Het eenvoudigste is om constante kracht te gebruiken, zodat het zwarte gat met dezelfde kracht trekt, ongeacht de afstand van het object. Een andere optie is om de kracht lineair te laten toenemen, van nul op een maximale afstand, tot de volledige sterkte voor objecten direct bovenop het zwarte gat. Als we de zwaartekracht meer realistisch willen modelleren, kunnen we het inverse kwadraat van de afstand gebruiken, wat betekent dat de zwaartekracht evenredig is met 1 / (afstand ^ 2).

We zullen eigenlijk elk van deze drie functies gebruiken om verschillende objecten te behandelen. De kogels worden afgestoten met een constante kracht; de vijanden en het schip van de speler zullen worden aangetrokken met een lineaire kracht; en de deeltjes zullen een inverse vierkante functie gebruiken.

We zullen een nieuwe les maken voor zwarte gaten. Laten we beginnen met de basisfunctionaliteit:

 class BlackHole: public Entity protected: int mHitPoints; publiek: BlackHole (const tVector2f & position); ongeldige update (); ongeldig tekenen (tSpriteBatch * spriteBatch); void wasShot (); void kill (); ; BlackHole :: BlackHole (const tVector2f & position): mHitPoints (10) mImage = Art :: getInstance () -> getBlackHole (); mPosition = positie; mRadius = mImage-> getSurfaceSize (). width / 2.0f; mKind = kBlackHole;  void BlackHole :: wasShot () mHitPoints--; if (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->draw (1, mImage, tPoint2f ((int32_t) mPosition.x, (int32_t) mPosition.y), tOptional(), mColor, mOrientation, getSize () / 2.0f, tVector2f (schaal)); 

De zwarte gaten nemen tien schoten om te doden. We passen de schaal van de sprite enigszins aan om het te laten pulseren. Als u besluit dat het vernietigen van zwarte gaten ook punten moet toestaan, moet u vergelijkbare aanpassingen aanbrengen in de BlackHole klasse zoals we deden met de Vijand klasse.

Vervolgens zorgen we ervoor dat de zwarte gaten daadwerkelijk een kracht op andere entiteiten toepassen. We hebben een kleine hulpmethode nodig van onze EntityManager:

 std :: list EntityManager :: getNearbyEntities (const tPoint2f & pos, zwevende straal) std :: lijst resultaat; voor (std :: list:: iterator iter = mEntities.begin (); iter! = mEntities.end (); iter ++) if (* iter) if (pos.distanceSquared ((* iter) -> getPosition ()) < radius * radius)  result.push_back(*iter);    return result; 

Deze methode kan efficiënter worden gemaakt door een meer gecompliceerd ruimtelijk partitioneringsschema te gebruiken, maar voor het aantal entiteiten dat we zullen hebben, is het prima zoals het is.

Nu kunnen we de zwarte gaten kracht laten gebruiken in hun BlackHole :: update () methode:

 void BlackHole :: update () std :: lijst entities = EntityManager :: getInstance () -> getNearbyEntities (mPosition, 250); voor (std :: list:: iterator iter = entities.begin (); iter! = entities.end (); iter ++) if ((* iter) -> getKind () == kEnemy &&! ((Enemy *) (* iter)) -> getIsActive ()) // Niets doen else if ((* iter) -> getKind () == kBullet) tVector2f temp = ((* iter) -> getPosition () - mPosition); (* iter) -> setVelocity ((* iter) -> getVelocity () + temp.normalize () * 0.3f);  else tVector2f dPos = mPosition - (* iter) -> getPosition (); vlotterlengte = dPos.length (); (* iter) -> setVelocity ((* iter) -> getVelocity () + dPos.normalize () * tMath :: mix (2.0f, 0.0f, length / 250.0f)); 

Zwarte gaten zijn alleen van invloed op entiteiten binnen een gekozen straal (250 pixels). Kogels binnen deze straal hebben een constante afstotende kracht toegepast, terwijl al het andere een lineaire aantrekkende kracht uitoefent.

We moeten botsingen voor zwarte gaten toevoegen aan de EntityManager. Voeg een toe std :: list voor zwarte gaten zoals we deden voor de andere typen entiteiten en voeg de volgende code toe EntityManager :: handleCollisions ():

 // omgaan met botsingen met zwarte gaten voor (std :: lijst:: iterator i = mBlackHoles.begin (); i! = mBlackHoles.end (); i ++) for (std :: lijst:: iterator j = mEnemies.begin (); j! = mEnemies.end (); j ++) if ((j) -> getIsActive () && isColliding (* i, * j)) (* j) -> wasShot ();  voor (std :: lijst:: iterator j = mBullets.begin (); j! = mBullets.end (); j ++) if (isColliding (* i, * j)) (* j) -> setExpired (); (* I) -> wasShot ();  if (isColliding (PlayerShip :: getInstance (), * i)) KillPlayer (); breken; 

Open ten slotte de EnemySpawner klasse en laat het een aantal zwarte gaten maken. Ik beperkte het maximale aantal zwarte gaten tot twee, en gaf een kans van één op 600 op een zwart gat dat elk frame spaweed.

 if (EntityManager :: getInstance () -> getBlackHoleCount () < 2 && int32_t(tMath::random() * mInverseBlackHoleChance) == 0)  EntityManager::getInstance()->toevoegen (nieuwe BlackHole (GetSpawnPosition ())); 

Conclusie

We hebben virtuele gamepads besproken en toegevoegd en zwarte gaten toegevoegd met behulp van verschillende force-formules. Shape Blaster begint er redelijk goed uit te zien. In het volgende deel zullen we wat gekke, overdreven deeltjeseffecten toevoegen.

Referenties

  • Foto credit: Wii-controller door kazuma jp.