Maak een Neon Vector Shooter voor iOS de eerste stappen

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 ontzagwekkende muziek, voor iOS met C ++ en OpenGL ES 2.0.

In plaats van te vertrouwen op een bestaand gamekader of een sprite-bibliotheek, zullen we proberen zo dicht mogelijk bij de hardware (of 'bare metal') te programmeren als we maar kunnen. Omdat apparaten met iOS op kleinschaliger hardware draaien in vergelijking met een desktop pc of een spelconsole, kunnen we zo veel mogelijk geld verdienen voor onze buck.

gerelateerde berichten
Deze tutorials zijn gebaseerd op de originele XNA-serie van Michael Hoffman, die is vertaald naar andere platforms:
  • Maak een Neon Vector Shooter in XNA
  • Maak een Neon Vector Shooter in jMonkeyEngine

Het doel van deze tutorials is om de noodzakelijke elementen door te nemen die je in staat zullen stellen om je eigen mobiele game van hoge kwaliteit voor iOS te maken, vanaf nul of op basis van een bestaand desktopgame. Ik moedig u aan om de code te downloaden en ermee te spelen, of zelfs om deze te gebruiken als basis voor uw eigen projecten.

We behandelen de volgende onderwerpen tijdens deze reeks:

  1. Eerste stappen, het introduceren van de Utility-bibliotheek, het instellen van de basisgameplay, het maken van het schip, het geluid en de muziek van de speler.
  2. Voltooi de implementatie van de gameplay-mechanica door vijanden toe te voegen, botsingsdetectie af te handelen en de score en levens van de speler bij te houden.
  3. Voeg een virtueel gamepad toe op het scherm, zodat we het spel kunnen besturen met behulp van multi-touch-invoer.
  4. Voeg gekke, overdreven deeltjeseffecten toe.
  5. Voeg het kromme achtergrondraster toe.

Dit is wat we zullen hebben aan het einde van de serie:


Waarschuwing: Loud!

En hier is wat we zullen hebben aan het einde van dit eerste deel:


Waarschuwing: Loud!

De muziek en geluidseffecten die je in deze video's kunt horen, zijn gemaakt door RetroModular en je kunt lezen hoe hij dat deed in onze audio sectie.

De sprites zijn van Jacob Zinman-Jeanes, onze residente Tuts + -ontwerper.

Het lettertype dat we gebruiken is een bitmap-lettertype (met andere woorden, geen echt "lettertype", maar een afbeeldingsbestand), iets dat ik voor deze zelfstudie heb gemaakt.

Alle illustraties zijn te vinden in de bronbestanden.

Laten we beginnen.


Overzicht

Voordat we ingaan op de specifieke kenmerken van het spel, laten we het hebben over de hulpprogramma-bibliotheek en de toepassing Bootstrap-code die ik heb verstrekt ter ondersteuning van de ontwikkeling van ons spel.

De hulpprogramma-bibliotheek

Hoewel we in de eerste plaats C ++ en OpenGL gebruiken om onze game te coderen, hebben we nog enkele extra hulpprogramma's nodig. Dit zijn alle lessen die ik heb geschreven om de ontwikkeling in andere projecten te helpen, dus ze zijn getest en bruikbaar voor nieuwe projecten zoals deze.

  • package.h: Een gemaksheader die wordt gebruikt om alle relevante headers uit de Utility-bibliotheek op te nemen. We zullen het opnemen door te zeggen # include "Utility / package.h" zonder iets anders te hoeven opnemen.

patronen

We gebruiken een aantal bestaande beproefde programmeerpatronen die worden gebruikt in C ++ en andere talen.

  • tSingleton: Implementeert een singleton-klasse met een patroon "Meyers Singleton". Het is gebaseerd op een sjabloon en uitbreidbaar, dus we kunnen alle singleton-code abstraheren naar een enkele klasse.
  • tOptional: Dit is een functie van C ++ 14 (genoemd std :: optionele) dat is nog niet helemaal beschikbaar in de huidige versies van C ++ (we zijn nog steeds op C ++ 11). Het is ook een functie beschikbaar in XNA en C # (waar het wordt genoemd nullable.) Het stelt ons in staat om "optionele" parameters voor methoden te hebben. Het wordt gebruikt in de tSpriteBatch klasse.

Vector wiskunde

Omdat we geen bestaand gameframework gebruiken, hebben we enkele lessen nodig om met de wiskunde achter de schermen om te gaan.

  • tMath: Een statische klasse biedt een aantal methoden die verder gaan dan wat beschikbaar is in C ++, zoals het converteren van graden naar radialen of het afronden van getallen naar machten van twee.
  • tVector: Een basisset van Vector-klassen, die varianten met twee elementen, drie elementen en vier elementen biedt. We hebben deze structuur ook getypt voor Punten en kleuren.
  • tMatrix: Twee matrixdefinities, een 2x2-variant (voor rotatiebewerkingen) en een 4x4-optie (voor de projectiematrix die nodig is om dingen op het scherm te krijgen),
  • Trect: Een rechthoekklasse die locatie, grootte en methode biedt om te bepalen of punten binnen rechthoeken liggen of niet.

OpenGL-wrapperklassen

Hoewel OpenGL een krachtige API is, is het gebaseerd op C en het beheren van objecten kan in de praktijk enigszins moeilijk zijn. Dus we hebben een klein aantal klassen om de OpenGL-objecten voor ons te beheren.

  • tSurface: Biedt een manier om een ​​bitmap te maken op basis van een afbeelding die is geladen vanuit de toepassingsbundel.
  • tTexture: Wrapt de interface naar de textuuropdrachten van OpenGL en laadt tSurfaces in texturen.
  • tShader: Wraps de interface naar de Shader-compiler van OpenGL, waardoor het eenvoudig is om shaders te compileren.
  • tProgram: Wrapt de interface naar de Shader-programma-interface van OpenGL, wat in feite de combinatie is van twee tShader klassen.

Spelondersteuningsklassen

Deze klassen vertegenwoordigen het dichtst dat we een 'gamekader' krijgen; ze bieden een aantal concepten op hoog niveau die niet typerend zijn voor OpenGL, maar die nuttig zijn voor de ontwikkeling van games.

  • tViewport: Bevat de status van het kijkvenster. We gebruiken dit voornamelijk om wijzigingen in de apparaatoriëntatie aan te pakken.
  • tAutosizeViewport: Een klasse die wijzigingen in de viewport beheert. Het verwerkt wijzigingen in de apparaatoriëntatie direct en schaalt de viewport zodat deze op het scherm van het apparaat past, zodat de beeldverhouding hetzelfde blijft, wat betekent dat dingen niet worden uitgerekt of platgedrukt.
  • tSpriteFont: Hiermee laden we een "bitmaplettertype" uit de toepassingsbundel en gebruiken we dit om tekst op het scherm te schrijven.
  • tSpriteBatch: Geïnspireerd door XNA's SpriteBatch klas, ik schreef deze les om het beste in te kapselen van wat nodig is voor onze game. Het stelt ons in staat om sprites te sorteren bij het tekenen op een dusdanige manier dat we de best mogelijke snelheidswinst behalen op de hardware die we hebben. We zullen het ook rechtstreeks gebruiken om tekst op het scherm te schrijven.

Diverse klassen

Een minimale reeks lessen om dingen af ​​te ronden.

  • TTimer: Een systeemtimer, hoofdzakelijk gebruikt voor animaties.
  • tInputEvent: Basisklasse-definities om richtingwijzigingen aan te brengen (kantelen van het apparaat), aanraakgebeurtenissen en een "virtueel toetsenbord" -gebeurtenis om een ​​gamepad discreter te emuleren.
  • tSound: Een klasse die is bedoeld voor het laden en afspelen van geluidseffecten en muziek.

Toepassing Bootstrap

We hebben ook de code "Boostrap" nodig, dat wil zeggen code die abstraheert hoe een applicatie start of "opstart".

Dit is wat erin zit bootstrap:

  • AppDelegate: Deze klasse verwerkt het starten van de toepassing, evenals het onderbreken en hervatten van gebeurtenissen voor wanneer de gebruiker op de startknop drukt.
  • ViewController: Deze klasse verwerkt apparaatoriëntatie-events en maakt onze OpenGL-weergave
  • OpenGLView: Deze klasse initialiseert OpenGL, vertelt het apparaat te verversen met 60 frames per seconde, en behandelt aanraakgebeurtenissen.

Overzicht van het spel

In deze tutorial zullen we een tweepijps shooter creëren; de speler zal het schip besturen met multi-touchbedieningen op het scherm.

We gebruiken een aantal klassen om dit te bereiken:

  • Entiteit: De basisklasse voor vijanden, kogels en het schip van de speler. Entiteiten kunnen bewegen en getekend worden.
  • Kogel en PlayerShip.
  • EntityManager: Houdt alle entiteiten in het spel bij en voert botsingsdetectie uit.
  • Invoer: Helpt de invoer van het aanraakscherm te beheren.
  • Kunst: Laadt en bevat verwijzingen naar de texturen die nodig zijn voor het spel.
  • Geluid: Laadt en bevat verwijzingen naar de geluiden en muziek.
  • MathUtil en uitbreidingen: Bevat enkele nuttige statische methoden en
    uitbreidingsmethoden.
  • GameRoot: Regelt de hoofdlus van het spel. Dit is onze hoofdklasse.

De code in deze zelfstudie is bedoeld om eenvoudig en gemakkelijk te begrijpen te zijn. Het zal niet alle functies hebben die zijn ontworpen om elke mogelijke behoefte te ondersteunen; in plaats daarvan doet het alleen wat het moet doen. Door het eenvoudig te houden, wordt het voor u eenvoudiger om de concepten te begrijpen en vervolgens te wijzigen en uit te breiden naar uw eigen unieke spel.


Entiteiten en het schip van de speler

Open het bestaande Xcode-project. GameRoot is de hoofdklasse van onze applicatie.

We beginnen met het maken van een basisklasse voor onze game-entiteiten. Bekijk de Entiteitsklasse:

 class Entity public: enum Kind kDontCare = 0, kBullet, kEnemy, kBlackHole,; beschermd: tTexture * mImage; tColor4f mColor; tPoint2f mPosition; tVector2f mVelocity; zweefmOriëntatie; zweven mRadius; bool mIsExpired; Vriendelijke mKind; publieke entiteit(); virtueel ~ Entiteit (); tDimension2f getSize () const; virtuele ongeldige update () = 0; virtuele ongeldige trekking (tSpriteBatch * spriteBatch); tPoint2f getPosition () const; tVector2f getVelocity () const; void setVelocity (const tVector2f & nv); zwevende getRadius () const; bool is Expired () const; Soort getKind () const; void setExpired (); ;

Al onze entiteiten (vijanden, kogels en het schip van de speler) hebben enkele basiseigenschappen, zoals een afbeelding en een positie. mIsExpired wordt gebruikt om aan te geven dat de entiteit vernietigd is en moet worden verwijderd van lijsten die er een verwijzing naar bevatten.

Vervolgens maken we een EntityManager om onze entiteiten te volgen en ze bij te werken en te tekenen:

 klasse EntityManager: openbare tSingleton protected: std :: lijst mEntities; std :: list mAddedEntities; std :: list mBullets; bool mIsUpdating; beschermd: EntityManager (); public: int getCount () const; void add (entiteit * entiteit); void addEntity (entiteit * entiteit); ongeldige update (); ongeldig tekenen (tSpriteBatch * spriteBatch); bool isColliding (Entiteit * a, Entiteit * b); vriend klas tSingleton; ; void EntityManager :: add (entiteit * entiteit) if (! mIsUpdating) addEntity (entity);  else mAddedEntities.push_back (entiteit);  void EntityManager :: update () mIsUpdating = true; voor (std :: list:: iterator iter = mEntities.begin (); iter! = mEntities.end (); iter ++) (* iter) -> update (); if ((* iter) -> is Expired ()) * iter = NULL;  mIsUpdating = false; voor (std :: list:: iterator iter = mAddedEntities.begin (); iter! = mAddedEntities.end (); iter ++) addEntity (* iter);  mAddedEntities.clear (); mEntities.remove (NULL); voor (std :: list:: iterator iter = mBullets.begin (); iter! = mBullets.end (); iter ++) if ((* iter) -> is Expired ()) delete * iter; * iter = NULL;  mBullets.remove (NULL);  void EntityManager :: draw (tSpriteBatch * spriteBatch) for (std :: lijst:: iterator iter = mEntities.begin (); iter! = mEntities.end (); iter ++) (* iter) -> draw (spriteBatch); 

Houd er rekening mee dat als u een lijst wijzigt terwijl u er overheen leest, u een runtime-uitzondering krijgt. De bovenstaande code zorgt ervoor door alle entiteiten die tijdens het bijwerken zijn toegevoegd in een aparte lijst in de wachtrij te plaatsen en ze toe te voegen nadat het bijwerken van de bestaande entiteiten is voltooid.

Ze zichtbaar maken

We zullen wat texturen moeten laden als we iets willen tekenen, dus we zullen een statische klasse maken om verwijzingen naar al onze texturen te houden:

 klasse Art: public tSingleton beschermd: tTexture * mPlayer; tTexture * mSeeker; tTexture * mWanderer; tTexture * mBullet; tTexture * mPointer; beschermd: Art (); publiek: tTexture * getPlayer () const; tTexture * getSeeker () const; tTexture * getWanderer () const; tTexture * getBullet () const; tTexture * getPointer () const; vriend klas tSingleton; ; Art :: Art () mPlayer = new tTexture (tSurface ("player.png")); mSeeker = new tTexture (tSurface ("seeker.png")); mWanderer = new tTexture (tSurface ("wanderer.png")); mBullet = new tTexture (tSurface ("bullet.png")); mPointer = new tTexture (tSurface ("pointer.png")); 

We laden de kunst door te bellen Art :: getInstance () in GameRoot :: onInitView (). Dit veroorzaakt de Kunst singleton om geconstrueerd te worden en de constructor te bellen, Art :: Art ().

Ook moet een aantal klassen de schermafmetingen weten, dus we hebben de volgende leden GameRoot:

 tDimension2f mViewportSize; tSpriteBatch * mSpriteBatch; tAutosizeViewport * mViewport;

En in de GameRoot constructor, we hebben de grootte ingesteld:

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

De resolutie 800x600px is wat de originele XNA-gebaseerde Shape Blaster gebruikte. We zouden elke gewenste resolutie kunnen gebruiken (zoals een dichter bij de specifieke resolutie van een iPhone of iPad), maar we houden vast aan de originele resolutie om er zeker van te zijn dat onze game overeenkomt met het uiterlijk en het gevoel van het origineel..

Nu gaan we over de PlayerShip klasse:

 class PlayerShip: public Entity, public tSingleton protected: static const int kCooldownFrames; int mCooldowmRemaining; int mFramesUntil Respawn; beschermd: PlayerShip (); public: void update (); ongeldig tekenen (tSpriteBatch * spriteBatch); bool getIsDead (); void kill (); vriend klas tSingleton; ; PlayerShip :: PlayerShip (): mCooldowmRemaining (0), mFramesUntilRespawn (0) mImage = Art :: getInstance () -> getPlayer (); mPosition = tPoint2f (GameRoot :: getInstance () -> getViewportSize (). x / 2, GameRoot :: getInstance () -> getViewportSize (). y / 2); mRadius = 10; 

We maakten PlayerShip een singleton, stel het beeld in en plaatste het in het midden van het scherm.

Laten we tot slot het spelersschip toevoegen aan de EntityManager. De code in GameRoot :: onInitView het lijkt op dit:

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

We tekenen de sprites met additief mengen, wat deel uitmaakt van wat hen hun "neon" uiterlijk zal geven. We willen ook geen blaren of blenden, dus gebruiken we GL_NEAREST voor onze filters. We hoeven niet te letten op dieptetests of backface culling (het voegt sowieso onnodige overhead toe), dus we zetten het uit.

De code in GameRoot :: onRedrawView het lijkt op dit:

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

Als je het spel nu uitvoert, zou je je schip in het midden van het scherm moeten zien. Het reageert echter niet op invoer. Laten we wat invoer toevoegen aan het spel.


Invoer

Voor beweging gebruiken we een multi-touch-interface. Voordat we volledig van start gaan met gamepads op het scherm, krijgen we gewoon een eenvoudige touch-interface die werkt.

In de originele Shape Blaster voor Windows kon de beweging van de speler worden uitgevoerd met de WASD-toetsen op het toetsenbord. Voor het richten kunnen ze de pijltjestoetsen of de muis gebruiken. Dit is bedoeld om de dubbele joystickbesturing van Geometry Wars na te bootsen: een analoge stick voor beweging, een voor richten.

Aangezien Shape Blaster al gebruikmaakt van het concept van toetsenbord- en muisbewegingen, zou de eenvoudigste manier om invoer toe te voegen door het toetsenbord en de muis te emuleren via aanraking. We beginnen met muisbewegingen, omdat zowel aanraking als muis een vergelijkbaar onderdeel delen: een punt met X- en Y-coördinaten.

We zullen een statische klasse maken om de verschillende invoerapparaten bij te houden en om te zorgen voor het schakelen tussen de verschillende soorten richten:

 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 onKeyboard (const tKeyboardEvent & msg); void onTouch (const tTouchEvent & msg); vriend klas tSingleton; ; 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; 

Wij bellen Input :: update () aan het begin van GameRoot :: onRedrawView () om de invoerklasse te laten werken.

Zoals eerder vermeld, gebruiken we de toetsenbord staat later in de serie om rekening te houden met beweging.

het schieten

Laten we nu het schip laten schieten.

Ten eerste hebben we een klasse nodig voor kogels.

 class Bullet: public Entity public: Bullet (const tPoint2f & position, const tVector2f & velocity); ongeldige update (); ; Bullet :: Bullet (const tPoint2f & position, const tVector2f & velocity) mImage = Art :: getInstance () -> getBullet (); mPosition = positie; mVelocity = snelheid; mOrientation = atan2f (mVelocity.y, mVelocity.x); mRadius = 8; mKind = kBullet;  void Bullet :: update () if (mVelocity.lengthSquared ()> 0) mOrientation = atan2f (mVelocity.y, mVelocity.x);  mPosition + = mVelocity; if (! tRectf (0, 0, GameRoot :: getInstance () -> getViewportSize ()). bevat (tPoint2f ((int32_t) mPosition.x, (int32_t) mPosition.y))) mIsExpired = true; 

We willen een korte afkoelperiode tussen kogels, dus daar hebben we een constante voor:

 const int PlayerShip :: kCooldownFrames = 6;

We voegen ook de volgende code toe aan PlayerShip :: Update ():

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

Deze code maakt twee kogels die parallel aan elkaar lopen. Het voegt een kleine hoeveelheid willekeurigheid toe aan de richting, waardoor de schoten zich een beetje verspreiden als een machinegeweer. We voegen twee willekeurige getallen bij elkaar, omdat dit hun som waarschijnlijk gecentreerd maakt (rond nul) en minder waarschijnlijk kogels ver weg stuurt. We gebruiken een tweedimensionale matrix om de beginpositie van de kogels in de richting waarin ze reizen te roteren.

We hebben ook twee nieuwe hulpmethoden gebruikt:

  • Extensions :: NextFloat (): Retourneert een willekeurige float tussen een minimum- en een maximumwaarde.
  • MathUtil :: FromPolar (): Maakt een tVector2f vanuit een hoek en magnitude.

Dus laten we eens kijken hoe ze eruitzien:

 // In uitbreidingen zwevende uitbreidingen :: nextFloat (zwevende min. Waarde, zwevende max. Waarde) return (zweven) tMath :: random () * (maxValue - minValue) + minWaarde;  // In MathUtil tVector2f MathUtil :: fromPolar (float angle, float magnitude) return magnitude * tVector2f ((float) cosf (angle), (float) sinf (angle)); 

Aangepaste cursor

Er is nog een ding dat we moeten doen nu we het essentiële hebben Invoer klasse: laten we een aangepaste muiscursor tekenen zodat u gemakkelijker kunt zien waar het schip naartoe gaat. In GameRoot.Draw, teken gewoon Art's mPointer op de positie van de muis.

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

Conclusie

Als je het spel nu test, kun je overal op het scherm aanraken om de continue stroom van kogels te richten, wat een goed begin is.


Waarschuwing: Loud!

In het volgende deel voltooien we de eerste gameplay door vijanden en een score toe te voegen.