În această serie de tutoriale, vă vom 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. În această ultimă parte, vom adăuga grila de fundal care se răsucește pe baza acțiunii în joc.
În seria de până acum am creat gameplay-ul, gamepadul virtual și efectele particulelor. În această ultimă parte vom crea o grilă dinamică de bază.
Așa cum am menționat în partea anterioară, veți observa o scădere dramatică a cadrelor dacă executați încă codul în modul de depanare. Consultați tutorialul pentru detalii despre modul de trecere la modul de eliberare pentru optimizarea completă a compilatorului (și o construire mai rapidă).
Unul dintre cele mai bune efecte în Geometry Wars este grila de fond de bază. Vom examina cum să creăm un efect similar în Shape Blaster. Grila va reacționa la gloanțe, găuri negre și player respawning. Nu este greu de făcut și arată minunat.
Vom face grila folosind o simulare de primăvară. La fiecare intersecție a grilajului, vom pune o greutate mică și vom atașa un arc pe fiecare parte. Aceste izvoare vor trage și nu se vor împinge niciodată, la fel ca o bandă de cauciuc. Pentru a menține grila în poziție, masele de la marginea grila vor fi ancorate în poziție. Mai jos este o diagramă a aspectului.
Vom crea o clasă numită Grilă
pentru a crea acest efect. Cu toate acestea, înainte de a lucra la grila în sine, trebuie să facem două clase de ajutor: Primăvară
și PointMass
.
PointMass
clasa reprezintă masele la care vom atașa izvoarele. Springs nu se conectează direct la celelalte izvoare; în schimb, aplică o forță maselor pe care le conectează, care, la rândul lor, pot întinde alte izvoare.
clasa PointMass protejat: tVector3f mAcceleration; float mDamping; public: tVector3f mPosition; tVector3f mVelocity; float mInversMass; public: PointMass (); PointMass (const tVector3f și poziție, float invMass); void applyForce (const tVector3f & force); void increaseDamping (factor de plutire); void update (); ; PointMass :: PointMass (): mAcceleration (0,0,0), mDamping (0,98f), mPoziție (0), mVelocitate (0,0,0), mInversMass (0) PointMass :: PointMass (constTVector3f & , float invMass): mAccelerație (0,0,0), mDamping (0,98f), mPoziție (poziție), mVelocitate (0,0,0), mInversMass (invMass) void PointMass :: applyForce (const tVector3f & mAcceleration + = forță * mInversMass; void PointMass :: increaseDamping (factor float) mDamping * = factor; void PointMass :: actualizare () mVelocity + = mAcceleration; mPoziția + = mVelocitate; mAcceleration = tVector3f (0,0,0); dacă (mVelocity.lengthSquared () < 0.001f * 0.001f) mVelocity = tVector3f(0,0,0); mVelocity *= mDamping; mDamping = 0.98f;
Există câteva puncte interesante despre această clasă. Mai întâi, observați că stochează invers din masa, 1 / masă
. Aceasta este adesea o idee bună în simulările fizicii deoarece ecuațiile fizicii tind să folosească mai des inversul masei și pentru că ne oferă o modalitate ușoară de a reprezenta obiecte infinit de grele și imobile prin stabilirea masei inverse la zero.
În al doilea rând, clasa conține, de asemenea, a amortizare variabil. Aceasta este folosită aproximativ ca rezistență la frecare sau la aer; încetinește treptat masa în jos. Acest lucru face ca grila să ajungă în cele din urmă să se odihnească și, de asemenea, crește stabilitatea simulării primăverii.
PointMass :: actualizare ()
metoda face munca de a muta masa punctului fiecare cadru. Aceasta începe prin a face integrarea simulctică Euler, ceea ce înseamnă doar că adăugăm accelerația la viteză și apoi adăugăm viteza actualizată la poziție. Aceasta diferă de integrarea standard Euler în care am actualiza viteza după actualizarea poziției.
Bacsis: Simulare Euler este mai bine pentru simulările de primăvară pentru că conservă energia. Dacă utilizați integrarea obișnuită a Euler și creați izvoare fără amortizare, acestea vor avea tendința să se extindă mai mult și mai mult, fiecare săriți pe măsură ce câștigă energie, eventual rupându-vă simularea.
După actualizarea vitezei și a poziției, verificăm dacă viteza este foarte mică și, dacă este așa, vom seta la zero. Acest lucru poate fi important pentru performanță datorită naturii numerelor cu puncte plutitoare denormalizate.
(Atunci când numerele cu virgulă mobilă devin foarte mici, ele utilizează o reprezentare specială numită a număr denormalizat. Acest lucru are avantajul de a permite plutitorilor să reprezinte numere mai mici, dar vine la un preț. Majoritatea chipset-urilor nu pot folosi operațiunile lor aritmetice standard pe numere denormalizate și trebuie să le emuleze folosind o serie de pași. Acest lucru poate fi de zeci la sute de ori mai lent decât executarea operațiunilor cu numere de puncte cu virgulă normalizate. Deoarece ne înmulțim viteza prin factorul de amortizare al fiecărui cadru, acesta va deveni în cele din urmă foarte mic. Nu ne pasă de viteze atât de mici, așa că am pus pur și simplu la zero.)
PointMass :: increaseDamping ()
metoda este utilizată pentru a mări temporar cantitatea de amortizare. Vom folosi acest lucru mai târziu pentru anumite efecte.
Un arc conectează două mase punctuale și, dacă este întins peste lungimea sa naturală, aplică o forță care trage masele împreună. Arcurile urmăresc o versiune modificată a legii lui Hooke cu amortizare:
\ [f = -kx-bv \]
Codul pentru Primăvară
clasa este după cum urmează:
Clasa de primăvară public: PointMass * mEnd1; PointMass * mEnd2; float mTargetLength; float mStiffness; float mDamping; publice: primăvară (PointMass * end1, PointMass * end2, rigiditate flotantă, amortizare flotantă); void update (); ; Primăvară :: Argintiu (PointMass * end1, PointMass * end2, rigiditate flotantă, amortizare flotantă): mEnd1 (end1), mEnd2 (end2), mTargetLength (mEnd1-> mPosition.distance (mEnd2-> mPosition) (rigiditate), mDamping (amortizare) void Spring :: actualizare () tVector3f x = mEnd1-> mPoziție - mEnd2-> mPoziție; float lungime = x.length (); dacă (lungime> mTargetLength) x = (x / lungime) * (lungime - mTargetLength); tVector3f dv = mEnd2-> mVelocitate - mEnd1-> mVelocitate; tVector3f forța = mStiftitudinea * x - dv * mDamping; mEnd1-> applyForce (-Force); mEnd2-> applyForce (forță);
Când creăm un arc, setăm lungimea naturală a arcului să fie doar puțin mai mică decât distanța dintre cele două puncte de capăt. Aceasta menține grilă întinsă chiar și atunci când este în repaus și îmbunătățește aspectul într-o oarecare măsură.
Spring :: actualizare ()
metoda verifică mai întâi dacă arcul este întins dincolo de lungimea sa naturală. Dacă nu este întins, nimic nu se întâmplă. Dacă este cazul, vom folosi Legea modificată a lui Hooke pentru a găsi forța din primăvară și ao aplica celor două mase conectate.
Acum că avem clasele necesare imbricate, suntem gata să creăm grila. Începem prin a crea PointMass
obiecte la fiecare intersecție pe grila. De asemenea, creăm niște ancore imobile PointMass
obiecte pentru a ține grila în poziție. Apoi, legăm masele cu izvoare.
std :: vectormSprings; PointMass * m Puncte; Grilă :: Grilă (const tRectf & rect, const tVector2f și spațiere) mScreenSize = tVector2f (GameRoot :: getInstance () -> getViewportSize () width. int numColumns = (int) (float) rect.size.width / spacing.x) + 1; int numRows = (int) ((float) rect.size.height / spațiere.y) + 1; mPoints = punct de masă nou [numColumns * numRows]; mCols = numColumns; mRows = numRows; PointMass * puncte fixe = punct de masă nou [numColumns * numRows]; int coloană = 0, rând = 0; pentru (float y = rect.location.y; y <= rect.location.y + rect.size.height; y += spacing.y) for (float x = rect.location.x; x <= rect.location.x + rect.size.width; x += spacing.x) SetPointMass(mPoints, column, row, PointMass(tVector3f(x, y, 0), 1)); SetPointMass(fixedPoints, column, row, PointMass(tVector3f(x, y, 0), 0)); column++; row++; column = 0; // link the point masses with springs for (int y = 0; y < numRows; y++) for (int x = 0; x < numColumns; x++) if (x == 0 || y == 0 || x == numColumns - 1 || y == numRows - 1) mSprings.push_back(Spring(GetPointMass(fixedPoints, x, y), GetPointMass(mPoints, x, y), 0.1f, 0.1f)); else if (x % 3 == 0 && y % 3 == 0) mSprings.push_back( Spring(GetPointMass(fixedPoints, x, y), GetPointMass(mPoints, x, y), 0.002f, 0.02f)); if (x > 0) mSprings.push_back (primăvară (GetPointMass (m puncte, x - 1, y), GetPointMass (m puncte, x, y), 0.28f, 0.06f)); dacă (y> 0) mSprings.push_back (primăvară (GetPointMass (mPoints, x, y - 1), GetPointMass (m puncte, x, y), 0.28f, 0.06f));
Primul pentru
buclă creează atât mase regulate cât și mase imobile la fiecare intersecție a rețelei. Nu vom folosi efectiv toate masele imobile, iar masele neutilizate vor fi pur și simplu gunoi colectat ceva timp după terminarea constructorului. Am putea optimiza evitând crearea obiectelor inutile, dar din moment ce grila este de obicei creată doar o singură dată, nu va face prea multă diferență.
În plus față de utilizarea masei punctului de ancorare în jurul marginii rețelei, vom folosi și câteva mase de ancore în interiorul rețelei. Acestea vor fi utilizate pentru a ajuta foarte ușor să trageți grila înapoi în poziția inițială după ce ați fost deformate.
Deoarece punctele de ancorare nu se mișcă niciodată, nu trebuie să fie actualizate fiecare cadru; putem pur și simplu să le prindem până la izvoare și să uităm de ele. Prin urmare, nu avem o variabilă membru în Grilă
pentru aceste mase.
Există o serie de valori pe care le puteți modifica în crearea rețelei. Cele mai importante sunt rigiditatea și amortizarea izvoarelor. (Rigiditatea și amortizarea ancorelor de frontieră și a ancorelor interioare sunt stabilite independent de arcurile principale.) Valori mai mari de rigiditate vor face ca arcurile să oscileze mai repede, iar valori mai ridicate de amortizare vor determina încetinirea arcurilor mai devreme.
Pentru ca grila să se miște, trebuie să o actualizăm în fiecare cadru. Acest lucru este foarte simplu, deoarece am făcut deja toată munca grea în PointMass
și Primăvară
clase:
void Grid :: actualizare () pentru (size_t i = 0; i < mSprings.size(); i++) mSprings[i].update(); for(int i = 0; i < mCols * mRows; i++) mPoints[i].update();
Acum vom adăuga câteva metode care manipulează grila. Puteți adăuga metode pentru orice fel de manipulare pe care vă puteți gândi. Vom implementa trei tipuri de manipulări aici: împingând o parte a grilei într-o anumită direcție, împingând grila spre exterior dintr-un anumit punct și trăgând grila spre un anumit punct. Toate cele trei vor afecta grila într-o anumită rază de la un punct țintă. Mai jos sunt câteva imagini ale acestor manipulări în acțiune:
Gloanțele respingă grilele spre exterior.
Sugeți grilele spre interior.
Wave creat prin împingerea grilajului de-a lungul axei z.
void Grid :: applyDirectedForce (const tVector3f & forță, const tVector3f și poziție, raft float) pentru (int i = 0; i < mCols * mRows; i++) if (position.distanceSquared(mPoints[i].mPosition) < radius * radius) mPoints[i].applyForce(10.0f * force / (10 + position.distance(mPoints[i].mPosition))); void Grid::applyImplosiveForce(float force, const tVector3f& position, float radius) for (int i = 0; i < mCols * mRows; i++) float dist2 = position.distanceSquared(mPoints[i].mPosition); if (dist2 < radius * radius) mPoints[i].applyForce(10.0f * force * (position - mPoints[i].mPosition) / (100 + dist2)); mPoints[i].increaseDamping(0.6f); void Grid::applyExplosiveForce(float force, const tVector3f& position, float radius) for (int i = 0; i < mCols * mRows; i++) float dist2 = position.distanceSquared(mPoints[i].mPosition); if (dist2 < radius * radius) mPoints[i].applyForce(100 * force * (mPoints[i].mPosition - position) / (10000 + dist2)); mPoints[i].increaseDamping(0.6f);
Vom folosi toate cele trei metode în Shape Blaster pentru diferite efecte.
Vom atrage grila prin trasarea segmentelor liniare între fiecare pereche de puncte învecinate. Mai întâi, vom adăuga o metodă de extensie luând a tSpriteBatch
pointer ca parametru care ne permite să desenăm segmente de linii luând o textură a unui singur pixel și întinzându-l într-o linie.
Deschide Artă
clasă și să declare o textură pentru pixel:
clasa Art: public tSingleton; protejat: tTexture * mPixel; ... public: tTexture * getPixel () const; ...;
Puteți seta textura pixelilor în același mod în care am setat celelalte imagini, așa că vom adăuga pixel.png
(o imagine 1x1px cu pixel unic setat pe alb) la proiect și încărcați-l în tTexture
:
mPixel = nou tTexture (tSurface ("pixel.png"));
Acum, să adăugăm următoarea metodă la Extensii
clasă:
void Extensii :: drawLine (tSpriteBatch * spriteBatch, const tVector2f & start, const tVector2f & end, const tColor4f & culoare, grosime float) tVector2f delta = end-start; spriteBatch-> desena (0, Art :: getInstance () -> getPixel (), tPoint2f ((int32_t) start.x, (int32_t) start.y)(), culoare, toAngle (delta), tPoint2f (0, 0), tVector2f (delta.length (), grosime));
Această metodă se întinde, rotește și dă textura pixelilor pentru a produce linia dorită.
Apoi, avem nevoie de o metodă de proiectare a punctelor rețelei 3D pe ecranul 2D. În mod normal, acest lucru se poate face folosind matrice, dar aici vom transforma manual coordonatele.
Adăugați următoarele la Grilă
clasă:
tVector2f Grilă :: toVec2 (const tVector3f & v) factor float = (v.z + 2000.0f) * 0.0005f; retur (tVector2f (v.x, v.y) - mScreenSize * 0.5f) * factor + mScreenSize * 0.5f;
Această transformare va oferi rețelei o vedere în perspectivă în cazul în care punctele îndepărtate apar mai aproape împreună pe ecran. Acum putem trage grila prin iterarea prin rânduri și coloane și desenarea liniilor între ele:
void Grid :: trageți (tSpriteBatch * spriteBatch) int width = mCols; int înălțime = mRows; tColor4f culoare (0,12f, 0,12f, 0,55f, 0,33f); pentru (int y = 1; y < height; y++) for (int x = 1; x < width; x++) tVector2f left, up; tVector2f p = toVec2(GetPointMass(mPoints, x, y)->mPosition); dacă (x> 1) left = toVec2 (GetPointMass (m puncte, x - 1, y) -> mPoziție); grosimea flotorului = (y% 3 == 1)? 3.0f: 1.0f; Extensii :: drawLine (spriteBatch, stânga, p, culoare, grosime); dacă (y> 1) up = toVec2 (GetPointMass (m puncte, x, y-1) -> mPoziție); float grosime = (x% 3 == 1)? 3.0f: 1.0f; Extensii :: drawLine (spriteBatch, sus, p, culoare, grosime);
În codul de mai sus, p
este punctul nostru actual pe grilă, stânga
este punctul direct în stânga și sus
este punctul direct deasupra lui. Desenăm fiecare al treilea rând mai gros atât pe orizontală cât și pe verticală pentru efect vizual.
Putem optimiza grila prin îmbunătățirea calității vizuale pentru un anumit număr de arcuri fără a crește în mod semnificativ costul de performanță. Vom face două astfel de optimizări.
Vom face grilarea mai densă adăugând segmente de linie în interiorul celulelor rețelei existente. Facem acest lucru prin trasarea liniilor de la mijlocul unei părți a celulei până la mijlocul părții opuse. Imaginea de mai jos prezintă liniile noi interpolate în roșu.
Desenarea liniilor interpolate este simplă. Dacă aveți două puncte, A
și b
, punctul lor de mijloc este (a + b) / 2
. Deci, pentru a desena linii interpolate, adaugam urmatorul cod in interiorul lui pentru
buclele noastre Grid :: remiză ()
metodă:
dacă (x> 1 && y> 1) tVector2f upLeft = toVec2 (GetPointMass (m puncte, x - 1, y - 1) -> mPoziție); Extensii :: drawLine (spriteBatch, 0.5f * (upLeft + up), 0.5f * (stânga + p), culoare, 1.0f); // Extensii linie verticală :: drawLine (spriteBatch, 0.5f * (upLeft + stânga), 0.5f * (până + p), culoare, 1.0f); // linie orizontală
Cea de-a doua îmbunătățire este de a efectua interpolarea pe segmentele noastre liniare pentru a le face în curbe mai netede. În versiunea originală XNA a acestui joc, codul sa bazat pe XNA Vector2.CatmullRom ()
care efectuează interpolarea Catmull-Rom. Transmiteți metoda patru puncte secvențiale pe o linie curbă și veți întoarce punctele de-a lungul unei curbe netede între al doilea și al treilea punct pe care l-ați furnizat.
Din moment ce acest algoritm nu există în biblioteca standard C sau C ++, va trebui să o implementăm noi înșine. Din fericire, există o aplicație de referință disponibilă pentru utilizare. Am oferit o MathUtil :: catmullRom ()
bazată pe această implementare de referință:
flotare MathUtil :: catmullRom (valoarea const float1, const float value2, const float value3, const float value4, suma float) // Folosind formula de la http://www.mvps.org/directx/articles/catmull/ float amountSquared = suma * suma; float amountCubed = amountSquared * amount; retur (float) (0.5f * (2.0f * valoare2 + (valoare3 - valoare1) * cantitate + (2.0f * value1 - 5.0f * value2 + 4.0f * value3 - value4) - 3.0f * valoare3 + valoare4) * sumaCubed)); tVector2f MathUtil :: catmullRom (const tVector2f și valoare1, const tVector2f & value2, const tVector2f & value3, const tVector2f & value4, float amount) retur tVector2f (MathUtil :: catmullRom (value1.x, value2.x, value3.x, value4.x , suma), MathUtil :: catmullRom (valoarea1.y, valoarea2.y, valoarea3.y, valoarea4.y, suma));
Al cincilea argument pentru MathUtil :: catmullRom ()
este un factor de ponderare care determină ce punct pe curba interpolată se întoarce. Factor de ponderare 0
sau 1
va reveni respectiv la al doilea sau al treilea punct pe care l-ați furnizat și la un factor de ponderare de 0.5
va întoarce punctul pe curba interpolată la jumătatea distanței dintre cele două puncte. Prin mutarea treptată a factorului de ponderare de la zero la unul și trasarea liniilor între punctele returnate, putem produce o curbă perfect netedă. Cu toate acestea, pentru a menține costul scăzut al performanței, vom lua în considerare doar un singur punct interpolat, cu un factor de ponderare de 0.5
. Apoi înlocuim linia dreaptă originală în rețea cu două linii care se întâlnesc la punctul interpolat.
Diagrama de mai jos prezintă efectul acestei interpolări:
Deoarece segmentele de linie din rețea sunt deja mici, utilizarea mai multor puncte interpolate, în general, nu face o diferență notabilă.
Adesea, liniile din grila noastră vor fi foarte drepte și nu vor necesita nici o netezire. Putem verifica acest lucru și evităm să tragem două linii în loc de una: verificăm dacă distanța dintre punctul interpolat și punctul median al liniei drepte este mai mare de un pixel; dacă este, presupunem că linia este curbată și desenăm două segmente de linie.
Modificarea la adresa noastră Grid :: remiză ()
metoda de adăugare a interpolației Catmull-Rom pentru liniile orizontale este prezentată mai jos.
stânga = toVec2 (GetPointMass (m puncte, x - 1, y) -> mPoziție); grosimea flotorului = (y% 3 == 1)? 3.0f: 1.0f; int clampedX = (int) tMath :: min (x + 1, lățime - 1); tVector2f mid = MathUtil :: catmullRom (toVec2 (GetPointMass (mPoints, x - 2, y) -> mPozi), stânga, p, toVec2 (GetPointMass (mPoints, clampedX, y) -> mPozi), 0,5f); dacă (mid.distanceSquared ((left + p) / 2)> 1) Extensii :: drawLine (spriteBatch, stânga, mijloc, culoare, grosime); Extensii :: drawLine (spriteBatch, mid, p, culoare, grosime); altceva Extensii :: drawLine (spriteBatch, stânga, p, culoare, grosime);
Imaginea de mai jos prezintă efectele de netezire. Un punct verde este desenat la fiecare punct interpolat pentru a ilustra mai bine unde liniile sunt netezite.
Acum este timpul să folosim grila în jocul nostru. Începem prin a declara un public, static Grilă
variabilă în GameRoot
și crearea rețelei în GameRoot :: onInitView
. Vom crea o rețea cu aproximativ 600 de puncte.
const int maxGridPoints = 600; tVector2f gridSpacing = tVector2f (float) sqrtf (mViewportSize.width * mViewportSize.height / maxGridPoints)); mGrid = rețea nouă (tRectf (0,0, mViewportSize), gridSpacing);
Deși versiunea originală XNA a jocului folosește 1.600 de puncte (mai degrabă decât 600), acest lucru devine prea mult pentru a face față chiar și pentru hardware-ul puternic din iPhone. Din fericire, codul original a lăsat numărul de puncte personalizabile și, la aproximativ 600 de puncte de rețea, putem să le oferim în continuare și să menținem încă o rată optimă a cadrelor.
Apoi sunăm Grid :: actualizare ()
și Grid :: remiză ()
de la GameRoot :: onRedrawView ()
metoda în GameRoot
. Acest lucru ne va permite să vedem grila când conducem jocul. Totuși, trebuie să facem ca obiectele jocului să interacționeze cu grila.
Gloanțele vor respinge grila. Am făcut deja o metodă pentru a face acest lucru numit Grid :: applyExplosiveForce ()
. Adăugați următoarea linie la Bullet :: actualizare ()
metodă.
GameRoot :: getInstance () -> getGrid () -> aplicațiExplosiveForce (0.5f * mVelocity.length (), mPosition, 80);
Acest lucru va face ca gloanțele să respingă grila proporțional cu viteza lor. A fost destul de ușor.
Acum să lucrăm la găurile negre. Adăugați această linie la Blackhole :: actualizare ()
:
GameRoot :: getInstance () -> getGrid () -> applyImplosiveForce (float) sinf (mSprayAngle / 2.0f) * 10 + 20, mPoziție, 200);
Acest lucru face gaura neagra suge in grila cu o forta variata de forta. Am reutilizat mSprayAngle
variabilă, ceea ce va determina forța pe rețea să pulseze în sincronizare cu unghiul pe care îl pulverizează particulele (deși la jumătate din frecvența datorată împărțirii cu două). Forța care a trecut va varia în mod sinusoidal între 10
și 30
.
În cele din urmă, vom crea o undă de șoc în rețea atunci când nava jucătorului respawns după moarte. Vom face acest lucru trăgând grilajul de-a lungul axei z și apoi permițând forței să se propagate și să sări prin arcuri. Din nou, aceasta necesită doar o mică modificare PlayerShip :: actualizare ()
.
dacă (getIsDead ()) mFramesUntilRespawn--; dacă (mFramesUntilRespawn == 0) GameRoot :: getInstance () -> getGrid () -> applyDirectedForce (tVector3f (0, 0, 5000), tVector3f (mPosition.x, mPosition.y, 0);
Avem jocul de bază și efectele implementate. Depinde de tine sa il transformi intr-un joc complet si lustruit cu propria ta aroma. Încearcă să adaugi niște mecanisme noi interesante, câteva efecte reci noi sau o poveste unică. În cazul în care nu sunteți sigur de unde să începeți, iată câteva sugestii:
Cerul este limita!