Cum să construiți un alergător infinit pe iOS: Cocos2D, automatizare și multe altele
Publicat: 2022-03-11Dezvoltarea jocurilor iOS poate fi o experiență îmbogățitoare atât în ceea ce privește creșterea personală, cât și financiară. La începutul acestui an, am implementat un joc bazat pe Cocos2D, Bee Race, în App Store. Gameplay-ul său este simplu: un alergător infinit în care jucătorii (în acest caz, albinele) adună puncte și evită obstacolele. Vezi aici pentru o demonstrație.
În acest tutorial, voi explica procesul din spatele dezvoltării jocurilor pentru iOS, de la Cocos2D până la publicare. Pentru referință, iată un scurt cuprins:
- Sprite și obiecte fizice
- O scurtă introducere în Cocos2D
- Utilizarea Cocos2D cu storyboard-uri
- Gameplay și (scurtă) descriere a proiectului
- Automatizați locurile de muncă. Folosiți unelte. Fii cool.
- Facturare în aplicație
- Joc multiplayer cu Game Center
- Loc pentru imbunatatiri
- Concluzie
Sprite și obiecte fizice
Înainte de a intra în detaliile serioase, va fi util să înțelegem distincția dintre sprite și obiecte fizice.
Pentru orice entitate dată care apare pe ecranul unui joc de alergători fără sfârșit, reprezentarea grafică a acelei entități este denumită sprite , în timp ce reprezentarea poligonală a acelei entități în motorul fizic este denumită obiect fizic .
Deci, sprite-ul este desenat pe ecran, susținut de obiectul său fizic corespunzător, care este apoi manipulat de motorul tău fizic. Această configurație poate fi vizualizată aici, unde sprite-urile sunt afișate pe ecran, cu omologii lor fizici poligonali conturați în verde:
Obiectele fizice nu sunt conectate implicit la sprite-urile lor respective, ceea ce înseamnă că tu, în calitate de dezvoltator iOS, poți alege ce motor de fizică să folosești și cum să conectezi sprite-urile și corpurile. Cea mai comună modalitate este de a subclasa sprite-ul implicit și de a-i adăuga un corp fizic concret.
Avand in vedere…
Un scurt tutorial despre dezvoltarea jocului Cocos2D iOS
Cocos2D-iphone este un cadru open source pentru iOS care utilizează OpenGL pentru accelerarea grafică hardware și acceptă motoarele fizice Chipmunk și Box2D.
În primul rând, de ce avem nevoie de un astfel de cadru? Ei bine, pentru început, cadrele implementează componentele des utilizate ale dezvoltării jocurilor. De exemplu, Cocos2D poate încărca sprite-uri (în special, foi de sprite (de ce?)), poate lansa sau opri un motor de fizică și poate gestiona corect sincronizarea și animația. Și face toate acestea cu un cod care a fost revizuit și testat pe larg - de ce să vă dedicați propriul timp rescrierii unui cod probabil inferior?
Poate cel mai important, totuși — dezvoltarea jocului Cocos2D folosește accelerarea hardware grafică . Fără o astfel de accelerare, orice joc de alergători infiniti iOS cu chiar și un număr moderat de sprites va rula cu performanțe deosebit de slabe. Dacă încercăm să facem o aplicație mai complicată, atunci probabil că vom începe să vedem un efect „bullet-time” pe ecran, adică mai multe copii ale fiecărui sprite pe măsură ce încearcă să se anime.
În cele din urmă, Cocos2D optimizează utilizarea memoriei, deoarece memorează în cache sprite-urile. Astfel, orice sprite duplicat necesită memorie suplimentară minimă, ceea ce este evident util pentru jocuri.
Utilizarea Cocos2D cu storyboard-uri
După toate laudele pe care le-am făcut pe Cocos2D, ar putea părea ilogic să sugerez folosirea Storyboard-urilor. De ce nu vă manipulați obiectele cu Cocos2D etc.? Ei bine, ca să fiu sincer, pentru ferestrele statice este adesea mai convenabil să folosești Interface Builder de la Xcode și mecanismul său Storyboard.
În primul rând, îmi permite să trageți și să poziționez toate elementele mele grafice pentru jocul meu de alergători fără sfârșit cu mouse-ul. În al doilea rând, API-ul Storyboard este foarte, foarte util. (Și da, știu despre Cocos Builder).
Iată o scurtă privire a Storyboard-ului meu:
Controlerul de vizualizare principal al jocului conține doar o scenă Cocos2D cu câteva elemente HUD deasupra:
Atenție la fundalul alb: este o scenă Cocos2D, care va încărca toate elementele grafice necesare în timpul rulării. Alte vizualizări (indicatoare live, păpădie, butoane etc.) sunt toate vizualizări standard Cocoa, adăugate pe ecran folosind Interface Builder.
Nu mă voi opri asupra detaliilor – dacă ești interesat, exemple pot fi găsite pe GitHub.
Gameplay și (scurtă) descriere a proiectului
(Pentru a oferi mai multă motivație, aș dori să descriu jocul meu de alergători fără sfârșit mai detaliat. Simțiți-vă liber să omiteți această secțiune dacă doriți să treceți la discuția tehnică.)
În timpul jocului live, albina este nemișcată, iar câmpul în sine se grăbește, aducând cu el diverse pericole (păianjeni și flori otrăvitoare) și avantaje (păpădie și semințele lor).
Cocos2D are obiect de cameră care a fost conceput pentru a urmări personajul; în practică, a fost mai puțin complicat să manipulezi CCLayer-ul care conținea lumea jocului.
Comenzile sunt simple: atingerea ecranului mută albina în sus, iar o altă atingere o mișcă în jos.
Stratul mondial în sine are de fapt două substraturi. Când începe jocul, primul substrat este populat de la 0 la BUF_LEN și afișat inițial. Al doilea substrat este populat în avans de la BUF_LEN la 2*BUF_LEN. Când albina ajunge la BUF_LEN, primul substrat este curățat și repopulat instantaneu de la 2*BUF_LEN la 3*BUF_LEN, iar al doilea substrat este prezentat. În acest fel, alternăm între straturi, fără a reține niciodată obiecte învechite, o parte importantă pentru evitarea scurgerilor de memorie.
În ceea ce privește motoarele fizice, am folosit Chipmunk din două motive:
- Este scris în pur Objective-C.
- Am mai lucrat cu Box2D, așa că am vrut să le compar pe cele două.
Motorul fizic a fost într-adevăr folosit doar pentru detectarea coliziunilor. Uneori, sunt întrebat: „De ce nu ți-ai scris propriul sistem de detectare a coliziunilor?”. În realitate, asta nu are prea mult sens. Motoarele fizice au fost concepute chiar în acest scop: pot detecta coliziuni între corpuri de forme complicate și pot optimiza acest proces. De exemplu, motoarele fizice împart adesea lumea în celule și efectuează verificări de coliziune numai pentru corpurile din aceeași celule sau din celule adiacente.
Automatizați locurile de muncă. Folosiți unelte. Fii cool.
O componentă cheie a dezvoltării jocului indie infinite runner este evitarea poticnirii cu probleme mici. Timpul este o resursă crucială atunci când se dezvoltă o aplicație, iar automatizarea poate economisi timp incredibil.
Dar uneori, automatizarea poate fi și un compromis între perfecționism și respectarea termenului limită. În acest sens, perfecționismul poate fi un ucigaș al Angry Birds.
De exemplu, într-un alt joc iOS pe care îl dezvolt în prezent, am construit un cadru pentru a crea machete folosind un instrument special (disponibil pe GitHub). Acest cadru are limitările sale (de exemplu, nu are tranziții frumoase între scene), dar folosirea lui îmi permite să-mi fac scenele într-o zecime din timp.
Deci, deși nu vă puteți construi propriul supercadru cu superinstrumente speciale, puteți și ar trebui să automatizați cât mai multe dintre aceste sarcini mici posibil.
În construirea acestui alergător infinit, automatizarea a fost cheia din nou. De exemplu, artistul meu îmi trimitea grafică de înaltă rezoluție printr-un folder special Dropbox. Pentru a economisi timp, am scris câteva scripturi pentru a construi automat seturi de fișiere pentru diferitele rezoluții țintă cerute de App Store, adăugând și -hd sau @2x (scripturile menționate sunt bazate pe ImageMagick).
În ceea ce privește instrumentele suplimentare, am găsit TexturePacker a fi foarte util - poate împacheta sprite-urile în foi de sprite, astfel încât aplicația dvs. să consume mai puțină memorie și să se încarce mai repede, deoarece toate sprite-urile dvs. vor fi citite dintr-un singur fișier. De asemenea, poate exporta texturi în aproape toate formatele posibile de cadre. (Rețineți că TexturePacker nu este un instrument gratuit, dar cred că merită prețul. Puteți consulta și alternative gratuite, cum ar fi ShoeBox.)

Principala dificultate asociată cu fizica jocului este de a crea poligoane potrivite pentru fiecare sprite. Cu alte cuvinte, crearea unei reprezentări poligonale a unei albine sau a unei flori de formă obscure. Nici măcar nu încercați să faceți acest lucru manual - folosiți întotdeauna aplicații speciale, dintre care sunt multe. Unele sunt chiar destul de... exotice, cum ar fi crearea de măști vectoriale cu Inkspace și apoi importarea lor în joc.
Pentru dezvoltarea propriului joc de alergători fără sfârșit, am creat un instrument de automatizare a acestui proces, pe care îl numesc Andengine Vertex Helper. După cum sugerează și numele, a fost proiectat inițial pentru cadrul Andengine, deși va funcționa în mod corespunzător cu o serie de formate în prezent.
În cazul nostru, trebuie să folosim modelul plist:
<real>%.5f</real><real>%.5f</real>
Apoi, creăm un fișier plist cu descrieri de obiecte:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>jet_ant</key> <dict> <key>vertices</key> <array> <real>-0.18262</real><real>0.08277</real> <real>-0.14786</real><real>-0.22326</real> <real>0.20242</real><real>-0.55282</real> <real>0.47047</real><real>0.41234</real> <real>0.03823</real><real>0.41234</real> </array> </dict> </dict> </plist>
Și un încărcător de obiecte:
- (void)createBodyAtLocation:(CGPoint)location{ float mass = 1.0; body = cpBodyNew(mass, cpMomentForBox(mass, self.sprite.contentSize.width*self.sprite.scale, self.sprite.contentSize.height*self.sprite.scale)); body->p = location; cpSpaceAddBody(space, body); NSString *path =[[NSBundle mainBundle] pathForResource:@"obj _descriptions" ofType:@"plist"]; // <- load plist NSDictionary *objConfigs = [[[NSDictionary alloc] initWithContentsOfFile:path] autorelease]; NSArray *vertices = [[objConfigs objectForKey:namePrefix] objectForKey:@"vertices"]; shape = [ChipmunkUtil polyShapeWithVertArray:vertices withBody:body width:self.sprite.contentSize.width height:self.sprite.contentSize.height]; shape->e = 0.7; shape->u = 1.0; shape->collision_type = OBJ_COLLISION_TYPE; cpSpaceAddShape(space, shape); }
Pentru a testa modul în care sprite-urile corespund corpurilor lor fizice, vezi aici.
Mult mai bine, nu?
În rezumat, automatizați întotdeauna când este posibil. Chiar și scripturile simple vă pot economisi o mulțime de timp. Și, important, acel timp poate fi folosit pentru programare în loc de clic pe mouse. (Pentru motivație suplimentară, iată un simbol XKCD.)
Facturare în aplicație
Blowballs colectate în joc acționează ca o monedă în aplicație, permițând utilizatorilor să cumpere noi skin-uri pentru albina lor. Totuși, această monedă poate fi achiziționată și cu bani reali. Un punct important de remarcat în ceea ce privește facturarea în aplicație este dacă trebuie sau nu să efectuați verificări pe server pentru valabilitatea achiziției. Deoarece toate bunurile achiziționabile sunt în esență egale în ceea ce privește jocul (doar modificând aspectul albinei), nu este nevoie să efectuați o verificare a serverului pentru valabilitatea achiziției. Cu toate acestea, în multe cazuri, cu siguranță va trebui să faceți acest lucru.
Pentru mai multe, Ray Wenderlich are tutorialul perfect de facturare în aplicație.
Joc multiplayer cu Game Center
În jocurile mobile, socializarea înseamnă mai mult decât adăugarea unui buton „Like” pe Facebook sau configurarea clasamentelor. Pentru a face jocul mai interesant, am implementat o versiune multiplayer.
Cum functioneazã? În primul rând, doi jucători sunt conectați folosind jocul iOS Game Center în timp real. Deoarece jucătorii joacă cu adevărat același joc de alergători infinit, trebuie să existe doar un singur set de obiecte de joc. Asta înseamnă că instanța unui jucător trebuie să genereze obiectele, iar celeilalte jocuri le vor citi. Cu alte cuvinte, dacă dispozitivele ambilor jucători ar genera obiecte de joc, ar fi greu să sincronizați experiența.
Având în vedere acest lucru, după ce conexiunea este stabilită, ambii jucători își trimit reciproc un număr aleatoriu. Jucătorul cu numărul mai mare acționează ca „server”, creând obiecte de joc.
Îți amintești discuția despre generația mondială împărțită? Unde am avut două substraturi, unul de la 0 la BUF_LEN și celălalt de la BUF_LEN la 2*BUF_LEN? Această arhitectură nu a fost folosită din întâmplare – a fost necesar să se ofere o grafică fluidă în rețelele întârziate. Când o porțiune de obiecte este generată, aceasta este împachetată într-un plist și trimisă celuilalt jucător. Buffer-ul este suficient de mare pentru a permite celui de-al doilea jucător să joace chiar și cu o întârziere de rețea. Ambii jucători își trimit reciproc poziția actuală cu o perioadă de jumătate de secundă, trimițându-și și mișcările de sus-jos imediat. Pentru a netezi experiența, poziția și viteza sunt corectate la fiecare 0,5 secunde cu o animație lină, astfel încât, în practică, pare că celălalt jucător se mișcă sau accelerează treptat.
Cu siguranță, sunt mai multe considerații de făcut în ceea ce privește jocul multiplayer care rulează fără sfârșit, dar sperăm că acest lucru vă oferă o idee pentru tipurile de provocări implicate.
Loc pentru imbunatatiri
Jocurile nu se termină niciodată. Desigur, există mai multe domenii în care aș dori să le îmbunătățesc pe ale mele, și anume:
- Probleme de control: atingerea este adesea un gest neintuitiv pentru jucătorii care preferă să alunece.
Stratul mondial este mutat folosind acțiunea CCMoveBy. Acest lucru a fost bine când viteza stratului mondial a fost constantă, deoarece acțiunea CCMoveBy a fost ciclică cu CCRepeatForever:
-(void) infiniteMove{ id actionBy = [CCMoveBy actionWithDuration: BUFFER_DURATION position: ccp(-BUFFER_LENGTH, 0)]; id actionCallFunc = [CCCallFunc actionWithTarget:self selector:@selector(requestFillingNextBuffer)]; id actionSequence = [CCSequence actions: actionBy, actionCallFunc, nil]; id repeateForever = [CCRepeatForever actionWithAction:actionSequence]; [self.bufferContainer runAction:repeateForever]; }
Dar mai târziu, am adăugat o creștere a vitezei mondiale pentru a îngreuna jocul pe măsură ce merge mai departe:
-(void) infiniteMoveWithAccel { float duration = BUFFER_DURATION-BUFFER_ACCEL*self.lastBufferNumber; duration = max(duration, MIN_BUFFER_DURATION); id actionBy = [CCMoveBy actionWithDuration: duration position: ccp(-BUFFER_LENGTH, 0)]; id restartMove = [CCCallFunc actionWithTarget:self selector:@selector(infiniteMoveWithAccel)]; id fillBuffer = [CCCallFunc actionWithTarget:self selector:@selector(requestFillingNextBuffer)]; id actionSequence = [CCSequence actions: actionBy, restartMove, fillBuffer, nil]; [self.bufferContainer runAction:actionSequence]; }
Această modificare a făcut ca animația să se destrame la fiecare repornire a acțiunii. Am încercat să rezolv problema, fără rezultat. Cu toate acestea, testerii mei beta nu au observat comportamentul, așa că am amânat remedierea.
- Pe de o parte, nu a fost nevoie să-mi scriu propria autorizație pentru multiplayer când folosesc Game Center sau când rulez propriul meu server de joc. Pe de altă parte, a făcut imposibilă crearea de roboți, ceea ce mi-ar plăcea să schimb.
Concluzie
Crearea propriului joc indie infinite runner poate fi o experiență grozavă. Și odată ce ați ajuns la etapa de publicare a procesului, poate fi un sentiment minunat când vă eliberați propria creație în sălbăticie.
Procesul de revizuire poate varia de la câteva zile la câteva săptămâni. Pentru mai multe, există aici un site util care utilizează date de la mulțime pentru a estima timpii actuali de revizuire.
În plus, vă recomand să utilizați AppAnnie pentru a examina diverse informații despre toate aplicațiile din App Store, iar înregistrarea la unele servicii de analiză precum Flurry Analytics poate fi de asemenea utilă.
Și dacă acest joc v-a intrigat, asigurați-vă că verificați Bee Race în magazin.