Witamy
Szanowni twórcy oprogramowania i specjaliści branży IT
Przed Wami pierwsze wydanie magazynu „Programista”, które ukazało się również w
postaci drukowanej. Od niniejszego numeru magazyn staje się miesięcznikiem.
Przychyliliśmy się też do Waszych próśb i przygotowaliśmy magazyn w wersji elektronicznej
w plikach ePub i .mobi oraz .pdf.
Rośnie ilość i złożoność otaczających nas systemów informatycznych. Rynek tabletów i
smartfonów ciągle odnotowuje wzrosty i naturalną koleją rzeczy powstają tysiące nowych
aplikacji na te urządzenia: od gier po programy użytkowe. Rozwijają się języki
programowania, powstają kolejne wersje „mobilnych” systemów operacyjnych. W tej sytuacji
nie ma wyjścia: chcąc pozostać konkurencyjnym w zawodzie Programisty, trzeba się na
bieżąco rozwijać, dokształcać. Naszym celem jest Wam to zadanie ułatwić.
W bieżącym numerze przedstawiamy bibliotekę Cocos2D: jeden z najpopularniejszych
silników do tworzenia gier na platformę iOS. Ponadto polecamy praktyczny artykuł na temat
języka Objective-C, w którym autor omawia przydatne jego właściwości, pozwalające znacznie
usprawnić proces tworzenia aplikacji na urządzenia mobilne ze stajni Apple. Znajdziecie u nas
również ciekawy artykuł traktujący o popularnym ostatnio podejściu Domain Driven Design.
Ponadto kontynuujemy tematykę poruszoną w premierowym wydaniu naszego miesięcznika:
omawiamy możliwości nowego standardu języka C++ oraz prezentujemy ciekawe tematy z
zakresu inżynierii oprogramowania.
Na koniec pragniemy serdecznie podziękować za konstruktywne opinie dotyczące
pierwszej, elektronicznej edycji magazynu. Wydanie to udostępniliśmy bezpłatnie, by każdy
mógł wyrobić sobie opinię o projekcie. Olbrzymie zainteresowanie, jakim cieszył się
premierowy egzemplarz, utwierdza nas w przekonaniu, że tego typu wydawnictwa brakowała
na polskim rynku i dopinguje do jeszcze bardziej wytężonej pracy nad magazynem.
Cieszy nas, że zdecydowana większość z Was pozytywnie oceniła inicjatywę! Dziękujemy za
wsparcie i życzymy przyjemnej lektury!
Z wyrazami szacunku, Redakcja
Spis treści
BIBLIOTEKI I NARZĘDZIA
Biblioteka Cocos2D: wprowadzenie
Rafał Kocisz
JĘZYKI PROGRAMOWANIA
C++11 część I
Bartosz Szurgot, Mariusz Uchroński, Wojciech Waga
Wybrane elementy języka Objective-C i ich wykorzystanie
Łukasz Mazur
Erlang - język inny niż C++ czy Java
Marek Sawerwain
PROGRAMOWANIE GRAFIKI
Direct3D – podstawy
Wojciech Sura
PROGRAMOWANIE URZĄDZEŃ
Wykorzystanie sensora Kinect w systemie Windows
Łukasz Górski
INŻYNIERIA OPROGRAMOWANIA
Domain Driven Design krok po kroku część II: Zaawansowane modelowanie DDD
– techniki strategiczne: konteksty i architektura zdarzeniowa
Sławomir Sobótka
KLUB LIDERA IT
Dokumentowanie architektury. Jak zorganizować proces rozwoju architektury?
Michał Bartyzel, Mariusz Sieraczkiewicz
KOMIKS
Maciej Mazurek
WDROŻENIA
Highsky.com – projekt, oprogramowanie i wdrożenie platformy inwestycyjnej highsky.com zintegrowanej z platformą
MetaTrader 5.
Wojciech Holisz
Redakcja
Wydawca:
Anna Adamczyk
annaadamczyk@programistamag.pl
Redaktor naczelny:
Łukasz Łopuszański
lukaszlopuszanski@programistamag.pl
Redaktor prowadzący:
Rafał Kocisz
rafal.kocisz@gmail.com
Korekta:
Tomasz Łopuszański
Kierownik produkcji:
Krzysztof Kopciowski
bok@keylight.com.pl
DTP:
Krzysztof Kopciowski
Dział reklamy:
reklama@programistamag.pl
tel. +48 663 220 102
tel. +48 604 312 716
Prenumerata:
prenumerata@programistamag.pl
Współpraca:
Michał Bartyzel, Mariusz Sieraczkiewicz,
Sławomir Sobótka, Artur Machura,
Marek Sawerwain, Łukasz Mazur,
Rafał Kułaga
Adres wydawcy:
Dereniowa 4/47
02-776 Warszawa
Druk:
Zamów wydanie w wersji papierowej przez www.programistamag.pl
O ile nie zaznaczono inaczej, wszelkie prawa do wszystkich materiałów zamieszczanych na łamach magazynu Programista są zastrzeżone. Kopiowanie i
rozpowszechnianie ich bez zezwolenia jest wzbronione. Naruszenie praw autorskich może skutkować odpowiedzialnością prawną, określoną w
szczególności w przepisach ustawy o prawie autorskim i prawach pokrewnych, ustawy o zwalczaniu nieuczciwej konkurencji i przepisach kodeksu
cywilnego oraz przepisach prawa prasowego.
Redakcja magazynu Programista nie ponosi odpowiedzialności za szkody bezpośrednie i pośrednie, jak również za inne straty i wydatki poniesione w
związku z wykorzystaniem informacji prezentowanych na łamach magazynu Programista. Wszelkie nazwy i znaki towarowe lub firmowe występujące na
łamach magazynu są zastrzeżone przez odpowiednie firmy.
Biblioteka Cocos2D: wprowadzenie
Rafał Kocisz
iOS to platforma, która rozbudza wyobraźnię wielu programistów. Któż z nas nie
marzy o karierze niezależnego twórcy gier i setkach tysięcy dolarów zarobionych
dzięki sprzedaży aplikacji na AppStore? W niniejszym artykule przedstawiona jest
biblioteka, która może być kluczem do spełnienia tych marzeń.
Głównym celem niniejszego artykułu jest zapoznanie Czytelnika z podstawowymi blokami
budulcowymi, które oferuje biblioteka Cocos2D. Po jego przeczytaniu będziesz wiedział, czego
możesz spodziewać się po tym silniku i na ile przydatny będzie on w Twoich projektach.
Cocos2D to potężna biblioteka i szczegółowe jej opisanie to temat, który kwalifikuje się
bardziej na książkę niż na artykuł. Z tego względu niniejszy tytuł kładzie nacisk na ogólne
zrozumienie koncepcji, na których opiera się biblioteka Cocos2D, oraz przedstawienie relacji
między nimi. W jednym z ostatnich sekcji artykułu wskazane są materiały, z których
zainteresowani Czytelnicy będą mogli skorzystać w celu poszerzenia swojej wiedzy na temat
prezentowanej tu biblioteki.
DLACZEGO COCOS2D?
Zanim przejdziemy do omówienia możliwości biblioteki Cocos2D, spróbujmy odpowiedzieć
sobie na podstawowe pytanie: co sprawia, że warto zainteresować się właśnie tym
konkretnym rozwiązaniem?
Pierwszy ważny powód, dla którego warto rozważyć używanie Cocos2D, to fakt, iż
biblioteka ta jest całkowicie darmowa, zaś jej licencja pozwala tworzyć zarówno aplikacje
komercyjne, jak i niekomercyjne. Licencja Cocos2D jest otwarta, silnik rozpowszechniany jest
razem z kodem źródłowym. Oznacza to, że nic nie stoi na przeszkodzie, aby w razie potrzeby
zajrzeć do kodu źródłowego biblioteki czy wręcz wprowadzić do niej własne modyfikacje.
Cocos2D napisany jest w języku Objective-C. Biorąc pod uwagę, że Cocos2D obsługuje
urządzenia z rodziny iOS oraz OS X, wybór ten wydaje się bardzo trafny: Objective-C to
natywny język programowania w wymienionych systemach. Dla osób, które znają inny język
obiektowy (np. C++, Java, C#), nauka Objective-C nie powinna sprawić większych trudności.
Osobom rozpoczynającym przygodę z programowaniem sugerowałbym poświęcenie nieco
czasu na solidne zapoznanie się z Objective-C, zanim na poważnie zaczną zajmować się
programowanie gier pod iOS przy użyciu biblioteki Cocos2D.
Jak sugeruje nazwa biblioteki, Cocos2D wspomaga programowanie gier 2D. Warto
podkreślić jednak, że mowa tutaj o nowoczesnych grach 2D, w których na porządku dziennym
są wykonywane w czasie rzeczywistym transformacje obrazów (np. rotacja czy skalowanie),
obsługa przeźroczystości czy post-processing (wszystko to oczywiście wspomagane
akceleracją sprzętową). Jeśli jesteś początkującym programistą gier, to zabawa z
dwuwymiarem jest wręcz zalecana (chociażby dlatego, że algorytmy stosowane w tego typu
grach są o wiele łatwiejsze w implementacji w stosunku do ich odpowiedników stosowanych
w grach 3D).
Dodatkowo, za Cocosem stoi bardzo liczna, prężna i otwarta społeczność złożona w dużej
mierze z niezależnych programistów gier, co daje możliwość stosunkowo łatwego uzyskania
wsparcia. Ze względu na swoją popularność Cocos2D może poszczycić się posiadaniem dużej
ilości wysokiej jakości materiałów edukacyjnych (samouczków, książek, forów dyskusyjnych)
oraz narzędzi, które wspierają i ułatwiają pracę z tą biblioteką.
Podsumowując, Cocos2D jest niewątpliwie biblioteką, z którą warto się zapoznać. Jeśli
masz ochotę zanurkować w jego barwny świat, zapraszam do lektury dalszej części
niniejszego artykułu.
GRAF SCENY
Najważniejsza, centralna koncepcja, wokół której zbudowana jest biblioteka Cocos2D to tzw.
graf sceny (zwany czasami drzewem sceny bądź hierarchią sceny). Zrozumienie tej koncepcji
jest kluczowe w przypadku gdy ktoś chce efektywnie korzystać z prezentowanego tu silnika.
Wyobraź sobie, że masz zaimplementować fragment graficznego interfejsu użytkownika w
grze, coś podobnego do menu przedstawionego na Rysunku 1. Spróbuj spojrzeć na ten obraz
okiem programisty i zastanów się, jak można by zorganizować model takiego interfejsu
użytkownika na poziomie kodu źródłowego. Pierwsza myśl, która zapewne przyjdzie Ci do
głowy, to przechowywanie płaskiej listy obiektów reprezentujących wszystkie widoczne
elementy na ekranie (elementy tła, przyciski i elementy tekstowe). Obiekty takie
przechowywałyby informacje o stanie poszczególnych elementów menu (np. ich pozycja, stan,
widoczność). Informacji tych można by użyć do rysowania całej sceny, obsługi zdarzeń
użytkownika itp.
Rysunek 1. Proste menu gry (źródło: opracowanie własne)
Podejście takie można by oczywiście z powodzeniem zastosować, ale... zanim zaczniesz
kodować, zastanów się, czy nie można by było zorganizować tego lepiej? Powiedzmy, że
chciałbyś zaimplementować prosty efekt polegający na tym, że druga warstwa tła z
umieszczonymi na niej kontrolkami płynnie wsuwa się przy rozpoczęciu gry, zaś przy jej
zakończeniu wysuwa się poza ekran (patrz: Rysunek 2).
Rysunek 2. Efekt wsuwania się menu (źródło: opracowanie własne)
Sprawa niby prosta, jednakże przy zastosowaniu płaskiej listy jako struktury danych
reprezentującej kolekcję kontrolek, implementacja takiego efektu staje się nieco uciążliwa:
trzeba ręcznie wybrać wszystkie obiekty, które chcemy wsuwać\wysuwać, i dla każdego z nich
odpowiednio modyfikować ich pozycje. W tej sytuacji aż się prosi, aby potraktować drugą
warstwę tła jako płaszczyznę, na której leżą wszystkie pozostałe kontrolki, i przesunąć ją,
razem ze wszystkim elementami, które są na niej umieszczone. W tym celu możemy
zastosować właśnie graf sceny.
Graf sceny to drzewiasta struktura danych pozwalająca reprezentować obrazy (zarówno 2D
i 3D) tak, aby zachować informację o hierarchii obiektów na nich występujących. Kluczowym
elementem grafu sceny jest węzeł (ang. node), który spełnia dwojaką rolę: po pierwsze,
reprezentuje wybrany element sceny; po drugie, jest kontenerem, który może przechowywać
inne węzły, stanowiące jego dzieci (ang. children). Węzeł posiadający dzieci nazywany jest
rodzicem (ang. parent). W grafie sceny występuje jeden węzeł, który nie posiada rodzica;
nazywamy go korzeniem (ang. root). Znamienne dla grafu sceny jest to, że każdy rodzic
definiuje dla swoich dzieci swoisty lokalny układ współrzędnych. Oznacza to, iż współrzędne
dzieci (a także inne ich właściwości) rozpatrywane są w odniesieniu do układu rodzica. Np.
jeśli węzeł rodzic (korzeń) ma pozycję (50, 50), zaś jego potomek znajduje się w punkcie (10,
15), to rzeczywista (ekranowa) pozycja tego drugiego wyniesie (60, 75). W takim ujęciu, gdy
zmienimy pozycję danego węzła, to automatycznie przemieszczone zostaną jego dzieci.
Każdy węzeł w grafie sceny posiada identyczny interfejs, opisany zazwyczaj przez
abstrakcyjną klasę bazową. Po tej klasie dziedziczą inne klasy, reprezentujące konkretne
węzły. Czytelnicy, którzy mieli styczność z tzw. wzorcami projektowymi (ang. design
patterns), słusznie skojarzą przedstawiony tutaj opis ze wzorcem kompozyt (ang. composite);
de facto graf sceny jest niemalże wzorcowym przykładem takiego podejścia projektowego.
Spróbujmy odnieść przedstawione wyżej rozważania do naszej przykładowej sceny. Na
Rysunku 3 przedstawiona jest wspomniana scena z oznaczeniem elementów hierarchii w
grafie. Korzeniem grafu jest pierwsza warstwa tła (czerwony prostokąt otaczający). Korzeń ma
jednego potomka: drugą warstwę tła (niebieski prostokąt otaczający). Ten z kolei posiada
czwórkę dzieci: przyciski (żółte prostokąty otaczające). Każdy przycisk posiada jednego
potomka, którym jest umieszczony pod nim napis. Do reprezentacji takiej sceny
potrzebowalibyśmy dwóch konkretnych klas-węzłów reprezentujących statyczny obrazek
(elementy tła i przyciski) oraz tekst (napisy pod przyciskami).
Mając do dyspozycji tak zorganizowaną scenę, zaprogramowanie efektu wsuwania się
drugiej warstwy tła z umieszczonymi na niej kontrolkami staje się bardzo proste; wystarczy
jedynie odpowiednio zmodyfikować pozycję węzła reprezentującego tę warstwę, a o właściwe
pozycjonowanie pozostałych elementów zadba graf sceny.
Rysunek 3. Hierarchia sceny dla prostego menu w grze (źródło: opracowanie własne)
Graf sceny to nieocenione narzędzie przy tworzeniu aplikacji wyświetlających złożone
obrazy zbudowane z grup powiązanych ze sobą obiektów (pod tę kategorię można z
powodzeniem podciągnąć większość nowoczesnych gier 2D oraz 3D). Jest on również bardzo
przydatny w innych zastosowaniach, np. określanie widoczności obiektów czy detekcja kolizji.
Na tym etapie ważne jest, abyś zrozumiał podstawową ideę tego wzorca projektowego, gdyż
stanowi on serce biblioteki Cocos2D.
KLASA BAZOWA CCNODE
Tak jak wspominałem w poprzednim punkcie, kluczowymi elementami w grafie sceny są
węzły. Każdy węzeł dziedziczy po klasie bazowej, która definiuje spójny interfejs dla
wszystkich obiektów umieszczanych w hierarchii. W przypadku biblioteki Cocos2D klasa ta
nazywa się
CCNode
. Po tej klasie dziedziczą kolejne klasy, reprezentujące konkretne obiekty, które można
umieszczać w grafie sceny. Na Rysunku 4 przedstawiona jest hierarchia klas wywodzących się
z klasy bazowej
CCNode
.
Rysunek 4. Hierarchia klas wywodzących się z CCNode (źródło: http://www.cocos2d-iphone.org/ ). Schemat w większej
rozdzielczości.
Jak widać, klas tych jest całkiem sporo. Cocos2D to bardzo aktywnie rozwijana biblioteka,
więc w momencie kiedy czytasz niniejszy artykuł, hierarchia ta może wyglądać nieco inaczej
od tej, która jest tutaj przedstawiona (dotyczy ona Cocos2D w wersji 1.0.1), jednakże szereg
koncepcji reprezentowanych przez wybrane klasy niewątpliwie pozostaną niezmienne. Klasy
te będą szczegółowo omówione w kolejnych podpunktach niniejszego artykułu.
Zanim jednak do tego przejdziemy, przyjrzyjmy się bardziej szczegółowo interfejsowi klasy
CCNode
. Jak już wcześniej wspominałem, jest to klasa abstrakcyjna i nie ma bezpośredniej
reprezentacji wizualnej, jednakże pełni bardzo istotną rolę, gdyż zawiera pola i metody
wspólne dla wszystkich węzłów przechowywanych w grafie sceny. Oto lista najważniejszych z
nich:
tworzenie nowego węzła:
CCNode* childNode = [CCNode node];
dodawanie węzła-dziecka:
[node addChild:child z:0 tag:73];
wyłuskiwanie węzła-dziecka:
CCNode* child = [node getChildByTag:73];
usuwanie węzła-dziecka określonego za pomocą identyfikatora: (z opcjonalnym
czyszczeniem, polegającym na zastopowaniu wszystkich akcji przypisanych do
usuwanego węzła):
[node removeChildByTag:73 cleanup:YES];
usuwanie węzła-dziecka za pośrednictwem wskaźnika:
[node removeChild:child];
usuwanie wszystkich dzieci z węzła:
[node removeAllChildrenWithCleanup:YES];
usuwanie siebie samego z węzła-rodzica:
[node removeFromParentAndCleanup:YES];
Jak łatwo się można domyśleć, parametr
z
przekazywany w metodzie
addChild
określa głębokość (ang. depth) węzła w scenie i pozwala kontrolować kolejność rysowania
elementów przechowywanych w grafie. Zasada jest prosta: elementy z małymi wartościami
z
rysowane są jako pierwsze, zaś te, które mają największą głębokość – jako ostatnie. Jeśli
część węzłów posiada taką samą wartość parametru
z
, to porządek ich rysowania określony jest kolejnością ich dodawania do węzła-rodzica.
Z kolei parametr
tag
to rodzaj identyfikatora, za pomocą którego możemy szybko wyłuskiwać czy usuwać węzły
za pomocą takich metod jak
getChildByTag
czy
removeChildByTag
. Korzystając z tagów, należy pamiętać o tym, że Cocos2D nie rozwiązuje problemu ich
unikalności, tj. jeśli umieścisz w scenie dwa, lub więcej węzłów o identycznych
identyfikatorach, to będziesz w stanie dostać się tylko do pierwszego z nich; pozostałe będą
niedostępne. Cocos2D wykorzystuje tagi również do identyfikacji tzw. akcji (co to są akcje,
dowiesz się już za moment); w tym miejscu chciałbym tylko wspomnieć, że identyfikatory
węzłów i akcji nie kolidują ze sobą, tak więc dopuszczalna jest sytuacja, gdy zarówno węzeł,
jak i przypisana do niego akcja mają ten sam tag.
Jak już kilka razy wspomniałem, węzły mogą mieć przypisane akcje (ang. actions).
Koncepcję akcji opiszę szczegółowo w oddzielnym podpunkcie niniejszego artykułu; tutaj
przedstawię je tylko pokrótce, tak abyś mógł zrozumieć relację pomiędzy nimi a węzłami.
Pisząc w największym skrócie, akcje to obiekty reprezentujące działania wykonywane na
obiektach sceny. Wyobraź sobie chcesz zaimplementować graficzny element na ekranie (np.
przycisk), który miarowo pulsuje (na przemian powiększa się i zmniejsza) albo miarowo miga
(na przemian pojawia się i znika) bądź po prostu przemieszcza się pomiędzy dwoma
punktami. Wszystkie te operacje możesz zrealizować przypisując do węzła reprezentującego
wspomniany element odpowiednie akcje. O tym, jakie są kategorie akcji, porozmawiamy za
moment; teraz kilka słów na temat tego, jak można obsługiwać je z poziomu interfejsu klasy
CCNode
. Na początek stwórzmy prostą akcję reprezentującą proces migania węzła i przypiszmy jej
identyfikator:
CCAction* action =
[CCBlink actionWithDuration:5
blinks:10];
action.tag = 37;
Akcja ta przypisana do określonego węzła sprawi, że zamiga on 10 razy w przeciągu 5 sekund.
Klasa
CCNode
pozwala kontrolować akcje w następujący sposób:
uruchomienie akcji:
[node runAction:action];
wyłuskanie akcji określonej za pomocą identyfikatora:
CCAction* retrievedAction = [node getActionByTag:37];
zastopowanie akcji określonej za pomocą identyfikatora:
[node stopActionByTag:37];
zastopowanie akcji określonej za pomocą wskaźnika:
[node stopAction:action];
zastopowanie wszystkich akcji przypisanych do danego węzła:
[node stopAllActions];
Ostatnią właściwością klasy
CCNode
, o której chciałbym opowiedzieć, to możliwość tzw. harmonogramowania wiadomości (ang.
message scheduling). Brzmi to nieco tajemniczo, jednakże w praktyce rzecz jest bardzo
prosta. Przede wszystkim należy wyjaśnić, że w nomenklaturze języka Objective-C wysłanie
wiadomości do obiektu oznacza po prostu wywołanie metody na tym obiekcie. W takim ujęciu
harmonogramowanie wiadomości oznacza cykliczne wywoływanie określonej metody.
Wyobraź sobie, że chciałbyś mieć możliwość oprogramowania jakiejś dedykowanej logiki dla
danego typu węzłów (np. detekcja kolizji bądź sprawdzanie warunku zakończenia gry). Tego
typu logikę umieszcza się zazwyczaj w metodzie
update
bądź
process
, która wywoływana jest po narysowaniu kolejnej ramki gry, przyjmującej jako parametr
przyrost czasu od poprzedniego jej wywołania (ang. time delta). Jeśli potrzebujesz takiej
funkcjonalności w węźle, to musisz odpowiednio przeładować metodę
scheduleUpdateMethod
(patrz: Listing 1).
Listing 1. Harmonogramowanie prostej funkcji obsługującej logikę węzła
-(void) scheduleUpdateMethod
{
[self scheduleUpdate];
}
-(void) update:(ccTime)delta
{
// Ta metoda będzie wywoływana po narysowaniu
// kolejnej ramki gry.
}
REŻYSER, SCENA, WARSTWA
Chciałbym przedstawić teraz trzy klasy, które pełnią w bibliotece Cocos2D bardzo ważne role.
Mowa tu o klasach
CCDirector
,
CCScene
oraz
CCLayer
. Dwie ostatnie z wymienionych dziedziczą z
CCNode
i podobnie jak ona, nie mają wizualnej reprezentacji.
CCScene
to kontener dla wszystkich obiektów znajdujących się na scenie. Obiekty tej klasy
reprezentują zazwyczaj ekrany gry: menu główne, tabelę wyników czy wreszcie – właściwą
rozgrywkę. Z kolei klasa
CCLayer
(warstwa) służy do grupowania obiektów, głównie w celu zapewnienia właściwej kolejności
ich renderowania. Można sobie np. wyobrazić, że ekran rozgrywki składa się z trzech warstw:
statycznego tła, części dynamicznej (ruchome obiekty gry) oraz paska statusu
przedstawiającego liczbę zdobytych punktów oraz żyć. Ważną cechą klasy
CCLayer
jest to, że potrafi ona przechwytywać zdarzenia generowane przez użytkownika za
pośrednictwem takich kontrolerów jak ekran dotykowy czy akcelerometr. Szczególną rolę w
tej układance pełni klasa
CCDirector
; pełni ona rolę reżysera – decyduje o tym, która scena (
CCScene
) będzie w danym momencie aktywna.
Na początek przyjrzyjmy się, co oferuje klasa
CCDirector
. Po pierwsze, klasa ta jest tzw. singletonem (singleton jest to wzorzec projektowy, który
zapewnia, że dana klasa posiada tylko jedną instancję). Fakt ten wydaje się być dość oczywisty
(czy widziałeś kiedyś film lub przedstawienie, za które odpowiadało dwóch reżyserów?).
Główne zadanie reżysera w bibliotece Cocos2D to zarządzanie scenami oraz przechowywanie
danych konfiguracyjnych. W szczególności klasa
CCDirector
odpowiada za:
startowanie sceny,
zamianę bieżącej sceny,
wkładanie (ang. pushing) nowej sceny na bieżącą,
zdejmowanie (ang. popping) bieżącej sceny,
gwarantowanie dostępu do bieżącej sceny,
pauzowanie, kontynuowanie oraz kończenie gry,
przechowywanie i gwarantowanie dostępu do globalnych danych konfiguracyjnych
biblioteki Cocos2D,
gwarantowanie dostępu do okna oraz widoku OpenGL,
konwertowanie współrzędnych UIKit i OpenGL,
kontrolowanie procesu uaktualniania stanu gry.
Pracując z biblioteką Cocos2D, mamy do wyboru cztery typy reżyserów. Typy te implikują
sposób kontrolowania procesu uaktualniania stanu gry i w rezultacie mają bardzo istotny
wpływ na wydajność aplikacji:
kCCDirectorTypeDisplayLink
: gwarantuje najlepszą wydajność oraz płynność renderowania, dzięki zastosowaniu
mechanizmu synchronizacji procesu uaktualniania ekranu z jego sprzętowym
odświeżeniem; niestety – mechanizm ten dostępny jest od wersji 3.1 systemu iOS wzwyż,
kCCDirectorTypeNSTimer
: gwarantuje największą przenośność (będzie działać w każdej wersji systemu iOS), ale
jest za to najwolniejszy,
kCCDirectorTypeThreadMainLoop
: szybki, ale sprawia problemy w sytuacji, kiedy chcemy używać z poziomu aplikacji
Cocos2D widoków UIKit,
KCCDirectorTypeMainLoop
: jak wyżej.
Domyślnie biblioteka Cocos2D korzysta z reżysera typu
kCCDirectorTypeDisplayLink
, zaś w sytuacji gdy nie jest on dostępny (na dzień dzisiejszy bardzo mało prawdopodobna
sytuacja), przełącza się na typ
kCCDirectorTypeNSTimer
. Jeśli chciałbyś sam określić typ reżysera, to musisz zmodyfikować następujący fragment
kodu źródłowego w klasie reprezentującej delegata aplikacji:
if ( ! [CCDirector
setDirectorType:
kCCDirectorTypeDisplayLink] )
[CCDirector
setDirectorType:
kCCDirectorTypeDefault];
Kolejną klasą z inwentarza biblioteki Cocos2D, z którą warto się szczegółowo zapoznać, jest
CCScene
. Obiekty tej klasy są zawsze korzeniami w grafie sceny. Rozwiązanie to wydaje się
momentami odrobinę sztuczne (klasa
CCScene
w zasadzie nie rozszerza w żaden sposób funkcjonalności
CCNode
), jednakże metody
runWithScene
,
replaceScene
czy
pushScene
z klasy
CCDirector
potrafią współpracować tylko z obiektami tego typu. Poza tym, sceny można opakować
obiektami klas dziedziczących po klasie bazowej
CCSceneTransition
, co pozwala uzyskać miłe dla oka efekty przejść między scenami gry.
Pracując z biblioteką Cocos2D, zazwyczaj przyjmuje się konwencję, że dziećmi sceny są
jedynie warstwy (tj. obiekty wywodzące się z klasy
CCLayer
), zaś one dopiero zawierają w sobie węzły reprezentujące konkretne obiekty występujące w
grze.
Bardzo przydatnym mechanizmem jest wkładanie (ang. pushing) oraz zdejmowanie (ang.
popping) bieżącej sceny z poziomu reżysera. Operacje te niewątpliwie kojarzą się ze strukturą
danych zwaną stosem. I rzeczywiście, myśląc o tym, w jaki sposób klasa
CCDirector
zarządza scenami, można posłużyć się modelem stosu. Oczywiście, wkładając nową scenę
na stos, bieżąca nadal pozostaje widoczna. Mechanizm ten jest bardzo przydatny np. w
sytuacji kiedy w trakcie gry chcemy wyświetlić okno dialogowe, bądź podręczne menu. W tej
sytuacji wkładamy nową scenę na bieżącą, czekamy na działanie użytkownika, a w końcu
zdejmujemy bieżącą scenę ze stosu i automatycznie wracamy do przerwanej rozgrywki.
Manipulując scenami, trzeba jednak uważać, aby nie przesadzić, gdyż za każdym razem kiedy
Cocos2D podmienia sceny, nowa scena jest tworzona w pamięci, zaś stara zostaje usuwana.
Jak przed momentem wspominałem, używanie scen pozwala uzyskiwać bardzo ciekawe
efekty przejść (ang. transition) pomiędzy ekranami gry reprezentowanymi przez różne
obiekty klasy
CCScene
. Służą ku temu klasy wywodzące się z klasy bazowej
CCSceneTransition
. Hierarchia tych klas przedstawiona jest na Rysunku 5. Na Listingu 2 pokazany jest
przykład zastosowania jednego z przejść, tzw. zanikania (ang. fade).
Rysunek 5. Hierarchia klas wywodzących się z CCSceneTransition (źródło: http://www.cocos2d-iphone.org/ ) Schemat w większej
rozdzielczości.
Listing 2. Przykład zastosowania efektu CCTransitionFade
// Inicjalizacja obiektu reprezentującego efekt
// przejścia.
CCTransitionFade* transition =
[CCTransitionFade
transitionWithDuration:1
scene:[MyScene scene]
withColor:ccBLACK];
// Zamiana scen z użyciem efektu przejścia.
[[CCDirector sharedDirector] replaceScene:transition];
Wspomniany efekt polega na tym, że aktualna scena powoli blaknie, aż do momentu kiedy
cały ekran stanie się czarny (lub inny; docelowy kolor można sobie dowolnie zdefiniować), po
czym dzieje się efekt odwrotny, tyle że pojawia się nowa scena. Zaimplementowanie takiego
efektu jest bardzo proste. W pierwszym kroku należy stworzyć obiekt reprezentujący efekt
przejścia; w naszym przypadku będzie on typu
CCTransitionFade
. W konstruktorze możemy określić długość efektu (w sekundach), kolor oraz docelową
scenę. Potem wystarczy tylko przekazać obiekt przejścia (na Listingu reprezentowany przez
zmienną
transition
) do odpowiedniej metody w klasie
CCDirector
. W tym przypadku należy pamiętać, iż przejścia będą działać ze wszystkimi metodami
reżysera przewidzianymi do manipulacji scenami z jednym wyjątkiem, który stanowi funkcja
popScene
. Dlaczego? Powód jest prosty:
popScene
nie przyjmuje żadnych argumentów, po prostu zdejmuje ze stosu bieżącą scenę i nie ma
możliwości opakowania tej sceny obiektem reprezentującym efekt przejścia.
Cocos2D oferuje cały szereg efektów przechodzenia między scenami, informacje na ich
temat można znaleźć w dokumentacji biblioteki.
Korzystając z efektów przejść, warto pamiętać o jednej zasadzie, która brzmi: lepsze jest
wrogiem dobrego. Przejścia między scenami wyglądają bardzo efektownie, jednakże jeśli
przesadzimy z ich stosowaniem, to użytkownicy naszej gry dostaną białej gorączki (szczególną
uwagę należy zwrócić na to, by czasy przejść między scenami nie były zbyt długie).
Często zdarza się sytuacja kiedy warto podzielić scenę na warstwy. Wtedy można
skorzystać z klasy
CCLayer
. Na Listingu 3 pokazane jest, jak można dodać kilka warstw do sceny.
Listing 3. Dodawanie warstw do sceny
CCScene* scene = [CCScene node];
CCLayer* backgroundLayer = [Background node];
[scene addChild: backgroundLayer];
CCLayer* spritesLayer = [Sprites node];
[scene addChild:spritesLayer];
CCLayer* hudLayer = [Hud node];
[scene addChild: hudLayer];
Tak zainicjowana scena posiada trzy warstwy, które będą (razem ze swoimi dziećmi)
renderowane w kolejności ich dodawania do sceny. Zaraz, zaraz! Ale czy podobnego efektu nie
można by uzyskać za pomocą zwykłych węzłów? Odpowiedź brzmi: owszem, można by. Co w
takim razie odróżnia warstwy (
CCLayer
) od zwykłych węzłów (
CCNode
)? Zasadnicza różnica polega na tym, że warstwy potrafią przechwytywać zdarzenia
generowane przez ekran dotykowy. Ponieważ takie przechwytywanie wiąże się ze sporym
narzutem wydajnościowym, domyślnie mechanizm ten jest wyłączony. Aby go włączyć,
wystarczy odpowiednio ustawić właściwość
isTouchEnabled
dostępną w klasie
CCLayer
:
self.isTouchEnabled = YES;
Kiedy tylko do właściwości
isTouchEnabled
zostanie przypisana wartość
YES
, Cocos2D zacznie wywoływać na obiekcie reprezentującym warstwę cały szereg funkcji
zwrotnych służących do obsługi zdarzeń generowanych przez ekran dotykowy:
-(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent
*)event
– ta funkcja jest wywoływana, kiedy palec dotknie ekranu dotykowego,
-(void) ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent
*)event
– ta funkcja jest wywoływana, kiedy palec przesuwa się po ekranie,
-(void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent
*)event
– ta funkcja jest wywoływana, kiedy palec odrywa się od ekranu,
-(void) ccTouchesCancelled:(NSSet *)touches withEvent:(UIEvent
*)event
– ta funkcja jest wywoływana, kiedy proces przesuwania palca na ekranie zostaje
przerwany (zdarzenie to występuje bardzo rzadko, powinno się je obsługiwać podobnie
jak zdarzenie
ccTouchesEnded
).
DUSZKI
Jak dotąd omawialiśmy niemalże same abstrakcyjne koncepcje: reżyser, graf sceny, warstwy,
węzły... Wszystkie to jest oczywiście potrzebne, jednakże trudno byłoby zrealizować ciekawą
gry składającą się jedynie z obiektów nie posiadających reprezentacji graficznej. Jak więc za
pomocą Cocos2D wyświetlić zwykły obrazek? Odpowiedzią na to jest klasa
CCSprite
. Klasa ta reprezentuje tzw. duszka (ang. sprite). Duszek w nomenklaturze nazewnictwa
używanego przez adeptów dziedziny wiedzy określanej jako grafika komputerowa oznacza
obraz (teksturę) renderowaną bezpośrednio na ekran, w określonej pozycji. Termin ten
pojawił się już w latach 70 zeszłego stulecia. Wiele systemów komputerowych z tamtych oraz
późniejszych lat używało duszków (których rysowanie często bywało wspierane sprzętowo) do
tworzenia dwuwymiarowych gier. Nowoczesne duszki (chociażby takie, jakie oferuje
Cocos2D) oprócz pozycji na ekranie pozwalają określać skalę (ang. scale) ich wielkości, kąt
obrotu (ang. rotation angle), punkt zaczepienia (ang. anchor point), poziom przeźroczystości
(ang. opacity level), efekt obrotu wokół osi (ang. flipping effect) czy ton koloru (ang. color
tint). Wszystkie te właściwości duszków stanowią bazę dla uzyskiwania szeregów efektów,
które na co dzień oglądamy w nowoczesnych grach 2D.
Stworzenie duszka za pomocą biblioteki Cocos2D jest bardzo proste. Najłatwiej można to
zrobić przez załadowanie obrazka do tekstury (
CCTexture2D
), która z kolei będzie przypisana do obiektu typu
CCSprite
. Z poziomu kodu źródłowego wygląda to następująco:
CCSprite* heroSprite =
[CCSprite spriteWithFile:@”hero.png”];
W tym momencie warto napisać kilka słów o tym, w jaki sposób Cocos2D pozycjonuje duszki.
Tutaj mała zagadka: jak według Ciebie będzie wyglądał ekran po narysowaniu duszka
załadowanego za pomocą przedstawionego wyżej fragmentu kodu (obrazek hero.png
przedstawiony jest na Rysunku 6)? W ramach podpowiedzi dodam, że obiekty klasy
CCSprite
mają domyślnie ustawioną pozycję (0, 0).
Rysunek 6. Przykładowa tekstura używana do wyświetlenia duszka (źródło: http://www.lostgarden.com)
Jeśli chcesz poznać odpowiedź na to pytanie, spójrz proszę na Rysunek 7. Jak widać, duszek
został wyświetlony tylko fragmentarycznie i na dodatek w lewym dolnym rogu ekranu. Czy
aby wszystko jest w porządku? Jak najbardziej! Kwestia wyświetlenia duszka w lewym
dolnym rogu wiąże się z tym, jak Cocos2D postrzega układ współrzędnych. Według konwencji
przyjętej w tej bibliotece, punkt (0, 0) tego układu znajduje się właśnie w lewym dolnym rogu
ekranu. Problem przycięcia duszka jest troszkę bardziej skomplikowany. Wiąże się on z tzw.
punktem zaczepienia, o którym wspominałem kilka akapitów wyżej. Punkt zaczepienia jest
punktem zdefiniowanym w układzie współrzędnych tekstury. Układ ten jest zorganizowany w
dość specyficzny sposób. Punkty umieszczane w tym układzie mogą przyjmować wartości ze
zmiennoprzecinkowego zakresu [0.0, 1.0]. Wartość 1.0 oznacza odpowiednio szerokość bądź
wysokość tekstury. Punkt zaczepienia w bibliotece Cocos2D ustawiony jest domyślnie na
wartość (0.5, 0.5), czyli środek tekstury. Gdybyśmy ustawili go na wartość (0.0, 0.0), to
wskazywałby on na lewy-górny róg tekstury itd. Wracając do problemu przycięcia, cały haczyk
polega na tym, że pozycja duszka na ekranie zawsze odnosi się do tego puntu zaczepiania.
Innymi słowy, gdy ustawimy ekranową pozycję duszka na punkt (0, 0), zaś jego punkt
zaczepienia określony będzie jako (0.5, 0.5), to zostanie on narysowany w taki sposób, iż
punkt centralny tekstury reprezentującej duszka pojawi się na układzie współrzędnych
ekranu w punkcie (0, 0), czyli – jak wyżej pisałem, w lewym-dolnym rogu. Jeśli chcielibyśmy
zmodyfikować pozycję naszego duszka na ekranie w taki sposób, aby był w całości
wyświetlony w lewym-dolnym rogu, można ustawić jego punkt zaczepienia na wartość (0.0,
0.0), bądź zmodyfikować jego pozycję, przesuwając go w prawo i w górę o wartości
odpowiadające połowie szerokości i wysokości reprezentującej go tekstury.
Rysunek 7. Duszek wyświetlony na ekranie (pozycja domyślna)
Z efektywnym korzystaniem z duszków wiąże się szereg technicznych ograniczeń,
wynikających przede wszystkim z charakterystyki nowoczesnych układów wspomagających
sprzętowo renderowanie grafiki. Cocos2D w miarę swoich możliwości ukrywa przed
programistą techniczne detale, jednakże znajomość i świadomość istnienia ww. ograniczeń
jest kluczem do tworzenia efektywnych i płynnie działających gier pod iOS. Niestety, od
pewnych kwestii nie da się uciec.
Pierwszy problem, którego należy być świadomym, to wymiarowość tekstur. Układy
graficzne, z których korzysta system iOS, potrafią współpracować jedynie z teksturami o
wielkościach będących potęgami dwójki z zakresu [2, 2048]. Ograniczenie to, połączone z
niefrasobliwym podejściem do kwestii korzystania z tekstur może prowadzić do poważnych
problemów związanych ze zużyciem pamięci. Dla przykładu, jeśli stworzymy za pomocą
biblioteki Cocos2D duszka reprezentowanego przez obraz o rozmiarach 257 na 257 pikseli, to
w celu jego załadowania silnik wygeneruje teksturę o rozmiarach 512 na 512 pikseli. Taka
tekstura, przy założeniu, że korzystamy z 32-bitowego formatu piksela, może zająć około 1MB
w pamięci karty graficznej. Dla nieświadomego tego problemu programisty może stanowić to
niemały szok...
Inny ważny problem związany z korzystaniem z duszków wiąże się z przełączaniem tekstur.
Rzecz polega na tym, że nowoczesne układy graficzne zaprojektowane zostały w taki sposób,
iż nie potrafią współdziałać z dużą ilością tekstur w tym samym czasie. Typowy schemat
współpracy układu graficznego z teksturami wygląda następująco: załaduj teksturę; rysuj
korzystając z danych zawartych w aktualnie załadowanej teksturze; załaduj kolejną teksturę;
itd. Haczyk w całej tej zabawie polega na tym, że operacja przeładowania tekstury jest bardzo
czasochłonna. Tak bardzo, że niefrasobliwe jej używanie może literalnie zabić wydajność gry.
Wyobraź sobie, że tworzysz dwuwymiarową grę typu shoot'em up, czyli klasyczną strzelaninę,
w której Twój bohater pędząc w uzbrojonym pojeździe kosmicznym niszczy hordy Obcych
próbujących podbić Ziemię. Gra oczywiście składa się z całego szeregu animacji opartych na
duszkach. Pół biedy, jeśli wpadniesz na pomysł, aby klatki animacji pojazdu gracza i
poszczególnych przeciwników umieścić w oddzielnych plikach. Gorzej, jeśli każdą klatkę
zechcesz trzymać w osobnym obrazku. Jeśli tak zrobisz, gwarantuję Ci, że Twoja gra będzie
(może) działać w granicach 2-3 FPS'ów, nawet na urządzeniach nowej generacji.
Lekarstwem na opisane wyżej problemy jest stosowanie tzw. atlasów tekstur (ang. texture
atlases). Są to po prostu obrazki o wymiarach kompatybilnych z wymiarami tekstur, do
których upakowane są wszystkie elementy graficzne wykorzystywane w grze. Oprócz tego, w
oddzielnym pliku umieszcza się informacje o położeniu oryginalnych obrazków na atlasie,
dzięki czemu można się do nich odwoływać w trakcie działania gry. Na Rysunku 8 pokazany
jest przykładowy atlas tekstur. Proces tworzenia takich atlasów jest zazwyczaj
zautomatyzowany. Istnieją dedykowane narzędzia, korzystające w zaawansowanych
algorytmów optymalizacyjnych służące po to aby składać dużą liczbę małych plików
graficznych w jeden duży. Część z tych narzędzi przystosowana jest do współpracy z biblioteką
Cocos2D. Jedną z ciekawszych opcji w tym zakresie jest TexturePacker
(http://www.texturepacker.com/), który w podstawowej wersji dostępny jest za darmo.
Rysunek 8. Przykładowy atlas tekstur (źródło: http://pocketgod.wikia.com)
Cocos2D oczywiście oferuje wsparcie dla atlasów tekstur, które obsługiwane są za pomocą
klas
CCSpriteFrameCache
oraz
CCSpriteFrame
. Przedstawienie procesu tworzenia atlasu i obsługiwania go z poziomu silnika wykracza
poza ramy niniejszego artykułu. Ważne jest, abyś był świadom istnienia tych klas, aczkolwiek
jeśli planujesz pracować z Cocosem na poważnie, to prędzej czy później będziesz musiał
zapoznać się z tym zagadnieniem. W razie czego ciekawy samouczek opisujący ten temat
możesz znaleźć tutaj: http://www.raywenderlich.com/1271/how-to-use-animations-and-
sprite-sheets-in-cocos2d.
RENDEROWANIE TEKSTU
Renderowanie napisów jest jednym z nieodzownych elementów każdej niemalże gry. W tej
sytuacji nie do pomyślenia jest, aby Cocos2D nie oferował żadnych usprawnień w tym
zakresie. Jak się zapewne domyślasz, usprawnienia takie istnieją i zamierzam je w tym
podpunkcie zaprezentować.
Najprostszym sposobem wyświetlenia tekstu przy pomocy biblioteki Cocos2D jest użycie
obiektu klasy
CCLabelTTF
. Klasa ta, jak się łatwo domyśleć po jej nazwie, służy do wyświetlania napisów w oparciu o
czcionki TrueType. Podstawowe użycie tej klasy pokazane jest na Listingu 4.
Listing 4. Proste użycie klasy CCLabelTTF
CCLabelTTF* label =
[CCLabelTTF labelWithString:@"Hello, Cocos2D!"
fontName:@"Arial"
fontSize:32];
Jak widać, rzecz jest bardzo prosta. Inna sprawa, że prosto nie zawsze oznacza wydajnie.
Zanim zaczniesz używać klasy
CCLabelTTF
, musisz być świadom, że pod maską działa ona w ten sposób, iż w locie generuje teksturę i
renderuje do niej żądany tekst, korzystając z odpowiedniej definicji czcionki TrueType. Takie
renderowanie do pośredniej tekstury jest operacją dość zasobożerną i jeśli planujesz na
bieżąco modyfikować zawartość tekstu reprezentowanego przez
CCLabelTTF
to licz się z potencjalnym spadkiem wydajności Twojej aplikacji.
Drugi problem związany z używaniem czcionek TrueType jest tego rodzaju, że czasami
trudno jest dobrać krój liter odpowiedni do Twojej gry. W praktyce często robi się tak, że
grafik przygotowuje dedykowany zestaw znaków dla danej gry w postaci tzw. czcionki
bitmapowej (ang. bitmap font). Czcionka taka zazwyczaj dostarczana jest w postaci dwóch
plików: tekstury, na której umieszczone są poszczególne znaki, oraz metadanych (np. pliku w
formacie XML, JSON czy plist), które definiują pozycje i atrybuty poszczególnych znaków na
teksturze. Praktyka pokazuje, że czcionki bitmapowe są o wiele częściej używane w grach niż
czcionki TrueType. Napis oparty na czcionce bitmapowej można też łatwiej i wydajniej
renderować „w locie”, bez posiłkowania się oddzielną teksturą. Z tej racji, obiektów klasy
CCLabelTTF
w praktyce używa się głównie do wyświetlania informacji użytecznych przy debugowaniu
aplikacji czy do szybkiego prototypownia.
Cocos2D oczywiście wspiera czcionki bitmapowe. Do ich obsługi wykorzystuje się klasę
CCLabelBMFont
. Korzystanie z tej klasy jest równie proste jak korzystanie z
CCLabelTTF
(patrz Listing 5), ale... Analizując ten Listing, można zauważyć dwie istotne kwestie. Po
pierwsze, aby wczytać pożądany font, musimy wskazać konkretny plik .fnt. Od razu pojawia
się pytanie: skąd go wziąć? Po drugie, przy wczytywaniu fonta nie podajemy jego wielkości, co
wydaje się zrozumiałe: przecież wymiar fonta bitmapowego zależy od tego, jak go sobie
narysujemy. Te dwa spostrzeżenia wiążą się z podstawowymi problemami dotyczącymi
stosowania fontów bitmapowych. Pierwsza kwestia w zasadzie nie jest problemem sensu
stricto. To raczej konsekwencja decyzji używania takiego a nie innego formatu fonta. Po
prostu w celu stworzenia pliku .fnt (a także towarzyszącego mu pliku .png) musimy
skorzystać z narzędzia. Opcji jest kilka:
Hiero (http://slick.cokeandcode.com/demos/hiero.jnlp): aplikacja webowa napisana w
Javie; jej główną zaletą jest fakt, iż jest ona darmowa; niestety, poza tym stwarza sporo
Witamy Szanowni twórcy oprogramowania i specjaliści branży IT Przed Wami pierwsze wydanie magazynu „Programista”, które ukazało się również w postaci drukowanej. Od niniejszego numeru magazyn staje się miesięcznikiem. Przychyliliśmy się też do Waszych próśb i przygotowaliśmy magazyn w wersji elektronicznej w plikach ePub i .mobi oraz .pdf. Rośnie ilość i złożoność otaczających nas systemów informatycznych. Rynek tabletów i smartfonów ciągle odnotowuje wzrosty i naturalną koleją rzeczy powstają tysiące nowych aplikacji na te urządzenia: od gier po programy użytkowe. Rozwijają się języki programowania, powstają kolejne wersje „mobilnych” systemów operacyjnych. W tej sytuacji nie ma wyjścia: chcąc pozostać konkurencyjnym w zawodzie Programisty, trzeba się na bieżąco rozwijać, dokształcać. Naszym celem jest Wam to zadanie ułatwić. W bieżącym numerze przedstawiamy bibliotekę Cocos2D: jeden z najpopularniejszych silników do tworzenia gier na platformę iOS. Ponadto polecamy praktyczny artykuł na temat języka Objective-C, w którym autor omawia przydatne jego właściwości, pozwalające znacznie usprawnić proces tworzenia aplikacji na urządzenia mobilne ze stajni Apple. Znajdziecie u nas również ciekawy artykuł traktujący o popularnym ostatnio podejściu Domain Driven Design. Ponadto kontynuujemy tematykę poruszoną w premierowym wydaniu naszego miesięcznika: omawiamy możliwości nowego standardu języka C++ oraz prezentujemy ciekawe tematy z zakresu inżynierii oprogramowania. Na koniec pragniemy serdecznie podziękować za konstruktywne opinie dotyczące pierwszej, elektronicznej edycji magazynu. Wydanie to udostępniliśmy bezpłatnie, by każdy mógł wyrobić sobie opinię o projekcie. Olbrzymie zainteresowanie, jakim cieszył się premierowy egzemplarz, utwierdza nas w przekonaniu, że tego typu wydawnictwa brakowała na polskim rynku i dopinguje do jeszcze bardziej wytężonej pracy nad magazynem. Cieszy nas, że zdecydowana większość z Was pozytywnie oceniła inicjatywę! Dziękujemy za wsparcie i życzymy przyjemnej lektury! Z wyrazami szacunku, Redakcja
Spis treści BIBLIOTEKI I NARZĘDZIA Biblioteka Cocos2D: wprowadzenie Rafał Kocisz JĘZYKI PROGRAMOWANIA C++11 część I Bartosz Szurgot, Mariusz Uchroński, Wojciech Waga Wybrane elementy języka Objective-C i ich wykorzystanie Łukasz Mazur Erlang - język inny niż C++ czy Java Marek Sawerwain PROGRAMOWANIE GRAFIKI Direct3D – podstawy Wojciech Sura PROGRAMOWANIE URZĄDZEŃ Wykorzystanie sensora Kinect w systemie Windows Łukasz Górski INŻYNIERIA OPROGRAMOWANIA Domain Driven Design krok po kroku część II: Zaawansowane modelowanie DDD – techniki strategiczne: konteksty i architektura zdarzeniowa Sławomir Sobótka KLUB LIDERA IT Dokumentowanie architektury. Jak zorganizować proces rozwoju architektury? Michał Bartyzel, Mariusz Sieraczkiewicz KOMIKS Maciej Mazurek WDROŻENIA Highsky.com – projekt, oprogramowanie i wdrożenie platformy inwestycyjnej highsky.com zintegrowanej z platformą MetaTrader 5. Wojciech Holisz
Redakcja Wydawca: Anna Adamczyk annaadamczyk@programistamag.pl Redaktor naczelny: Łukasz Łopuszański lukaszlopuszanski@programistamag.pl Redaktor prowadzący: Rafał Kocisz rafal.kocisz@gmail.com Korekta: Tomasz Łopuszański Kierownik produkcji: Krzysztof Kopciowski bok@keylight.com.pl DTP: Krzysztof Kopciowski Dział reklamy: reklama@programistamag.pl tel. +48 663 220 102 tel. +48 604 312 716 Prenumerata: prenumerata@programistamag.pl Współpraca: Michał Bartyzel, Mariusz Sieraczkiewicz, Sławomir Sobótka, Artur Machura, Marek Sawerwain, Łukasz Mazur, Rafał Kułaga Adres wydawcy: Dereniowa 4/47 02-776 Warszawa Druk: Zamów wydanie w wersji papierowej przez www.programistamag.pl O ile nie zaznaczono inaczej, wszelkie prawa do wszystkich materiałów zamieszczanych na łamach magazynu Programista są zastrzeżone. Kopiowanie i rozpowszechnianie ich bez zezwolenia jest wzbronione. Naruszenie praw autorskich może skutkować odpowiedzialnością prawną, określoną w szczególności w przepisach ustawy o prawie autorskim i prawach pokrewnych, ustawy o zwalczaniu nieuczciwej konkurencji i przepisach kodeksu cywilnego oraz przepisach prawa prasowego. Redakcja magazynu Programista nie ponosi odpowiedzialności za szkody bezpośrednie i pośrednie, jak również za inne straty i wydatki poniesione w związku z wykorzystaniem informacji prezentowanych na łamach magazynu Programista. Wszelkie nazwy i znaki towarowe lub firmowe występujące na łamach magazynu są zastrzeżone przez odpowiednie firmy.
Biblioteka Cocos2D: wprowadzenie Rafał Kocisz iOS to platforma, która rozbudza wyobraźnię wielu programistów. Któż z nas nie marzy o karierze niezależnego twórcy gier i setkach tysięcy dolarów zarobionych dzięki sprzedaży aplikacji na AppStore? W niniejszym artykule przedstawiona jest biblioteka, która może być kluczem do spełnienia tych marzeń. Głównym celem niniejszego artykułu jest zapoznanie Czytelnika z podstawowymi blokami budulcowymi, które oferuje biblioteka Cocos2D. Po jego przeczytaniu będziesz wiedział, czego możesz spodziewać się po tym silniku i na ile przydatny będzie on w Twoich projektach. Cocos2D to potężna biblioteka i szczegółowe jej opisanie to temat, który kwalifikuje się bardziej na książkę niż na artykuł. Z tego względu niniejszy tytuł kładzie nacisk na ogólne zrozumienie koncepcji, na których opiera się biblioteka Cocos2D, oraz przedstawienie relacji między nimi. W jednym z ostatnich sekcji artykułu wskazane są materiały, z których zainteresowani Czytelnicy będą mogli skorzystać w celu poszerzenia swojej wiedzy na temat prezentowanej tu biblioteki. DLACZEGO COCOS2D? Zanim przejdziemy do omówienia możliwości biblioteki Cocos2D, spróbujmy odpowiedzieć sobie na podstawowe pytanie: co sprawia, że warto zainteresować się właśnie tym konkretnym rozwiązaniem? Pierwszy ważny powód, dla którego warto rozważyć używanie Cocos2D, to fakt, iż biblioteka ta jest całkowicie darmowa, zaś jej licencja pozwala tworzyć zarówno aplikacje komercyjne, jak i niekomercyjne. Licencja Cocos2D jest otwarta, silnik rozpowszechniany jest razem z kodem źródłowym. Oznacza to, że nic nie stoi na przeszkodzie, aby w razie potrzeby zajrzeć do kodu źródłowego biblioteki czy wręcz wprowadzić do niej własne modyfikacje. Cocos2D napisany jest w języku Objective-C. Biorąc pod uwagę, że Cocos2D obsługuje urządzenia z rodziny iOS oraz OS X, wybór ten wydaje się bardzo trafny: Objective-C to natywny język programowania w wymienionych systemach. Dla osób, które znają inny język obiektowy (np. C++, Java, C#), nauka Objective-C nie powinna sprawić większych trudności. Osobom rozpoczynającym przygodę z programowaniem sugerowałbym poświęcenie nieco czasu na solidne zapoznanie się z Objective-C, zanim na poważnie zaczną zajmować się programowanie gier pod iOS przy użyciu biblioteki Cocos2D. Jak sugeruje nazwa biblioteki, Cocos2D wspomaga programowanie gier 2D. Warto podkreślić jednak, że mowa tutaj o nowoczesnych grach 2D, w których na porządku dziennym są wykonywane w czasie rzeczywistym transformacje obrazów (np. rotacja czy skalowanie), obsługa przeźroczystości czy post-processing (wszystko to oczywiście wspomagane akceleracją sprzętową). Jeśli jesteś początkującym programistą gier, to zabawa z dwuwymiarem jest wręcz zalecana (chociażby dlatego, że algorytmy stosowane w tego typu grach są o wiele łatwiejsze w implementacji w stosunku do ich odpowiedników stosowanych w grach 3D). Dodatkowo, za Cocosem stoi bardzo liczna, prężna i otwarta społeczność złożona w dużej
mierze z niezależnych programistów gier, co daje możliwość stosunkowo łatwego uzyskania wsparcia. Ze względu na swoją popularność Cocos2D może poszczycić się posiadaniem dużej ilości wysokiej jakości materiałów edukacyjnych (samouczków, książek, forów dyskusyjnych) oraz narzędzi, które wspierają i ułatwiają pracę z tą biblioteką. Podsumowując, Cocos2D jest niewątpliwie biblioteką, z którą warto się zapoznać. Jeśli masz ochotę zanurkować w jego barwny świat, zapraszam do lektury dalszej części niniejszego artykułu. GRAF SCENY Najważniejsza, centralna koncepcja, wokół której zbudowana jest biblioteka Cocos2D to tzw. graf sceny (zwany czasami drzewem sceny bądź hierarchią sceny). Zrozumienie tej koncepcji jest kluczowe w przypadku gdy ktoś chce efektywnie korzystać z prezentowanego tu silnika. Wyobraź sobie, że masz zaimplementować fragment graficznego interfejsu użytkownika w grze, coś podobnego do menu przedstawionego na Rysunku 1. Spróbuj spojrzeć na ten obraz okiem programisty i zastanów się, jak można by zorganizować model takiego interfejsu użytkownika na poziomie kodu źródłowego. Pierwsza myśl, która zapewne przyjdzie Ci do głowy, to przechowywanie płaskiej listy obiektów reprezentujących wszystkie widoczne elementy na ekranie (elementy tła, przyciski i elementy tekstowe). Obiekty takie przechowywałyby informacje o stanie poszczególnych elementów menu (np. ich pozycja, stan, widoczność). Informacji tych można by użyć do rysowania całej sceny, obsługi zdarzeń użytkownika itp. Rysunek 1. Proste menu gry (źródło: opracowanie własne) Podejście takie można by oczywiście z powodzeniem zastosować, ale... zanim zaczniesz kodować, zastanów się, czy nie można by było zorganizować tego lepiej? Powiedzmy, że chciałbyś zaimplementować prosty efekt polegający na tym, że druga warstwa tła z umieszczonymi na niej kontrolkami płynnie wsuwa się przy rozpoczęciu gry, zaś przy jej zakończeniu wysuwa się poza ekran (patrz: Rysunek 2).
Rysunek 2. Efekt wsuwania się menu (źródło: opracowanie własne) Sprawa niby prosta, jednakże przy zastosowaniu płaskiej listy jako struktury danych reprezentującej kolekcję kontrolek, implementacja takiego efektu staje się nieco uciążliwa: trzeba ręcznie wybrać wszystkie obiekty, które chcemy wsuwać\wysuwać, i dla każdego z nich odpowiednio modyfikować ich pozycje. W tej sytuacji aż się prosi, aby potraktować drugą warstwę tła jako płaszczyznę, na której leżą wszystkie pozostałe kontrolki, i przesunąć ją, razem ze wszystkim elementami, które są na niej umieszczone. W tym celu możemy zastosować właśnie graf sceny. Graf sceny to drzewiasta struktura danych pozwalająca reprezentować obrazy (zarówno 2D i 3D) tak, aby zachować informację o hierarchii obiektów na nich występujących. Kluczowym elementem grafu sceny jest węzeł (ang. node), który spełnia dwojaką rolę: po pierwsze, reprezentuje wybrany element sceny; po drugie, jest kontenerem, który może przechowywać inne węzły, stanowiące jego dzieci (ang. children). Węzeł posiadający dzieci nazywany jest rodzicem (ang. parent). W grafie sceny występuje jeden węzeł, który nie posiada rodzica; nazywamy go korzeniem (ang. root). Znamienne dla grafu sceny jest to, że każdy rodzic definiuje dla swoich dzieci swoisty lokalny układ współrzędnych. Oznacza to, iż współrzędne dzieci (a także inne ich właściwości) rozpatrywane są w odniesieniu do układu rodzica. Np. jeśli węzeł rodzic (korzeń) ma pozycję (50, 50), zaś jego potomek znajduje się w punkcie (10, 15), to rzeczywista (ekranowa) pozycja tego drugiego wyniesie (60, 75). W takim ujęciu, gdy zmienimy pozycję danego węzła, to automatycznie przemieszczone zostaną jego dzieci. Każdy węzeł w grafie sceny posiada identyczny interfejs, opisany zazwyczaj przez abstrakcyjną klasę bazową. Po tej klasie dziedziczą inne klasy, reprezentujące konkretne węzły. Czytelnicy, którzy mieli styczność z tzw. wzorcami projektowymi (ang. design patterns), słusznie skojarzą przedstawiony tutaj opis ze wzorcem kompozyt (ang. composite); de facto graf sceny jest niemalże wzorcowym przykładem takiego podejścia projektowego. Spróbujmy odnieść przedstawione wyżej rozważania do naszej przykładowej sceny. Na Rysunku 3 przedstawiona jest wspomniana scena z oznaczeniem elementów hierarchii w grafie. Korzeniem grafu jest pierwsza warstwa tła (czerwony prostokąt otaczający). Korzeń ma jednego potomka: drugą warstwę tła (niebieski prostokąt otaczający). Ten z kolei posiada czwórkę dzieci: przyciski (żółte prostokąty otaczające). Każdy przycisk posiada jednego potomka, którym jest umieszczony pod nim napis. Do reprezentacji takiej sceny potrzebowalibyśmy dwóch konkretnych klas-węzłów reprezentujących statyczny obrazek (elementy tła i przyciski) oraz tekst (napisy pod przyciskami). Mając do dyspozycji tak zorganizowaną scenę, zaprogramowanie efektu wsuwania się drugiej warstwy tła z umieszczonymi na niej kontrolkami staje się bardzo proste; wystarczy jedynie odpowiednio zmodyfikować pozycję węzła reprezentującego tę warstwę, a o właściwe pozycjonowanie pozostałych elementów zadba graf sceny.
Rysunek 3. Hierarchia sceny dla prostego menu w grze (źródło: opracowanie własne) Graf sceny to nieocenione narzędzie przy tworzeniu aplikacji wyświetlających złożone obrazy zbudowane z grup powiązanych ze sobą obiektów (pod tę kategorię można z powodzeniem podciągnąć większość nowoczesnych gier 2D oraz 3D). Jest on również bardzo przydatny w innych zastosowaniach, np. określanie widoczności obiektów czy detekcja kolizji. Na tym etapie ważne jest, abyś zrozumiał podstawową ideę tego wzorca projektowego, gdyż stanowi on serce biblioteki Cocos2D. KLASA BAZOWA CCNODE Tak jak wspominałem w poprzednim punkcie, kluczowymi elementami w grafie sceny są węzły. Każdy węzeł dziedziczy po klasie bazowej, która definiuje spójny interfejs dla wszystkich obiektów umieszczanych w hierarchii. W przypadku biblioteki Cocos2D klasa ta nazywa się CCNode . Po tej klasie dziedziczą kolejne klasy, reprezentujące konkretne obiekty, które można umieszczać w grafie sceny. Na Rysunku 4 przedstawiona jest hierarchia klas wywodzących się z klasy bazowej CCNode .
Rysunek 4. Hierarchia klas wywodzących się z CCNode (źródło: http://www.cocos2d-iphone.org/ ). Schemat w większej rozdzielczości. Jak widać, klas tych jest całkiem sporo. Cocos2D to bardzo aktywnie rozwijana biblioteka, więc w momencie kiedy czytasz niniejszy artykuł, hierarchia ta może wyglądać nieco inaczej od tej, która jest tutaj przedstawiona (dotyczy ona Cocos2D w wersji 1.0.1), jednakże szereg koncepcji reprezentowanych przez wybrane klasy niewątpliwie pozostaną niezmienne. Klasy te będą szczegółowo omówione w kolejnych podpunktach niniejszego artykułu. Zanim jednak do tego przejdziemy, przyjrzyjmy się bardziej szczegółowo interfejsowi klasy CCNode . Jak już wcześniej wspominałem, jest to klasa abstrakcyjna i nie ma bezpośredniej reprezentacji wizualnej, jednakże pełni bardzo istotną rolę, gdyż zawiera pola i metody wspólne dla wszystkich węzłów przechowywanych w grafie sceny. Oto lista najważniejszych z nich: tworzenie nowego węzła: CCNode* childNode = [CCNode node]; dodawanie węzła-dziecka: [node addChild:child z:0 tag:73]; wyłuskiwanie węzła-dziecka: CCNode* child = [node getChildByTag:73]; usuwanie węzła-dziecka określonego za pomocą identyfikatora: (z opcjonalnym
czyszczeniem, polegającym na zastopowaniu wszystkich akcji przypisanych do usuwanego węzła): [node removeChildByTag:73 cleanup:YES]; usuwanie węzła-dziecka za pośrednictwem wskaźnika: [node removeChild:child]; usuwanie wszystkich dzieci z węzła: [node removeAllChildrenWithCleanup:YES]; usuwanie siebie samego z węzła-rodzica: [node removeFromParentAndCleanup:YES]; Jak łatwo się można domyśleć, parametr z przekazywany w metodzie addChild określa głębokość (ang. depth) węzła w scenie i pozwala kontrolować kolejność rysowania elementów przechowywanych w grafie. Zasada jest prosta: elementy z małymi wartościami z rysowane są jako pierwsze, zaś te, które mają największą głębokość – jako ostatnie. Jeśli część węzłów posiada taką samą wartość parametru z , to porządek ich rysowania określony jest kolejnością ich dodawania do węzła-rodzica. Z kolei parametr tag to rodzaj identyfikatora, za pomocą którego możemy szybko wyłuskiwać czy usuwać węzły za pomocą takich metod jak getChildByTag czy removeChildByTag . Korzystając z tagów, należy pamiętać o tym, że Cocos2D nie rozwiązuje problemu ich unikalności, tj. jeśli umieścisz w scenie dwa, lub więcej węzłów o identycznych identyfikatorach, to będziesz w stanie dostać się tylko do pierwszego z nich; pozostałe będą niedostępne. Cocos2D wykorzystuje tagi również do identyfikacji tzw. akcji (co to są akcje, dowiesz się już za moment); w tym miejscu chciałbym tylko wspomnieć, że identyfikatory węzłów i akcji nie kolidują ze sobą, tak więc dopuszczalna jest sytuacja, gdy zarówno węzeł, jak i przypisana do niego akcja mają ten sam tag. Jak już kilka razy wspomniałem, węzły mogą mieć przypisane akcje (ang. actions). Koncepcję akcji opiszę szczegółowo w oddzielnym podpunkcie niniejszego artykułu; tutaj przedstawię je tylko pokrótce, tak abyś mógł zrozumieć relację pomiędzy nimi a węzłami. Pisząc w największym skrócie, akcje to obiekty reprezentujące działania wykonywane na
obiektach sceny. Wyobraź sobie chcesz zaimplementować graficzny element na ekranie (np. przycisk), który miarowo pulsuje (na przemian powiększa się i zmniejsza) albo miarowo miga (na przemian pojawia się i znika) bądź po prostu przemieszcza się pomiędzy dwoma punktami. Wszystkie te operacje możesz zrealizować przypisując do węzła reprezentującego wspomniany element odpowiednie akcje. O tym, jakie są kategorie akcji, porozmawiamy za moment; teraz kilka słów na temat tego, jak można obsługiwać je z poziomu interfejsu klasy CCNode . Na początek stwórzmy prostą akcję reprezentującą proces migania węzła i przypiszmy jej identyfikator: CCAction* action = [CCBlink actionWithDuration:5 blinks:10]; action.tag = 37; Akcja ta przypisana do określonego węzła sprawi, że zamiga on 10 razy w przeciągu 5 sekund. Klasa CCNode pozwala kontrolować akcje w następujący sposób: uruchomienie akcji: [node runAction:action]; wyłuskanie akcji określonej za pomocą identyfikatora: CCAction* retrievedAction = [node getActionByTag:37]; zastopowanie akcji określonej za pomocą identyfikatora: [node stopActionByTag:37]; zastopowanie akcji określonej za pomocą wskaźnika: [node stopAction:action]; zastopowanie wszystkich akcji przypisanych do danego węzła: [node stopAllActions]; Ostatnią właściwością klasy CCNode , o której chciałbym opowiedzieć, to możliwość tzw. harmonogramowania wiadomości (ang. message scheduling). Brzmi to nieco tajemniczo, jednakże w praktyce rzecz jest bardzo prosta. Przede wszystkim należy wyjaśnić, że w nomenklaturze języka Objective-C wysłanie wiadomości do obiektu oznacza po prostu wywołanie metody na tym obiekcie. W takim ujęciu harmonogramowanie wiadomości oznacza cykliczne wywoływanie określonej metody. Wyobraź sobie, że chciałbyś mieć możliwość oprogramowania jakiejś dedykowanej logiki dla
danego typu węzłów (np. detekcja kolizji bądź sprawdzanie warunku zakończenia gry). Tego typu logikę umieszcza się zazwyczaj w metodzie update bądź process , która wywoływana jest po narysowaniu kolejnej ramki gry, przyjmującej jako parametr przyrost czasu od poprzedniego jej wywołania (ang. time delta). Jeśli potrzebujesz takiej funkcjonalności w węźle, to musisz odpowiednio przeładować metodę scheduleUpdateMethod (patrz: Listing 1). Listing 1. Harmonogramowanie prostej funkcji obsługującej logikę węzła -(void) scheduleUpdateMethod { [self scheduleUpdate]; } -(void) update:(ccTime)delta { // Ta metoda będzie wywoływana po narysowaniu // kolejnej ramki gry. } REŻYSER, SCENA, WARSTWA Chciałbym przedstawić teraz trzy klasy, które pełnią w bibliotece Cocos2D bardzo ważne role. Mowa tu o klasach CCDirector , CCScene oraz CCLayer . Dwie ostatnie z wymienionych dziedziczą z CCNode i podobnie jak ona, nie mają wizualnej reprezentacji. CCScene to kontener dla wszystkich obiektów znajdujących się na scenie. Obiekty tej klasy reprezentują zazwyczaj ekrany gry: menu główne, tabelę wyników czy wreszcie – właściwą rozgrywkę. Z kolei klasa CCLayer (warstwa) służy do grupowania obiektów, głównie w celu zapewnienia właściwej kolejności ich renderowania. Można sobie np. wyobrazić, że ekran rozgrywki składa się z trzech warstw: statycznego tła, części dynamicznej (ruchome obiekty gry) oraz paska statusu przedstawiającego liczbę zdobytych punktów oraz żyć. Ważną cechą klasy CCLayer jest to, że potrafi ona przechwytywać zdarzenia generowane przez użytkownika za pośrednictwem takich kontrolerów jak ekran dotykowy czy akcelerometr. Szczególną rolę w
tej układance pełni klasa CCDirector ; pełni ona rolę reżysera – decyduje o tym, która scena ( CCScene ) będzie w danym momencie aktywna. Na początek przyjrzyjmy się, co oferuje klasa CCDirector . Po pierwsze, klasa ta jest tzw. singletonem (singleton jest to wzorzec projektowy, który zapewnia, że dana klasa posiada tylko jedną instancję). Fakt ten wydaje się być dość oczywisty (czy widziałeś kiedyś film lub przedstawienie, za które odpowiadało dwóch reżyserów?). Główne zadanie reżysera w bibliotece Cocos2D to zarządzanie scenami oraz przechowywanie danych konfiguracyjnych. W szczególności klasa CCDirector odpowiada za: startowanie sceny, zamianę bieżącej sceny, wkładanie (ang. pushing) nowej sceny na bieżącą, zdejmowanie (ang. popping) bieżącej sceny, gwarantowanie dostępu do bieżącej sceny, pauzowanie, kontynuowanie oraz kończenie gry, przechowywanie i gwarantowanie dostępu do globalnych danych konfiguracyjnych biblioteki Cocos2D, gwarantowanie dostępu do okna oraz widoku OpenGL, konwertowanie współrzędnych UIKit i OpenGL, kontrolowanie procesu uaktualniania stanu gry. Pracując z biblioteką Cocos2D, mamy do wyboru cztery typy reżyserów. Typy te implikują sposób kontrolowania procesu uaktualniania stanu gry i w rezultacie mają bardzo istotny wpływ na wydajność aplikacji: kCCDirectorTypeDisplayLink : gwarantuje najlepszą wydajność oraz płynność renderowania, dzięki zastosowaniu mechanizmu synchronizacji procesu uaktualniania ekranu z jego sprzętowym odświeżeniem; niestety – mechanizm ten dostępny jest od wersji 3.1 systemu iOS wzwyż, kCCDirectorTypeNSTimer : gwarantuje największą przenośność (będzie działać w każdej wersji systemu iOS), ale jest za to najwolniejszy, kCCDirectorTypeThreadMainLoop : szybki, ale sprawia problemy w sytuacji, kiedy chcemy używać z poziomu aplikacji Cocos2D widoków UIKit, KCCDirectorTypeMainLoop : jak wyżej. Domyślnie biblioteka Cocos2D korzysta z reżysera typu
kCCDirectorTypeDisplayLink , zaś w sytuacji gdy nie jest on dostępny (na dzień dzisiejszy bardzo mało prawdopodobna sytuacja), przełącza się na typ kCCDirectorTypeNSTimer . Jeśli chciałbyś sam określić typ reżysera, to musisz zmodyfikować następujący fragment kodu źródłowego w klasie reprezentującej delegata aplikacji: if ( ! [CCDirector setDirectorType: kCCDirectorTypeDisplayLink] ) [CCDirector setDirectorType: kCCDirectorTypeDefault]; Kolejną klasą z inwentarza biblioteki Cocos2D, z którą warto się szczegółowo zapoznać, jest CCScene . Obiekty tej klasy są zawsze korzeniami w grafie sceny. Rozwiązanie to wydaje się momentami odrobinę sztuczne (klasa CCScene w zasadzie nie rozszerza w żaden sposób funkcjonalności CCNode ), jednakże metody runWithScene , replaceScene czy pushScene z klasy CCDirector potrafią współpracować tylko z obiektami tego typu. Poza tym, sceny można opakować obiektami klas dziedziczących po klasie bazowej CCSceneTransition , co pozwala uzyskać miłe dla oka efekty przejść między scenami gry. Pracując z biblioteką Cocos2D, zazwyczaj przyjmuje się konwencję, że dziećmi sceny są jedynie warstwy (tj. obiekty wywodzące się z klasy CCLayer ), zaś one dopiero zawierają w sobie węzły reprezentujące konkretne obiekty występujące w grze. Bardzo przydatnym mechanizmem jest wkładanie (ang. pushing) oraz zdejmowanie (ang. popping) bieżącej sceny z poziomu reżysera. Operacje te niewątpliwie kojarzą się ze strukturą danych zwaną stosem. I rzeczywiście, myśląc o tym, w jaki sposób klasa CCDirector zarządza scenami, można posłużyć się modelem stosu. Oczywiście, wkładając nową scenę na stos, bieżąca nadal pozostaje widoczna. Mechanizm ten jest bardzo przydatny np. w sytuacji kiedy w trakcie gry chcemy wyświetlić okno dialogowe, bądź podręczne menu. W tej sytuacji wkładamy nową scenę na bieżącą, czekamy na działanie użytkownika, a w końcu
zdejmujemy bieżącą scenę ze stosu i automatycznie wracamy do przerwanej rozgrywki. Manipulując scenami, trzeba jednak uważać, aby nie przesadzić, gdyż za każdym razem kiedy Cocos2D podmienia sceny, nowa scena jest tworzona w pamięci, zaś stara zostaje usuwana. Jak przed momentem wspominałem, używanie scen pozwala uzyskiwać bardzo ciekawe efekty przejść (ang. transition) pomiędzy ekranami gry reprezentowanymi przez różne obiekty klasy CCScene . Służą ku temu klasy wywodzące się z klasy bazowej CCSceneTransition . Hierarchia tych klas przedstawiona jest na Rysunku 5. Na Listingu 2 pokazany jest przykład zastosowania jednego z przejść, tzw. zanikania (ang. fade). Rysunek 5. Hierarchia klas wywodzących się z CCSceneTransition (źródło: http://www.cocos2d-iphone.org/ ) Schemat w większej rozdzielczości. Listing 2. Przykład zastosowania efektu CCTransitionFade // Inicjalizacja obiektu reprezentującego efekt // przejścia. CCTransitionFade* transition = [CCTransitionFade transitionWithDuration:1 scene:[MyScene scene] withColor:ccBLACK]; // Zamiana scen z użyciem efektu przejścia. [[CCDirector sharedDirector] replaceScene:transition];
Wspomniany efekt polega na tym, że aktualna scena powoli blaknie, aż do momentu kiedy cały ekran stanie się czarny (lub inny; docelowy kolor można sobie dowolnie zdefiniować), po czym dzieje się efekt odwrotny, tyle że pojawia się nowa scena. Zaimplementowanie takiego efektu jest bardzo proste. W pierwszym kroku należy stworzyć obiekt reprezentujący efekt przejścia; w naszym przypadku będzie on typu CCTransitionFade . W konstruktorze możemy określić długość efektu (w sekundach), kolor oraz docelową scenę. Potem wystarczy tylko przekazać obiekt przejścia (na Listingu reprezentowany przez zmienną transition ) do odpowiedniej metody w klasie CCDirector . W tym przypadku należy pamiętać, iż przejścia będą działać ze wszystkimi metodami reżysera przewidzianymi do manipulacji scenami z jednym wyjątkiem, który stanowi funkcja popScene . Dlaczego? Powód jest prosty: popScene nie przyjmuje żadnych argumentów, po prostu zdejmuje ze stosu bieżącą scenę i nie ma możliwości opakowania tej sceny obiektem reprezentującym efekt przejścia. Cocos2D oferuje cały szereg efektów przechodzenia między scenami, informacje na ich temat można znaleźć w dokumentacji biblioteki. Korzystając z efektów przejść, warto pamiętać o jednej zasadzie, która brzmi: lepsze jest wrogiem dobrego. Przejścia między scenami wyglądają bardzo efektownie, jednakże jeśli przesadzimy z ich stosowaniem, to użytkownicy naszej gry dostaną białej gorączki (szczególną uwagę należy zwrócić na to, by czasy przejść między scenami nie były zbyt długie). Często zdarza się sytuacja kiedy warto podzielić scenę na warstwy. Wtedy można skorzystać z klasy CCLayer . Na Listingu 3 pokazane jest, jak można dodać kilka warstw do sceny. Listing 3. Dodawanie warstw do sceny CCScene* scene = [CCScene node]; CCLayer* backgroundLayer = [Background node]; [scene addChild: backgroundLayer]; CCLayer* spritesLayer = [Sprites node]; [scene addChild:spritesLayer]; CCLayer* hudLayer = [Hud node]; [scene addChild: hudLayer]; Tak zainicjowana scena posiada trzy warstwy, które będą (razem ze swoimi dziećmi) renderowane w kolejności ich dodawania do sceny. Zaraz, zaraz! Ale czy podobnego efektu nie można by uzyskać za pomocą zwykłych węzłów? Odpowiedź brzmi: owszem, można by. Co w takim razie odróżnia warstwy ( CCLayer ) od zwykłych węzłów (
CCNode )? Zasadnicza różnica polega na tym, że warstwy potrafią przechwytywać zdarzenia generowane przez ekran dotykowy. Ponieważ takie przechwytywanie wiąże się ze sporym narzutem wydajnościowym, domyślnie mechanizm ten jest wyłączony. Aby go włączyć, wystarczy odpowiednio ustawić właściwość isTouchEnabled dostępną w klasie CCLayer : self.isTouchEnabled = YES; Kiedy tylko do właściwości isTouchEnabled zostanie przypisana wartość YES , Cocos2D zacznie wywoływać na obiekcie reprezentującym warstwę cały szereg funkcji zwrotnych służących do obsługi zdarzeń generowanych przez ekran dotykowy: -(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event – ta funkcja jest wywoływana, kiedy palec dotknie ekranu dotykowego, -(void) ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event – ta funkcja jest wywoływana, kiedy palec przesuwa się po ekranie, -(void) ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event – ta funkcja jest wywoływana, kiedy palec odrywa się od ekranu, -(void) ccTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event – ta funkcja jest wywoływana, kiedy proces przesuwania palca na ekranie zostaje przerwany (zdarzenie to występuje bardzo rzadko, powinno się je obsługiwać podobnie jak zdarzenie ccTouchesEnded ). DUSZKI Jak dotąd omawialiśmy niemalże same abstrakcyjne koncepcje: reżyser, graf sceny, warstwy, węzły... Wszystkie to jest oczywiście potrzebne, jednakże trudno byłoby zrealizować ciekawą gry składającą się jedynie z obiektów nie posiadających reprezentacji graficznej. Jak więc za pomocą Cocos2D wyświetlić zwykły obrazek? Odpowiedzią na to jest klasa CCSprite . Klasa ta reprezentuje tzw. duszka (ang. sprite). Duszek w nomenklaturze nazewnictwa używanego przez adeptów dziedziny wiedzy określanej jako grafika komputerowa oznacza obraz (teksturę) renderowaną bezpośrednio na ekran, w określonej pozycji. Termin ten
pojawił się już w latach 70 zeszłego stulecia. Wiele systemów komputerowych z tamtych oraz późniejszych lat używało duszków (których rysowanie często bywało wspierane sprzętowo) do tworzenia dwuwymiarowych gier. Nowoczesne duszki (chociażby takie, jakie oferuje Cocos2D) oprócz pozycji na ekranie pozwalają określać skalę (ang. scale) ich wielkości, kąt obrotu (ang. rotation angle), punkt zaczepienia (ang. anchor point), poziom przeźroczystości (ang. opacity level), efekt obrotu wokół osi (ang. flipping effect) czy ton koloru (ang. color tint). Wszystkie te właściwości duszków stanowią bazę dla uzyskiwania szeregów efektów, które na co dzień oglądamy w nowoczesnych grach 2D. Stworzenie duszka za pomocą biblioteki Cocos2D jest bardzo proste. Najłatwiej można to zrobić przez załadowanie obrazka do tekstury ( CCTexture2D ), która z kolei będzie przypisana do obiektu typu CCSprite . Z poziomu kodu źródłowego wygląda to następująco: CCSprite* heroSprite = [CCSprite spriteWithFile:@”hero.png”]; W tym momencie warto napisać kilka słów o tym, w jaki sposób Cocos2D pozycjonuje duszki. Tutaj mała zagadka: jak według Ciebie będzie wyglądał ekran po narysowaniu duszka załadowanego za pomocą przedstawionego wyżej fragmentu kodu (obrazek hero.png przedstawiony jest na Rysunku 6)? W ramach podpowiedzi dodam, że obiekty klasy CCSprite mają domyślnie ustawioną pozycję (0, 0). Rysunek 6. Przykładowa tekstura używana do wyświetlenia duszka (źródło: http://www.lostgarden.com) Jeśli chcesz poznać odpowiedź na to pytanie, spójrz proszę na Rysunek 7. Jak widać, duszek został wyświetlony tylko fragmentarycznie i na dodatek w lewym dolnym rogu ekranu. Czy aby wszystko jest w porządku? Jak najbardziej! Kwestia wyświetlenia duszka w lewym dolnym rogu wiąże się z tym, jak Cocos2D postrzega układ współrzędnych. Według konwencji przyjętej w tej bibliotece, punkt (0, 0) tego układu znajduje się właśnie w lewym dolnym rogu ekranu. Problem przycięcia duszka jest troszkę bardziej skomplikowany. Wiąże się on z tzw. punktem zaczepienia, o którym wspominałem kilka akapitów wyżej. Punkt zaczepienia jest punktem zdefiniowanym w układzie współrzędnych tekstury. Układ ten jest zorganizowany w dość specyficzny sposób. Punkty umieszczane w tym układzie mogą przyjmować wartości ze zmiennoprzecinkowego zakresu [0.0, 1.0]. Wartość 1.0 oznacza odpowiednio szerokość bądź wysokość tekstury. Punkt zaczepienia w bibliotece Cocos2D ustawiony jest domyślnie na wartość (0.5, 0.5), czyli środek tekstury. Gdybyśmy ustawili go na wartość (0.0, 0.0), to wskazywałby on na lewy-górny róg tekstury itd. Wracając do problemu przycięcia, cały haczyk polega na tym, że pozycja duszka na ekranie zawsze odnosi się do tego puntu zaczepiania. Innymi słowy, gdy ustawimy ekranową pozycję duszka na punkt (0, 0), zaś jego punkt
zaczepienia określony będzie jako (0.5, 0.5), to zostanie on narysowany w taki sposób, iż punkt centralny tekstury reprezentującej duszka pojawi się na układzie współrzędnych ekranu w punkcie (0, 0), czyli – jak wyżej pisałem, w lewym-dolnym rogu. Jeśli chcielibyśmy zmodyfikować pozycję naszego duszka na ekranie w taki sposób, aby był w całości wyświetlony w lewym-dolnym rogu, można ustawić jego punkt zaczepienia na wartość (0.0, 0.0), bądź zmodyfikować jego pozycję, przesuwając go w prawo i w górę o wartości odpowiadające połowie szerokości i wysokości reprezentującej go tekstury. Rysunek 7. Duszek wyświetlony na ekranie (pozycja domyślna) Z efektywnym korzystaniem z duszków wiąże się szereg technicznych ograniczeń, wynikających przede wszystkim z charakterystyki nowoczesnych układów wspomagających sprzętowo renderowanie grafiki. Cocos2D w miarę swoich możliwości ukrywa przed programistą techniczne detale, jednakże znajomość i świadomość istnienia ww. ograniczeń jest kluczem do tworzenia efektywnych i płynnie działających gier pod iOS. Niestety, od pewnych kwestii nie da się uciec. Pierwszy problem, którego należy być świadomym, to wymiarowość tekstur. Układy graficzne, z których korzysta system iOS, potrafią współpracować jedynie z teksturami o wielkościach będących potęgami dwójki z zakresu [2, 2048]. Ograniczenie to, połączone z niefrasobliwym podejściem do kwestii korzystania z tekstur może prowadzić do poważnych problemów związanych ze zużyciem pamięci. Dla przykładu, jeśli stworzymy za pomocą biblioteki Cocos2D duszka reprezentowanego przez obraz o rozmiarach 257 na 257 pikseli, to w celu jego załadowania silnik wygeneruje teksturę o rozmiarach 512 na 512 pikseli. Taka tekstura, przy założeniu, że korzystamy z 32-bitowego formatu piksela, może zająć około 1MB w pamięci karty graficznej. Dla nieświadomego tego problemu programisty może stanowić to niemały szok...
Inny ważny problem związany z korzystaniem z duszków wiąże się z przełączaniem tekstur. Rzecz polega na tym, że nowoczesne układy graficzne zaprojektowane zostały w taki sposób, iż nie potrafią współdziałać z dużą ilością tekstur w tym samym czasie. Typowy schemat współpracy układu graficznego z teksturami wygląda następująco: załaduj teksturę; rysuj korzystając z danych zawartych w aktualnie załadowanej teksturze; załaduj kolejną teksturę; itd. Haczyk w całej tej zabawie polega na tym, że operacja przeładowania tekstury jest bardzo czasochłonna. Tak bardzo, że niefrasobliwe jej używanie może literalnie zabić wydajność gry. Wyobraź sobie, że tworzysz dwuwymiarową grę typu shoot'em up, czyli klasyczną strzelaninę, w której Twój bohater pędząc w uzbrojonym pojeździe kosmicznym niszczy hordy Obcych próbujących podbić Ziemię. Gra oczywiście składa się z całego szeregu animacji opartych na duszkach. Pół biedy, jeśli wpadniesz na pomysł, aby klatki animacji pojazdu gracza i poszczególnych przeciwników umieścić w oddzielnych plikach. Gorzej, jeśli każdą klatkę zechcesz trzymać w osobnym obrazku. Jeśli tak zrobisz, gwarantuję Ci, że Twoja gra będzie (może) działać w granicach 2-3 FPS'ów, nawet na urządzeniach nowej generacji. Lekarstwem na opisane wyżej problemy jest stosowanie tzw. atlasów tekstur (ang. texture atlases). Są to po prostu obrazki o wymiarach kompatybilnych z wymiarami tekstur, do których upakowane są wszystkie elementy graficzne wykorzystywane w grze. Oprócz tego, w oddzielnym pliku umieszcza się informacje o położeniu oryginalnych obrazków na atlasie, dzięki czemu można się do nich odwoływać w trakcie działania gry. Na Rysunku 8 pokazany jest przykładowy atlas tekstur. Proces tworzenia takich atlasów jest zazwyczaj zautomatyzowany. Istnieją dedykowane narzędzia, korzystające w zaawansowanych algorytmów optymalizacyjnych służące po to aby składać dużą liczbę małych plików graficznych w jeden duży. Część z tych narzędzi przystosowana jest do współpracy z biblioteką Cocos2D. Jedną z ciekawszych opcji w tym zakresie jest TexturePacker (http://www.texturepacker.com/), który w podstawowej wersji dostępny jest za darmo.
Rysunek 8. Przykładowy atlas tekstur (źródło: http://pocketgod.wikia.com) Cocos2D oczywiście oferuje wsparcie dla atlasów tekstur, które obsługiwane są za pomocą klas CCSpriteFrameCache oraz CCSpriteFrame . Przedstawienie procesu tworzenia atlasu i obsługiwania go z poziomu silnika wykracza poza ramy niniejszego artykułu. Ważne jest, abyś był świadom istnienia tych klas, aczkolwiek jeśli planujesz pracować z Cocosem na poważnie, to prędzej czy później będziesz musiał zapoznać się z tym zagadnieniem. W razie czego ciekawy samouczek opisujący ten temat możesz znaleźć tutaj: http://www.raywenderlich.com/1271/how-to-use-animations-and- sprite-sheets-in-cocos2d. RENDEROWANIE TEKSTU Renderowanie napisów jest jednym z nieodzownych elementów każdej niemalże gry. W tej sytuacji nie do pomyślenia jest, aby Cocos2D nie oferował żadnych usprawnień w tym zakresie. Jak się zapewne domyślasz, usprawnienia takie istnieją i zamierzam je w tym podpunkcie zaprezentować. Najprostszym sposobem wyświetlenia tekstu przy pomocy biblioteki Cocos2D jest użycie obiektu klasy CCLabelTTF . Klasa ta, jak się łatwo domyśleć po jej nazwie, służy do wyświetlania napisów w oparciu o
czcionki TrueType. Podstawowe użycie tej klasy pokazane jest na Listingu 4. Listing 4. Proste użycie klasy CCLabelTTF CCLabelTTF* label = [CCLabelTTF labelWithString:@"Hello, Cocos2D!" fontName:@"Arial" fontSize:32]; Jak widać, rzecz jest bardzo prosta. Inna sprawa, że prosto nie zawsze oznacza wydajnie. Zanim zaczniesz używać klasy CCLabelTTF , musisz być świadom, że pod maską działa ona w ten sposób, iż w locie generuje teksturę i renderuje do niej żądany tekst, korzystając z odpowiedniej definicji czcionki TrueType. Takie renderowanie do pośredniej tekstury jest operacją dość zasobożerną i jeśli planujesz na bieżąco modyfikować zawartość tekstu reprezentowanego przez CCLabelTTF to licz się z potencjalnym spadkiem wydajności Twojej aplikacji. Drugi problem związany z używaniem czcionek TrueType jest tego rodzaju, że czasami trudno jest dobrać krój liter odpowiedni do Twojej gry. W praktyce często robi się tak, że grafik przygotowuje dedykowany zestaw znaków dla danej gry w postaci tzw. czcionki bitmapowej (ang. bitmap font). Czcionka taka zazwyczaj dostarczana jest w postaci dwóch plików: tekstury, na której umieszczone są poszczególne znaki, oraz metadanych (np. pliku w formacie XML, JSON czy plist), które definiują pozycje i atrybuty poszczególnych znaków na teksturze. Praktyka pokazuje, że czcionki bitmapowe są o wiele częściej używane w grach niż czcionki TrueType. Napis oparty na czcionce bitmapowej można też łatwiej i wydajniej renderować „w locie”, bez posiłkowania się oddzielną teksturą. Z tej racji, obiektów klasy CCLabelTTF w praktyce używa się głównie do wyświetlania informacji użytecznych przy debugowaniu aplikacji czy do szybkiego prototypownia. Cocos2D oczywiście wspiera czcionki bitmapowe. Do ich obsługi wykorzystuje się klasę CCLabelBMFont . Korzystanie z tej klasy jest równie proste jak korzystanie z CCLabelTTF (patrz Listing 5), ale... Analizując ten Listing, można zauważyć dwie istotne kwestie. Po pierwsze, aby wczytać pożądany font, musimy wskazać konkretny plik .fnt. Od razu pojawia się pytanie: skąd go wziąć? Po drugie, przy wczytywaniu fonta nie podajemy jego wielkości, co wydaje się zrozumiałe: przecież wymiar fonta bitmapowego zależy od tego, jak go sobie narysujemy. Te dwa spostrzeżenia wiążą się z podstawowymi problemami dotyczącymi stosowania fontów bitmapowych. Pierwsza kwestia w zasadzie nie jest problemem sensu stricto. To raczej konsekwencja decyzji używania takiego a nie innego formatu fonta. Po prostu w celu stworzenia pliku .fnt (a także towarzyszącego mu pliku .png) musimy skorzystać z narzędzia. Opcji jest kilka: Hiero (http://slick.cokeandcode.com/demos/hiero.jnlp): aplikacja webowa napisana w Javie; jej główną zaletą jest fakt, iż jest ona darmowa; niestety, poza tym stwarza sporo