Jak zbudować nieskończonego biegacza na iOS: Cocos2D, automatyzacja i więcej
Opublikowany: 2022-03-11Tworzenie gier na iOS może być wzbogacającym doświadczeniem zarówno pod względem rozwoju osobistego, jak i finansowego. Na początku tego roku wdrożyłem do App Store grę opartą na Cocos2D, Bee Race. Jego rozgrywka jest prosta: nieskończony biegacz, w którym gracze (w tym przypadku pszczoły) zbierają punkty i omijają przeszkody. Zobacz tutaj demo.
W tym samouczku wyjaśnię proces tworzenia gier na iOS, od Cocos2D po publikację. Dla odniesienia, oto krótki spis treści:
- duszki i obiekty fizyczne
- Krótkie wprowadzenie do Cocos2D
- Używanie Cocos2D ze scenorysami
- Rozgrywka i (krótki) opis projektu
- Zautomatyzuj zadania. Używaj narzędzi. Bądź fajny.
- Rozliczenia w aplikacji
- Rozgrywka wieloosobowa z Game Center
- Miejsce na udoskonalenie
- Wniosek
duszki i obiekty fizyczne
Zanim przejdziemy do szczegółów, pomocne będzie zrozumienie różnicy między duszkami a obiektami fizycznymi.
W przypadku dowolnej jednostki, która pojawia się na ekranie gry typu endless runner, graficzna reprezentacja tej jednostki jest określana jako sprite , podczas gdy wielokątna reprezentacja tej jednostki w silniku fizycznym jest określana jako obiekt fizyczny .
Tak więc duszek jest rysowany na ekranie, wspierany przez odpowiadający mu obiekt fizyczny, który jest następnie obsługiwany przez silnik fizyki. Ten układ można zwizualizować tutaj, gdzie na ekranie wyświetlane są duszki, a ich fizyczne wielokątne odpowiedniki są zaznaczone na zielono:
Obiekty fizyczne nie są domyślnie połączone z odpowiednimi duszkami, co oznacza, że jako programista iOS możesz wybrać silnik fizyczny, którego chcesz użyć i jak połączyć duszki i ciała. Najpopularniejszym sposobem jest podklasowanie domyślnego duszka i dodanie do niego konkretnego ciała fizycznego.
Pamiętając o tym…
Krótki samouczek dotyczący tworzenia gier Cocos2D na iOS
Cocos2D-iphone to platforma open source dla systemu iOS, która używa OpenGL do sprzętowej akceleracji grafiki i obsługuje silniki fizyki Chipmunk i Box2D.
Przede wszystkim po co nam taki framework? Cóż, na początek, frameworki implementują często używane komponenty tworzenia gier. Na przykład Cocos2D może ładować duszki (w szczególności arkusze ikonek (dlaczego?)), uruchamiać lub zatrzymywać silnik fizyki oraz prawidłowo obsługiwać synchronizację i animację. A wszystko to za pomocą kodu, który został dokładnie sprawdzony i przetestowany — po co poświęcać swój czas na ponowne pisanie prawdopodobnie gorszego kodu?
Być może jednak najważniejsze — tworzenie gier Cocos2D wykorzystuje akcelerację sprzętową grafiki . Bez takiego przyspieszenia każda gra typu nieskończony biegacz na iOS z nawet umiarkowaną liczbą sprite'ów będzie działać ze szczególnie słabą wydajnością. Jeśli spróbujemy stworzyć bardziej skomplikowaną aplikację, prawdopodobnie zaczniemy widzieć na ekranie efekt „bullet-time”, tj. wiele kopii każdego duszka podczas próby animowania.
Wreszcie, Cocos2D optymalizuje wykorzystanie pamięci, ponieważ buforuje sprite'y. Tak więc wszelkie zduplikowane duszki wymagają minimalnej dodatkowej pamięci, co jest oczywiście przydatne w grach.
Używanie Cocos2D ze scenorysami
Po wszystkich pochwałach, jakie otrzymałem w Cocos2D, sugerowanie używania Storyboards może wydawać się nielogiczne. Dlaczego nie po prostu manipulować swoimi obiektami za pomocą Cocos2D itp.? Cóż, szczerze mówiąc, w przypadku statycznych okien często wygodniej jest użyć Xcode's Interface Builder i jego mechanizm Storyboard.
Po pierwsze, pozwala mi przeciągać i ustawiać wszystkie moje elementy graficzne dla mojej niekończącej się gry w biegacza za pomocą myszy. Po drugie, Storyboard API jest bardzo, bardzo przydatne. (I tak, wiem o Cocos Builder).
Oto krótki rzut oka na mój Storyboard:
Kontroler głównego widoku gry zawiera tylko scenę Cocos2D z kilkoma elementami HUD na górze:
Zwróć uwagę na białe tło: to scena Cocos2D, która załaduje wszystkie niezbędne elementy graficzne w czasie wykonywania. Inne widoki (wskaźniki na żywo, dmuchawce, przyciski itp.) to standardowe widoki Cocoa dodane do ekranu za pomocą narzędzia Interface Builder.
Nie będę się rozwodził nad szczegółami — jeśli jesteś zainteresowany, przykłady można znaleźć na GitHub.
Rozgrywka i (krótki) opis projektu
(Aby zapewnić trochę więcej motywacji, chciałbym nieco bardziej szczegółowo opisać moją grę niekończącego się biegacza. Jeśli chcesz przejść do dyskusji technicznej, możesz pominąć tę sekcję.)
Podczas rozgrywki na żywo pszczoła jest nieruchoma, a samo pole pędzi, niosąc ze sobą rozmaite niebezpieczeństwa (pająki i jadowite kwiaty) oraz korzyści (mlecze i ich nasiona).
Cocos2D ma obiekt kamery, który został zaprojektowany, aby podążać za postacią; w praktyce manipulowanie CCLayer zawierającym świat gry było mniej skomplikowane.
Sterowanie jest proste: stuknięcie w ekran przesuwa pszczołę w górę, a kolejne stuknięcie w dół.
Sama warstwa świata ma w rzeczywistości dwie podwarstwy. Po rozpoczęciu gry pierwsza podwarstwa jest wypełniana od 0 do BUF_LEN i początkowo wyświetlana. Druga podwarstwa jest wypełniana z wyprzedzeniem od BUF_LEN do 2*BUF_LEN. Kiedy pszczoła osiągnie BUF_LEN, pierwsza podwarstwa jest czyszczona i natychmiast ponownie zaludniana z 2*BUF_LEN do 3*BUF_LEN, a druga podwarstwa jest prezentowana. W ten sposób naprzemiennie przechodzimy między warstwami, nigdy nie zachowując przestarzałych obiektów, co jest ważnym elementem unikania wycieków pamięci.
Jeśli chodzi o silniki fizyczne, użyłem Chipmunk z dwóch powodów:
- Jest napisany w czystym Objective-C.
- Pracowałem już wcześniej z Box2D, więc chciałem je porównać.
Silnik fizyczny był tak naprawdę używany tylko do wykrywania kolizji. Czasami jestem pytany: „Dlaczego nie napisałeś własnego wykrywania kolizji?”. W rzeczywistości nie ma to większego sensu. Silniki fizyki zostały zaprojektowane właśnie do tego celu: mogą wykrywać kolizje między ciałami o skomplikowanych kształtach i optymalizować ten proces. Na przykład silniki fizyczne często dzielą świat na komórki i przeprowadzają kontrolę kolizji tylko dla ciał w tej samej lub sąsiednich komórkach.
Zautomatyzuj zadania. Używaj narzędzi. Bądź fajny.
Kluczowym elementem tworzenia niezależnych gier typu endless runner jest unikanie potykania się o drobne problemy. Czas jest kluczowym zasobem podczas tworzenia aplikacji, a automatyzacja może być niezwykle czasochłonna.
Ale czasami automatyzacja może być również kompromisem między perfekcjonizmem a dotrzymaniem terminu. W tym sensie perfekcjonizm może być zabójcą Angry Birds.
Na przykład w innej grze na iOS, którą aktualnie rozwijam, zbudowałem framework do tworzenia układów za pomocą specjalnego narzędzia (dostępnego na GitHubie). Ten framework ma swoje ograniczenia (na przykład nie ma ładnych przejść między scenami), ale używanie go pozwala mi tworzyć sceny w dziesiątej części czasu.
Więc chociaż nie możesz zbudować własnego superframeworku za pomocą specjalnych supernarzędzi, nadal możesz i powinieneś zautomatyzować jak najwięcej tych małych zadań.
W tworzeniu tego nieskończonego biegacza kluczowa była po raz kolejny automatyzacja. Na przykład mój artysta wysyłał mi grafikę w wysokiej rozdzielczości przez specjalny folder Dropbox. Aby zaoszczędzić czas, napisałem kilka skryptów, które automatycznie budują zestawy plików dla różnych docelowych rozdzielczości wymaganych przez App Store, dodając również -hd lub @2x (wspomniane skrypty bazują na ImageMagick).
Jeśli chodzi o dodatkowe narzędzia, uważam, że TexturePacker jest bardzo przydatny — może pakować sprite'y w arkusze sprite'ów, dzięki czemu Twoja aplikacja będzie zużywać mniej pamięci i ładować się szybciej, ponieważ wszystkie sprite'y będą odczytywane z jednego pliku. Może również eksportować tekstury w prawie wszystkich możliwych formatach frameworków. (Zauważ, że TexturePacker nie jest darmowym narzędziem, ale myślę, że jest wart swojej ceny. Możesz także sprawdzić darmowe alternatywy, takie jak ShoeBox.)

Główną trudnością związaną z fizyką gry jest tworzenie odpowiednich wielokątów dla każdego duszka. Innymi słowy, tworząc wielokątną reprezentację jakiejś pszczoły lub kwiatu o niejasnym kształcie. Nawet nie próbuj robić tego ręcznie — zawsze używaj specjalnych aplikacji, których jest wiele. Niektóre są nawet dość… egzotyczne – jak tworzenie masek wektorowych za pomocą Inkspace, a następnie importowanie ich do gry.
Na potrzeby własnego tworzenia gier typu endless runner, stworzyłem narzędzie do automatyzacji tego procesu, które nazywam Andengine Vertex Helper. Jak sama nazwa wskazuje, został pierwotnie zaprojektowany dla frameworka Andengine, chociaż obecnie będzie działał poprawnie z wieloma formatami.
W naszym przypadku musimy użyć wzoru plist:
<real>%.5f</real><real>%.5f</real>
Następnie tworzymy plik plist z opisami obiektów:
<?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>
Oraz moduł ładujący obiekty:
- (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); }
Aby sprawdzić, jak duszki odpowiadają ich fizycznym ciałom, zobacz tutaj.
Dużo lepiej, prawda?
Podsumowując, zawsze automatyzuj, kiedy to możliwe. Nawet proste skrypty mogą zaoszczędzić mnóstwo czasu. I co ważne, ten czas można wykorzystać na programowanie zamiast na klikanie myszką. (Dla dodatkowej motywacji, oto symbol XKCD.)
Rozliczenia w aplikacji
Zebrane kule dmuchane w grze działają jak waluta w aplikacji, umożliwiając użytkownikom kupowanie nowych skórek dla swojej pszczoły. Jednak tę walutę można również kupić za prawdziwe pieniądze. Ważną kwestią, na którą należy zwrócić uwagę w odniesieniu do rozliczeń w aplikacji, jest to, czy konieczne jest sprawdzenie ważności zakupu po stronie serwera. Ponieważ wszystkie towary, które można kupić, są zasadniczo takie same pod względem rozgrywki (tylko zmiana wyglądu pszczoły), nie ma potrzeby sprawdzania serwera pod kątem ważności zakupu. Jednak w wielu przypadkach na pewno będziesz musiał to zrobić.
Aby uzyskać więcej informacji, Ray Wenderlich ma doskonały samouczek dotyczący rozliczeń w aplikacji.
Rozgrywka wieloosobowa z Game Center
W grach mobilnych utrzymywanie kontaktów towarzyskich to coś więcej niż tylko dodanie przycisku „Lubię to” na Facebooku lub tworzenie tabel wyników. Aby gra była bardziej ekscytująca, zaimplementowałem wersję dla wielu graczy.
Jak to działa? Po pierwsze, dwóch graczy jest połączonych za pomocą systemu dobierania graczy w czasie rzeczywistym w iOS Game Center. Ponieważ gracze naprawdę grają w tę samą nieskończoną grę biegacza, musi być tylko jeden zestaw obiektów gry. Oznacza to, że instancja jednego gracza musi generować obiekty, a druga gra je odczytuje. Innymi słowy, gdyby urządzenia obu graczy generowały obiekty w grze, trudno byłoby zsynchronizować wrażenia.
Mając to na uwadze, po nawiązaniu połączenia obaj gracze wysyłają sobie losową liczbę. Gracz o wyższym numerze pełni rolę „serwera”, tworząc obiekty gry.
Czy pamiętasz dyskusję o porcjowanym pokoleniu świata? Gdzie mieliśmy dwie podwarstwy, jedną od 0 do BUF_LEN, a drugą od BUF_LEN do 2*BUF_LEN? Ta architektura nie została użyta przypadkowo — konieczne było zapewnienie płynnej grafiki w opóźnionych sieciach. Kiedy część obiektów jest generowana, jest pakowana do plisty i wysyłana do drugiego gracza. Bufor jest wystarczająco duży, aby drugi gracz mógł grać nawet z opóźnieniem sieciowym. Obaj gracze wysyłają sobie nawzajem swoją aktualną pozycję w ciągu pół sekundy, jednocześnie wysyłając natychmiast swoje ruchy góra-dół. Aby wygładzić wrażenia, pozycja i prędkość są korygowane co 0,5 sekundy za pomocą płynnej animacji, więc w praktyce wygląda na to, że drugi gracz stopniowo się porusza lub przyspiesza.
Z pewnością należy wziąć pod uwagę więcej kwestii związanych z niekończącą się rozgrywką w trybie wieloosobowym, ale miejmy nadzieję, że da ci to wyczucie rodzaju wyzwań, z którymi się zmagasz.
Miejsce na udoskonalenie
Gry nigdy się nie kończą. Trzeba przyznać, że jest kilka obszarów, w których chciałbym poprawić swoje własne, a mianowicie:
- Problemy z kontrolą: stukanie jest często nieintuicyjnym gestem dla graczy, którzy wolą się ślizgać.
Warstwa świata jest przesuwana za pomocą akcji CCMoveBy. Było to w porządku, gdy prędkość warstwy świata była stała, ponieważ akcja CCMoveBy była cykliczna za pomocą 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]; }
Ale później dodałem wzrost prędkości świata, aby utrudnić grę w miarę jej trwania:
-(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]; }
Ta zmiana powodowała niszczenie animacji przy każdym ponownym uruchomieniu akcji. Próbowałem rozwiązać problem, ale bezskutecznie. Jednak moi beta testerzy nie zauważyli tego zachowania, więc odłożyłem poprawkę.
- Z jednej strony nie było potrzeby pisania własnej autoryzacji dla trybu wieloosobowego podczas korzystania z Game Center lub prowadzenia własnego serwera gier. Z drugiej strony uniemożliwiło to tworzenie botów, co chciałbym zmienić.
Wniosek
Tworzenie własnej niezależnej gry typu endless runner może być wspaniałym przeżyciem. A kiedy dojdziesz do etapu publikacji, może to być cudowne uczucie, gdy wypuszczasz własne dzieło na wolność.
Proces weryfikacji może trwać od kilku dni do kilku tygodni. Aby uzyskać więcej informacji, dostępna jest tutaj pomocna witryna, która wykorzystuje dane pochodzące z tłumu do oszacowania bieżących czasów recenzji.
Dodatkowo polecam używanie AppAnnie do sprawdzania różnych informacji o wszystkich aplikacjach w App Store, a rejestracja w niektórych usługach analitycznych, takich jak Flurry Analytics, również może być pomocna.
A jeśli ta gra Cię zaintrygowała, koniecznie sprawdź Bee Race w sklepie.