În această serie de tutoriale, vă voi arăta cum să faceți un shooter twin-stick inspirat de Geometry Wars, cu grafică neon, efecte particulare nebune și muzică minunată, pentru iOS folosind C ++ și OpenGL ES 2.0.
Mai degrabă decât să ne bazăm pe un cadru de joc existent sau pe o bibliotecă sprite, vom încerca să programăm cât mai aproape posibil hardware-ul (sau "metalul gol"). Deoarece dispozitivele care rulează iOS rulează pe un hardware de dimensiuni mai mici, comparativ cu un PC desktop sau o consolă de jocuri, acest lucru ne va permite să obținem cât mai multă bătaie pentru banii noștri.
postări asemănatoareScopul acestor tutoriale este de a trece peste elementele necesare care vă vor permite să vă creați propriul joc mobil de înaltă calitate pentru iOS, fie de la zero, fie pe baza unui joc desktop existent. Vă încurajez să descărcați și să vă jucați cu codul, sau chiar să îl utilizați ca bază pentru proiectele proprii.
Vom acoperi următoarele subiecte în această serie:
Iată ce vom avea până la sfârșitul seriei:
Iată ce vom avea până la sfârșitul acestei prime părți:
Efectele muzicale și sonore pe care le puteți auzi în aceste videoclipuri au fost create de RetroModular și puteți citi despre modul în care a făcut acest lucru la secțiunea noastră audio.
Spritele sunt de Jacob Zinman-Jeanes, designerul nostru rezident Tuts +.
Fontul pe care îl vom folosi este un font bitmap (cu alte cuvinte, nu un "font" real, ci un fișier imagine), ceea ce am creat pentru acest tutorial.
Toate lucrările pot fi găsite în fișierele sursă.
Să începem.
Înainte de a se arunca cu capul în specificul jocului, hai să vorbim despre codul de bibliotecă utilitar și aplicația Bootstrap pe care l-am furnizat pentru a susține dezvoltarea jocului.
Deși vom folosi în primul rând C ++ și OpenGL pentru a codifica jocul nostru, vom avea nevoie de câteva clase de utilitate suplimentare. Acestea sunt toate clase pe care le-am scris pentru a ajuta la dezvoltarea în alte proiecte, astfel încât acestea sunt testate în timp și pot fi utilizate pentru proiecte noi, cum ar fi acesta.
package.h
: Un antet de utilizare folosit pentru a include toate anteturile relevante din biblioteca utilitar. O vom include prin afirmare #include "Utilitate / pachet.h"
fără a fi nevoie să includeți altceva.Vom utiliza câteva modele de programare existente, încercate și reale, folosite în C ++ și în alte limbi.
tSingleton
: Implementează o clasă singleton folosind un model "Meyers Singleton". Este bazat pe șabloane și extensibil, astfel încât să putem abroga toate codul singleton la o singură clasă.tOptional
: Aceasta este o caracteristică din C ++ 14 (numită std :: opțional
) care nu este destul de disponibil în versiunile curente ale C ++ (încă suntem la C ++ 11). Este, de asemenea, o caracteristică disponibilă în XNA și C # (unde se numește poate fi nulă
.) Aceasta ne permite să avem parametri "opționali" pentru metode. Este folosit în tSpriteBatch
clasă.Deoarece nu folosim un cadru de joc existent, vom avea nevoie de câteva clase pentru a face față matematicii din spatele scenei.
tMath
: O clasă statică oferă anumite metode dincolo de ceea ce este disponibil în C ++, cum ar fi conversia de la grade la radiani sau rotunjirea numerelor la puterile a două.tVector
: Un set de bază de clase Vector, care oferă variante cu 2 elemente, 3 elemente și 4 elemente. De asemenea, am prezentat această structură pentru puncte și culori.tMatrix
: Două definiții de matrice, o variantă 2x2 (pentru operațiile de rotație) și o opțiune 4x4 (pentru matricea de proiecție necesară pentru a obține lucrurile pe ecran),tRect
: O clasă de dreptunghi care furnizează locația, dimensiunea și o metodă pentru a determina dacă punctele se află în interiorul dreptunghiurilor sau nu.Deși OpenGL este un API puternic, este bazat pe C, iar gestionarea obiectelor poate fi oarecum dificil de realizat în practică. Deci, vom avea o mână de ore pentru a gestiona obiectele OpenGL pentru noi.
tSurface
: Oferă o modalitate de a crea un bitmap bazat pe o imagine încărcată din pachetul aplicației.tTexture
: Împachetează interfața cu comenzile de textură OpenGL și încărcările tSurfaces
în texturi.tShader
: Împachetează interfața cu compilatorul de shader OpenGL, facilitând compilarea shaderelor.tProgram
: Împachetează interfața cu interfața programelor OpenGL Shader, care este, în esență, combinația a două tShader
clase.Aceste clase reprezintă cel mai apropiat posibilitate de a avea un "cadru de joc"; acestea oferă câteva concepte de nivel înalt care nu sunt tipice pentru OpenGL, dar care sunt utile pentru dezvoltarea jocurilor.
tViewport
: Conține starea portului de vizualizare. Utilizăm acest lucru în primul rând pentru a face față modificărilor orientării dispozitivului.tAutosizeViewport
: O clasă care gestionează modificările în fereastra de vizualizare. Mă ocupă direct de modificările orientării dispozitivului și mărește portul de vizualizare pentru a se potrivi ecranului dispozitivului, astfel încât raportul de aspect să rămână același - ceea ce înseamnă că lucrurile nu se întind sau se răsucesc.tSpriteFont
: Permite încărcarea unui "font bitmap" din pachetul de aplicații și utilizarea acestuia pentru a scrie text pe ecran.tSpriteBatch
: Inspirat de XNA SpriteBatch
am scris această clasă pentru a încapsula cele mai bune lucruri necesare jocului nostru. Ne permite să sortăm sprite atunci când desenăm așa încât să obținem cele mai bune câștiguri de viteză pe hardware-ul pe care-l avem. De asemenea, o vom folosi direct pentru a scrie text pe ecran.Un set minim de clase pentru a înlătura lucrurile.
tTimer
: Un cronometru de sistem, utilizat în principal pentru animații.tInputEvent
: Definiții de clasă de bază pentru a oferi schimbări de orientare (înclinarea dispozitivului), atingeți evenimente și un eveniment "tastatură virtuală" pentru a emula un gamepad mai discret.tSound
: O clasă dedicată încărcării și redării efectelor de sunet și muzicii.De asemenea, vom avea nevoie de ceea ce numesc codul "Boostrap" - adică un cod care abstractează modul în care începe o aplicație sau "cizme în sus".
Iată ce se află bootstrap
:
AppDelegate
: Această clasă se ocupă de lansarea aplicației, precum și suspendarea și reluarea evenimentelor atunci când utilizatorul apasă butonul Pagina principală.ViewController
: Această clasă gestionează evenimentele de orientare a dispozitivului și creează vizualizarea OpenGLOpenGLView
: Această clasă inițializează OpenGL, îi spune dispozitivului să se reîmprospăteze la 60 de cadre pe secundă și să gestioneze evenimentele touch.În acest tutorial vom crea un shooter cu două gemuri; jucătorul va controla nava folosind comenzi multi-touch pe ecran.
Vom folosi o serie de clase pentru a realiza acest lucru:
Entitate
: Clasa de bază pentru dușmani, gloanțe și nava jucătorului. Entitățile se pot deplasa și pot fi trase.Glonţ
și PlayerShip
.EntityManager
: Ține evidența tuturor entităților din joc și efectuează detectarea coliziunilor.Intrare
: Ajută la gestionarea intrărilor de pe ecranul tactil.Artă
: Încarcă și deține referințe la texturile necesare jocului.Sunet
: Încarcă și deține referințe la sunete și muzică.MathUtil
și Extensii
: Conține câteva metode statice utile șiGameRoot
: Controlează bucla principală a jocului. Aceasta este clasa noastră principală.Codul din acest tutorial își propune să fie simplu și ușor de înțeles. Nu va avea toate caracteristicile concepute pentru a suporta toate nevoile posibile; mai degrabă, va face doar ceea ce trebuie să facă. Ținând-o simplu, vă va fi mai ușor să înțelegeți conceptele și apoi să le modificați și să le extindeți în propriul joc unic.
Deschideți proiectul Xcode existent. GameRoot este clasa principală a aplicației noastre.
Vom începe prin crearea unei clase de bază pentru entitățile noastre de joc. Uitați-vă la
Entitatea de clasă public: enum Kind kDontCare = 0, kBullet, kEnemy, kBlackHole,; protejate: tTexture * mImage; tColor4f mColor; tPoint2f mPosition; tVector2f mVelocity; float mOrientation; float mRadius; bool mEsExpired; MKind; public: Entitatea (); virtual ~ Entity (); tDimension2f getSize () const; virtual void update () = 0; retragerea voidă virtuală (tSpriteBatch * spriteBatch); tPoint2f getPozi () const; tVector2f getVelocitate () const; void setVelocity (const tVector2f & nv); float getRadius () const; bool esteExpired () const; Tip getKind () const; void setExpired (); ;
Toate entitățile noastre (dușmani, gloanțe și nava jucătorului) au câteva proprietăți de bază, cum ar fi o imagine și o poziție. mIsExpired
va fi folosit pentru a indica faptul că entitatea a fost distrusă și ar trebui eliminată din orice listă care deține o referință la ea.
Apoi vom crea un EntityManager
pentru a urmări entitățile noastre și pentru a le actualiza și trage:
clasa EntityManager: public tSingletonprotected: std :: lista mEntities; std :: Lista mAddedEntities; std :: Lista mBullets; bool mIsUpdating; protejate: EntityManager (); public: int getCount () const; void add (Entitatea * entitate); void addEntity (Entitatea * entitate); void update (); redactarea void (tSpriteBatch * spriteBatch); bool isColliding (Entitatea * a, Entitatea * b); clasa prietenului tSingleton ; ; void EntityManager :: adăuga (entitate Entity *) if (! mIsUpdating) addEntity (entitate); altceva mAddedEntities.push_back (entitate); void EntityManager :: actualizare () mIsUpdating = true; pentru (std :: lista :: iterator iter = mEntities.begin (); iter! = mEntities.end (); iter ++) (* iter) -> actualizare (); dacă ((iter) -> esteExpirată ()) * iter = NULL; mIsUpdating = false; pentru (std :: lista :: iterator iter = mAddedEntities.begin (); iter! = mAddedEntities.end (); iter + +) addEntity (* iter); mAddedEntities.clear (); mEntities.remove (NULL); pentru (std :: lista :: iterator iter = mBullets.begin (); iter! = mBullets.end (); iter ++) dacă ((* iter) -> esteExpirată ()) delete * iter; * iter = NULL; mBullets.remove (NULL); void EntityManager :: trage (tSpriteBatch * spriteBatch) pentru (std :: lista :: iterator iter = mEntities.begin (); iter! = mEntities.end (); iter ++) (* iter) -> remiză (spriteBatch);
Rețineți că, dacă modificați o listă în timp ce iterați peste ea, veți primi o excepție de rulare. Codul de mai sus se ocupă de acest lucru prin așteptarea oricăror entități adăugate în timpul actualizării într-o listă separată și adăugarea acestora după finalizarea actualizării entităților existente.
Va trebui să încărcăm niște texturi dacă vrem să tragem ceva, așa că vom face o clasă statică care să dețină referințe la texturile noastre:
clasa Art: public tSingletonprotejat: tTexture * mPlayer; tTexture * mSeeker; Testarea * mWanderer; tTexture * mBullet; tTexture * mPointer; protejate: Art (); publică: tTexture * getPlayer () const; tTexture * getSeeker () const; tTexture * getWanderer () const; tTexture * getBullet () const; tTexture * getPointer () const; clasa prietenului tSingleton ; ; Arta :: Arta () mPlayer = noua tTexture (tSurface ("player.png")); mSeeker = tTexture nou (tSurface ("seeker.png")); mWanderer = nou tTexture (tSurface ("wanderer.png")); mBullet = tTexture nou (tSurface ("bullet.png")); mPointer = tTexture nou (tSurface ("pointer.png"));
Încărcăm arta prin chemare Art :: getInstance ()
în GameRoot :: onInitView ()
. Acest lucru cauzează Artă
singleton pentru a construi și pentru a apela constructorul, Art :: Art ()
.
De asemenea, un număr de clase va trebui să cunoască dimensiunile ecranului, deci avem următorii membri GameRoot
:
tDimension2f mViewportSize; tSpriteBatch * mSpriteBatch; tAutosizeViewport * mViewport;
Și în GameRoot
constructor, am setat dimensiunea:
GameRoot :: GameRoot (): mViewportSize (800, 600), mSpriteBatch (NULL)
Rezoluția de 800x600px este cea folosită de formatul inițial XNA Shape Blaster. Am putea folosi orice rezoluție dorim (ca una mai apropiată de rezoluția specifică iPhone sau iPad), dar vom respecta rezoluția originală doar pentru a ne asigura că jocul se potrivește cu aspectul original.
Acum vom trece peste PlayerShip
clasă:
clasa PlayerShip: entitate publică, public tSingletonprotejat: static const int kCooldownFrames; int mCooldowmRemaining; int mFramesUntilRespawn; protejat: PlayerShip (); public: void update (); redactarea void (tSpriteBatch * spriteBatch); bool getIsDead (); void kill (); clasa prietenului tSingleton ; ; PlayerShip :: PlayerShip (): mCooldowmRemaining (0), mFramesUntilRespawn (0) mImage = Arta :: getInstance () -> getPlayer (); mPosition = tPoint2f (GameRoot :: getInstance () -> getViewportSize () ./ 2, GameRoot :: getInstance () -> getViewportSize () y / 2); mRadius = 10;
Am făcut PlayerShip
un singur ton, și-a stabilit imaginea și la plasat în centrul ecranului.
În cele din urmă, să adăugăm nava jucătorului la EntityManager
. Codul din GameRoot :: onInitView
arata asa:
// În GameRoot :: onInitView EntityManager :: getInstance () -> adăugați (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);
Noi îi atragem spritele amestecarea aditivilor, care face parte din ceea ce le va da aspectul lor "neon". De asemenea, nu vrem să se amestece sau să se amestece, așa că folosim GL_NEAREST
pentru filtrele noastre. Nu avem nevoie sau ne pasa de testarea adâncimii sau de cedarea de pe spate (totuși adaugă oricum cheltuieli inutile), așa că o oprim.
Codul din GameRoot :: onRedrawView
arata asa:
// În GameRoot :: onRedrawView EntityManager :: getInstance () -> update (); EntityManager :: getInstance () -> tragere (mSpriteBatch); mSpriteBatch-> tragere (0, Art :: getInstance () -> getPointer (), intrare :: getInstance () -> getMousePosition()); mViewport-> run (); glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); mSpriteBatch-> end (); glFlush ();
Dacă executați jocul în acest moment, ar trebui să vă vedeți nava în centrul ecranului. Cu toate acestea, nu răspunde la intrare. Să adăugăm câteva intrări în jocul următor.
Pentru mișcare, vom folosi o interfață multi-touch. Înainte de a ajunge la forță cu gamepad-urile de pe ecran, vom primi doar o interfață touch touch de bază și de funcționare.
În blasterul original Shape Blaster pentru Windows, mișcarea jucătorului ar putea fi efectuată cu tastele WASD de pe tastatură. Pentru direcționare, ei ar putea folosi tastele săgeată sau mouse-ul. Acest lucru este menit să emuleze controalele Geometry Wars twin-stick: un stick analog pentru mișcare, unul pentru țintire.
Deoarece Shape Blaster utilizează deja conceptul de mișcare a tastaturii și a mouse-ului, cel mai simplu mod de a adăuga intrare ar fi emularea comenzilor tastaturii și mouse-ului prin atingere. Vom începe cu mișcarea mouse-ului, deoarece atât mouse-ul, cât și mouse-ul împart o componentă similară: un punct care conține coordonatele X și Y.
Vom face o clasă statică pentru a urmări diferitele dispozitive de intrare și pentru a avea grijă să comutați între diferitele tipuri de direcționare:
Clasa de intrare: public tSingleton protejat: tPoint2f mMouseState; tPoint2f mLastMouseState; tPoint2f mFreshMouseState; std :: vectormKeyboardState; 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,; protejate: tVector2f GetMouseAimDirection () const; protejate: Intrare (); publică: tPoint2f getMousePosition () const; void update (); // Verifică dacă o tastă a fost doar apăsată bool wasKeyPressed (KeyType) const; tVector2f getMovementDirection () const; tVector2f getAimDirection () const; void onKeyboard (const tKeyboardEvent & msg); void onTouch (const tTouchEvent & msg); clasa prietenului tSingleton; ; void Input :: update () mLastKeyboardState = mKeyboardState; mLastMouseState = mMouseState; mKeyboardState = mFreshKeyboardState; mMouseState = mFreshMouseState; dacă [mKeyboardState [kLeft] || mKeyboardState [kRight] || mKeyboardState [kUp] || mKeyboardState [kDown]) mIsAimingWithMouse = false; altfel dacă (mMouseState! = mLastMouseState) mIsAimingWithMouse = true;
Noi sunam Intrare :: actualizare ()
la inceputul GameRoot :: onRedrawView ()
pentru ca clasa de intrare să funcționeze.
După cum sa menționat anterior, vom folosi tastatură
starea ulterioară a seriei pentru a explica mișcarea.
Acum, să facem nava să tragă.
În primul rând, avem nevoie de o clasă pentru gloanțe.
clasa Bullet: entitate publică public: Bullet (const tPoint2f & position, const tVector2f & viteza); void update (); ; Bullet :: Bullet (const tPoint2f și poziție, const tVector2f & viteza) mImage = Art :: getInstance () -> getBullet (); mPoziția = poziție; mVelocity = viteza; mOrientation = atan2f (mVelocity.y, mVelocity.x); mRadius = 8; mKind = kBullet; void Bullet :: actualizare () if (mVelocity.lengthSquared ()> 0) mOrientation = atan2f (mVelocity.y, mVelocity.x); mPoziția + = mVelocitate; dacă tRectf (0, 0, GameRoot :: getInstance () -> getViewportSize ()) conține (tPoint2f ((int32_t) mPosition.x, (int32_t) mPosition.y))) mIsExpired = true;
Vrem o scurtă perioadă de cooldown între gloanțe, așa că vom avea o constantă pentru asta:
const int PlayerShip :: kCooldownFrames = 6;
De asemenea, vom adăuga următorul cod la PlayerShip :: Actualizare ()
:
tVector2f aim = intrare :: getInstance () -> getAimDirection (); dacă (target.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()->adăugați (Bullet nou (mPosition + offset, vel)); offset = aimMat * tVector2f (35, 8); EntityManager :: getInstance () -> adăugați (noul Bullet (mPosition + offset, vel)); tSound * curShot = Sunet :: getInstance () -> getShot (); dacă (! curShot-> isPlaying ()) curShot-> play (0, 1); dacă (mCooldowmRemaining> 0) mCooldowmRemaining--;
Acest cod creează două gloanțe care călătoresc paralele între ele. Se adaugă o mică cantitate de aleatorie în direcție, ceea ce face ca focurile să se împrăștie puțin ca un mitralieră. Adăugăm două numere aleatorii împreună, deoarece acest lucru face probabil ca suma lor să fie centrat (în jurul valorii de zero) și este mai puțin probabil să trimită gloanțe la distanță. Utilizăm o matrice bidimensională pentru a roti poziția inițială a gloanțelor în direcția în care călătoresc.
De asemenea, am folosit două noi metode de ajutor:
Extensiile :: NextFloat ()
: Returnează un flotant aleator între valoarea minimă și cea maximă.MathUtil :: FromPolar ()
: Creează o tVector2f
dintr-un unghi și amploare.Deci, sa vedem cum arata:
// în extensii float Extensions :: nextFloat (float minValue, float maxValue) return (float) tMath :: random () * (maxValue - minValue) + minValue; // În MathUtil tVector2f MathUtil :: de la Polar (unghi de flotare, magnitudine flotantă) întoarcere magnitudine * tVector2f (float) cosf (unghi), (float) sinf (unghi));
Mai avem încă un lucru pe care ar trebui să-l facem acum că avem initalul Intrare
clasa: să trasăm un cursor personalizat al mouse-ului pentru a face mai ușor să vedem unde se îndreaptă vasul. În GameRoot.Draw
, pur și simplu trageți lui Art mPointer
la poziția "mouse-ului".
mSpriteBatch-> tragere (0, Art :: getInstance () -> getPointer (), intrare :: getInstance () -> getMousePosition());
Dacă încercați acum jocul, veți putea atinge oriunde pe ecran pentru a viza fluxul continuu de gloanțe, ceea ce reprezintă un bun început.
În următoarea parte, vom finaliza jocul inițial prin adăugarea de dușmani și un scor.