216 37 6MB
Polish Pages 192 Year 2012
Tytuł oryginału: Beginning Android Tablet Games Programming Tłumaczenie: Rafał Szpoton ISBN: 978-83-246-6764-2 Original edition copyright © 2011 by Jeremy Kerfs. All rights reserved. Polish edition copyright 2012 by HELION SA. All rights reserved. All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną, fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym powoduje naruszenie praw autorskich niniejszej publikacji. Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich właścicieli. Wydawnictwo HELION dołożyło wszelkich starań, by zawarte w tej książce informacje były kompletne i rzetelne. Nie bierze jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Wydawnictwo HELION nie ponosi również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji zawartych w książce. Wydawnictwo HELION ul. Kościuszki 1c, 44-100 GLIWICE tel. 32 231 22 19, 32 230 98 63 e-mail: [email protected] WWW: http://helion.pl (księgarnia internetowa, katalog książek) Pliki z przykładami omawianymi w książce można znaleźć pod adresem: ftp://ftp.helion.pl/przyklady/androt.zip
Drogi Czytelniku! Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres http://helion.pl/user/opinie/androt_ebook Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.
Printed in Poland.
• Poleć książkę na Facebook.com
• Księgarnia internetowa
• Kup w wersji papierowej
• Lubię to! » Nasza społeczność
• Oceń książkę
Spis treści O autorze .......................................................................................................7 O redaktorze technicznym . ...........................................................................8 Podziękowania . ............................................................................................9 Rozdział 1.
Konfiguracja środowiska programistycznego Java dla systemu Android 3.0 . ............................................................................11 Czym jest system Android? . ................................................................................................ 11 Początki systemu Android . ........................................................................................... 11 Główne cechy systemu Android 3.0 . ........................................................................... 13 Czego potrzeba do tworzenia gier w systemie Android? . ............................................... 14 Co należy wiedzieć? . ...................................................................................................... 14 Środowisko programistyczne . ...................................................................................... 15 Konfiguracja środowiska programistycznego . ................................................................. 16 Instalacja pakietu Java JDK . ......................................................................................... 16 Instalacja środowiska Eclipse . ...................................................................................... 17 Instalacja pakietu SDK dla systemu Android . ........................................................... 20 Konfiguracja narzędzi Androida oraz urządzenia wirtualnego w środowisku Eclipse . ................................................................................................. 23 Sprawdzanie działania narzędzi programistycznych . ...................................................... 26 Tworzenie projektu dla systemu Android . ................................................................ 27 Projekt programu dla systemu Android w Eclipsie . ................................................. 29 Tworzenie wirtualnego urządzenia z Androidem . ................................................... 31 Uruchamianie aplikacji . ................................................................................................ 33 Pierwsze zmiany w aplikacji . ........................................................................................ 33 Podsumowanie ...................................................................................................................... 35
Rozdział 2.
Tworzenie prostych gier z użyciem ruchomych sprajtów . ...........................37 Praca z obrazami ................................................................................................................... 37 Tworzenie podłoża do wyświetlania obrazów . .......................................................... 38 Rysowanie obrazu . ......................................................................................................... 42 Używanie sprajtów . ....................................................................................................... 44 Uruchomienie gry .......................................................................................................... 49
SPIS TREŚCI
Nadawanie grze profesjonalnego wyglądu . ....................................................................... 51 Implementacja zarządzania czasem oraz złożonym ruchem . ........................................ 52 Wykrywanie kolizji ............................................................................................................... 53 Podsumowanie ...................................................................................................................... 54
Rozdział 3.
Pobieranie danych od użytkownika . ...........................................................55 Sposoby pobierania danych wejściowych . ........................................................................ 55 Pobieranie danych wejściowych w tablecie . ...................................................................... 57 Reagowanie na dotyk ............................................................................................................59 Reagowanie na gesty . ............................................................................................................ 61 Korzystanie z kolejek wejścia . ............................................................................................. 64 Reagowanie na dane pochodzące z czujników . ................................................................ 70 Korzystanie z danych z czujnika . ........................................................................................ 73 Podsumowanie ...................................................................................................................... 75
Rozdział 4.
Dodawanie efektów dźwiękowych, muzyki oraz sekwencji filmowych ......77 Przygotowanie do odtwarzania dźwięków . ....................................................................... 78 Szukanie oraz dodawanie efektów dźwiękowych . ..................................................... 78 Odtwarzanie efektów dźwiękowych . ........................................................................... 79 Odtwarzanie wielu efektów dźwiękowych . ................................................................ 80 Dopasowanie efektów dźwiękowych do zdarzeń . ..................................................... 84 Dodawanie muzyki ............................................................................................................... 85 Dodawanie sekwencji filmowych . ...................................................................................... 86 Zarządzanie obsługą muzyki . .............................................................................................. 87 Podsumowanie ...................................................................................................................... 94
Rozdział 5.
Tworzenie jednoosobowej gry z utrudnieniami . .........................................95 Planowanie gry jednoosobowej — AllTogether . .............................................................. 95 Tworzenie gry jednoosobowej . ........................................................................................... 96 Ulepszanie sprajtów gry . ............................................................................................... 97 Dodawanie nagrody za ukończenie gry . ................................................................... 100 Śledzenie stanu sprajtów . ............................................................................................ 101 Podsumowanie .................................................................................................................... 109
Rozdział 6.
Gra w odbijaną piłkę .................................................................................111 Początki ................................................................................................................................ 111 Gromadzenie zasobów używanych w grze . .............................................................. 112 Tworzenie nowego projektu . ..................................................................................... 113 Przygotowanie środowiska gry . ........................................................................................ 114 Modyfikacja pliku SpriteObject.java . ........................................................................ 114 Modyfikacja pliku GameView.java . .......................................................................... 114 Dodawanie wykrywania kolizji oraz obsługi zdarzeń . .................................................. 117 Dodawanie obsługi dotyku, dźwięku oraz nagród . ........................................................ 121 Dodawanie dotykowego sterowania rakietką . ......................................................... 121 Dodawanie dźwięków . ................................................................................................ 122 Inicjalizacja bloków . .................................................................................................... 123 Usuwanie nieaktywnych bloków . .............................................................................. 125 Podsumowanie .................................................................................................................... 126
4
SPIS TREŚCI
Rozdział 7.
Tworzenie gry dwuosobowej . ..................................................................127 Podstawy gier wieloosobowych . ....................................................................................... 127 Gry wieloosobowe wykorzystujące serwer gier . ...................................................... 128 Gry wieloosobowe z połączeniami równorzędnymi . .............................................. 128 Wybór metody rozgrywki wieloosobowej . .............................................................. 129 Gra dwuosobowa z połączeniami równorzędnymi . ...................................................... 130 Dodawanie połączeń Bluetooth . ................................................................................ 130 Zarządzanie połączeniami Bluetooth . ....................................................................... 134 Modyfikacja kodu gry dla dwóch graczy . ................................................................. 140 Testowanie gry . ............................................................................................................ 141 Podsumowanie .................................................................................................................... 142
Rozdział 8.
Jednoosobowa gra strategiczna. Część I. Tworzenie gry . .........................143 Wprowadzenie do obrony portu . ..................................................................................... 144 Składanie elementów gry . .................................................................................................. 144 Tworzenie falochronu . ................................................................................................ 145 Dodawanie gruntu oraz zamku . ................................................................................ 148 Tworzenie łodzi ............................................................................................................ 149 Dodawanie dział ........................................................................................................... 151 Dodawanie obrazów . ................................................................................................... 151 Testowanie gry .................................................................................................................... 152 Podsumowanie .................................................................................................................... 154
Rozdział 9.
Jednoosobowa gra strategiczna. Część II. Programowanie gry . ...............155 Rozszerzenie sprajtów używanych w grze . ...................................................................... 156 Projektowanie sterowania grą . .......................................................................................... 157 Rozmieszczanie elementów na ekranie . .......................................................................... 162 Dodawanie łodzi oraz sterowanie nimi . .......................................................................... 163 Strzelanie z dział .................................................................................................................. 164 Wynik działania gry ........................................................................................................... 167 Analiza gry ........................................................................................................................... 168 Podsumowanie .................................................................................................................... 169
Rozdział 10. Publikacja gry ............................................................................................171 Poprawianie aplikacji ......................................................................................................... 171 Dodawanie ekranu początkowego . ............................................................................ 171 Reakcja na wciśnięcie przycisku . ............................................................................... 174 Opakowywanie gry ............................................................................................................. 175 Rozpowszechnianie gry . ..................................................................................................... 176 Otwieranie konta w usłudze Google Play . ................................................................ 179 Wysyłanie aplikacji do sklepu Google Play . ............................................................. 180 Reklamowanie gry .............................................................................................................. 180 Podsumowanie .................................................................................................................... 181
Dodatek A
Testowanie gier dla systemu Android na prawdziwym urządzeniu ..........183 Skorowidz ..................................................................................................187
5
SPIS TREŚCI
6
O autorze
Jeremy Kerfs pełni funkcję redaktora technicznego w wielu magazynach poświęconych tematyce robotyki oraz technologii konsumenckich. Wcześniej prowadził zajęcia z podstaw informatyki dla dzieci. W chwili obecnej pracuje również w charakterze konsultanta ds. tworzenia aplikacji internetowych. Jego pasją jest prowadzenie firmy. Dzięki połączeniu tej pasji z wiedzą na temat programowania założył firmę Laughing Studios, która ma na celu tworzenie mobilnych gier oraz aplikacji. Aby zachować równowagę, Jeremy gra na pianinie oraz uprawia jogging, a także — kiedy nad zatoką San Francisco wieje wystarczająco silna bryza — windsurfing.
O redaktorze technicznym
Jelani John jest niezależnym programistą oraz animatorem z Brooklynu uwielbiającym tworzenie gier oraz zabawę z nowymi technologiami. Więcej informacji na jego temat można znaleźć pod adresem: www.jelanijohn.com.
Podziękowania
Frank Pohlmann, dyrektor redakcji w wydawnictwie Apress, dał mi niesamowitą okazję do napisania tej książki. Jestem mu niesamowicie wdzięczny za rady oraz pomoc w trakcie ustalania jej planu. Specjalne podziękowania należą się również Anicie Castro — redaktorowi pomocniczemu — która dodawała mi sił w trakcie czasami bardzo uciążliwej pracy nad dokończeniem rozdziałów, rysunków oraz kodu umieszczonego w tej książce. Anito, dałaś mi wiele niesamowicie cennych wskazówek i wykazywałaś się ogromną cierpliwością w trakcie pracy. Dziękuję również wszystkim redaktorom oraz korektorom, którzy pracowali wraz ze mną nad tym projektem. Wasze rady techniczne, pomoc w dopracowywaniu stylu tej książki oraz pomysły organizacyjne zwiększyły bez wątpienia jakość tej książki. W trakcie pracy napotkałem tak wielu wspaniałych mentorów oraz kolegów, którzy doprowadzili do końca coś, co na początku uznawałem za niemożliwe. Dziękuję Dave’owi Briccettiemu za wprowadzenie mnie w arkana informatyki. Paul Spinrad był niesamowitym redaktorem magazynu „Make Magazine”. Lektura dwóch jego książek stanowiła dla mnie inspirację do napisania niniejszej książki. Paul dawał mi dobre rady, cierpliwie odpowiadał na moje pytania i nie złościły go moje kaprysy. Jestem mu za to bardzo wdzięczny.
ROZDZIAŁ 1
Konfiguracja środowiska programistycznego Java dla systemu Android 3.0 Celem niniejszej książki jest przekazanie Czytelnikowi dogłębnej wiedzy o tworzeniu własnych gier na tablety działające pod kontrolą systemu Android 3.0. Po przeczytaniu tej książki oraz zapoznaniu się z zawartymi w niej przykładami Czytelnik nabędzie wiedzę na temat sposobów kontrolowania czujników, ekranu dotykowego, interfejsów sieciowych, a także mocy obliczeniowej wielu nowych tabletów. Czyż nie brzmi to zachęcająco? Zamiast mozolnie spędzać czas na tworzeniu nudnych aplikacji biznesowych, których celem jest dostosowanie aplikacji sklepu internetowego lub wygenerowanie kuponu rabatowego, Czytelnik dowie się, w jaki sposób tworzyć zabawne oraz intrygujące gry. Każda osoba, która kiedykolwiek tworzyła już gry, będzie miło zaskoczona tym, jak łatwo — w porównaniu z grami dla tradycyjnych komputerów PC lub konsol — pisze się je dla systemu Android. Mimo że żadna książka, która została wydana dotychczas, nie zmieniła laika w mistrza programowania, to jednak podstawy zaprezentowane w tej książce pozwolą Czytelnikowi wcielić w życie każdy pomysł na grę 2D. Aby skoncentrować się na najbardziej pomysłowych aspektach procesu tworzenia gier, temat programowania został tu przedstawiony w możliwie najprostszy sposób.
Czym jest system Android? System Android jest wyjątkowy, a uznanie dla niego zwiększa się wraz z zaawansowaniem doświadczenia w programowaniu. Wielu producentów urządzeń mobilnych zdecydowało się już wyprodukować tablety działające pod tym systemem, co spowodowało powstanie olbrzymiego rynku dla tworzonych dlań gier. W tym rozdziale przedstawione zostaną główne cechy systemu oraz jego historia.
Początki systemu Android Firma Android została założona w Krzemowej Dolinie w roku 2003 jako niewielka firma informatyczna. Jej celem było stworzenie bardziej interakcyjnego oraz przyjaznego interfejsu użytkownika dla smartfonów. W roku 2005 Android został przejęty przez firmę Google, co było elementem strategii wejścia na rynek telefonów komórkowych tej ostatniej. Wkrótce po przejęciu firmy, w roku 2007, został wydany pierwszy system operacyjny Android. W kolejnych latach ukazało się wiele nowych wersji systemu (powstało więcej niż siedem głównych zmian), co uczyniło go jednym z wiodących systemów operacyjnych na smartfony. Niektóre źródła przyjmują, że Android ma prawie 50% rynku systemów operacyjnych dla urządzeń mobilnych. Poznanie sposobu numerowania kolejnych wydań systemu Android jest niezwykle ważne dla zrozumienia podstaw programowania dla tej platformy. Firma Google dołożyła wszelkich starań,
ROZDZIAŁ 1. KONFIGURACJA ŚRODOWISKA PROGRAMISTYCZNEGO JAVA DLA SYSTEMU ANDROID 3.0
aby zapewnić wsteczną zgodność wszystkich wersji systemu Android. Niemniej jednak aplikacje są zazwyczaj projektowane do pracy w kilku głównych, wybranych wersjach systemu. Jest to spowodowane chęcią zagwarantowania najlepszej wydajności aplikacji oraz możliwie najlepszych odczuć użytkownika. W chwili obecnej najbardziej popularną wśród programistów wersją systemu jest ta nosząca nazwę kodową Gingerbread1. Niemniej jednak wraz z rozpowszechnianiem się nowoczesnych urządzeń potrzebujących bardziej zaawansowanych systemów operacyjnych (jak chociażby tablety) kolejne wersje systemu zaczynają zyskiwać obecnie na znaczeniu. Zamieszczona poniżej lista wersji systemu Android wraz z informacją o odpowiadającym każdej z nich udziale w rynku pozwala zorientować się, które z wersji pozostają wciąż najpopularniejsze, a przez to stanowią główny obiekt zainteresowania programistów aplikacji. Obok numeru wersji nadanego przez firmę Google podana jest nazwa kodowa danej wersji. Ta ostatnia jest bardzo często używana przez programistów. Należy jednocześnie pamiętać, że z wyjątkiem wersji systemu Android 3.0 oraz późniejszych wszystkie wersje systemu projektowane były z myślą o telefonach komórkowych. • Android 1.5 Cupcake (0,6%), • Android 1.6 Donut (1,0%), • Android 2.1 Éclair (7,6%), • Android 2.2 Froyo (27,8%), • Android 2.3 Gingerbread (0,5%), • Android 2.3.3 Gingerbread (58,1%), • Android 3.0 Honeycomb (3,6%), • Android 4.0 Ice Cream Sandwich (1%). Dla osób zainteresowanych aktualnym podziałem rynku pomiędzy różne wersje systemu firma Google tworzy odpowiednie zestawienie, dostępne pod adresem: http://developer.android.com/ resources/dashboard/platform-versions.html. Po zapoznaniu się z powyższą listą Czytelnik mógłby uznać, że powinien tworzyć gry dla wersji Gingerbread, ponieważ właśnie ta wersja systemu Android ma zdecydowaną przewagę w udziale w rynku nad pozostałymi. Powodem takiej przewagi jest to, że ta wersja systemu jest zainstalowana na wielu prostszych i starszych telefonach, w przypadku których proces uaktualniania oprogramowania jest skomplikowany. Urządzenia tego typu będą mogły zostać wkrótce zupełnie pominięte, ponieważ wraz z upływem czasu zwiększy się znaczenie nowszych wersji systemu. Tworzenie gier dla większości użytkowników jest w pewnym stopniu racjonalne. Niemniej jednak każdego dnia nowi użytkownicy kupują coraz bardziej zaawansowane telefony, używające najnowszych wersji systemu. Prawdopodobnie najważniejszym argumentem za używaniem wersji Gingerbread byłoby to, że na tej wersji działają setki tysięcy aplikacji i chociażby z tego powodu trudno byłoby ją zupełnie zbagatelizować. Biorąc pod uwagę wszystkie fakty przytoczone powyżej, w niniejszej książce skupimy się na projektowaniu gier dla jednej z najnowszych wersji systemu, jaką jest Android 3.0 Honeycomb. Przemawiają za tym dwa podstawowe argumenty. Po pierwsze, ta wersja systemu jest pierwszą wersją zoptymalizowaną pod kątem tabletów, które są znacznie bardziej pociągające i zabawniejsze od smartfonów. Po drugie, rynek urządzeń typu tablet działających na bazie systemu Android rozwija się bardzo gwałtownie. Jest to głównie spowodowane tym, że coraz więcej firm decyduje się tworzyć tablety, które mogłyby konkurować z tabletem iPad firmy Apple. Po porażce systemu WebOS2 systemy Android oraz iOS stały się jedynymi graczami na rynku tabletów. Próbę rywalizacji podjęła wprawdzie jeszcze 1
Są to wersje systemu z rodziny 2.3.x. Aplikacje tworzone na tę rodzinę systemu stanowią obecnie blisko 60% wszystkich powstających programów. Nazwa Gingerbread oznacza dosłownie piernik — przyp. tłum.
2
System WebOS został stworzony przez firmę Palm dla urządzeń typu Palm Pre. Po przejęciu przez firmę Hewlett-Packard oraz niepowodzeniu marketingowym tabletu tej firmy kontrolowanego przez system WebOS firma HP zdecydowała o upublicznieniu licencji na ten system na zasadach open source. Być może ten ruch sprawi, że zostanie on spopularyzowany przez środowisko programistów na niedrogich smartfonach — przyp. tłum.
12
CZYM JEST SYSTEM ANDROID?
firma Microsoft, ale do chwili obecnej nie uzyskała znaczącego udziału w rynku. O gigantycznej skali rozwoju tego rynku świadczyć może często przytaczany cytat firmy Google, mówiący o rejestracji każdego dnia 500 000 urządzeń działających pod kontrolą systemu Android.
Główne cechy systemu Android 3.0 Wersja Honeycomb stanowi olbrzymi postęp w stosunku do poprzednich wersji systemu Android. Została ona zaprojektowana w celu wykorzystania znacznie większych ekranów oraz znacznie mocniejszych procesorów, w które są wyposażone tablety. Ta wersja systemu pozwala programistom na znaczne wzbogacenie zazwyczaj skromnych gier na smartfony. Wiele z zaprezentowanych zmian związanych jest z interfejsem użytkownika, a ma na celu umożliwienie użytkownikom korzystania z ekranów kilka razy większych od tych dla smartfonów. Dla przykładu — typowy telefon ma ekran o rozmiarze dwóch bądź trzech cali, podczas gdy ekrany tabletów osiągają imponujące rozmiary od dziewięciu do dziesięciu cali. Oczywiście tego rodzaju modyfikacje zwiększają wygodę, niemniej programiści bardziej koncentrują się na tych zmianach w systemie, które są związane z szybszym generowaniem grafiki, na korzystaniu z nowych czujników bądź też na nowych możliwościach sieciowych. Nie wszystkie gry wykorzystują wszystkie nowe możliwości systemu. Jednakże istotne jest dostrzeżenie znaczenia tych możliwości w projektowaniu wyjątkowych gier. Warte zainteresowania jest już samo powiększenie ekranu. Większe rozdzielczości wymagają użycia grafik, które są zarówno skalowalne, jak też wymagające pod względem wizualnym. Wiele tabletów opartych na systemie Android posiada rozdzielczość ekranu 1280×800. Jest to rozdzielczość porównywalna z tą używaną wciąż jeszcze przez wiele komputerów. Z tego powodu grafika musi dorównywać tej wykorzystywanej w grach komputerowych. Tabela 1.1 przedstawia listę najważniejszych zmian w systemie Android 3.0, o szczególnym znaczeniu dla programistów gier. Tabela 1.1. Główne cechy systemu Android 3.0 Nowa cecha systemu Android 3.0
Znaczenie w programowaniu gier
Interfejs użytkownika 3D
Gry oraz aplikacje mogą wykorzystywać nowe kompozycje zapewniające profesjonalny wygląd przy minimalnym nakładzie pracy.
Lepsze widżety pulpitu
Gry udostępniające tryb wieloosobowy pozwalają użytkownikom na wykonywanie prostych zmian bezpośrednio na pulpicie.
Większe możliwości graficzne
Gry mogą wykorzystywać bardziej realistyczne obrazy o wysokiej rozdzielczości bez utraty dużej wydajności działania.
Wsparcie dla procesorów wielordzeniowych
Wszystkie elementy gry mogą być przyspieszone dzięki przypisaniu różnych procedur do oddzielnych rdzeni procesora.
Modyfikowalny pasek zadań
Niektóre gry mogą wykorzystywać pasek na górze ekranu aplikacji do wyświetlania uaktualnień lub prezentowania wyników.
Pasek systemowy oraz powiadomień
Chociaż nie jest to modyfikacja typowo pod kątem gier, to jednak może być przydatna w celu umożliwienia użytkownikom śledzenia zmian oraz uaktualnień w grze.
Zmiany w obsłudze połączeń Bluetooth
Od tej wersji systemu do urządzenia mogą być podłączane joysticki oraz klawiatury, co udostępnia całkiem nowe metody komunikacji z użytkownikiem.
13
ROZDZIAŁ 1. KONFIGURACJA ŚRODOWISKA PROGRAMISTYCZNEGO JAVA DLA SYSTEMU ANDROID 3.0
W dalszej części tej książki umieszczone zostaną porady, w jaki sposób można wykorzystać większość nowych funkcji systemu Android przeznaczonych dla tabletów. Jeżeli Czytelnik chciałby uczynić tworzenie gier swoim nowym hobby, powinien zwrócić uwagę na zamieszczone dalej informacje o możliwościach pobrania bez opłat licencyjnych dźwięków oraz obrazów całkiem dobrej jakości. W rozdziale 2. zostaną również szczegółowo opisane narzędzia, których autor tej książki używa do tworzenia grafiki oraz muzyki przeznaczonej dla jego gier. Teraz gdy Czytelnik zaznajomił się już z systemem Android, możemy przejść do programowania. Niemniej warto jeszcze zwrócić szczególną uwagę na opisane w kolejnym podrozdziale wymagania dotyczące odpowiednich umiejętności oraz konfiguracji sprzętowych niezbędnych podczas tworzenia gier dla tego systemu.
Czego potrzeba do tworzenia gier w systemie Android? Zastanówmy się, jakie wymagania należy spełniać, aby stać się programistą gier dla systemu Android. Przyjrzyjmy się też, jakie umiejętności powinien posiadać Czytelnik, by jak najlepiej wykorzystać tę książkę wraz z zamieszczonymi w niej przykładami oraz wszystkie możliwości systemu.
Co należy wiedzieć? Jak trudne jest programowanie gier dla systemu Android? Właściwie zależy to od posiadanego już doświadczenia w programowaniu przy użyciu języka Java, a także od znajomości systemu Android. Dla osób mających solidną wiedzę o języku Java lektura tej książki nie będzie stanowić żadnej trudności. Dla tych, które posiadają już doświadczenie w tworzeniu programów dla systemu Android, prawdopodobnie żaden fragment kodu zamieszczony w tej książce nie będzie stanowił wyzwania. Dlatego też takie osoby zachęcam do eksperymentowania w trakcie lektury. Aby dokładnie poznać wymagania, przed kontynuacją lektury dalszej części książki należy zapoznać się szczególnie z niniejszym podrozdziałem. Ogólnie rzecz biorąc, osoby zainteresowane nauką tworzenia gier na tablety z systemem Android wywodzą się z trzech różnych środowisk. Każde z nich daje dobre przygotowanie do pracy z przykładami zamieszczonymi w książce, niemniej każde wymaga przyjęcia nieco innego podejścia. W przypadku gdy Czytelnik dysponuje wiedzą zarówno o języku Java, jak też o systemie Android, jest już całkowicie przygotowany do lektury. Przykładowy kod zamieszczony w książce będzie przypominać wszystko to, co widział już wcześniej. Niemniej jednak, ponieważ książka koncentruje się na kwestiach grafiki, pętlach logiki w grach, a także szybkości reakcji na działania użytkownika, część zagadnień może stanowić dla Czytelnika pewną nowość. Niezależnie od posiadanego przez Czytelnika doświadczenia książka ta pomoże mu poprawić znajomość technik i zagadnień związanych z tworzeniem gier na tablety. Być może jednak Czytelnik czuje się komfortowo w programowaniu w języku Java, ale nigdy wcześniej nie pracował z systemem Android. To również nic strasznego. W takim przypadku analiza przykładów oraz kodu programów nie będzie stanowić większej trudności. Należy jednak pamiętać, że tak jak w przypadku każdego nowego środowiska oraz interfejsu API, należy cyklicznie sprawdzać znaczenie prezentowanych w kodzie funkcji oraz klas. Oczywiście zaznajomienie się z systemem Android wymaga czasu, jednak jest to czas warty poświęcenia. Ostatnią grupą czytelników będą ci, którzy być może nie wyszli z umiejętnościami programowania w Javie poza instrukcję warunkową if oraz nie mają dużej znajomości podstaw pracy z systemem Android. Takie osoby oczywiście mogą próbować zapoznać się z tą książką, niemniej muszą dokształcić się z programowania w Javie. W tym celu szczególnie polecana jest lektura książki Jeffa Friesena pt. Java. Przygotowanie do programowania na platformę Android, wydanej nakładem wydawnictwa Helion w 2011 r. Po zaznajomieniu się z zasadami działania języka Java można od razu kontynuować lekturę niniejszej książki. Większe doświadczenie w pracy z tym językiem będzie nabywane sukcesywnie w trakcie lektury.
14
CZEGO POTRZEBA DO TWORZENIA GIER W SYSTEMIE ANDROID?
Czytelnikowi przydać może się również znajomość zasad języka XML. Język ten jest względnie łatwy do zrozumienia i jego podstawowe użycie w tej książce nie powinno stanowić większego kłopotu. Teraz kiedy omówione już zostały wymagane kwalifikacje programistyczne, możemy zastanowić się nad środowiskiem niezbędnym do tworzenia gier.
Środowisko programistyczne Nadszedł już odpowiedni czas, aby zabrać się do dzieła i poznać rzeczywiste narzędzia niezbędne do tworzenia gier w systemie Android. Na całe szczęście nie trzeba w tym celu kupować żadnego oprogramowania! Jedynym jednorazowym wydatkiem, który należy ponieść, będzie ok. 80 złotych niezbędne do publikowania gier w sklepie Android Market. Na początek upewnijmy się, czy komputer spełnia minimalne wymagania dla środowiska programistycznego: • Windows XP (32-bitowy), Vista (32 lub 64-bitowy) albo Windows 7 (32 lub 64-bitowy), • Mac OS X 10.5.8 lub późniejszy (jedynie wersja x86), • Linux (testowano na Ubuntu Linux, Lucid Lynx). Powyższa lista została utworzona na podstawie wymagań własnych systemu Android. Lista najbardziej aktualnych zmian jest dostępna pod adresem: http://developer.android.com/sdk/ requirements.html. System spełniający wymagania minimalne pozwoli już wprawdzie tworzyć aplikacje dla systemu Android, niemniej testowanie programów może być raczej mozolne i długotrwałe. Ogólnie rzecz biorąc, w przypadku gdy komputer Czytelnika nadaje się do uruchamiania najnowszych gier, powinien również być odpowiedni do tworzenia aplikacji. Jeżeli jednak Czytelnik ma wolniejszy komputer, nie należy wpadać w panikę. Komputer ten będzie nadawać się perfekcyjnie do tworzenia gier w systemie Android, a do ich testowania zamiast symulatora urządzenia Android na komputerze lokalnym będzie trzeba użyć tabletu. Do przetestowania przykładów umieszczonych w niniejszej książce nie będzie wymagany tablet z systemem Android. Nic jednak nie zastąpi testowania aplikacji na takim właśnie urządzeniu. Obecnie na rynku dostępna jest ogromna liczba tabletów, z czego tańsze modele kosztują już od 1500 do 2000 zł3. Kwota tego rzędu wydaje się dobrą inwestycją w przypadku, gdy Czytelnik odkryje, że tworzenie gier jest dla niego czynnością tak samo uzależniającą jak dla autora. Najbardziej popularne tablety tworzą firmy Motorola oraz Samsung4. Aby zorientować się w najnowszych trendach dla tabletów Android, należy zapoznać się z ofertą tych firm. Teraz gdy Czytelnik upewnił się już co do swoich umiejętności, a także zdecydował, na jakim urządzeniu będzie pracował, może przystąpić do pobrania wymaganych narzędzi oraz skonfigurowania środowiska programistycznego.
3
Książka była przygotowywana na rynek amerykański, na którym dominują markowe urządzenia większych firm. Polski rynek obfituje w tańsze urządzenia produkcji chińskiej, dostępne już od 500 zł. Przed zakupem takiego urządzenia należy jednak zwrócić szczególną uwagę na to, czy posiada ono wszystkie wymagane czujniki oraz rozszerzenia. Tańsze modele bywają pozbawione czujników położenia, GPS-a, odpowiednio dużej pamięci, a także dostępu do sklepu Android Market. Niektóre z nich mają trudności z pełną integracją z rozszerzeniem ADT dla środowiska Eclipse, co znacznie spowalnia proces tworzenia oprogramowania. W skrajnych przypadkach firmy ograniczają się do najprostszego przeniesienia systemu z telefonu komórkowego bez zaadaptowania go do warunków lokalnych oraz wymogów urządzenia typu tablet. Dlatego też przed zakupem należy sprawdzić praktycznie, czy dany tablet spełnia oczekiwane wymagania — przyp. tłum.
4
Na polskim rynku do grona popularnych firm tworzących markowe tablety należą jeszcze Asus, Sony oraz grupa polskich firm sprowadzająca pod swoją marką tańsze tablety zaadaptowane z rynku chińskiego — przyp. tłum.
15
ROZDZIAŁ 1. KONFIGURACJA ŚRODOWISKA PROGRAMISTYCZNEGO JAVA DLA SYSTEMU ANDROID 3.0
Konfiguracja środowiska programistycznego Już prawie dotarliśmy do najciekawszej części książki. Najpierw jednak musimy upewnić się co do poprawnej konfiguracji komputera. W tym celu musimy najpierw pobrać i zainstalować trzy pakiety, które będą niezbędne do dalszej pracy: • pakiet Java Development Kit (JDK), • program Eclipse, będący zintegrowanym środowiskiem programistycznym5, • pakiet SDK do programowania w środowisku Android. Bardzo prawdopodobne jest, że aktywni programiści języka Java mają już zainstalowaną wersję pakietu JDK, a nawet całe środowisko Eclipse. W takim przypadku mogą oni przejść bezpośrednio do instalacji pakietu Android SDK opisanej w dalszej części tego rozdziału. Niemniej w przypadku jakichkolwiek problemów warto przejrzeć kolejne dwa podrozdziały, ponieważ problemy te mogą być spowodowane używaniem złej wersji pakietu JDK lub środowiska Eclipse. W kolejnych podrozdziałach Czytelnik zapozna się z konfiguracją każdego ze wspomnianych wcześniej pakietów. Zaraz po ich zainstalowaniu możliwe będzie stworzenie pierwszego programu na tablet działający w systemie Android. Cały proces nie powinien zająć więcej niż 20 minut.
Instalacja pakietu Java JDK Pierwszą czynnością, którą należy wykonać, będzie pobranie oraz zainstalowanie na komputerze najnowszej wersji pakietu JDK. Oto w jaki sposób należy to zrobić: 1. Aby pobrać wersję JDK6 właściwą dla danego systemu, należy odwiedzić stronę www.oracle.com/ technetwork/java/javase/downloads/index.html. Instalacja pakietu JDK jest niezbędna do umożliwienia działania języka Java na danym komputerze. W celu jego zainstalowania należy na stronie pod podanym powyżej adresem odnaleźć znajdującą się w lewym górnym rogu ikonę Javy, a następnie wybrać odpowiedni odnośnik. Czynność ta została przedstawiona na rysunku 1.1, umieszczonym poniżej. Jej wykonanie spowoduje wyświetlenie strony, z której możliwe będzie pobranie pakietu JDK SE7. 2. Po przejściu do kolejnej strony należy zaakceptować licencję użytkownika umieszczoną na zakładce o nazwie Downloads. Czynność ta została przedstawiona na rysunku 1.2. W dalszej kolejności należy wybrać wersję pakietu odpowiednią dla używanego systemu operacyjnego, a następnie kliknąć odnośnik do pobierania pakietu. 3. Po pobraniu pliku należy uruchomić program instalacyjny. Na niektórych komputerach program ten zostanie uruchomiony w sposób automatyczny. Jeżeli tak się nie stanie, należy odszukać katalog, w którym zapisywane są pobierane pliki, a następnie posortować jego zawartość według daty modyfikacji. Ostatnio pobrany plik będzie właśnie poszukiwanym programem instalacyjnym — wystarczy go uruchomić. 5
Ang. Integrated Development Environment, czyli w skrócie IDE — przyp. tłum.
6
Do tworzenia aplikacji przy użyciu Android SDK potrzebny jest pakiet Java JDK. Uproszczony pakiet uruchomieniowy języka Java (JRE) jest niewystarczający — przyp. tłum.
7
Szczególną uwagę należy zwrócić na rodzaj platformy uruchomieniowej, dla której pobierany jest pakiet Java JDK. Użytkownicy 64-bitowej wersji systemu Windows mają do wyboru pobranie 64 lub 32-bitowej wersji JDK (ta ostatnia będzie uruchamiana na emulatorze maszyny 32-bitowej). Najnowsza wersja strony Oracle rozpoznaje rodzaj używanego systemu po wersji przeglądarki i często ukrywa zbędne wersje JDK. Aby sprawdzić, jaka wersji przeglądarki Internet Explorer używana jest w danym momencie, należy wyświetlić listę procesów systemu. W wersji 32-bitowej po nazwie występuje znacznik „*32”, którego brak w przypadku wersji 64-bitowej. Z powodu wielu problemów, jakie wciąż jeszcze powoduje używanie 64-bitowej wersji JDK oraz SDK, należy rozważyć skorzystanie z emulowanej wersji 32-bitowej. Więcej informacji znajduje się na stronie http://www.java.com/pl/download/ faq/java_win64bit.xml — przyp. tłum.
16
KONFIGURACJA ŚRODOWISKA PROGRAMISTYCZNEGO
Rysunek 1.1. Strona wyboru wersji pakietu JDK 4. Po uruchomieniu programu zostanie wyświetlona strona powitalna kreatora instalacji, co zostało przedstawione na rysunku 1.3, znajdującym się poniżej. W celu ukończenia instalacji należy wybrać przycisk Next (dalej), a następnie postępować zgodnie z wyświetlanymi instrukcjami. W tym momencie jesteśmy gotowi do instalacji środowiska programistycznego Eclipse, które będzie wykorzystywane w dalszej części tej książki do tworzenia gier. Bez tego programu bylibyśmy prawdopodobnie zmuszeni do kompilacji kodu programu przy użyciu systemowego wiersza poleceń. Wykorzystanie środowiska programistycznego oszczędza więc olbrzymią ilość czasu.
Instalacja środowiska Eclipse Po zainstalowaniu pakietu JDK można przystąpić do konfiguracji środowiska programistycznego. Będziemy używać środowiska Eclipse, które stanowi pakiet oprogramowania w znacznym stopniu wspierający programowanie w języku Java dla systemu Android. W celu zainstalowania programu należy wykonać następujące czynności: 1. Aby odnaleźć wersję pakietu Eclipse odpowiednią dla używanego systemu operacyjnego, należy wyświetlić stronę www.eclipse.org/downloads/. Na tej stronie trzeba skorzystać z niewielkiego
17
ROZDZIAŁ 1. KONFIGURACJA ŚRODOWISKA PROGRAMISTYCZNEGO JAVA DLA SYSTEMU ANDROID 3.0
Rysunek 1.2. Postanowienia licencyjne oraz wybór wersji JDK menu rozwijalnego, umożliwiającego wybór używanego systemu operacyjnego. Zostało to przedstawione na rysunku 1.4, znajdującym się poniżej. Po odświeżeniu strony należy wybrać pozycję: Eclipse IDE for Java Developers8, a następnie łącze odpowiednie dla wersji posiadanego systemu operacyjnego9. Po wykonaniu tej czynności zostanie wyświetlona strona pobierania pakietu Eclipse. 8
Środowisko IDE dla programistów Javy — przyp. tłum.
9
W rzeczywistości wersja pobieranego środowiska Eclipse powinna być zgodna z wersją pakietu JDK używanego w systemie. W przypadku systemu 64-bitowego oraz 32-bitowego pakietu JDK należy wybrać również 32-bitową wersję środowiska Ecplise. W przypadku gdyby Czytelnik zdecydował się jednak na instalację 64-bitowej wersji środowiska Eclipse oraz 64-bitowej wersji JDK, musi liczyć się z możliwością wystąpienia wielu problemów powodujących wyjątki krytyczne aplikacji. Problemy te dotyczą kompatybilności klas programu z różnymi wersjami VM i są dobrze udokumentowane na stronie www.eclipse.org (np. https://bugs.eclipse.org/bugs/show_ bug.cgi?id=214092). W takim przypadku zalecane jest użycie najnowszej dostępnej wersji JDK oraz pakietu Eclipse — przyp. tłum.
18
KONFIGURACJA ŚRODOWISKA PROGRAMISTYCZNEGO
Rysunek 1.3. Kreator instalacji pakietu JDK
Rysunek 1.4. Strona wyboru opcji pobierania dla środowiska Eclipse
19
ROZDZIAŁ 1. KONFIGURACJA ŚRODOWISKA PROGRAMISTYCZNEGO JAVA DLA SYSTEMU ANDROID 3.0
2. W dalszej kolejności należy pobrać skompresowany programem zip pakiet zawierający wybraną wersję środowiska, a następnie rozpakować go we wskazanym przez siebie miejscu. Po rozpakowaniu pakietu należy uruchomić program instalacyjny. Podczas instalacji trzeba upewnić się, czy zaznaczone zostało pole wyboru zapewniające utworzenie skrótu programu na pulpicie komputera10. Ułatwi to znacznie późniejsze używanie programu. 3. Po zakończeniu instalacji możliwe będzie uruchomienie środowiska Eclipse przy użyciu zainstalowanego na pulpicie skrótu do programu. Po uruchomieniu środowiska powinno pojawić się okno podobne do tego przedstawionego na rysunku 1.5. Będzie to oznaczać poprawną konfigurację środowiska11.
Rysunek 1.5. Środowisko Eclipse podczas uruchamiania Po zainstalowaniu platformy programistycznej możemy już dodać pakiet Android SDK, który udostępni biblioteki oraz narzędzia niezbędne do rozpoczęcia tworzenia gier. Do tej chwili zajmowaliśmy się jedynie podstawami obejmującymi język Java oraz środowisko programistyczne.
Instalacja pakietu SDK dla systemu Android Ostatnim pakietem potrzebnym do skompletowania platformy programistycznej będzie pakiet SDK dla systemu Android udostępniany przez firmę Google. 1. W celu odnalezienia pakietu właściwego dla używanego systemu operacyjnego należy wyświetlić stronę http://developer.android.com/sdk/index.html, która została przedstawiona na rysunku 1.6. Następnie wystarczy wybrać pakiet Android SDK przeznaczony dla używanego systemu operacyjnego. Po wybraniu odpowiedniego łącza rozpocznie się pobieranie pliku. 2. Po pobraniu pliku należy odnaleźć go w systemie oraz uruchomić. Po wykonaniu tej czynności zostanie wyświetlony kreator instalacji pakietu Android SDK, przedstawiony na rysunku 1.7.
10
Środowisko można również pobrać w postaci samego archiwum. W takim przypadku po rozpakowaniu archiwum należy ręcznie utworzyć skrót na pulpicie oraz skonfigurować parametry używanego pakietu JRE zgodnie z informacjami zawartymi w pliku readme_eclipse.html dostępnym w katalogu readme — przyp. tłum.
11
Niekiedy w przypadku instalacji oraz używania więcej niż jednego pakietu JDK na komputerze użytkownika konieczne będzie ręczne skonfigurowanie pliku eclipse.ini umieszczonego w katalogu programu Eclipse. Plik ten umożliwia wskazanie konkretnego JDK, które ma być wykorzystywane podczas uruchamiania środowiska — przyp. tłum.
20
KONFIGURACJA ŚRODOWISKA PROGRAMISTYCZNEGO
Rysunek 1.6. Strona pobierania pakietu Android SDK
Rysunek 1.7. Kreator instalacji pakietu Android SDK 21
ROZDZIAŁ 1. KONFIGURACJA ŚRODOWISKA PROGRAMISTYCZNEGO JAVA DLA SYSTEMU ANDROID 3.0
Uwaga! Należy koniecznie zapamiętać położenie docelowe plików SDK. Autor książki osobiście używa katalogu C:\Android\android_sdk\. Położenie katalogu należy zapisać niezależnie od używanego systemu operacyjnego, będzie on bowiem potrzebny na późniejszym etapie konfiguracji środowiska Eclipse.
3. W celu zainstalowania SDK należy nacisnąć przycisk Next (dalej) oraz postępować zgodnie z instrukcjami kreatora instalacji. Ostatecznie po dotarciu do ostatniej strony należy zaznaczyć opcję Start SDK Manager (uruchom menedżera SDK) w sposób przedstawiony poniżej na rysunku 1.8. Zaznaczenie tej opcji spowoduje uruchomienie menedżera SDK natychmiast po zakończeniu instalacji.
Rysunek 1.8. Strona końcowa kreatora instalacji pakietu Android SDK 4. Po otwarciu okna dialogowego o nazwie Android SDK Manager (menedżer pakietu Android SDK), przedstawionego na rysunku 1.9, należy zaznaczyć pole wyboru znajdujące się obok pozycji Android 3.0 (API11) oraz usunąć zaznaczenie domyślnie zaznaczonego pola wyboru obok pozycji Android 4.0.3 (API15)12. Trzeba upewnić się również, czy zaznaczone są pola wyboru na pozycji Tools (narzędzia) oraz Google USB Driver Package (pakiet sterownika USB firmy Google)13. Po zweryfikowaniu wybranych opcji należy nacisnąć przycisk Install Packages (zainstaluj pakiety). W ten sposób zainstalowane zostaną domyślne i zalecane przez firmę Google pakiety systemu Android, które będzie można później wykorzystać do programowania gier. Bez ich zainstalowania nie będzie możliwe użycie wielu narzędzi oraz przykładowych aplikacji. 5. Po kliknięciu przycisku Install packages (zainstaluj pakiety) zostanie wyświetlone okno z pytaniem o akceptację postanowień licencyjnych, a po ich zaakceptowaniu okno dialogowe przypominające to z rysunku 1.10. W oknie będzie wyświetlany postęp instalacji (może ona zająć kilka minut). Teraz kiedy posiadamy już zainstalowany język Java, środowisko programistyczne oraz narzędzia Androida, wystarczy je ze sobą połączyć.
12
O ile Czytelnik nie zamierza tworzyć programów używających najnowszej wersji API. W tej książce tworzymy kod dla platformy Android 3.0 — przyp. tłum.
13
Pola powinny być zaznaczone domyślnie. Sterownik USB firmy Google umożliwia testowanie programów bezpośrednio na urządzeniu — przyp. tłum.
22
KONFIGURACJA ŚRODOWISKA PROGRAMISTYCZNEGO
Rysunek 1.9. Menedżer pakietu SDK. Należy zwrócić uwagę na wybrane domyślnie pakiety
Rysunek 1.10. Instalacja pakietów oraz archiwów
Konfiguracja narzędzi Androida oraz urządzenia wirtualnego w środowisku Eclipse Ostatnią pozostałą do wykonania czynnością będzie przystosowanie środowiska Eclipse do współpracy z nowymi narzędziami Androida oraz tworzonymi w Eclipsie programami. Dzięki temu będziemy mogli tworzyć kod programów oraz testować go bezpośrednio w środowisku programistycznym. W przeciwnym 23
ROZDZIAŁ 1. KONFIGURACJA ŚRODOWISKA PROGRAMISTYCZNEGO JAVA DLA SYSTEMU ANDROID 3.0
przypadku zmuszeni bylibyśmy do każdorazowego zapisywania kodu, a następnie do jego przetestowania używalibyśmy innego programu. W celu zintegrowania środowiska Eclipse z SDK należy wykonać następujące czynności: 1. Aby zainstalować narzędzia systemu Android w środowisku Eclipse, należy uruchomić ten ostatni program, a następnie wybrać z menu pozycje Help (pomoc) oraz Install New Software (instaluj nowe oprogramowanie). Po wykonaniu tych czynności pojawi się okno dialogowe przedstawione na rysunku 1.11. Do tego okna będziemy powracać zawsze, ilekroć będzie konieczne dodanie do środowiska Eclipse nowych funkcji.
Rysunek 1.11. Okno dialogowe instalacji nowych funkcji środowiska Eclipse 2. Na początku należy wskazać środowisku Eclipse, gdzie ma szukać narzędzi. W tym celu na ekranie Install (zainstaluj) należy kliknąć znajdujący się w prawym górnym rogu przycisk Add (dodaj), po czym wyświetlone zostanie okno dialogowe Add Repository (dodaj repozytorium) przedstawione na rysunku 1.12. 3. W dalszej kolejności należy wykonać następujące czynności: a) W polu Name (nazwa) należy wpisać Android Tools, która to nazwa będzie od tej pory używana w celu odwoływania się do narzędzi. b) W polu Location (położenie) należy podać następujący adres URL: https://dl-ssl.google.com/ android/eclipse/. Adres ten wskazuje fizyczne położenie dodawanych narzędzi.
24
KONFIGURACJA ŚRODOWISKA PROGRAMISTYCZNEGO
Rysunek 1.12. Pola Name oraz Location używane do dodawania narzędzi Androida w środowisku Eclipse 4. Po zakończeniu wprowadzania położenia narzędzi należy kliknąć przycisk OK, co spowoduje powrót do okna Install przedstawionego na rysunku 1.13.
Rysunek 1.13. Narzędzia programistyczne systemu Android 5. W kolejnym kroku należy zaznaczyć pole wyboru Developer Tools (narzędzia programistyczne), a następnie zaakceptować prośby o uaktualnienie oprogramowania. W ten sposób do środowiska Eclipse dodane zostaną narzędzia wymagane do tworzenia programów dla tabletów działających w systemie Android14. 14
W trakcie instalacji może zostać wyświetlony komunikat o braku podpisu cyfrowego dodatków. Należy ten komunikat zatwierdzić przyciskiem OK — przyp. tłum.
25
ROZDZIAŁ 1. KONFIGURACJA ŚRODOWISKA PROGRAMISTYCZNEGO JAVA DLA SYSTEMU ANDROID 3.0
6. Po instalacji należy ponownie uruchomić środowisko Eclipse, o czym przypomni stosowny komunikat w oknie. 7. Po zrestartowaniu środowiska należy z menu Window (okno) wybrać opcję Preferences (właściwości). W lewej części okna, które zostanie następnie wyświetlone, należy wybrać zakładkę Android. Po wykonaniu tych czynności ekran Czytelnika powinien przypominać ten przedstawiony na rysunku 1.14. W tym momencie możemy już wskazać środowisku miejsce instalacji pakietu Android SDK. Pozwoli to nam skompilować program bezpośrednio z okna środowiska Eclipse.
Rysunek 1.14. Opcje konfiguracji dla Androida w środowisku Eclipse 8. W polu SDK Location (położenie SDK) należy podać pełną nazwę katalogu, do którego pobrany został pakiet Android SDK. W naszym przypadku będzie to katalog C:\Android\Android-sdk. Po wykonaniu wymienionych powyżej czynności proces konfiguracji środowiska Eclipse jest już zakończony. Od tej chwili będziemy koncentrować się na budowie rzeczywistych aplikacji Androida oraz sposobach urzeczywistniania naszych wizji nowych gier. Konfiguracja narzędzi programistycznych opisana w tym podrozdziale zdecydowanie ułatwi wypróbowywanie w grach wielu różnych technik. Możliwość szybkiej zmiany kodu oraz sprawdzenia jej wyniku jest bezcenna.
Sprawdzanie działania narzędzi programistycznych W tej chwili Czytelnik prawdopodobnie już z niecierpliwością oczekuje na moment stworzenia rzeczywistej gry dla Androida. W tym podrozdziale dowie się on, w jaki sposób wykorzystać przed chwilą zainstalowane narzędzia do sprawdzenia dostarczonej z nimi biblioteki przykładowych programów. Wprowadzone
26
SPRAWDZANIE DZIAŁANIA NARZĘDZI PROGRAMISTYCZNYCH
zostaną również pewne podstawy projektowania wyglądu aplikacji. W kolejnych rozdziałach te przykładowe projekty zostaną rozwinięte do postaci pełnej gry. Każda z gier, które zostaną stworzone dla systemu Android, będzie wchodzić w skład projektu przechowującego w jednym miejscu wszystkie obrazy oraz dźwięki. W trakcie lektury Czytelnik szybko nauczy się sporo na temat środowiska Eclipse. Zrozumienie sposobu przechowywania zasobów oraz sposobu odwoływania się w tym środowisku do plików stanowi jedną z najważniejszych umiejętności do opanowania. Przykładowe programy mogą stanowić wspaniałe źródło wiedzy nawet dla najbardziej zaawansowanych programistów. Większość z prostych funkcji potrzebnych do stworzenia jakiejkolwiek gry została już umieszczona w jednym z przykładowych programów i bardzo prawdopodobnie jest dostępna bezpłatnie. Pobieżne przejrzenie dostępnych zasobów może w przyszłości oszczędzić wiele godzin pracy. Niestety większość aplikacji została stworzona dla starszych wersji systemu Android i z tego powodu na dużych ekranach tabletów mają one bardzo małe rozmiary. Tę niedogodność równoważy to, że Czytelnik może dołączyć część kodów z przykładowych programów do swojego projektu, podmieniając w nich jedynie procedury obsługi grafiki. W dalszej części tego podrozdziału przedstawione zostaną czynności niezbędne do utworzenia gry na tablet działający pod kontrolą Androida. Aby poznać najbardziej podstawowy szkielet gry, należy chociaż raz samodzielnie przejść od początku przez wszystkie etapy tworzenia projektu. Dlatego też zaczniemy od utworzenia pierwszego podstawowego projektu w Eclipsie.
Tworzenie projektu dla systemu Android Pierwszą czynnością podczas opracowywania jakiejkolwiek gry dla systemu Android jest stworzenie projektu w Eclipsie. 1. W programie Eclipse należy z menu File (plik) wybrać pozycje New (nowy), a następnie Project (projekt). Następnie w wyświetlonym oknie należy wybrać katalog Android, a z niego Android Project (projekt Android). Spowoduje to pojawienie się okna o nazwie New Android Project (nowy projekt Android) przedstawionego na rysunku 1.15. 2. W pola formularza należy wprowadzić brakujące informacje: a) W pole Project name (nazwa projektu) należy wpisać FirstApp lub podać jakąkolwiek inną znaczącą nazwę. b) W pozostałych pozycjach należy pozostawić wartości domyślne i kliknąć przycisk Next (dalej). Na kolejnym ekranie o nazwie Select Build Target (wybierz środowisko docelowe), przedstawionym na rysunku 1.16, należy zdecydować, dla której wersji systemu Android będzie tworzona aplikacja. Ponieważ chcemy, aby aplikacja działała na najnowszych tabletach, wybierzemy opcję Android 3.0. Ten wybór staje się bardzo istotny w przypadku, gdy chcemy, aby w momencie testowania gry działała ona poprawnie na emulowanym tablecie, a nie na małym ekranie telefonu. Po wybraniu API środowiska należy kliknąć przycisk Next. c) W kolejnym oknie, przedstawionym na rysunku 1.17, wprowadzane będą informacje konfiguracyjne o projekcie. W naszym przypadku pole Application Name (nazwa aplikacji) jest, ogólnie rzecz biorąc, identyczne z nazwą projektu. W tym miejscu możemy przepisać FirstApp lub podać inną nazwę używaną dla projektu. d) Pole Package Name (nazwa pakietu) powinno być już znajome dla programistów Javy, natomiast dla tych osób, które nie znają tego języka, może być nieco mylące. W tym miejscu należy zadeklarować nazwę w rodzaju com.gameproject.firstapp. Pakiety służą do organizowania kodu w języku Java i ułatwiają wykorzystanie wcześniej utworzonych plików. Więcej na ten temat można przeczytać pod adresem: http://java.sun.com/docs/books/jls/third_edition/html/ packages.html. Nie jest to jednak w tej chwili szczególnie istotna wiedza. Na podaną stronę można zajrzeć w chwili podjęcia decyzji o szerszej dystrybucji utworzonej aplikacji.
27
ROZDZIAŁ 1. KONFIGURACJA ŚRODOWISKA PROGRAMISTYCZNEGO JAVA DLA SYSTEMU ANDROID 3.0
Rysunek 1.15. Wypełniony formularz tworzenia nowego projektu Androida — krok 1. e) W polu Create Activity (utwórz aktywność) należy podać main. Klasy aktywności są składnikiem podstawowym programów pisanych dla systemu Android. Więcej o nich dowiemy się w dalszej części książki. W tej chwili można uznawać aktywność za podstawową funkcję aplikacji. Jest ona wywoływana w celu skonfigurowania gry, a następnie w trakcie jej działania do przetwarzania danych wejściowych oraz kierowania ruchem sprajtów15. Aktywności powinny być nazywane odpowiednio do swojej roli. Z tego właśnie powodu podstawowa aktywność jest zazwyczaj nazywana main, mainActivity lub w analogiczny sposób. f) W polu Minimum SDK (najniższa wersja SDK) należy wprowadzić liczbę 1116. Oznacza to, że do poprawnego działania gry będzie wymagane urządzenie działające pod co najmniej 11. wersją systemu Android. Czytelnik może się zastanawiać, dlaczego nagle przeskoczyliśmy do wersji 11., podczas gdy wcześniej otrzymał informację, że jedna z ostatnich wersji tego systemu nosi numer 3.0. Cóż, system Android posiada dość zwariowany system nazywania wersji. Oznaczenie 3.0 odnosi się do wersji platformy i jako takie zgodne jest ze zwyczajową konwencją nazywania oprogramowania, w której drobne zmiany powodują zmianę dziesiętnej części numeru wersji, a poważne uaktualnienia powodują zwiększenie numeru do kolejnej liczby całkowitej. Aby jednak zachować pełną spójność, twórcy systemu Android oznaczają każdą wersję platformy 15
Ang. sprite — sprajt lub równie często spotykany w literaturze duszek — przyp. tłum.
16
W tym miejscu firma Google wykazuje się brakiem konsekwencji. Numer SDK nie jest równoważny numerowi API. Na przykład w chwili obecnej najnowsze wydanie pakietu SDK ma numer 16, natomiast wersja API obsługiwana przez ten pakiet to API 14 dla systemu Android 4.0. Gdy dodamy do tego oddzielną numerację dodatku ADT, powstaje całkiem spore zamieszanie w nazewnictwie. Oczywiście na tym ekranie chodzi o wersję API pomimo mylącej nazwy pola Min SDK version — przyp. tłum.
28
SPRAWDZANIE DZIAŁANIA NARZĘDZI PROGRAMISTYCZNYCH
Rysunek 1.16. Wypełniony formularz tworzenia nowego projektu Androida — krok 2. kolejnym numerem. W ten sposób system Android 3.0 otrzymał numer 11, podczas gdy system Android 2.3.3 oznaczany jest numerem 10. Ponieważ w tej książce opisujemy programowanie dla systemu Android 3.0, w polu oznaczającym najmniejszy dopuszczalny numer pakietu SDK należy wprowadzić numer 1117. 3. Na rysunkach od 1.15 do 1.17 przedstawione zostały kolejne kroki tworzenia nowego projektu Androida. Czytelnik powinien upewnić się, czy jego okno wygląda identycznie, ponieważ w kodzie oraz w przykładach znajdujących się w dalszej części rozdziału będą używane właśnie te nazwy. Po zakończeniu wypełniania pól okna należy nacisnąć przycisk Finish (zakończ). W tym momencie otwarte zostanie puste okno środowiska Eclipse z umieszczonym po lewej stronie katalogiem utworzonego przed chwilą projektu. Przyjrzyjmy się teraz plikom oraz kodom tworzonym w programie Eclipse.
Projekt programu dla systemu Android w Eclipsie Aby dowiedzieć się, jakie pliki zostały utworzone w nowym projekcie, należy rozwinąć katalog o nazwie FirstApp. Następnie należy rozwinąć podkatalog src i kolejne com.gameproject.firstapp, aż do chwili, gdy na liście pojawi się plik Main.java. Po dwukrotnym kliknięciu ikony pliku jego zawartość zostanie
17
W chwili tłumaczenia tej książki najnowsza wersja systemu Android to 4.0, jej nazwa kodowa to Ice Cream Sandwich, natomiast jej API ma numer 14. W opracowaniu jest już wersja Android 5.0 o nazwie kodowej Jelly Bean i API 15. Jej pojawienie się zapowiadane jest na połowę roku 2012. W tym miejscu należy stwierdzić, że udział w rynku wersji Android 4.0 jest marginalny — przyp. tłum.
29
ROZDZIAŁ 1. KONFIGURACJA ŚRODOWISKA PROGRAMISTYCZNEGO JAVA DLA SYSTEMU ANDROID 3.0
Rysunek 1.17. Wypełniony formularz tworzenia nowego projektu Androida — krok 3. wyświetlona w oknie edytora Eclipse’a (duże okno na środku ekranu). Ten kod stanowić będzie podstawowy szkielet gry, chociaż w tej chwili jest to jedynie prosty szablon. Kod w pliku powinien przypominać ten umieszczony na listingu 1.1. Listing 1.1. Zawartość pliku Main.java package com.gameproject.firstapp; import android.app.Activity; import android.os.Bundle; public class Main extends Activity { /** Kod wywoływany w chwili pierwszego uruchomienia programu . */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } }
Wykonanie kodu przedstawionego na listingu 1.1 spowoduje utworzenie nowej klasy, w której następnie odbywać się będzie uaktualnianie widoku dostępnego dla użytkownika. Pierwsze trzy wiersze określają pakiet klasy, a następnie powodują dołączenie klas, które będą konieczne w aplikacji. Zauważmy, że obie instrukcje import odwołują się do klas stanowiących część pakietu Android SDK. W miarę tworzenia coraz bardziej skomplikowanych gier lista importu klas będzie rozszerzać się na wiele innych,
30
SPRAWDZANIE DZIAŁANIA NARZĘDZI PROGRAMISTYCZNYCH
umożliwiających programowi wykonywanie wielu różnych czynności. Przyjrzyjmy się teraz bliżej kodowi programu przedstawionemu na listingu i przeanalizujmy go wiersz po wierszu: package com.gameproject.firstapp
Powyższa prosta instrukcja określa, że ten plik jest częścią pakietu o nazwie firstapp. Pakiety w Javie są sposobem grupowania plików należących do tego samego programu. import android.app.Activity; import android.os.Bundle; Instrukcje import pozwalają na dodanie do projektu pewnych funkcji. Wskazują one na inne pakiety, których będziemy używać. Pakiet Activity zawiera metody obsługujące proces działania aplikacji. Pakiet Bundle natomiast pozwala na przechowywanie informacji dla aplikacji. public class Main extends Activity W tym wierszu klasie Main nadawane są wszystkie funkcje oraz zmienne z klasy Activity. Zawsze wtedy, gdy klasa używa słowa extends w celu rozszerzania innej klasy, dziedziczy ona lub uzyskuje
dostęp do funkcji innej klasy. public void onCreate(Bundle savedInstanceState)
Funkcja zdefiniowana w tym wierszu pochodzi w rzeczywistości z klasy Activity. Obsługuje ona wszystkie procedury, które muszą być wykonane w chwili uruchomienia aplikacji. Parametr typu Bundle o nazwie savedInstanceState przechowuje poprzedni stan aplikacji. W chwili pierwszego uruchomienia aplikacji będzie on zawierać wartość typu null. super.onCreate(savedInstanceState);
W tym miejscu wywoływana jest metoda onCreate z klasy Activity. Spowoduje to uruchomienie aplikacji. Zwróćmy uwagę, że przed nazwą funkcji pojawia się słowo kluczowe super. Oznacza to, że program wywołuje oryginalną metodę onCreate z pakietu Android SDK, a nie nową metodę onCreate utworzoną w poprzednim wierszu. SetContentView(R.layout.main);
Na koniec aplikacja wykonuje pierwsze poważne zadanie, inicjalizując ekran według definicji z pliku XML. W tym miejscu R jest identyfikatorem oznaczającym zasoby, czyli resource. Słowo layout określa, jakiego rodzaju zasób ma być brany pod uwagę, a słowo main odnosi się do nazwy pliku. W skrócie plik o nazwie main.xml jest modyfikowany w celu zmiany wyglądu programu. Nadszedł moment uruchomienia programu i sprawdzenia jego działania w praktyce. Zanim to jednak zrobimy, należy wcześniej utworzyć wirtualne urządzenie Androida, na którym nasz program będzie testowany. Jeżeli Czytelnik posiada tablet działający pod kontrolą systemu Android 3.0 (lub nowszej wersji), wtedy może on testować program bezpośrednio na urządzeniu. Sposób konfiguracji urządzenia przedstawiony jest w dodatku A.
Tworzenie wirtualnego urządzenia z Androidem Utworzenie własnego urządzenia wirtualnego w programie Eclipse jest bardzo proste: 1. Z głównego menu programu należy wybrać Window, a w nim pozycję AVD Manager. Spowoduje to otwarcie okna Android AVD Manager (menedżer urządzenia wirtualnego AVD). 2. Ponieważ w oknie nie są jeszcze dostępne żadne urządzenia, należy wcisnąć przycisk New umieszczony w prawym górnym rogu okna. Spowoduje to wyświetlenie okna dialogowego o nazwie Create New Android Virtual Device (AVD) (utwórz nowe urządzenie wirtualne Androida). Okno to pozwala na zdefiniowanie nowego symulatora w sposób przedstawiony na rysunku 1.18. Formularz dostępny w oknie powinien być wypełniony w następujący sposób: a) Nazwa urządzenia nie ma większego znaczenia. W tym przypadku wybrana została mało odkrywcza nazwa: Tablet_device. b) Dla przykładów z tej książki docelową platformą Android będzie Android 3.0.
31
ROZDZIAŁ 1. KONFIGURACJA ŚRODOWISKA PROGRAMISTYCZNEGO JAVA DLA SYSTEMU ANDROID 3.0
Rysunek 1.18. Tworzenie wirtualnego urządzenia z Androidem (AVD) c) W przypadku większości aplikacji nie trzeba się przejmować zadeklarowanym rozmiarem karty SD. Jeżeli jednak będzie tworzona gra wymagająca składowania na urządzeniu danych lub zapisu wyników, należy podać rozmiar karty. d) Opcje Skin (skórka), jak też Hardware (ustawienia sprzętowe) nie muszą być zmieniane. Niemniej warto zwrócić uwagę na specyfikację parametrów sprzętowych urządzenia. W przypadku tworzenia grafiki dla gry należy przyjąć rozdzielczość panelu LCD (ang. LCD density) o wartości 160 (co stanowi pewien standard). Pozwoli to określić rozdzielczość przyszłych grafik. Rozmiar pamięci RAM urządzenia (ang. Device RAM size) w symulatorze jest w rzeczywistości wartością całkiem małą w porównaniu z wieloma tabletami. Niemniej należy pamiętać, że symulator nie odzwierciedla dokładnie ani rozmiaru pamięci RAM, ani mocy procesora. Aby sprawdzić rzeczywiste zachowanie gry, należy przetestować ją na prawdziwym urządzeniu. 3. Po naciśnięciu przycisku Create AVD (utwórz urządzenie AVD) symulator będzie mógł zostać wykorzystany do uruchamiania aplikacji. W przypadku gdyby Czytelnik oczekiwał natychmiastowego uruchomienia symulatora, może być rozczarowany. Nowo utworzone urządzenie wirtualne uruchamia się dopiero po uruchomieniu aplikacji. W kolejnym podrozdziale spróbujemy uruchomić to urządzenie.
32
SPRAWDZANIE DZIAŁANIA NARZĘDZI PROGRAMISTYCZNYCH
Uruchamianie aplikacji Aby uruchomić aplikację, należy wykonać następujące czynności: 1. W centralnej części paska narzędziowego, praktycznie u góry okna programu Eclipse, znajduje się zielony przycisk do uruchamiania aplikacji (zielony trójkąt). Po jego naciśnięciu powinno otworzyć się duże czarne okno. Jest ono naszym nowym symulatorem. Po upływie pewnej chwili na ekranie zostanie wyświetlony napis Android oznaczający wczytywanie systemu. Kiedy napis stanie się większy i zostanie przesunięty do góry, system będzie całkowicie wczytany. 2. Po zniknięciu ekranu ładowania systemu na kolejnym ekranie należy przesunąć mały okrągły guzik w prawo. Jeżeli odczekamy odpowiednio długą chwilę, aplikacja zostanie uruchomiona automatycznie. W takim przypadku pojawi się napis Witaj, świecie, klasa Main! W przeciwnym razie należy przejść do kolejnego punktu. 3. Na ekranie startowym systemu po lewej stronie u góry umieszczony jest pasek wyszukiwania Google. Natomiast na dole ekranu znajdziemy kilka przycisków. W przypadku prawdziwego urządzenia do wyboru aplikacji wykorzystywane są gesty dotykowe, jednak w symulatorze do sterowania można wykorzystać kursor myszy. Aby uruchomić własny program, należy najzwyczajniej kliknąć ikonę aplikacji umieszczoną w górnej części ekranu po prawej. 4. Po wykonaniu powyższej czynności zostanie wyświetlona lista wszystkich programów zainstalowanych na urządzeniu. Nasza nowo utworzona aplikacja oznaczona jest ikoną z robotem Android, natomiast pod ikoną umieszczona jest nazwa aplikacji (FirstApp). Po jej kliknięciu na ekranie po chwili zostanie wyświetlony napis: Witaj, świecie, klasa Main! W najprostszy możliwy sposób uruchomiliśmy właśnie pierwszą aplikację w systemie Android. Po nacieszeniu oczu jej widokiem możemy kliknąć zwróconą w lewą stronę strzałkę w dolnym lewym rogu ekranu. Spowoduje to powrót do ekranu startowego. Nadeszła dobra chwila, aby wypróbować niektóre z innych aplikacji w symulatorze. Zadziwiające może być to, że przeglądarka, program pocztowy oraz inne programy działają zgodnie z oczekiwaniami. W rzeczywistości symulator AVD bardzo przypomina prawdziwe urządzenie. Umożliwia on nawet przetestowanie czujników oraz odczyt danych GPS. Wykorzystując to narzędzie do granic jego możliwości, można tworzyć naprawdę niesamowite aplikacje. W kolejnym podrozdziale Czytelnik dowie się, w jaki sposób będziemy pracować z naszym kodem aplikacji.
Pierwsze zmiany w aplikacji Chociaż teoretycznie stworzyliśmy już własną aplikację, to jednak nie modyfikowaliśmy niczego ponad kod utworzony automatycznie. Nadszedł czas, aby dokonać modyfikacji treści programu. 1. W drzewie projektowym rozwińmy katalog o nazwie Res. 2. Po otwarciu katalogu values odnajdziemy w nim pojedynczy plik o nazwie strings.xml. Po dwukrotnym kliknięciu tego katalogu jego zawartość zostanie wyświetlona w oknie podglądu. 3. Plik zawiera dwa łańcuchy znakowe. Jeden z nich jest nazwą aplikacji, drugi natomiast nosi nazwę hello. Kliknijmy go i zmieńmy tę wartość na dowolny inny tekst. 4. W dalszej kolejności zapiszmy zmiany do programu i uruchommy go ponownie. Po uruchomieniu programu FirstApp powinien zostać wyświetlony zmieniony przed chwilą tekst. Aby zrozumieć, w jaki sposób powyższa zmiana została wprowadzona, należy poznać ważny w przypadku programowania dla systemu Android temat zasobów (ang. resources). Takim zasobem jest modyfikowany przed chwilą plik strings.xml podobnie jak wszystkie pliki znajdujące się w dużym podkatalogu Res. Jeżeli przypomnimy sobie tworzenie pliku main.java, przy jego omawianiu został wspomniany jeden z plików zasobów o nazwie main.xml w sekcji definiującej wygląd programu. Teraz należy dokonać w nim kilku zmian.
33
ROZDZIAŁ 1. KONFIGURACJA ŚRODOWISKA PROGRAMISTYCZNEGO JAVA DLA SYSTEMU ANDROID 3.0
5. Aby wyświetlić zawartość pliku, należy rozwinąć podfolder layout, a następnie dwukrotnie kliknąć plik main.xml. Spowoduje to otwarcie edytora typu WYSIWYG18. Wprowadzony tekst będzie znajdował się w prawym górnym rogu niewielkiego okna. 6. Niestety ekran edytora dostosowany jest do rozmiaru telefonu komórkowego. Można to w dość prosty sposób zmienić, sięgając do pozycji menu z wyświetlonym rozmiarem u góry ekranu (2.7in QVGA). W tym celu należy przewinąć dostępną listę aż do pozycji 10.1 WXGA. Czynność ta spowoduje zmianę wielkości ekranu do rozmiaru nieco powyżej 10 cali, co jest wielkością standardową dla tabletów. 7. Zmiana układu ekranu w edytorze jest bardzo prosta. Na pasku po lewej stronie znajduje się już wiele różnych obiektów, które mogą być w prosty sposób przeciągnięte na okno aplikacji. Na przykład możemy spróbować umieścić przycisk nieco poniżej utworzonego tekstu. 8. Mimo że edytor graficzny jest bardzo wygodnym narzędziem, to nie jest on niestety zbyt przydatny do tworzenia gier. Podczas ich tworzenia konieczne będzie zmodyfikowanie rzeczywistego pliku definiującego układ na stronie. W tym celu należy kliknąć plik main.xml (u dołu ekranu niedaleko sekcji Graphical layout). Na listingu 1.2. znajduje się kod programu, który powinien być dostępny po dodaniu do układu strony dodatkowego przycisku. Listing 1.2. Plik main.xml
W przypadku gdy Czytelnik nie jest zaznajomiony z językiem XML, powyższy kod może być dla niego zupełnie nieczytelny. Niemniej w rzeczywistości jest on bardzo łatwy do zrozumienia. Pierwszy wiersz zawiera deklarację rodzaju używanego standardu XML. W kolejnej części pliku tworzony jest specjalny rodzaj liniowego układu strony o nazwie LinearLayout. Ta prosta instrukcja przekazuje do urządzenia informację, jaki powinien być układ aplikacji, a także jaki rozmiar powinno mieć okno w stosunku do całego ekranu urządzenia. W dalszej kolejności tworzony jest obiekt TextView wypełniający całą dostępną przestrzeń (fill_parent). Zawartość obiektu jest następnie zawijana (wrap_content), co ogranicza dostępny widok do tego, który jest dostępny na urządzeniu. Na koniec tekst jest umieszczany na ekranie poprzez wywołanie łańcucha znakowego o nazwie hello umieszczonego w pliku zasobów. W rzeczywistości jest to przed chwilą modyfikowany łańcuch znakowy.
18
Ang. What You See Is What You Get, czyli edytor graficzny pozwalający na modyfikację programu/grafiki w sposób reprezentujący docelowy wygląd na urządzeniu (np. drukarce, ekranie, tablecie) — przyp. tłum.
34
PODSUMOWANIE
W kolejnym fragmencie pliku zawarta jest informacja o przycisku klasy Button, który został przeciągnięty do okna aplikacji. Należy w tym miejscu przypomnieć, że pliki XML definiujące układ strony nie tworzą żadnych nowych funkcji programu, a jedynie wpływają na jego wygląd. Dla przykładu kliknięcie utworzonego w ten sposób przycisku nie spowoduje wykonania żadnej akcji do chwili jawnego dopisania programu reagującego na naciśnięcie przycisku.
Podsumowanie W niniejszym rozdziale poruszonych zostało wiele podstawowych zagadnień dotyczących konfiguracji środowiska oraz jego uruchamiania. Przedstawione zostały ogólne założenia systemu Android, a także możliwości jego wykorzystania do tworzenia gier. W kolejnych rozdziałach przyjrzymy się szczególnie definiowaniu układów ekranu oraz projektowaniu atrakcyjnego tła dla gry. Następnie utworzymy sprajty i rozpoczniemy dodawanie do gry logiki poprzez rozmieszczanie na ekranie dynamicznych obiektów. W dalszych rozdziałach do aplikacji dołączona zostanie obsługa interfejsu użytkownika, efektów dźwiękowych oraz algorytmów sztucznej inteligencji, co ostatecznie pozwoli na zakończenie tworzonej przez nas gry.
35
ROZDZIAŁ 1. KONFIGURACJA ŚRODOWISKA PROGRAMISTYCZNEGO JAVA DLA SYSTEMU ANDROID 3.0
36
ROZDZIAŁ 2
Tworzenie prostych gier z użyciem ruchomych sprajtów Gratulacje! Do tej pory udało się pomyślnie skonfigurować środowisko programistyczne i teraz jesteśmy gotowi, aby zająć się bardziej twórczymi czynnościami w procesie tworzenia gier. Jeżeli Czytelnik przypomni sobie swoją ulubioną grę, prawdopodobnie natychmiast wyobrazi sobie, jaki miała ona wygląd, niezależnie od tego, czy były to potwory zmierzające w stronę Czytelnika, czy też samochód jeżdżący wokół toru. W tym rozdziale postaramy się tchnąć w tablety trochę życia. Biorąc pod uwagę, że na rynku znajdują się tysiące gier, to właśnie szata graficzna gry może stać się czynnikiem determinującym to, czy jej sprzedaż zakończy się sukcesem. W niniejszym rozdziale zajmiemy się podstawami związanymi z wyświetlaniem obrazów na ekranie tabletu, a następnie wprawianiem ich w ruch. Czytelnik zapozna się również z pojęciem sprajtów1. Dla celów tego rozdziału sprajtem nazywać będziemy dowolny obiekt w grze, który będzie mógł być poruszany podczas jej działania. Zazwyczaj zarówno główna postać gry, jak też jej przeciwnicy są właśnie sprajtami, w przeciwieństwie do statycznego tła gry. Nowe pojęcia oraz zagadnienia w tym rozdziale będą wprowadzane w całkiem szybkim tempie.
Praca z obrazami Sprajty są podstawowym elementem gry. Zanim w ogóle zaczniemy tworzyć grę, będziemy zmuszeni narysować używane w niej karty, postacie lub dowolne inne obiekty pojawiające się na ekranie. Dlatego też w tym podrozdziale zajmiemy się podstawowymi elementami składającymi się na wyświetlanie grafiki w systemie Android 3.0. Przeanalizujemy również poszczególne składniki sprajtów, a także postaramy się przesunąć obrazy po ekranie. To właśnie będzie podstawą dla przyszłych projektów. Na rysunku 2.1 przedstawiony został zarys przyszłego wyglądu gry. Ten początkowy sprajt w rzeczywistości nie robi nic więcej, niż tylko odbija się tam i z powrotem od brzegów ekranu.
1
Sprajt (ang. sprite) w literaturze nazywany jest również duszkiem. Pierwsze użycie tego określenia datuje się na lata 80., kiedy to w komputerach 8-bitowych zastosowano po raz pierwszy specjalizowane układy do niezależnego przetwarzania części obszaru grafiki. Przyspieszało to znacznie obróbkę grafiki oraz pozwalało sprzętowo wykrywać kolizje pomiędzy różnymi obiektami pola gry. W zależności od typu komputera grafikę tworzoną w ten sposób nazywano grafiką gracza i pocisku (ang. player–missile), duszkami, chochlikami itp. Ponieważ nie ma jednoznacznej wykładni Rady Języka Polskiego dot. tego określenia, w tej książce używamy dopuszczalnego spolszczenia sprajt — przyp. tłum.
ROZDZIAŁ 2. TWORZENIE PROSTYCH GIER Z UŻYCIEM RUCHOMYCH SPRAJTÓW
Rysunek 2.1. Wygląd ukończonego programu GraphicsTest Uwaga! W przypadku gdy Czytelnik poczuje się zagubiony, może pobrać kod źródłowy dołączony do książki. Następnie może powrócić do lektury kolejnych rozdziałów, modyfikując jednocześnie fragmenty kodu, co z pewnością ułatwi zrozumienie na bieżąco poruszanych tematów.
Tworzenie podłoża do wyświetlania obrazów Przed rozpoczęciem dalszej lektury należy utworzyć nowy projekt w środowisku Eclipse. Taki projekt utworzyliśmy już w poprzednim rozdziale i nazwaliśmy go FirstApp. Ponieważ ten projekt nie jest już dla nas przydatny, rozpocznijmy ponownie od utworzenia całkiem nowego: 1. Z głównego menu programu Eclipse należy wybrać File/New/Project, a następnie Android Project. 2. Po otwarciu nowego okna dialogowego o nazwie New Android Project należy wypełnić wszystkie umieszczone w nim pola. Proces ten jest znany z poprzedniego przykładu, dlatego w tym miejscu nie będziemy się na nim koncentrować. 3. Aplikacja będzie nosić nazwę GraphicsTest. Po wypełnieniu pól wszystkich okien ich poprawność należy sprawdzić z tymi przedstawionymi na rysunkach od 2.2 do 2.4. W przypadku gdy z projektem dzieje się coś złego, bardzo często występuje konieczność rozpoczęcia pracy od samego początku. Dlatego też bardzo ważne jest nabycie pewnej rutyny w tworzeniu nowych projektów w programie Eclipse. 4. Po wypełnieniu pól formularza w oknie należy nacisnąć przycisk Finish (koniec). W przypadku gdyby konieczna była dalsza pomoc w poprawnym uzupełnieniu pól, należy powrócić do rozdziału 1. Zanim na ekranie tabletu będą mogły zostać wyświetlone jakiekolwiek obrazy, konieczne będzie utworzenie obszaru płótna (ang. canvas), na którym będą one umieszczane. Obszar ten zostanie utworzony w głównej procedurze programu. Aby to zrobić, należy wykonać następujące czynności:
38
PRACA Z OBRAZAMI
Rysunek 2.2. Okno tworzenia projektu dla programu GraphicsTest — krok 1. 1. W głównym oknie edycyjnym środowiska prawdopodobnie wciąż wyświetlana jest zawartość plików z pierwszego projektu. W tym momencie należy je zamknąć, klikając w tym celu prawym przyciskiem myszy przy zakładkach z nazwami plików, a następnie wybrać z wyświetlonego menu pozycję Close All (zamknij wszystkie). Czynność ta nie spowoduje usunięcia kodu, ale raczej zamknięcie okien edycyjnych wyświetlających ten kod. 2. W oknie przeglądarki pakietów (położonej po lewej stronie okna programu Eclipse) należy rozwinąć drzewa projektu o nazwie GraphicsTest. Ponieważ chcemy sprawdzić zawartość kodu źródłowego programu, otwórzmy katalog o nazwie src, a następnie kontynuujmy jego przeglądanie aż do pojawienia się pliku o nazwie MainActivity.java. Miejsce położenia plików przedstawione jest na rysunku 2.5. 3. W oknie edycyjnym programu należy otworzyć zawartość pliku MainActivity.java. Ujrzymy w nim ten sam ogólny kod przypominający kod utworzony automatycznie w rozdziale 1. 4. W rozdziale 1. do obsługi wyglądu programu wystarczył pojedynczy plik Javy oraz pojedynczy plik XML. Niestety do utworzenia gry, która prezentuje dużo poruszających się oraz zmiennych obiektów graficznych na ekranie, nie wystarczy opieranie się wyłącznie na plikach XML. Dlatego właśnie potrzebny będzie plik z kodem Javy kontrolujący wyświetlanie grafiki w grze. 5. W celu utworzenia wspomnianego powyżej pliku należy utworzyć nową klasę. Aby to zrobić, należy kliknąć prawym klawiszem myszy pakiet o nazwie com.gameproject.graphicstest wyświetlany w przeglądarce pakietów programu Eclipse. Z wyświetlonego menu trzeba wybrać pozycję New (nowy), a następnie Class (klasa). Pojawi się okno dialogowe z pytaniem o nazwę klasy. W tym miejscu wpiszmy GameView, a wszystkie pozostałe pola pozostawmy z ich domyślnymi wartościami. Po wykonaniu tych czynności w katalogu src będą znajdować się już dwa pliki (MainActivity.java oraz GameView.java).
39
ROZDZIAŁ 2. TWORZENIE PROSTYCH GIER Z UŻYCIEM RUCHOMYCH SPRAJTÓW
Rysunek 2.3. Okno tworzenia projektu dla programu GraphicsTest — krok 2. 6. W oknie edycyjnym programu Eclipse otwórzmy plik GameView.java. W pliku tym powinien znajdować się kod przedstawiony na listingu 2.1. Listing 2.1. Zawartość pliku GameView.java package com.gameproject.graphicstest; public class GameView { }
Do tego nieco prymitywnego szkieletu klasy dodamy niedługo dodatkowy kod wyświetlający na ekranie obrazy. Zanim jednak to zrobimy, musimy zapoznać się z podstawami tworzenia widoków oraz obrazów w systemie Android.
Sposób działania klasy View Dotychczas opieraliśmy się jedynie na wykorzystaniu w projektach dwóch klas systemu Android: Activity oraz Bundle. Obiekty klasy Activity zawierają funkcje używane do tworzenia, obsługi działania oraz zamykania każdej z aplikacji. To jest podstawa każdej gry w Androidzie. Natomiast obiekty klasy Bundle umożliwiają jedynie zachowywanie bieżącego stanu programu. W tej chwili jednak zapoznamy się z klasą View. W trakcie działania programu obiekty tej klasy obsługują grafikę oraz wygląd ekranu. Wszystkie tworzone gry będą zawierać klasę rozszerzającą klasę View, a więc będą udostępniać również jej funkcje. Bardzo często klasa pochodna od klasy View obejmować będzie zdecydowanie większą ilość kodu niż ta pochodząca od klasy Activity, ponieważ większość gier modyfikuje obiekty wyświetlane na ekranie.
40
PRACA Z OBRAZAMI
Rysunek 2.4. Okno tworzenia projektu dla programu GraphicsTest — krok 3.
Rysunek 2.5. Wygląd okna przeglądarki pakietów dla programu GraphicsTest Wszystkie funkcjonalne klasy View muszą składać się z dwóch oddzielnych części. Pierwszą z nich jest metoda konstruktora. Podobnie jak w przypadku dowolnej innej klasy w chwili tworzenia jej obiektu konieczne jest wywołanie funkcji definiującej jego podstawowe parametry. W klasie View można wczytać obrazy oraz określić położenie początkowe wszystkich sprajtów w grze. Kolejną ważną częścią klasy View jest metoda wyświetlająca obrazy na ekranie. Jest ona wywoływana za każdym razem, gdy obraz zmienia położenie, ponieważ musi on zostać od nowa narysowany w nowym miejscu. Chociaż to nieco abstrakcyjny opis klasy, to jednak ułatwi on zrozumienie kodu. Zanim jednak zagłębimy się w dalszą lekturę, przyjrzymy się, w jaki sposób w rzeczywistości można pobrać plik grafiki i wyświetlić go na ekranie.
41
ROZDZIAŁ 2. TWORZENIE PROSTYCH GIER Z UŻYCIEM RUCHOMYCH SPRAJTÓW
Wskazówka Jeżeli Czytelnik jest zainteresowany szczegółami klasy View lub dowolnej innej klasy Androida, powinien odwiedzić stronę znajdującą się pod adresem http://developer.android.com/reference/packages.html, a następnie odnaleźć interesujący go pakiet. Na wspomnianej stronie firma Google zamieszcza dokumentację na temat sposobu wykorzystania każdej z klas systemu Android, a także opis metod zawartych w każdej z nich.
Sposób wyświetlania grafiki w Androidzie Klasa View stanowi jedynie element całego procesu wyświetlania grafiki na ekranie. Kolejnymi są: właściwy plik z grafiką, sposób jej przechowywania, metoda jej wyświetlania, a na samym końcu sposób, w jaki zostanie ona przedstawiona na ekranie. Grafiki są przechowywane w projekcie. W kolejnym podrozdziale Czytelnik dowie się, w jaki sposób można dodać plik obrazu do projektu. Gdy tylko obraz zostanie zachowany w aplikacji, będzie można go przypisać do obiektu grafiki rastrowej (ang. bitmap). Obiekt ten jest sposobem opisania obrazu oraz przygotowuje ten obraz do wyświetlania na ekranie. Zanim jednak grafika zostanie wyświetlona na ekranie, musi zostać najpierw narysowana na tzw. obszarze płótna (ang. canvas). Obiekt Canvas zawiera metody rysujące grafikę na ekranie. To właśnie metody obiektu tej klasy wywoływane są w obiekcie klasy View w celu zarządzania procesem rysowania obrazu. Obiekt klasy View odpowiada za określony obszar ekranu, który może nadzorować. W naszym przypadku obiekt ten zarządzać będzie bez wyjątku całym obszarem ekranu. W dalszej kolejności to właśnie obiekt klasy Canvas spowoduje fizyczne narysowanie obrazu na ekranie.
Rysowanie obrazu Aby zrozumieć, w jaki sposób w systemie Android działa klasa View, wykorzystajmy ją do wyświetlenia obrazu. 1. W pierwszej kolejności będziemy potrzebować pliku graficznego, który będzie mógł zostać załadowany do systemu. Czytelnik może mieć już taki plik gotowy albo też dopiero będzie musiał go od podstaw stworzyć. Do naszego przykładu będzie nadawać się dowolny plik w komputerze z rozszerzeniem .png lub .bmp. a) Gotowy obraz nie powinien mieć rozmiaru przekraczającego 500×500 pikseli. b) Jeżeli zachodzi konieczność utworzenia grafiki od podstaw, autor książki używa w tym celu zazwyczaj programu Inkspace (http://inkscape.org/) lub GIMP (www.gimp.org/), ponieważ oba te programy są dostępne za darmo. Jeżeli jednak Czytelnik ma swój ulubiony program graficzny, może go również wykorzystać w tym celu. 2. Plik graficzny należy przeciągnąć do podkatalogu o nazwie drawable-mdpi znajdującego się w katalogu res projektu GraphicsTest. Program Eclipse spyta, czy plik ma być przekopiowany. Na to pytanie należy odpowiedzieć twierdząco. 3. Gdy przyjrzymy się dokładnie zawartości katalogu res w projekcie, zauważymy, że znajdują się w nim trzy podkatalogi o nazwie rozpoczynającej się od słowa drawable. Katalogi te są związane z odpowiednią rozdzielczością ekranu używanego urządzenia. W przypadku gier tworzonych dla tabletu będziemy używać podkatalogu przeznaczonego dla grafik średniego rozmiaru. Jeżeli jednak tworzylibyśmy gry dla telefonów, wtedy z pewnością chcielibyśmy mieć do dyspozycji różne wersje każdej z grafik dla każdej z trzech dostępnych rozdzielczości. W ten sposób mielibyśmy pewność, że dowolny telefon będzie w stanie wyświetlić obraz tak szybko, jak będzie to tylko możliwe. 4. W pliku GameView.java wyświetlanym w oknie edycyjnym należy zastąpić kod z listingu 2.1 kodem przedstawionym na listingu 2.2. Kod ten wyświetla plik obrazu na ekranie tabletu. Za chwilę wyjaśnimy sobie, co robi każdy z fragmentów kodu.
42
PRACA Z OBRAZAMI
Listing 2.2. Zawartość pliku GameView.java package com.gameproject.graphicstest; import import import import import import
android.content.Context; android.graphics.Bitmap; android.graphics.BitmapFactory; android.graphics.Canvas; android.graphics.Color; android.view.View;
class GameView extends View { public GameView(Context context) { super(context); } @Override public void onDraw(Canvas canvas) { Bitmap star = BitmapFactory.decodeResource(getResources(), R.drawable.star); canvas.drawColor(Color.BLACK); canvas.drawBitmap(star, 10, 10, null); } }
5. No cóż, sprawy komplikują się bardzo szybko. Kod przedstawiony na listingu 2.2 jest w rzeczywistości bardzo prosty i Czytelnik powinien zrozumieć jego większą część bez konieczności zbyt obszernego tłumaczenia. 6. Pierwszą zmianą w programie jest dodanie wielu nowych instrukcji import. Większość z nich dołącza pakiety graficzne z systemu Android, podczas gdy ostatnia dodaje klasę View. Pierwsza instrukcja import dołącza klasę Context, której obiekty będziemy wykorzystywać jako parametry funkcji. 7. Początek właściwego kodu programu ukazuje, w jaki sposób tworzona klasa rozszerza funkcje klasy View. Dziedziczenie metod oraz zmiennych klasy View w celu ich późniejszego wykorzystania we własnym celu jest dość powszechną praktyką w języku Java. Bez tego nie bylibyśmy w stanie rysować obrazów na ekranie. 8. Pierwsza metoda klasy GameView nie inicjuje w rzeczywistości żadnego działania. Będziemy jej używać później. W tej chwili natomiast zachowajmy ją jedynie w celu zaspokojenia wymagań Javy w stosunku do tej klasy. 9. Ostatecznie najważniejszą metodą klasy jest metoda o nazwie onDraw, która obsługuje zmiany zawartości ekranu. Słowo @Override wskazuje, że będziemy uruchamiać własną metodę onDraw, a nie tę dostarczaną wraz klasą View. Parametrem tej metody jest bardzo ważny obiekt klasy Canvas, który będzie odpowiedzialny za rysowanie obrazu na ekranie. W kolejnym wierszu programu tworzony jest nowy obiekt klasy Bitmap przechowujący grafikę rastrową. Do obiektu tej klasy pobierany jest następnie plik obrazu znajdujący się w systemie. Ponieważ plik grafiki nosi nazwę star.png, obiekt klasy Bitmap będzie nosić nazwę star. Nazwę tę trzeba zmienić w trzech miejscach programu na nazwę odpowiadającą tej używanej przez plik graficzny Czytelnika. Inną możliwością jest nazwanie pliku star.png bez konieczności wykonywania żadnych modyfikacji kodu. 10. W kolejnym wierszu programu nakazujemy obiektowi Canvas pokolorowanie całego obszaru ekranu na czarno. Jest to w istocie niepotrzebne, ponieważ kolor czarny jest wartością domyślną. Zachowanie tego wiersza należy jednak do dobrych praktyk. Jeżeli Czytelnik preferuje inny kolor tła, wtedy może zastąpić stałą Color.BLACK inną, związaną z innym kolorem. Należy zauważyć, że nazwy tych stałych w Androidzie w większości przypadków pokrywają się z tradycyjnymi nazwami kolorów w języku angielskim, jeżeli jednak Czytelnik chciałby ujrzeć pewien specyficzny odcień koloru różowego, powinien wtedy użyć bezpośredniego wskazania wartości RGB tego koloru w sposób przedstawiony poniżej: 43
ROZDZIAŁ 2. TWORZENIE PROSTYCH GIER Z UŻYCIEM RUCHOMYCH SPRAJTÓW
canvas.drawColor(Color.argb(0, 100, 100, 100));
11. Metoda argb klasy Color jako parametry wejściowe pobiera liczby całkowite określające w kolejności poziom kanału przezroczystości (alfa — ang. alpha), czerwonego, zielonego, a na koniec niebieskiego. 12. W ostatnim wierszu programu przedstawionego na listingu 2.2 wywoływana jest metoda o nazwie drawBitmap, która powoduje wyświetlenie obrazu graficznego na ekranie. Parametrami tej metody są w kolejności obiekty następujących klas: (Bitmap bitmap, float left, float top, Paint paint). Ponieważ nie używamy obiektu klasy Paint, w miejscu tego parametru przekazujemy wartość typu null. Położenie obrazu możemy kontrolować poprzez zmianę wartości określających jego odległość od lewego górnego rogu ekranu. Po utworzeniu tego programu z pewnością chcielibyśmy zobaczyć efekty naszej pracy. Niestety mimo że mamy już sposób na wyświetlanie obrazu na ekranie, aplikacja nigdy go nie użyje, ponieważ na początku programu nie wywołujemy metody rysującej obraz. Możemy to zmienić poprzez utworzenie obiektu klasy GameView w klasie MainActivity. W tym celu musimy zmienić pojedynczy wiersz w pliku MainActivity.java, aby wskazywał na klasę GameView. 13. Aby utworzyć obiekt klasy GameView, wyświetlmy zawartość pliku MainActivity.java, a następnie odnajdźmy wiersz programu przypominający ten poniżej: setContentView(R.layout.main);
14. Czytelnik przypomina sobie prawdopodobnie, że ten wiersz programu nakazuje urządzeniu załadowanie pliku main.xml oraz użycie go w charakterze definicji wyglądu aplikacji. W tej chwili chcemy zastąpić plik XML plikiem GameView.java. W tym celu należy dodać wiersz przedstawiony na listingu 2.3 wewnątrz konstruktora klasy MainActivity: Listing 2.3. Wykorzystanie GameView.java w charakterze widoku aplikacji setContentView(new GameView(this));
15. Dodanie wiersza przedstawionego powyżej spowoduje utworzenie nowego obiektu klasy GameView, a następnie wczytanie go w charakterze widoku aplikacji. W tym momencie możemy już wypróbować nasz program. 16. Aby uruchomić program, należy wcisnąć zielony przycisk z symbolem trójkąta znajdujący się u góry ekranu programu Eclipse. W chwili wczytania programu do symulatora należy zastosować procedurę opisaną w rozdziale 1. Jeżeli wszystko pójdzie zgodnie z oczekiwaniami, nasz plik graficzny, który z początku był jedynie plikiem .png, zostanie wyświetlony na ekranie. Osiągnięty wynik nie jest z pewnością bardzo pasjonujący, dlatego też kolejnym celem będzie przesunięcie grafiki na ekranie.
Używanie sprajtów Zanim będziemy w stanie przesunąć grafikę na ekranie, musimy ją jakoś nazwać. Gry nie przesuwają grafik lub kształtów. W zamian używają tzw. sprajtów, czyli obiektów, których obecność na ekranie jest reprezentowana przez wyświetlaną grafikę, zawierających dodatkowo metody oraz właściwości służące do nadzorowania oraz modyfikowania stanu tych obiektów. Istnieje szereg korzyści wynikających z utworzenia dedykowanej klasy Sprite. W ten sposób można dodać sekwencje animacji lub obrót obiektu, a nawet dodatkowo śledzić liczbę żyć oraz ilość amunicji każdej z postaci w grze reprezentowanej przez sprajty. Zanim utworzymy obiekt klasy Sprite, poświęćmy chwilę na ulepszenie sposobu wyświetlania tego typu obiektów, jak również stworzenie bardziej zaawansowanej pętli gry, która mogłaby zająć się przesuwaniem oraz uaktualnianiem stanu sprajta.
44
PRACA Z OBRAZAMI
Wyświetlanie sprajtów Na początku należy dokonać kilku istotnych zmian uprzednio utworzonej klasy View. Po pierwsze, zamiast klasy View użyjemy klasy SurfaceView. Jest to subtelna różnica, ponieważ klasa SurfaceView posiada pewne cechy korzystne dla szybkości wyświetlania grafiki. Jej zalety oraz wady omówimy w chwili zapoznania się z animacjami w dalszej części tego rozdziału. Nowa wersja pliku GameView.java została przedstawiona na listingu 2.4. Czytelnik powinien w tej chwili odpowiednio uaktualnić swoją wersję kodu. Nowy kod będzie stanowić podstawę do tworzenia bardziej zaawansowanych grafik oraz sprajtów. Listing 2.4. Zawartość pliku GameView.java package com.gameproject.graphicstest; import import import import import import
android.content.Context; android.graphics.BitmapFactory; android.graphics.Canvas; android.graphics.Color; android.view.SurfaceHolder; android.view.SurfaceView;
public class GameView extends SurfaceView implements SurfaceHolder.Callback { public GameView(Context context) { super(context); setFocusable(true); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceCreated(SurfaceHolder holder) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { } public void onDraw(Canvas canvas) { canvas.drawColor(Color.BLACK); } public void update() { } }
W tym momencie kod umieszczony w pliku GameView.java nie wykonuje żadnych znaczących czynności poza pomalowaniem obszaru płótna na kolor czarny. Funkcje rysujące zostały usunięte z wnętrza klasy, abyśmy mogli je za chwilę zaimplementować w klasach Sprite oraz Thread. Pierwszą ważną cechą nowej klasy GameView jest to, że teraz implementuje ona interfejs SurfaceHolder.Callback. Jest to konieczne w celu nadzorowania działania obiektu klasy SurfaceHolder, a także umożliwienia 45
ROZDZIAŁ 2. TWORZENIE PROSTYCH GIER Z UŻYCIEM RUCHOMYCH SPRAJTÓW
rysowania na tym obszarze od chwili jego utworzenia aż do jego usunięcia. Interfejs ten udostępnia trzy metody, które będziemy nadpisywać: surfaceChanged, surfaceCreated oraz surfaceDestroyed. Wkrótce niektóre z nich wypełnimy kodem nadzorującym sprajty oraz pętlę gry. Metoda konstruktora klasy GameView będzie również wykorzystywana w chwili, kiedy konieczne będzie utworzenie obiektów klasy Sprite. W końcowej części nowego kodu znajdują się również metody onDraw oraz update. Pierwsza z nich powinna wyglądać znajomo, ponieważ była wykorzystywana do wyświetlania obrazu na ekranie w przykładzie zamieszczonym wcześniej w tym rozdziale. Natomiast metoda update jest nowa. Służy ona do wymuszania na każdym ze sprajtów uaktualnienia swojego stanu. Teraz kiedy umiemy już posługiwać się obrazami, możemy sprawdzić, jak działają gry.
Tworzenie pętli gry Aby stworzyć dobrze działającą grę, należy skorzystać z potęgi klasy Thread. Jeżeli tylko Czytelnik tworzył programy w nowoczesnych językach programowania, prawdopodobnie spotkał się już wcześniej z użyciem wątków. Wątek jest niezależną procedurą wykonywaną przez urządzenie. Wątki są prawie zawsze używane z innymi wątkami, co określa się mianem wielowątkowości. Najprościej mówiąc, oznacza to, że wątki istnieją niezależnie i bardzo często działają równolegle, aby obsługiwać różne funkcje programu. Na przykład możemy wyobrazić sobie grę, w której grafika jest obsługiwana przez jeden wątek, natomiast fizyka przez zupełnie inny. Oczywiście te dwa procesy muszą zachodzić w tym samym czasie, dlatego też program musi być wielowątkowy. Do tworzenia gier w Androidzie można użyć klasy Javy o nazwie Thread. Jej kod źródłowy umieszczony jest w Java.lang.Thread. Klasy tej nie trzeba importować, ponieważ zakłada się, że jest ona dostępna stale. Niemniej istotne jest, aby pamiętać, że to właśnie tej klasy będziemy używać. W naszym przypadku wątki będą bardzo proste. Utworzymy klasę rozszerzającą klasę Thread, a następnie nadpiszemy jej metodę run, w której umieszczona będzie główna pętla gry. Właśnie z tego miejsca będzie można zmieniać widok gry, obsługiwać kolizje pomiędzy obiektami, a także pobierać informacje od użytkownika. Teraz kiedy już wiemy, jakie zmiany zostały wprowadzone w klasie GameView, utwórzmy wszystkie niezbędne rozszerzenia klasy Thread. 1. Zacznijmy od utworzenia w programie Eclipse nowej klasy o nazwie GameLogic. Ponieważ GameView.java obsługuje kwestie związane z wyglądem gry, właściwe będzie, aby klasa GameLogic obsługiwała jedynie wszystkie obliczenia zachodzące w tle. Wskazówka W miarę tworzenia coraz większej liczby plików z kodem źródłowym bardzo przydatne jest nadawanie klasom bardzo znaczących nazw. W przypadku gdy Czytelnik będzie tworzyć grę używającą różnych rodzajów sprajtów lub obiektów, nie powinien nazywać klas SprajtJeden, SprajtDwa i tak dalej. Zawsze starajmy się nazywać klasę, opierając się na jej rzeczywistej roli, na przykład SprajtWroga, SprajtLatajacy itp.
2. Na listingu 2.5 przedstawiona jest cała zawartość pliku GameLogic.java. Podobnie jak w przypadku implementacji kodu klasy SurfaceView początkowo plik ten jest bardzo ubogi. Listing 2.5. Zawartość pliku GameLogic.java package com.gameproject.graphicstest; import android.graphics.Canvas; import android.view.SurfaceHolder; public class GameLogic extends Thread { private SurfaceHolder surfaceHolder; private GameView mGameView; private int game_state; public static final int PAUSE = 0;
46
PRACA Z OBRAZAMI
public static final int READY = 1; public static final int RUNNING = 2; public GameLogic(SurfaceHolder surfaceHolder, GameView mGameView) { super(); this.surfaceHolder = surfaceHolder; this.mGameView = mGameView; } public void setGameState(int gamestate) { this.game_state = gamestate; } public int getGameState(){ return game_state; } @Override public void run() { Canvas canvas; while (game_state == RUNNING) { canvas = null; try { canvas = this.surfaceHolder.lockCanvas(); synchronized (surfaceHolder) { this.mGameView.update(); this.mGameView.onDraw(canvas); } } finally { if (canvas != null) { surfaceHolder.unlockCanvasAndPost(canvas); } } } } }
Oto lista najważniejszych metod klasy GameLogic z opisem ich działania: • SurfaceHolder() — metoda ta umożliwia modyfikację obszaru płótna. Wewnątrz metody run() blokuje oraz zwalnia blokadę na używanym obiekcie klasy Canvas. Zablokowanie tego obiektu oznacza, że tylko jeden wątek może z niego korzystać. Aby umożliwić korzystanie z tego obiektu innemu wątkowi, należy zwolnić blokadę. • GameView() — tworzy instancję obiektu klasy GameView, a następnie używa go do wywołania metod update oraz onDraw, które omówione były w poprzednim podrozdziale. • setGameState() — tworzy system zachowywania stanu gry w jej dowolnym momencie. Później będzie można jej użyć do obsługi ekranu przerwy w grze lub wyświetlania komunikatu o zwycięstwie albo przegranej gracza. Stan gry określa również, jak długo trwa pętla gry. • run() — w chwili gdy gra jest w stanie działania, metoda ta próbuje założyć blokadę na obszarze płótna, wykonać wszystkie konieczne operacje, zwolnić blokadę oraz przygotować proces do ponownego uruchomienia. Mimo że klasa GameLogic może wydawać się wystarczająco prosta, to jednak nie obsługuje ona wielu problemów, jakie mogą wystąpić w przypadku gry. Po pierwsze, nie ma tu żadnego systemu kontrolowania
47
ROZDZIAŁ 2. TWORZENIE PROSTYCH GIER Z UŻYCIEM RUCHOMYCH SPRAJTÓW
czasu wykonania. Pętla będzie działać tak szybko, jak tylko pozwoli procesor. Dlatego na szybkich tabletach będzie działać błyskawicznie, na wolniejszych zaś o wiele wolniej. W dalszej części tego rozdziału w raczej prosty sposób spróbujemy uregulować ruch sprajta, zakładając docelową szybkość odświeżania ekranu na poziomie ok. 30 klatek na sekundę (ang. fps — frames per second). Klasa GameLogic nie obsługuje też żadnych innych zadań, jak chociażby pobierania informacji od użytkownika czy też wykrywania kolizji między obiektami. Te procedury zostaną zaimplementowane później. W tej chwili GameLogic jest jedynie prostym narzędziem umożliwiającym cykliczne powtarzanie podstawowych czynności bez zbytniego komplikowania klasy GameView.
Tworzenie sprajta Następną czynnością podczas pisania gry jest utworzenie klasy sprajta. Mimo że gra będzie wymagać tylko jednego obiektu klasy GameLogic lub GameView, to jednak w grze będzie w użyciu wiele sprajtów. Z tego powodu kod musi być uniwersalny, a jednocześnie powinien umożliwiać wykonanie na sprajtach wszystkich wymaganych czynności. Ponieważ w żadnym z pakietów Androida nie istnieje żaden realny wzorzec dla klasy sprajta, konieczne będzie utworzenie tej klasy od podstaw. Główną treścią klasy będą podstawowe zmienne, na przykład definiujące współrzędne x oraz y lub pozwalające przechowywać sam obraz sprajta. Chcielibyśmy również zachować szybkość poruszania się sprajta w każdym z kierunków. Ostatecznie mogłyby tu być przechowywane również informacje o stanie życia sprajta lub też o innych jego cechach. Aby stworzyć pierwotną klasę Sprite, wszystkie te zmienne muszą być zadeklarowane jako private (prywatne), a do zmiany oraz odczytu ich wartości będą wykorzystywane metody. Jest to dość powszechna praktyka, która zapobiega przypadkowej modyfikacji wartości w chwili, gdy chcemy je tylko odczytać, oraz na odwrót. Kod klasy SpriteObject został przedstawiony na listingu 2.6. Aby dodać go do projektu, należy zastosować standardową procedurę tworzenia w Eclipsie nowej klasy, a następnie wypełnić tę klasę przedstawionym kodem. Ponieważ kod zamieszczony na listingu wykonuje bardzo proste czynności, Czytelnik nie powinien mieć większych trudności ze zrozumieniem jego działania. Listing 2.6. Zawartość pliku SpriteObject.java package com.gameproject.graphicstest; import android.graphics.Bitmap; import android.graphics.Canvas; public class SpriteObject { private private private private private
Bitmap bitmap; int x; int y; int x_move = 5; int y_move = 5;
public SpriteObject(Bitmap bitmap, int x, int y) { this.bitmap = bitmap; this.x = x; this.y = y; }
public int getX() { return x; }
48
PRACA Z OBRAZAMI
public int getY() { return y; } public Bitmap getBitmap() { return bitmap; } public void setX(int x) { this.x = x; } public void setY(int y) { this.y = y; } public void setBitmap(Bitmap bitmap) { this.bitmap = bitmap; }
public void draw(Canvas canvas) { canvas.drawBitmap(bitmap, x - (bitmap.getWidth() / 2), ´y - (bitmap.getHeight() / 2), null); } public void update() { x += (x_move); y += (y_move); } }
Ostatnie dwie metody klasy — draw() oraz update() — stanowią największą zagadkę. Metoda draw() jest wywoływana z wewnątrz pętli gry w pliku GameLogic.java. Operacja update jest natomiast używana w celu zwiększenia wartości współrzędnych x oraz y przed wyświetleniem obrazu na ekranie. Warto zauważyć, że szybkość przemieszczania się obiektu można zmienić ręcznie, modyfikując zawartość zmiennych, lub też można w tym celu utworzyć metody, które pozwolą na zmianę szybkości sprajta na podstawie zdarzeń w rodzaju kolizji lub też działania użytkownika.
Uruchomienie gry Po niewielkiej modyfikacji klasy GameView możemy uzyskać kompletną aplikację, która będzie przesuwać sprajta z góry na dół ekranu. W tym celu w klasie GameView musimy najpierw utworzyć instancję zarówno klasy GameLogic, jak też SpriteObject. 1. Najpierw należy otworzyć kod źródłowy klasy GameView, tak aby możliwa była jego modyfikacja. 2. Przed konstruktorem klasy GameView należy dodać dwie instancje nowych klas (patrz listing 2.7). Listing 2.7. Utworzenie instancji nowych klas private SpriteObject sprite; private GameLogic mGameLogic;
49
ROZDZIAŁ 2. TWORZENIE PROSTYCH GIER Z UŻYCIEM RUCHOMYCH SPRAJTÓW
3. Wewnątrz klasy GameView należy wywołać konstruktor z obu klas. Niemniej należy zachować ostrożność i przyjrzeć się, w jaki sposób są zdefiniowane parametry obu konstruktorów. Kilka ostatnich wierszy pozwoli na dodanie animacji wyświetlanej na ekranie urządzenia. W tym celu wewnątrz konstruktora klasy GameView należy dodać kod przedstawiony na listingu 2.8. Listing 2.8. Tworzenie obiektów nowych klas sprite = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.star), ´50,50); mGameLogic = new GameLogic(getHolder(), this); getHolder().addCallback(this);
4. Obiekt klasy SpriteObject przyjmuje obiekt grafiki oraz współrzędne sprajta. Sposób przekazania grafiki z pliku zasobu do klasy jest identyczny z tym użytym w pierwszym przykładzie umieszczonym w tym rozdziale. Obiekt klasy GameLogic przyjmuje obiekt klasy SurfaceHolder oraz obiekt klasy GameView. Metoda getHolder stanowi część klasy SurfaceView i pozwala pobrać bieżący obiekt klasy SurfaceHolder. 5. W tym momencie warto skorzystać z nowych obiektów w metodzie o nazwie surfaceCreated. Na listingu 2.9 przedstawiony jest kod wykorzystywany do rozpoczęcia działania pętli gry natychmiast po wywołaniu przez aplikację metody surfaceCreated. Listing 2.9. Rozpoczęcie pętli gry @Override public void surfaceCreated(SurfaceHolder holder) { mGameLogic.setGameState(GameLogic.RUNNING); mGameLogic.start(); }
6. Kiedy już stworzyliśmy serce gry, konieczne będzie jeszcze dodanie metod do procedur onDraw oraz update, tak jak to zostało przedstawione na listingu 2.10. Zwróćmy uwagę, że klasa GameView nie odwołuje się bezpośrednio do tych funkcji. Są one za to wywoływane z klasy GameLogic. Listing 2.10. Wykorzystanie obiektów w grze public void onDraw(Canvas canvas) { canvas.drawColor(Color.BLACK); sprite.draw(canvas); } public void update() { sprite.update(); }
7. Metoda onDraw wymusza na sprajcie jego przerysowanie, natomiast funkcja update wywołuje własną funkcję sprajta o nazwie update. Wydzielenie metod update z klasy GameView zmniejsza poszatkowanie wewnątrz klasy. Zawsze wtedy, gdy konieczne jest wykonanie określonego zadania, należy wykonać je w oddzielnej funkcji, co spowoduje utrzymanie porządku w reszcie kodu. 8. Kiedy cały kod jest już gotowy, można przystąpić do uruchomienia gry. Po upewnieniu się, że wszystkie kody źródłowe programu zostały zapisane, można uruchomić symulator poprzez wciśnięcie w Eclipsie zielonego przycisku odtwarzania. Uwaga! Jeżeli otrzymamy komunikaty o błędach spowodowanych niemożliwością odnalezienia klasy, może to oznaczać, że pliki Javy zostały utworzone w innym katalogu. Aby to wykluczyć, należy w drzewku plików umieszczonym po lewej stronie ekranu sprawdzić, czy wszystkie cztery pliki znajdują się w katalogu src.
50
NADAWANIE GRZE PROFESJONALNEGO WYGLĄDU
Jeżeli kompilacja przebiegnie pomyślnie, powinniśmy ujrzeć obrazek przemieszczający się szybko z lewego górnego do prawego dolnego rogu ekranu. Jak już zauważyliśmy wcześniej, w zależności od wydajności komputera lub urządzenia, na którym uruchamiany jest program, sprajt może przemieszczać się szybko lub wolno. Oczywiście sprajt może być zwolniony lub przyspieszony poprzez zmianę wartości x_move oraz y_move. W kolejnym podrozdziale zajmiemy się porządkowaniem interfejsu użytkownika oraz przygotowaniem się do tworzenia naprawdę dużych gier.
Nadawanie grze profesjonalnego wyglądu Gry powinny angażować zmysły użytkownika tak bardzo, jak to tylko możliwe. W tym celu musimy na tablecie lub jakimkolwiek innym urządzeniu usunąć z okna wszystkie ramki oraz menu, które mogłyby graczowi przypominać o świecie poza grą. System Android ma narzędzia, które pomagają zrobić to w sposób efektywny, niemniej jednak wersja 3.0 jest wyposażona w funkcje właściwie sankcjonujące stałą obecność paska systemu. Niezależnie od tego pasek zadań można ukryć przy użyciu jednego prostego wiersza w pliku MainActivity.java. Ta instrukcja, która powinna znaleźć się zaraz za instrukcją super, została przedstawiona na listingu 2.11. Listing 2.11. Ukrywanie paska zadań getActionBar().hide();
Jeżeli teraz uruchomimy projekt, górny pasek z ikoną robota Androida zniknie. Rysunek powinien natomiast tak jak poprzednio przesuwać się w poprzek ekranu. Aby nadać grze jeszcze bardziej profesjonalny wygląd, można zmienić jej ikonę na bardziej odpowiednią dla danej gry. W chwili gdy gracz chce uruchomić grę, najczęściej udaje się w tym celu na stronę domową programu i tam wybiera aplikację. Z tego powodu dołączenie do gry kolorowej ikony przykuwającej uwagę jest rzeczą bardzo istotną. Zanim utworzymy ikonę, wspomnijmy o jej rozmiarach. Będziemy potrzebować wersji ikony o rozmiarach 72×72, 48×48 oraz 32×32 pikseli. W tym celu w wybranym przez siebie edytorze graficznym należy stworzyć ikonę o największych wymiarach, a następnie zmienić jej rozmiar do dwóch pozostałych wersji. Po utworzeniu plików trzeba nadać im nazwę icon.png, a następnie umieścić odpowiednio w każdym z podkatalogów dla różnych kategorii w katalogu res. W tej chwili wystarczy już tylko umieścić w pliku źródłowym nagłówek przypominający ten przedstawiony na listingu 2.12. Dzięki temu będziemy mogli rozpowszechniać grę bez obaw, że niektórzy programiści użyją kodu bez wzmianki o autorze. Oczywiście wszystko, co jest umieszczone w sieci, może być wykorzystane w sposób nieodpowiedni. Niemniej podpisanie kodu pozwala osobom zainteresowanym zadawać pytania albo przynajmniej wspomnieć o autorze kodu wszędzie tam, gdzie jest to oczekiwane. Listing 2.12. Przykładowy komentarz z nagłówkiem rozpoczynający kod /************************************************************************* * GraphicsTest – przykład ukazujący podstawy wykorzystania sprajtów * * * * Autor: Kerfs, Jeremy * * * * Ostatnio modyfikowany: 1 Stycznia 2000 * * * * Wersja: 1.0 * * * **********************************************************************/
W przypadku gdyby Czytelnik był poważnie zainteresowany ochroną swojej pracy, może objąć kod postanowieniami licencyjnymi. Na przykład sam kod systemu Android jest rozpowszechniany zgodnie z zasadami licencji Apache License Version 2.0, która jest dość liberalna, jeżeli wziąć pod uwagę zezwalanie użytkownikom na szerokie zastosowanie kodu w dowolnym projekcie. Jeżeli Czytelnik zamierza umieszczać
51
ROZDZIAŁ 2. TWORZENIE PROSTYCH GIER Z UŻYCIEM RUCHOMYCH SPRAJTÓW
swój kod w sieci, powinien zastanowić się nad opublikowaniem go na zasadach licencji open source, która pozwoli innym rozwijać ten kod dalej. Wskazówka Więcej informacji na temat licencji Creative Commons oraz sposobu funkcjonowania projektów typu open source znajduje się na stronie http://creativecommons.org/.
Implementacja zarządzania czasem oraz złożonym ruchem W tej chwili możemy już utworzyć system, który umożliwi w sposób precyzyjny ustawienie szybkości działania gry. Pętla gry nie będzie już dłużej zależna od szybkości działania urządzenia. W tym celu uruchomimy zegar, a następnie będziemy dostosowywać ruch animacji na podstawie czasu, który upłynął. Inaczej mówiąc, jeżeli jeden cykl zajmie dużo czasu, a inny mniej, wtedy sprajt zostanie przesunięty w zależności od tej wartości o odpowiednią odległość. Najlepszą metodą na zrozumienie tej zależności jest analiza kodu. W tym celu należy wykonać następujące czynności: 1. Kod umieszczony w bloku synchronized klasy GameLogic należy zastąpić kodem umieszczonym na listingu 2.13. Listing 2.13. Test gry o stałej liczbie klatek animacji try { Thread.sleep(30); } catch (InterruptedException e1) { } long time_interim = System.currentTimeMillis(); int adj_mov = (int)(time_interim - time_orig); mGameView.update(adj_mov); time_orig = time_interim; this.mGameView.onDraw(canvas);
2. W pierwszej chwili cały fragment kodu może wydawać się obcy. W rzeczywistości wykonuje on kilka prostych zadań: • Instrukcja znajdująca się w bloku try-catch przed kontynuacją dalszego działania nakazuje tabletowi odczekanie 30 milisekund. Ta czynność może spowodować powstanie wyjątku, którym nie będziemy się zajmować. • Poprzednio tuż obok deklaracji obiektu Canvas zadeklarowane były dwie zmienne typu long o nazwach time_orig oraz time_interim. Wcześniej zmienna time_orig była inicjalizowana przy użyciu bieżącego czasu systemowego za pomocą instrukcji long time_orig = System.currentTimeMillis();. W tym przykładzie zmienna time_interim również inicjalizowana jest czasem systemowym w celu sprawdzenia, ile czasu upłynęło od poprzedniego pomiaru. Rezultat porównania umieszczany jest jako wartość całkowita w zmiennej o nazwie adj_mov. Funkcja update z klasy GameView została zmieniona w sposób umożliwiający przyjmowanie jej parametru typu integer. Po wywołaniu metody update wartość zmiennej przechowującej czas oryginalny jest nadpisywana czasem bieżącym, a na koniec widok jest odświeżany poprzez wywołanie metody onDraw. 3. Do metody update z klasy GameView należy dodać kod umieszczony na listingu 2.14.
52
WYKRYWANIE KOLIZJI
Listing 2.14. Klasa GameView ze zmodyfikowaną postacią metody update public void update(int adj_mov) { sprite.update(adj_mov); }
4. Z listingu 2.15 wynika, że zmienna adj_mov przekazywana jest do sprajta w ten sposób, aby miała ona wpływ na jego ruch. Listing 2.15. Klasa SpriteObject ze zmodyfikowaną funkcją update public void update(int adj_mov) { x += (adj_mov * x_move); y += (adj_mov * y_move); }
5. W tym przypadku metoda update mnoży wartości zmiennych x_move oraz y_move przez wartość zmiany czasu. W celu utrzymania wciąż rozsądnej szybkości ruchu dla stałej określającej stałą ruchu została ustawiona wartość 1. Jest to całkiem rozsądne, ponieważ w przypadku gdy obliczenia zajmują dłuższy czas, wtedy odległość pokonywana w czasie jednego cyklu jest mnożona przez większą liczbę. Jeżeli przetwarzanie jest szybkie, wtedy sprajt nie przesunie się aż tak daleko. Pomysł kontrolowania liczby klatek na sekundę wyświetlanych przez grę ma szereg konsekwencji, z których skorzystamy w kolejnych projektach. Pomimo iż mogłoby się wydawać, że większość gier powinna uwzględniać wpływ czasu, to jednak w wielu aplikacjach wcale się tego nie robi. Pomyślmy na przykład o szachach albo grze w kółko i krzyżyk. W grach turowych dostosowanie czasu nie jest aż tak istotną kwestią. Uwaga! W dokumentacji systemu Android dostępne są przykładowe programy, z którymi warto się zapoznać, aby poznać różne typy gier. W tym celu należy wyświetlić stronę znajdującą się pod adresem: http://developer. android.com/resources/browser.html?tag=sample. Trzeba jednak mieć na uwadze, że większość znajdujących się tam programów została utworzona dla wcześniejszych wersji systemu Android, takich jak 2.2 lub 2.3. Jeżeli Czytelnik byłby rzeczywiście zainteresowany zapoznaniem się z tymi przykładami, może zostać zmuszony do utworzenia emulatora właśnie dla tych wersji systemu. Przeniesienie programów na system Android 3.0 nie jest rzeczą trudną. Można tego dokonać, zwiększając jedynie rozmiar grafik oraz obszar ekranu.
Wykrywanie kolizji Mimo że jeszcze nie poruszyliśmy kwestii pobierania danych od użytkownika, wciąż możemy dodać do gry pewną interakcję poprzez dołączenie obsługi prostych kolizji sprajta z brzegami ekranu tabletu. Szybka oraz prosta implementacja takiego przykładu przedstawiona jest na listingu 2.16. Listing 2.16. Kod wykrywający kolizje if (sprite.getX() >= getWidth()){ sprite.setMoveX(-2); } if (sprite.getX() = getWidth()){ sprite.setMoveX(0); } if (sprite.getX() 0) { for (int i = 0; i < hist; i++) { InputObject input = inputObjectPool.take(); input.useEventHistory(event, i); mGameLogic.feedInput(input); } } InputObject input = inputObjectPool.take(); input.useEvent(event); mGameLogic.feedInput(input); } catch (InterruptedException e) { } try { Thread.sleep(16); } catch (InterruptedException e) { } return true; }
11. Zauważmy, że po wykonaniu powyższej modyfikacji tracimy możliwość przesuwania sprajta w zależności od miejsca wystąpienia zdarzenia dotyku ekranu. Podobny fragment kodu zostanie dodany z powrotem w kolejnym podrozdziale. W nowej metodzie onTouchEvent kod zawarty w bloku try – catch podejmuje próbę przypisania zdarzenia do obiektu klasy InputObject(), a następnie zachowania go do późniejszej obsługi. Dodanie metody mGameLogic.feedInput(input) umożliwia późniejszy dostęp do szczegółów zdarzenia w chwili bezczynności wątku. Na koniec główny wątek usypiany jest na 16 milisekund, aby mieć pewność, że jednorazowo nie zostanie pobranych zbyt wiele danych wejściowych. 67
ROZDZIAŁ 3. POBIERANIE DANYCH OD UŻYTKOWNIKA
12. Analizując odwołania do metod useEvent oraz useEventHistory, cofnijmy się odrobinę do ich deklaracji w klasie InputObject. Po ich przeanalizowaniu powinno być już oczywiste, w jaki sposób tworzona jest lista zdarzeń wejściowych, które wystąpiły. 13. Do klasy GameView znajdującej się w pliku GameView.java musimy dodać jeszcze dwie nowe metody (przedstawione na listingu 3.9). Będą one wywoływane przez obiekt klasy GameLogic w celu przetwarzania obiektów z danymi wejściowymi. W tym momencie możemy pominąć omówienie metody processKeyEvent, ponieważ tablety niezbyt często używają klawiatury. Druga metoda, przetwarzająca zdarzenie ruchu (processMotionEvent), będzie obsługiwana w sposób podobny do przedstawionego we wcześniejszym przykładzie, w którym sprajt przesuwał się do ostatniego wskazanego na ekranie miejsca. Listing 3.9. Przetwarzanie zdarzeń związanych z ruchem oraz klawiaturą public void processMotionEvent(InputObject input){ sprite.setX(input.x); sprite.setY(input.y); } public void processKeyEvent(InputObject input){ }
14. Aby ustawić współrzędne x oraz y sprajta, musimy odczytać ostatnie współrzędne zapisane w obiekcie klasy InputObject. Zrozumienie całego procesu jest w tej chwili prostsze, gdy mamy przed oczami metodę obsługującą zdarzenie typu MotionEvent. 15. Aby zakończyć cały proces przetwarzania danych wejściowych, dodamy jeszcze pewien kod do klasy GameLogic. Na listingu 3.10. zadeklarowane są dwa konieczne do utworzenia obiekty. Umieszczony tam kod powinien być wklejony tuż za zmiennymi, które przechowują stan gry, tzn. informację, czy gra jest obecnie w trybie pauzy (PAUSE), gotowości (READY), czy też już działa (RUNNING). Listing 3.10. Deklaracja nowych obiektów dla metod przetwarzających dane wejściowe private ArrayBlockingQueue inputQueue = new ArrayBlockingQueue(20); private Object inputQueueMutex = new Object();
16. Konieczne będzie wprowadzenie jeszcze jednej zmiany do metody run(). Istotne będzie również miejsce jej umieszczenia. Na listingu 3.11 przedstawiona jest cała metoda run() z wyróżnionym dodanym fragmentem kodu. Listing 3.11. Konfiguracja głównego wątku do przetwarzania danych wejściowych @Override public void run() { long time_orig = System.currentTimeMillis(); long time_interim; Canvas canvas; while (game_state == RUNNING) { canvas = null; try { canvas = this.surfaceHolder.lockCanvas(); synchronized (surfaceHolder) { try { Thread.sleep(30); } catch (InterruptedException e1) {
68
KORZYSTANIE Z KOLEJEK WEJŚCIA
} time_interim = System.currentTimeMillis(); int adj_mov = (int)(time_interim - time_orig); mGameView.update(adj_mov); processInput(); //tu właśnie nastąpi przetwarzanie danych wejściowych time_orig = time_interim; this.mGameView.onDraw(canvas); } } finally { if (canvas != null) { surfaceHolder.unlockCanvasAndPost(canvas); } } }
17. W kolejnym kroku musimy zdefiniować dwie nowe funkcje, ponieważ ich wywołanie dodaliśmy już do poprzednio zdefiniowanych metod. Metoda processInput() jest miejscem, w którym wątek przetwarza dane wejściowe, natomiast metoda feedInput() troszczy się o działanie kolejki utworzonej z klasy ArrayBlockingQueue. Obie metody przedstawione na listingu 3.12 powinny być umieszczone tuż pod definicją funkcji run(). Listing 3.12. Pobieranie oraz przetwarzanie danych wejściowych public void feedInput(InputObject input) { synchronized(inputQueueMutex) { try { inputQueue.put(input); } catch (InterruptedException e) { } } } private void processInput() { synchronized(inputQueueMutex) { ArrayBlockingQueue inputQueue = this.inputQueue; while (!inputQueue.isEmpty()) { try { InputObject input = inputQueue.take(); if (input.eventType == InputObject.EVENT_TYPE_KEY) { mGameView.processKeyEvent(input); } else if (input.eventType == InputObject.EVENT_TYPE_TOUCH) { mGameView.processMotionEvent(input); } input.returnToPool(); } catch (InterruptedException e) { } } } }
Metoda feedInput() ma całkiem prostą logikę. Na początku rezerwuje ona wątek dzięki instrukcji synchronized(), a następnie pobiera dane wejściowe do kolejki inputQueue. Metoda ta jest wywoływana przez klasę GameView po skutecznym sklasyfikowaniu obiektu z danymi wejściowymi. Metoda processInput() ma nieco bardziej złożony algorytm obsługi kolejki inputQueue. Ona również korzysta z instrukcji synchronized() w celu zarezerwowania wątku na wyłączność. Następnie przegląda obiekty znajdujące się w kolejce inputQueue i w zależności od ich rodzaju przekazuje ich obsługę do funkcji
69
ROZDZIAŁ 3. POBIERANIE DANYCH OD UŻYTKOWNIKA
processKeyEvent() lub processMotionEvent(). Obie te funkcje są zdefiniowane w pliku GameView.java,
ponieważ chcemy mieć w nich możliwość wydawania poleceń obiektom sprajtów. Po tak dużych zmianach kodu program wykonuje obecnie dokładnie to samo, co robił na początku tego rozdziału. Niemniej użycie tego algorytmu oszczędzi nam w przyszłości wielu problemów, w przypadku gdyby główny wątek gry był zbyt mocno obciążony przetwarzaniem danych wejściowych. Taki stan użytkownik mógłby zinterpretować jako zawieszenie się programu. W tej chwili możemy już uruchomić program InputTest. Jeżeli kompilacja przebiegnie poprawnie, powinniśmy być w stanie przeciągać sprajta po ekranie tabletu. Ponieważ program nie używa teraz prawie żadnych algorytmów fizycznych i w tle nie zachodzą żadne inne obliczenia, pomiędzy działaniem tej aplikacji i poprzedniej nie powinno być żadnej zauważalnej różnicy. Z tego zgrabnego sposobu obsługi danych wejściowych skorzystamy dopiero w chwili dodania do programu procedur obsługujących algorytmy sztucznej inteligencji lub szeregu dodatkowych sprajtów. Mając już zdefiniowane podstawowe strategie przetwarzania zdarzeń pochodzących z ekranów dotykowych, możemy teraz zająć się bardziej pasjonującymi czujnikami, które czynią system Android tak interesującym.
Reagowanie na dane pochodzące z czujników System Android dostarcza wprawdzie prostych metod pobierania zdarzeń z ekranu dotykowego, jednak obsługa czujników jest nieco bardziej skomplikowana. Nie oznacza to wcale, że pobieranie danych jest szczególnie trudne, lecz ich sensowna interpretacja może już stanowić nie lada wyzwanie. W tym miejscu skoncentrujemy się na danych pochodzących z akcelerometru, ponieważ jest on najczęściej używany, a inne czujniki są do niego podobne (np. żyroskop). Dane dostarczane przez czujniki tabletu są bardzo precyzyjne i dlatego do ich obsługi zazwyczaj używamy w Javie danych zmiennoprzecinkowych typu float. Rodzi to mieszane uczucia, ponieważ dane tego rodzaju są dość trudne do interpretacji. Co więcej, trudność zwiększa to, że tablety mogą być trzymane w szeregu różnych pozycji. I tak, trzymanie tabletu w pozycji poziomej całkowicie zmienia oś obrotu. Aby na moment nie brać tego pod uwagę, możemy założyć, że tablet jest trzymany poziomo. W dalszej części książki poznamy sposoby rozpoznawania położenia tabletu, a także poinformujemy użytkownika o poprawnym położeniu tabletu dla naszej gry. Dodajmy więc kolejny fragment kodu do projektu i sprawdźmy w praktyce dane pochodzące z czujników. 1. Do projektu musimy dodać kolejną bibliotekę Androida. W tym celu do pliku MainActivity.java dołączmy kod przedstawiony na listingu 3.13. Listing 3.13. Uzyskiwanie dostępu do danych pochodzących z czujników import import import import
android.hardware.Sensor; android.hardware.SensorEvent; android.hardware.SensorEventListener; android.hardware.SensorManager;
2. Czytelnik mógł już zauważyć, że powyższe instrukcje import odnoszą się do szczegółów specyfikacji technicznej urządzenia. Na przykład istnieje możliwość, że urządzenie, na którym będzie uruchamiana gra, może nie mieć danego czujnika. 3. W klasie MainActivity dodajmy implementację interfejsu SensorEventListener. W tym celu bezpośrednio po wierszu z instrukcją extends Activity dołączmy instrukcję implements SensorEventListener. Po pojawieniu się komunikatu o wystąpieniu błędu kliknijmy go dwa razy, co spowoduje utworzenie dwóch zdarzeń przedstawionych na listingu 3.14. 4. Wykonane do tej pory czynności są całkiem oczywiste. Dodaliśmy implementację interfejsu SensorEventListener, a następnie dwie metody rejestrujące zmianę czułości czujnika lub jego
70
REAGOWANIE NA DANE POCHODZĄCE Z CZUJNIKÓW
Listing 3.14. Automatycznie utworzone metody do obsługi czujników @Override public void onAccuracyChanged(Sensor arg0, int arg1) { // TODO W tym miejscu automatycznie generowany szkielet } @Override public void onSensorChanged(SensorEvent arg0) { // TODO W tym miejscu automatycznie generowany szkielet }
wartości. Skoncentrujemy się głównie na metodzie onSensorChanged(), ponieważ interesują nas dane. Oczywiście poza tymi dwiema funkcjami istnieje wiele innych, których można użyć w chwili, gdy pragniemy odczytać z czujników bardzo szczegółowe informacje. 5. Fragment kodu przedstawiony na listingu 3.15 należy dodać do metody onCreate() znajdującej się w klasie MainActivity. Listing 3.15. Tworzenie obiektów reprezentujących czujniki private SensorManager mSensorManager; private Sensor mAccelerometer;
6. W metodzie onCreate() należy jeszcze zainicjalizować utworzone przed chwilą czujniki, dodając w tym celu fragment kodu przedstawiony na listingu 3.16. Listing 3.16. Inicjalizacja obiektów czujników mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE); mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
7. Do obsługi czujników brakuje jeszcze dwóch prostych metod, dostępnych już z każdą aktywnością, czyli onPause() oraz onResume(). Potrzebujemy ich, ponieważ nie chcemy w programie poszukiwać nowych danych z czujników, w chwili gdy urządzenie znajduje się w stanie uśpienia. Ten problem został rozwiązany przez kod na listingu 3.17. Listing 3.17. Implementacja metod onPause() oraz onResume() protected void onResume() { super.onResume(); mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL); } protected void onPause() { super.onPause(); mSensorManager.unregisterListener(this); }
8. Wartości pochylenia (ang. pitch), przechylenia (ang. roll) oraz azymutu (ang. azimuth) urządzenia możemy odczytać ze zmodyfikowanej metody onSensorChanged(). I tak na przykład azymut jest kątem obrotu urządzenia wokół osi z, pochylenie wokół osi x, a przechylenie wokół osi y. Aby sprawdzić wartości pochodzące z akcelerometru, wprowadzimy teraz nową technikę testowania w programie Eclipse oraz w Androidzie. W tym celu na początku pliku dodajmy instrukcję import Android.util.Log. Następnie zmieńmy metodę onSensorChanged(), dodając kod przedstawiony na listingu 3.18.
71
ROZDZIAŁ 3. POBIERANIE DANYCH OD UŻYTKOWNIKA
Listing 3.18. Nowa postać metody onSensorChanged (dodaj import android.util.Log) @Override public void onSensorChanged(SensorEvent event) { float R[] = new float[9]; float orientation[] = new float[3]; SensorManager.getOrientation(R, orientation); Log.d("azymut",Float.toString(orientation[0])); Log.d("pochylenie",Float.toString(orientation[1])); Log.d("przechylenie",Float.toString(orientation[2])); }
9. Najogólniej mówiąc, w funkcji tworzymy dwie tablice, które będą służyć do przechowywania wartości. Później wywołujemy metodę obiektu klasy SensorManager, która odczytuje położenie urządzenia. Na koniec wypisujemy wartości znajdujące się w tablicy ze współrzędnymi położenia. Funkcja Log.d może wydawać się dla Czytelnika nowa, lecz jej zadaniem jest najzwyczajniej przesłanie danych do debugera. Przed uruchomieniem programu możemy ustawić w środowisku odpowiedni widok służący do odczytywania tego rodzaju wartości. W tym celu wybierzmy z menu Window pozycję Show View/Other/Android, a na koniec LogCat. Od tej chwili zamiast przeglądać dane wyjściowe na konsoli, po uruchomieniu emulatora w nowym widoku ujrzymy dziesiątki danych. Po uruchomieniu aplikacji zostaną tam też wyświetlone dane pochodzące z akcelerometru. Na rysunku 3.7 przedstawione zostało, co stanie się, gdy do testowania programu na komputerze użyjemy emulatora, który nie będzie zmieniać swojego położenia.
Rysunek 3.7. Wartości azymutu, pochylenia oraz przechylenia urządzenia Do testowania danych pochodzących z czujników najlepiej nadaje się prawdziwy tablet, ponieważ łatwo jest go trzymać w różnych pozycjach. Jeżeli Czytelnik jeszcze tego nie zrobił, to dodatek A zawiera informacje na temat konfigurowania tabletu w celu testowania. Jeżeli jednak Czytelnik jest odważny lub nie posiada takiego urządzenia, to Android posiada symulator czujnika akcelerometru, który może pomóc podczas tworzenia tego rodzaju kodu. Kod projektu symulatora znajduje się pod adresem: http://code.google.com/p/openintents/wiki/ SensorSimulator. Oczywiście całkiem kuszące byłoby skonfigurowanie całego systemu za jednym zamachem, ale w tym miejscu tego nie zrobimy. W pewnym momencie tylko prawdziwe urządzenie będzie w stanie dostarczyć odpowiedzi z odpowiednim minimalnym opóźnieniem, co pozwoli na sprawdzenie możliwości czujnika. Symulator akcelerometru ma jednak nad rzeczywistym urządzeniem pewną przewagę. Dzięki możliwości wprowadzenia konkretnych wartości ruchu urządzenia da się lepiej sprawdzić program. Dla większości programistów bardzo trudną rzeczą będzie zmierzenie na przykład idealnego obrotu o 37 stopni, gdy trzymają w rękach rzeczywiste urządzenie.
72
KORZYSTANIE Z DANYCH Z CZUJNIKA
Korzystanie z danych z czujnika Aby dane pochodzące z czujnika wykorzystać w kodzie gry, konieczne jest przekazanie ich do klasy View. W tym celu do klasy MainActivity dodajmy następujący wiersz: GameView mGameView;
Natomiast do metody onCreate dodajmy kod przedstawiony na listingu 3.19. Listing 3.19. Instancja klasy GameView mGameView = new GameView(this); setContentView(mGameView);
W tym momencie dysponujemy już instancją klasy GameView, której możemy użyć do wywołania szeregu różnych metod. Następnie do klasy GameView należy dodać nową funkcję, do której będą przekazywane dane o położeniu urządzenia. Na listingu 3.20 przedstawione zostało odwołanie, które powinno być dołączone wewnątrz metody onSensorChanged(). Listing 3.20. Wysyłanie danych czujnika @Override public void onSensorChanged(SensorEvent event) { if(event.sensor.getType() == Sensor.TYPE_ACCELEROMETER){ float orientation[] = new float[3]; for(int i = 0; i < 3; i++){ orientation[i] = event.values[i]; } mGameView.processOrientationEvent(orientation); Log.d("azymut",Float.toString(event.values[0])); Log.d("pochylenie",Float.toString(event.values[1])); Log.d("przechylenie",Float.toString(event.values[2])); } }
Kolejnym fragmentem kodu będzie metoda processOrientationEvent() w klasie GameView. Zwróćmy uwagę, że do metody tej przekazywana jest tablica. Listing 3.21 zawiera kod metody processOrientationEvent(), który należy skopiować do pliku GameView.java. Listing 3.21. Przetwarzanie danych pochodzących z sensora public void processOrientationEvent(float orientation[]){ float roll = orientation[2]; if (roll < -40) { sprite.setMoveX(2); } else if (roll > 40) { sprite.setMoveX(-2); } }
W tym momencie sprawdzamy jedynie przechylenie urządzenia. Jeżeli jest ono wystarczająco małe, sprajt powinien przesunąć się w prawo. Jeśli natomiast jest wystarczająco duże, sprajt powinien
73
ROZDZIAŁ 3. POBIERANIE DANYCH OD UŻYTKOWNIKA
skierować się w lewą stronę. Aby zapewnić dodatkowy dreszczyk emocji, należy zakomentować kilka wierszy w metodzie update(). Na listingu 3.22. przedstawiony jest oczekiwany wygląd tego fragmentu kodu. Listing 3.22. Uwolnienie sprajta public void update(int adj_mov) { if (sprite.getX() >= getWidth()){ //sprite.setMoveX(0); } if (sprite.getX() 0) { for (int i = 0; i < hist; i++) { InputObject input = inputObjectPool.take(); input.useEventHistory(event, i); mGameLogic.feedInput(input); } } InputObject input = inputObjectPool.take(); input.useEvent(event); mGameLogic.feedInput(input); } catch (InterruptedException e) { } try { Thread.sleep(16); } catch (InterruptedException e) { } return true; } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { }
81
ROZDZIAŁ 4. DODAWANIE EFEKTÓW DŹWIĘKOWYCH, MUZYKI ORAZ SEKWENCJI FILMOWYCH
@Override public void surfaceCreated(SurfaceHolder holder) { mGameLogic.setGameState(mGameLogic.RUNNING); mGameLogic.start(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { soundPool.release(); } @Override public void onDraw(Canvas canvas) { canvas.drawColor(Color.BLACK); sprite.draw(canvas); } public void update(int adj_mov) { if (sprite.getX() >= getWidth()){ //sprite.setMoveX(0); } if (sprite.getX() 40) { sprite.setMoveX(-2); } } }
82
PRZYGOTOWANIE DO ODTWARZANIA DŹWIĘKÓW
Nowy kod na listingu został wyróżniony pogrubieniem. Szczególną uwagę należy zwrócić na implementację nowych pakietów oraz na sposób działania klasy SoundPool. Wszystkie operacje na dźwiękach wykonywane są w klasie GameView.java, bez modyfikowania żadnych innych klas. Na listingu 4.3 przedstawiona została cała zawartość pliku GameView.java, dlatego też możemy być pewni, że wszystko będzie działać poprawnie. A oto w jaki sposób działa powyższy program. Implementacja klasy rozpoczyna się od zadeklarowania kilku zmiennych: • Sound_id — jest to licznik przechowujący informację o tym, który dźwięk ma być odtwarzany. • Context — umożliwia przechowanie instancji głównej aktywności oraz przekazanie jej do funkcji wczytującej dźwięk. Zmiennej tej używaliśmy już wcześniej. • SoundPool — jest to obiekt zarządzający odtwarzaniem szeregu dźwięków. • ID_robot_noise — numeryczna wartość całkowita określająca numer pliku z dźwiękiem robota. • ID_alien_noise — numeryczna wartość całkowita określająca numer pliku z dźwiękiem obcego kosmity. • ID_human_noise — numeryczna wartość całkowita określająca numer pliku z dźwiękiem człowieka. W dalszej kolejności obiekt soundPool jest inicjalizowany wewnątrz metody konstruktora klasy GameView. Konstruktor klasy SoundPool przyjmuje trzy argumenty: liczbę całkowitą określającą liczbę jednoczesnych strumieni dźwiękowych, liczbę całkowitą określającą typ strumienia dźwiękowego (do jego wyboru służy klasa AudioManager) oraz, jako ostatnią, liczbę całkowitą określającą jakość dźwięku (ta liczba obecnie nie będzie wykorzystywana). Typ strumienia dźwiękowego warty jest chwili uwagi, ponieważ wybraliśmy najbardziej powszechnie używaną opcję. Klasa AudioManager ma jednak inne możliwości, na przykład typy STREAM_ALARM oraz STREAM_RING, które przechowują pliki dźwiękowe skojarzone z daną aktywnością. Nasza gra prawdopodobnie nigdy nie wykorzysta innego rodzaju strumienia niż STREAM_MUSIC. W kolejnych trzech wierszach następuje wczytanie trzech różnych plików dźwiękowych. Podczas tworzenia tego projektu należy dysponować trzema plikami dźwiękowymi umieszczonymi w katalogu res/raw, odpowiadającymi identyfikatorom plików zasobów przekazanym do funkcji load(). Parametry metody load() są całkiem oczywiste. Pierwszy z nich jest kontekstem aplikacji, a drugi identyfikatorem zasobu. Ostatni parametr w tej wersji systemu Android jest niewykorzystywany. Metoda load() zwraca identyfikator dźwięku. Jest on następnie używany do wywołania konkretnego pliku dźwiękowego, który ma zostać odtworzony. Na końcu zmiennej sound_ID przypisywany jest identyfikator pierwszego dźwięku z listy przeznaczonego do odtworzenia. W funkcji processMotionEvent() obiekt soundPool zostanie wykorzystany do odtworzenia własnych plików dźwiękowych. Parametry metody play z obiektu soundPool są przedstawione poniżej: • Wartość typu int określająca numer dźwięku do odtworzenia. • Wartość typu float określająca poziom głośności w lewym kanale. W tym przypadku korzystamy z maksymalnego poziomu głośności równego 1.0. • Wartość typu float określająca poziom głośności w prawym kanale. W tym przypadku korzystamy z maksymalnego poziomu głośności równego 1.0. • Wartość typu int określająca priorytet zadania. W tym przypadku używamy wartości 10. Im większa liczba, tym wyższy priorytet. • Wartość typu int określająca rodzaj pętli. Użycie wartości 0 oznacza wyłączenie pętli. Wartość -1 oznacza pętlę nieskończoną, natomiast każda dodatnia liczba całkowita powoduje użycie liczby pętli o jeden większej (na przykład wartość 5 oznacza 6 pętli). • Wartość typu float określająca szybkość odtwarzania. Odtwarzanie z normalną szybkością wymaga użycia wartości 1.0. Po użyciu wartości 0.5 lub 2.0 odtwarzanie będzie odbywało się odpowiednio z szybkością zmniejszoną o połowę lub zwiększoną dwukrotnie.
83
ROZDZIAŁ 4. DODAWANIE EFEKTÓW DŹWIĘKOWYCH, MUZYKI ORAZ SEKWENCJI FILMOWYCH
W kolejnym fragmencie kodu wartość licznika sound_ID zwiększana jest o jeden, natomiast w przypadku, gdy odtworzone zostały już wszystkie dźwięki z zestawu, wartość licznika jest zerowana. Metoda onSurfaceDestroyed() wywołuje metodę release() z obiektu soundPool, co powoduje usunięcie obiektu oraz zwolnienie używanej przez niego pamięci. Aby zobaczyć, w jaki sposób po zmianach działa nasz program, należy go uruchomić, a następnie wykonać te same czynności co w poprzednim przykładzie. Obecnie podczas przeciągania sprajta po obszarze ekranu za każdym razem powinien być odtwarzany inny dźwięk. Po odegraniu wszystkich dźwięków ich odtwarzanie powinno rozpocząć się od nowa. Tego rodzaju technika może zostać wykorzystana w wielu grach. Na przykład po zniszczeniu różnych typów potworów mogą być odtwarzane różne dźwięki. W kolejnym podrozdziale zajmiemy się dopasowaniem odtwarzanych dźwięków do rodzaju zdarzeń.
Dopasowanie efektów dźwiękowych do zdarzeń Poprzedni przykład jest wystarczający do zrozumienia obsługi zmiennej kolejki dźwięków. W większości przypadków jednak każdemu określonemu zdarzeniu przyporządkowany jest osobny dźwięk. Taką logikę jest bardzo łatwo zaimplementować i wymagać to będzie jedynie tego, by w każdym momencie, gdy odtwarzany ma być dany plik dźwiękowy, przekazany został jego poprawny identyfikator. Wyobraźmy sobie na przykład sytuację, w której główny bohater gry napotka przerażającego robota. Aby poinformować gracza o nowym zdarzeniu, można w takiej chwili odtworzyć głos robota. Zanim jednak zaczniemy martwić się o rodzaj efektu dźwiękowego, musimy się domyślić, czy robot znajduje się w pobliżu głównego bohatera. W tym celu możemy utworzyć sprajt reprezentujący robota i sprawdzić, czy oba sprajty znajdują się od siebie w odległości równej określonej liczbie pikseli. Dla celów tego przykładu wystarczy jednak, jeżeli założymy, że mamy pewną metodę określenia bliskości obiektu. W funkcji update() w pliku GameView.java znajduje się instrukcja if, która w przypadku prawdziwego warunku wywołuje nową metodę. A oto w jaki sposób metodę tę można przedstawić przy użyciu pseudokodu: public void update(adj_mov){ If(near_robot){ playsound(robot_noise); } }
W miarę postępu w tworzeniu całej gry funkcja update będzie używać różnych warunków służących do sprawdzania listy czynności do wykonania. Zamiast odtwarzać dźwięki bezpośrednio z metody update(), można utworzyć osobną funkcję pod nazwą robot_encounter(), która będzie zawierać wszystkie czynności konieczne do wykonania po napotkaniu robota. W tym momencie jednak użyjmy prostej funkcji playsound(). Funkcja playsound() jest w rzeczywistości szybszym sposobem użycia metody soundPool.play(). Na listingu 4.4. przedstawiony jest fragment kodu, który powinien zostać dodany do pliku GameView.java. Listing 4.4. Funkcja playsound() public void playsound(int sound_id){ soundPool.play(sound_id, 1.0f, 1.0f, 1, 0, 1.0f); }
Wszędzie tam, gdzie wewnątrz klasy GameView zachodzi konieczność odtworzenia pliku dźwiękowego, można wywołać funkcję zdefiniowaną powyżej. W momencie gdy inne tworzone gry wymagać będą większej liczby dźwięków, każdemu z nich będzie można przypisać nowy identyfikator, który następnie będzie mógł zostać przekazany do tej poręcznej funkcji.
84
DODAWANIE MUZYKI
W tym momencie z całą pewnością zwiększyliśmy funkcjonalność gry bez konieczności dodawania zbyt wielkiej ilości kodu. Ponieważ w przeciwieństwie do grafik podczas gry dźwięki nie podlegają przesunięciom lub obrotom, mogą być zainicjalizowane, a następnie pozostawione samym sobie. Nowy wymiar, jaki nadaliśmy grze, z pewnością pozwoli użytkownikom na większe zaangażowanie się w jej akcję.
Dodawanie muzyki Używanie muzyki w Androidzie jest pasjonujące. Ta interesująca technologia udostępnia zadziwiające funkcje. Zanim przyjrzymy się bliżej dostępnym możliwościom, spróbujmy odtworzyć w trakcie gry utwór w formacie MIDI. Do tego celu doskonale nadaje się poznana już klasa MediaPlayer, ponieważ została ona zaprojektowana do odtwarzania w Androidzie różnych plików multimedialnych. Mimo że muzyka jest zazwyczaj dłuższa od efektów dźwiękowych i używa innych rodzajów formatów plików, to jednak jest obsługiwana w niemalże taki sam sposób jak efekty dźwiękowe w poprzednich podrozdziałach. Aby pobrać pliki z efektami dźwiękowymi, odwiedzaliśmy witrynę www.freesounds.org. Natomiast do poszukiwania melodii w formacie MIDI doskonale nadaje się strona www.midiworld.com. Znajduje się na niej olbrzymia biblioteka plików .midi, które mogą być wykorzystywane we własnych programach użytkownika. Na przykład w kategorii Pop umieszczona jest piosenka pt. Take a Chance on Me, wykonywana przez zespół ABBA. Spróbujmy teraz dodać ją do aplikacji. 1. W tym celu pobierzmy na pulpit utwór Take a Chance on Me (lub inny dowolnie wybrany przez Czytelnika). Jeżeli Czytelnik posiada swoje własne pliki MIDI, powinien zwrócić uwagę na to, że Android jest wybredny i nie akceptuje używania rozszerzenia .midi zamiast .mid. W przyszłości być może Android będzie obsługiwać oba formaty, ale opisana sytuacja była już źródłem wielu błędów. 2. Podobnie jak w przypadku tworzenia obsługi efektów dźwiękowych, pliki .mid powinny być przekopiowane do katalogu res/raw umieszczonego w projekcie. Zanim jednak to zrobimy, nadajmy plikom znaczącą nazwę. W tym celu zmieńmy tymczasowo nazwę piosenki na background_music.mid. 3. Kiedy już poprawnie umieścimy plik zasobów, możemy przyjrzeć się przez chwilę prostemu fragmentowi kodu używanemu do jego odtwarzania. W tym celu na początku klasy GameView utwórzmy najpierw zmienną prywatną o nazwie MediaPlayer. private MediaPlayer mp;
4. Następnie wyróżniony poniżej fragment kodu dodajmy do metody surfaceCreated(). W ten sposób przekażemy do tabletu informację o zamiarze rozpoczęcia odtwarzania muzyki natychmiast po utworzeniu ekranu początkowego aplikacji. @Override public void surfaceCreated(SurfaceHolder holder) { mGameLogic.setGameState(mGameLogic.RUNNING); mGameLogic.start(); mp = MediaPlayer.create(getContext(), R.raw.background_music); mp.setLooping(true); mp.start(); }
5. Ponieważ klasy MediaPlayer używaliśmy już wcześniej, kod umieszczony powyżej nie powinien wymagać komentarza. Obiekt klasy MediaPlayer tworzony jest poprzez wczytanie odpowiedniego pliku i przekazanie kontekstu aplikacji. W ten sposób muzyka przygotowywana jest do odtwarzania. Następnie przed ostatecznym odtworzeniem muzyki informujemy obiekt klasy MediaPlayer o konieczności jej zapętlenia. 6. Aby posprzątać po sobie, po skończeniu używania programu zmieńmy funkcję surfaceDestroyed(), dodając do niej następujący fragment kodu:
85
ROZDZIAŁ 4. DODAWANIE EFEKTÓW DŹWIĘKOWYCH, MUZYKI ORAZ SEKWENCJI FILMOWYCH
@Override public void surfaceDestroyed(SurfaceHolder holder) { soundPool.release(); mp.stop(); mp.release(); }
7. I to właściwie byłoby na tyle. 8. Po uruchomieniu programu SoundsTest od początku powinniśmy już być w stanie usłyszeć muzykę. Po przesunięciu kursora po ekranie wraz z muzyką odtwarzane będą dźwięki z obiektu soundPool. Metody utworzonej przed chwilą będzie można używać do odtwarzania muzyki w grze zawsze wtedy, gdy zajdzie taka potrzeba. Nabywszy umiejętność odtwarzania efektów dźwiękowych oraz muzyki, Czytelnik zakończył już poznawanie możliwości dźwiękowych oferowanych przez system Android dla gier. Kolejnym ważnym obiektem multimedialnym będą oczywiście sekwencje filmowe. W kolejnym podrozdziale dowiemy się, w jaki sposób podczas gry odtworzyć klip filmowy. Ponieważ filmy są również plikami multimedialnymi, są one obsługiwane analogicznie do plików dźwiękowych.
Dodawanie sekwencji filmowych Odtwarzanie sekwencji filmowych w trakcie działania gry jest dość niespotykane, jednakże takie sekwencje mają bardzo istotne znaczenie w momencie jej rozpoczynania oraz przed przejściem na każdy kolejny poziom. Na szczęście pliki z filmami są obsługiwane prawie tak samo jak pliki z muzyką lub dźwiękami. W rzeczywistości aby przetestować możliwość odtwarzania sekwencji filmowych, można zastąpić plik .mid plikiem .3gp. Spowoduje to odtworzenie pliku zaraz po utworzeniu obiektu ekranu aplikacji. Szybkie przeszukanie internetu pod kątem obecności plików z rozszerzeniem 3GP przynosi wiele rezultatów. Jeżeli Czytelnik dysponuje teledyskami w formacie .mp4, może je również dodać do podkatalogu raw w katalogu res. Na listingu 4.5 przedstawiony został kod używany do odtwarzania jednego z tych rodzajów plików. Listing 4.5. Odtwarzanie sekwencji filmowych @Override public void surfaceCreated(SurfaceHolder holder) { mGameLogic.setGameState(mGameLogic.RUNNING); mGameLogic.start(); mp = MediaPlayer.create(context, R.raw.intro_video, holder); mp.setLooping(true); mp.start(); }
Zwróćmy uwagę na to, w jaki sposób wyróżniony parametr metody create() różni się od tego używanego do odtwarzania dźwięków. W tym przypadku używany jest obiekt klasy SurfaceHolder, który został przekazany do funkcji surfaceCreated(). Ponieważ obraz wideo musi być wyświetlany na jakiejkolwiek powierzchni, do metody create przekazywany jest obiekt SurfaceView, który może być później użyty do wyświetlenia pliku wideo. Plik ten zostanie odtworzony w górnym lewym rogu ekranu tabletu. Po tej jednej szybkiej zmianie obiekt klasy MediaPlayer będzie w stanie odtwarzać pliki filmowe. Aby wyświetlać proste pliki tego rodzaju, nie jest konieczne wprowadzanie do programu żadnych kolejnych zmian. W tym momencie jesteśmy w stanie odtwarzać efekty dźwiękowe, muzykę, a nawet sekwencje filmowe. W kolejnym podrozdziale cofniemy się z powrotem do muzyki i wprowadzimy zagadnienie dynamicznych dźwięków. Ta funkcjonalność umożliwia Androidowi zmianę odtwarzanej muzyki
86
ZARZĄDZANIE OBSŁUGĄ MUZYKI
na podstawie zmian akcji zachodzących w grze. W tym momencie nie musimy rozumieć całości tego zagadnienia, niemniej jest to unikalna funkcjonalność, którą można spróbować wykorzystać w swoich grach.
Zarządzanie obsługą muzyki Obrazy w grze mogą być modyfikowane poprzez ich obracanie, przekształcenie oraz przesunięcie. W przeciwieństwie do nich muzyka jest statyczna. Pliki z muzyką mogą być jedynie odtwarzane oraz zatrzymywane. Niemniej w systemie Android istnieją sposoby, aby muzykę modyfikować w trakcie działania programu. To całkiem skomplikowana technika, której zastosowanie w grze zajmie użytkownikowi prawdopodobnie chwilę czasu. W tym podrozdziale dotkniemy jedynie czubka góry lodowej związanej z tym zagadnieniem. Po lekturze Czytelnik będzie w stanie kontynuować analizowanie zagadnienia samodzielnie oraz dodać jego obsługę do własnych aplikacji. Celem zarządzania muzyką w opisany powyżej sposób jest stworzenie jeszcze bardziej realistycznego wrażenia z gry. Podczas oglądania filmów nastrój muzyki dostosowuje się do przebiegu akcji. I tak na przykład w chwili, gdy główny bohater szykuje się do bitwy, poruszająca muzyka przygotowuje widza na epickie wyzwania. W czułych scenach grana jest wolna, romantyczna muzyka. Ten sam efekt można uzyskać w grze. Idealnym rezultatem byłoby dostosowanie muzyki do akcji. I tak, kiedy gracz dotrze do ryzykownego mostu, muzyka powinna zwiastować coś nieoczekiwanego. Natomiast gdy dotrze on do celu, powinna rozbrzmieć muzyka zwycięska. Gra staje się znacznie bardziej realistyczna, gdy podkład muzyczny nie jest stały, ale raczej zmienny. W tym przykładzie do zarządzania muzyką użyjemy klasy JetPlayer. Podobnie do klasy MediaPlayer, również ta klasa potrafi odtwarzać pliki MIDI, oferując jednakże kilka dodatkowych funkcji. Używa ona bowiem plików JET, które określają sposób odtwarzania różnych fragmentów plików MIDI. Zanim poeksperymentujemy w celu poznania sposobu działania klasy JetPlayer, sprawdźmy, w jaki sposób utworzyć plik JET. Twórcy systemu Android dostarczyli wspaniałe środowisko służące do tego celu. Nosi ono nazwę JET Creator, a w celu jego wykorzystania konieczne jest posiadanie programu Python zainstalowanego na komputerze. Aby skonfigurować ten ostatni program, należy wykonać poniższe czynności: 1. Na komputer należy pobrać odpowiednią wersję języka Python, dostępną na stronie: www.python.org/download/releases/2.7.2/. 2. Następnie należy wykonać polecenia wyświetlane przez pobrany program instalacyjny. Podczas konfiguracji instalacji należy wskazać położenie docelowe instalacji Pythona (patrz rysunek 4.1). 3. Po poprawnej instalacji Pythona konieczne będzie doinstalowanie rozszerzenia wxPython, dostępnego pod adresem: www.wxpython.org/download.php. Również w tym przypadku wybierzmy wersję, która jest odpowiednia dla danego systemu, i rozpocznijmy proces instalacji. 4. W kreatorze instalacji należy wskazać miejsce instalacji programu Python, co zostało przedstawione na rysunku 4.2. Uwaga! Rozszerzenie wxPython jest narzędziem obsługującym interfejs graficzny użytkownika dla języka Python. W przypadku jego braku bylibyśmy zmuszeni do wykonania wszystkich czynności z wiersza poleceń.
5. W kolejnym kroku należy uruchomić program JET Creator. W tym celu w głównym katalogu instalacyjnym Androida odnajdźmy podkatalog sdk\tools\Jet\JetCreator, a następnie dwukrotnie kliknijmy ikonę pliku JetCreator.py. Po chwili pojawi się okno dialogowe przypominające to na rysunku 4.3. 6. Kliknijmy przycisk o nazwie Import (importuj) umieszczony po prawej stronie okna dialogowego. 7. W nowym oknie odszukajmy katalog android-sdk/tools/Jet/demo_content, a następnie wybierzmy skompresowany programem ZIP katalog o nazwie democontent_1. 87
ROZDZIAŁ 4. DODAWANIE EFEKTÓW DŹWIĘKOWYCH, MUZYKI ORAZ SEKWENCJI FILMOWYCH
Rysunek 4.1. Należy zapamiętać położenie instalacji dystrybucji Pythona
Rysunek 4.2. W przypadku gdyby program wxPython nie mógł odnaleźć katalogu instalacyjnego Pythona, może zaistnieć konieczność ręcznego wskazania katalogu lib\site-packages 8. Po wyświetleniu pytania wyraźmy zgodę na rozpakowanie katalogu w domyślnym położeniu, jakim jest najczęściej podkatalog umieszczony w katalogu bieżącym programu JET. 9. Na zakończenie wyświetlone zostanie okno programu JET Creator zawierające szereg różnych plików MIDI. W wolnej chwili Czytelnik może spróbować samodzielnie poeksperymentować z programem JET Creator, jednak w tym momencie najważniejsze jest, aby wiedział, że każdemu z fragmentów plików MIDI może być przyporządkowane zdarzenie, które będzie go uruchamiać. To właśnie zdarzenia mają moc zmiany muzyki z jednego utworu na drugi. W przypadku gdyby Czytelnik był dalej zainteresowany
88
ZARZĄDZANIE OBSŁUGĄ MUZYKI
Rysunek 4.3. Jeśli w oknie dialogowym Open Jet File nie będzie wskazanej ścieżki, nie należy się tym przejmować tworzeniem własnej muzyki sterowanej zdarzeniami, musi dokładnie poznać działanie programu JET Creator. Najlepszym sposobem będzie lektura dokumentacji Androida umieszczonej pod adresem: http://developer.android.com/guide/topics/media/jet/jetcreator_manual.html. Od tej chwili Czytelnik będzie w stanie modyfikować przykładową zawartość programu JET i dostosowywać ją do potrzeb swojej własnej gry. Ponieważ w obecnej chwili nasza przykładowa gra nie ma zbyt dużo gotowych zdarzeń, przyjrzyjmy się implementacji wykorzystania klasy JetPlayer w przykładowym projekcie Androida o nazwie JetBoy. Po zapoznaniu się z tym kodem Czytelnik będzie gotowy do wykorzystania programu JET Creator w swoich przyszłych projektach. W celu przetestowania całej gry utwórzmy w Eclipsie nowy projekt, wypełniając okna kreatora wartościami pól zgodnie z rysunkami 4.4 – 4.6. Utworzony projekt zawiera kilka nowych plików oraz obiektów wartych naszego zainteresowania. Ponieważ jest to pełna gra, kod jest skomplikowany, niemniej zrozumienie go w całości nie będzie wcale konieczne. Musimy jedynie skupić się na implementacji klasy JetPlayer. Oto krótka lista plików, z którymi będziemy pracować: • JetBoy.zip — plik umieszczony został w katalogu JetBoy_content. Zawiera on sekwencje MIDI oraz inne informacje niezbędne do strumieniowania muzyki. • Level1.jtc — plik umieszczony w katalogu res/raw. Został utworzony przez program JET Creator i zawiera instrukcje dotyczące sposobu odtwarzania dźwięku. • Asteroid.java — klasa Asteroid zawierająca pewne zmienne. • Explosion.java — klasa Explosion obsługująca zmienne eksplozji. • JetBoy.java — główna aktywność przesyłająca większość logiki obsługującej program do klasy JetBoyView. • JetBoyView.java — największa część kodu, działająca z klasą JetPlayer odtwarzającą muzykę oraz zawierająca dodatkowy kod obsługujący logikę gry. Aby umożliwić pełne zrozumienie tej implementacji, najważniejsze metody z pliku JetBoyView.java zostały skopiowane na listingu 4.6. Tuż za listingiem umieszczony został ich krótki opis. Listing 4.6. Metoda initializeJetPlayer private void initializeJetPlayer() { mJet = JetPlayer.getJetPlayer(); mJetPlaying = false;
89
ROZDZIAŁ 4. DODAWANIE EFEKTÓW DŹWIĘKOWYCH, MUZYKI ORAZ SEKWENCJI FILMOWYCH
Rysunek 4.4. Testowanie projektu JetBoy umieszczonego w przykładach dołączonych do systemu Android — krok 1.
Rysunek 4.5. Testowanie projektu JetBoy umieszczonego w przykładach dołączonych do systemu Android — krok 2. 90
ZARZĄDZANIE OBSŁUGĄ MUZYKI
Rysunek 4.6. Testowanie projektu JetBoy umieszczonego w przykładach dołączonych do systemu Android — krok 3. mJet.clearQueue(); mJet.setEventListener(this); Log.d(TAG, "opening jet file"); mJet.loadJetFile(mContext.getResources().openRawResourceFd(R.raw.level1)); Log.d(TAG, "opening jet file DONE"); mCurrentBed = 0; byte sSegmentID = 0; Log.d(TAG, " start queuing jet file"); mJet.queueJetSegment(0, 0, 0, 0, 0, sSegmentID); mJet.queueJetSegment(1, 0, 4, 0, 0, sSegmentID); mJet.queueJetSegment(1, 0, 4, 1, 0, sSegmentID); mJet.setMuteArray(muteMask[0], true); Log.d(TAG, " start queuing jet file DONE"); }
A oto w jaki sposób działa metoda initializeJetPlayer. Najpierw czyści ona kolejkę ze wszystkich poprzednich plików. W ten sposób przygotowuje miejsce na przyszłe operacje. W dalszej kolejności metoda pobiera opisany przez nas wcześniej plik zawierający potrzebne informacje. Pamiętajmy, że plik został utworzony przez aplikację JET Creator. Początkowa sekwencja jest ustawiana na wartość 0. Ciekawym miejscem funkcji jest wywołanie metody o nazwie queueJetSegment(), która pobiera sekwencję MIDI. Metoda ta jest wyposażona w długi zestaw obsługiwanych parametrów modyfikujących odtwarzany dźwięk. Zostały one objaśnione w tabeli 4.1 pobranej z pakietu Android SDK. Dodawanie segmentów zacznie mieć większy sens w chwili, gdy przyjrzymy się implementacji. Na listingu 4.7 znajduje się kod funkcji run() oraz updateGameState() umieszczonych w pliku JetBoyView.java.
91
ROZDZIAŁ 4. DODAWANIE EFEKTÓW DŹWIĘKOWYCH, MUZYKI ORAZ SEKWENCJI FILMOWYCH
Tabela 4.1. Parametry metody queueJetSegment() pobrane z dokumentacji Androida Parametr
Opis
segmentNum
Identyfikator segmentu.
libNum
Indeks banku dźwięku skojarzony z segmentem. Użycie wartości -1 sygnalizuje, że z danym segmentem nie jest skojarzony żaden bank dźwięku (plik DLS). W takim przypadku program JET używa ogólnej biblioteki MIDI (ang. general MIDI).
repeatCount
Liczba powtórzeń danego segmentu. Wartość 0 oznacza, że segment zostanie odtworzony tylko raz, wartość -1 oznacza nieskończone powtarzanie segmentu.
transpose
Wielkość transpozycji dźwięku. Wartość 0 oznacza normalne odtwarzanie dźwięku. Wartość może być z zakresu -12 do 12.
muteFlags
Maska bitowa określająca, które ścieżki MIDI podczas odtwarzania są wyciszone. Bit 0 dotyczy ścieżki 0, bit 1 ścieżki 1 itd.
userID
Wartość określana przez aplikację, identyfikująca jednoznacznie dany segment. Wartość ta jest uzyskiwana w metodzie nasłuchującej zdarzenia onJetUserIdUpdate(JetPlayer, int, int). Zazwyczaj aplikacja przechowuje tu wartość typu byte zwiększaną za każdym razem, gdy do kolejki dodawany jest nowy segment. Wartość ta może być wykorzystana do odszukania charakterystycznych cech utworu, jak na przykład flag określających wyciszenie.
Listing 4.7. Pętla gry programu JetBoy public void run() { while (mRun) { Canvas c = null; if (mState == STATE_RUNNING) { updateGameState(); if (!mJetPlaying) { mInitialized = false; Log.d(TAG, "------> STARTING JET PLAY"); mJet.play(); mJetPlaying = true; } mPassedTime = System.currentTimeMillis(); if (mTimerTask == null) { mTimerTask = new TimerTask() { public void run() { doCountDown(); } }; mTimer.schedule(mTimerTask, mTaskIntervalInMillis); } } else if (mState == STATE_PLAY && !mInitialized) { setInitialGameState(); } else if (mState == STATE_LOSE) { mInitialized = false; } try { c = mSurfaceHolder.lockCanvas(null); doDraw(c);
92
ZARZĄDZANIE OBSŁUGĄ MUZYKI
} finally { if (c != null) { mSurfaceHolder.unlockCanvasAndPost(c); } } } } /** * Ta metoda obsługuje uaktualnienie modelu stanu gry. Nie jest tu wykonywane * żadne tworzenie widoku, a jedynie przetwarzanie danych wejściowych oraz * uaktualnianie stanu gry. * Metoda uaktualnia położenie wszystkich obiektów w grze (asteroidów, graczy, * eksplozji), ich stanu (klatek animacji, zderzeń), tworzenia nowych * obiektów itd. */ protected void updateGameState() { while (true) { GameEvent event = mEventQueue.poll(); if (event == null) break; if (event instanceof KeyGameEvent) { mKeyContext = processKeyEvent((KeyGameEvent)event, mKeyContext); updateLaser(mKeyContext); } else if (event instanceof JetGameEvent) { JetGameEvent jetEvent = (JetGameEvent)event; if (jetEvent.value == TIMER_EVENT) { mLastBeatTime = System.currentTimeMillis(); updateLaser(mKeyContext); updateExplosions(mKeyContext); updateAsteroids(mKeyContext); } processJetEvent(jetEvent.player, jetEvent.segment, jetEvent.track, jetEvent.channel, jetEvent.controller, jetEvent.value); } } }
Mimo że cały kod jest trudny, do samej klasy JetPlayer wprowadzonych musi być właściwie bardzo niewiele zmian. Zaraz po ich dodaniu inne gry nie będą potrzebować już żadnych istotnych modyfikacji. Zauważmy, że w funkcji run() umieszczona jest metoda mjet.play(). Powoduje ona inicjalizację sekwencji dźwiękowych przeznaczonych do odtworzenia. Metoda updateGameState() wprowadza zmiany do klasy JetPlayer, zmieniając zdarzenie jetEvent. W tym miejscu następuje również obsługa dźwięków eksplozji, lasera oraz asteroidów. Uaktualnianie bieżącego zdarzenia jest bardzo proste. Zdarzenie jest zrzucane do formatu JetGameEvent. Ostatecznie w ostatnim wierszu wywoływana jest funkcja określająca reakcję ścieżki dźwiękowej na nowe zdarzenie. Kiedy Czytelnik zrozumie już zasadę działania klasy JetPlayer, będzie gotowy, aby zaimplementować ją w swoim programie, modyfikując w niewielkim stopniu grę JetBoy. Jeżeli jednak Czytelnik nie jest pewien, w jaki sposób działa kod, to nie powinien się przejmować. Obsługiwanie dźwięków w zaprezentowany sposób jest wprawdzie ciekawe, ale nie jest krytycznym aspektem możliwości dźwiękowych Androida.
93
ROZDZIAŁ 4. DODAWANIE EFEKTÓW DŹWIĘKOWYCH, MUZYKI ORAZ SEKWENCJI FILMOWYCH
Podsumowanie W tym rozdziale Czytelnik zapoznał się z możliwościami multimedialnymi systemu Android, włączając w to odtwarzanie efektów dźwiękowych, muzyki oraz sekwencji filmowych. Sprawdziliśmy również, w jaki sposób te media mogą być dołączone do gry. Oczywiście wszystkie te funkcje zostały opisane bardzo pobieżnie. Czytelnik będzie wciąż zapoznawał się z ich zaawansowanymi cechami w miarę tworzenia swoich gier. Te ostatnie mogą być bardzo pasjonujące dzięki poprawnej implementacji efektów dźwiękowych, muzyki oraz sekwencji filmowych. Ponieważ do odkrycia pozostały jeszcze bardziej pasjonujące funkcje systemu Android, w kolejnym rozdziale zajmiemy się technikami nadającymi grze jeszcze bardziej realistyczny charakter.
94
ROZDZIAŁ 5
Tworzenie jednoosobowej gry z utrudnieniami Teraz gdy przyjrzeliśmy się już, w jaki sposób w grach przeznaczonych na tablety można obsługiwać grafikę, dźwięki oraz polecenia użytkownika, dysponujemy wszystkimi składnikami umożliwiającymi utworzenie prostej gry. W tym rozdziale połączymy je wspólnie w całość, tworząc prosty przykład gry, jednocześnie przygotowując się na prawdziwie niesamowite programy. Projektując nawet najprostszą grę, musimy być przygotowani do śledzenia ruchu sprajtów, zastosowania do ich ruchu prostych praw fizyki, a także połączenia tych elementów w sposób zachęcający użytkownika do dalszej gry. W tym rozdziale stworzymy grę jednoosobową z niewielkimi utrudnieniami dla gracza. W rezultacie otrzymamy prostą, aczkolwiek interesującą dla użytkownika grę. Efekt ten uzyskamy dzięki wykorzystaniu sprajtów. Główną część tego rozdziału stanowić będzie tworzenie interakcji pomiędzy użytkownikiem a sprajtami, jak również pomiędzy samymi sprajtami. W kolejnym podrozdziale Czytelnik dowie się, w jaki sposób opracować zasady pierwszej prawdziwej gry.
Planowanie gry jednoosobowej — AllTogether W naszej pierwszej grywalnej grze możemy stworzyć pole z bombami oraz bohatera, którego zadaniem będzie przedostać się z jednej strony pola gry na drugą bez dotykania żadnej z bomb. Aby jeszcze bardziej uatrakcyjnić grę, możemy bomby wprawić w ruch. Nazwijmy tę grę AllTogether, ponieważ będzie ona łączyć w całość wszystkie zagadnienia omówione do tej pory. Zanim zajmiemy się opracowaniem kodu, musimy parę rzeczy zaplanować. I tak na przykład poniżej wymienione są pewne wspólne elementy, typowe dla większości grywalnych gier. Oczywiście nie wszystkie gry je mają, ale w typowych grach można oczekiwać wystąpienia większości z nich: • Bohater gry sterowany przez gracza, napotykający w grze przeróżne utrudnienia oraz wyzwania, które w trakcie gry muszą być przez niego przezwyciężone. • Wyraźne konsekwencje nieporadzenia sobie przez bohatera gry z utrudnieniami. • Nagroda za pomyślne ukończenie zadania. Wszystkie powyższe elementy mogą wyglądać na zbyt oczywiste, są jednak niezbędne do nadania grze poprawnego kształtu. Zauważmy, że pierwsze kryterium nie jest odpowiednie dla gier strategicznych, w których gracz kontroluje cały świat gry. Grami strategicznymi zajmiemy się w rozdziale 9., w którym przedstawiony zostanie sposób ich programowania. Na pierwszy element z listy przypada około 90% pracy nad kodem. Jeżeli Czytelnik przypomni sobie ulubioną grę, to prawie wszystkie wspomnienia związane będą z podróżą w grze oraz z rozwiązywaniem
ROZDZIAŁ 5. TWORZENIE JEDNOOSOBOWEJ GRY Z UTRUDNIENIAMI
odpowiednich zadań, a następnie z osiąganiem kolejnych poziomów. Ostatnie dwa etapy gry mijają zwykle bardzo szybko i w bardzo niewielkim stopniu określają jej charakter. Porażkę w grze może oznaczać wyczerpanie się zapasu powietrza pod wodą, co spowoduje śmierć bohatera. Może też zabraknąć czasu przeznaczonego na ukończenie poziomu, co będzie oznaczać konieczność rozpoczęcia go od początku. Sukces w grze jest oczywiście związany z dotarciem do końca rozgrywki lub też zabiciem ostatecznego przeciwnika. Gra stworzona na potrzeby tego przykładu wykorzystuje jedynie pierwsze dwa elementy z listy. Ostatniemu z nich przyjrzymy się w dalszej części tego rozdziału. Mówiąc najprościej, będziemy tworzyć grę, w której gracz będzie musiał poradzić sobie z trzema obiektami poruszającymi się w górę i w dół. Gracz będzie musiał ostrożnie skoordynować wzajemne położenie bomb, a następnie wykazać się refleksem, aby szybko przejść pomiędzy nimi. W przypadku gdy gracz dotknie którejś z bomb, postać zostanie przeniesiona z powrotem do punktu wyjścia i gracz będzie mógł rozpocząć rozgrywkę od nowa. Gra będzie działać do chwili, w której gracz znudzi się i ją wyłączy. Oczywiście po zapoznaniu się z kodem Czytelnik będzie w stanie uwzględnić dodatkowe funkcje, jak na przykład wyświetlanie komunikatu o zwycięstwie, tak aby gracz miał satysfakcję z osiągnięcia tego etapu. Ostateczny wygląd gry przedstawiony jest na rysunku 5.1.
Rysunek 5.1. Ostateczny wygląd gry Po tym krótkim wstępie do gry będziemy gotowi, aby zająć się jej utworzeniem.
Tworzenie gry jednoosobowej Ponieważ większość pracy została już wykonana w poprzednich rozdziałach, do utworzenia pierwszej prawdziwej gry konieczne będzie już tylko wykonanie niewielkiej liczby czynności. Jedynymi plikami z przykładu w poprzednim rozdziale, które będzie trzeba zmienić, są SpriteObject.java oraz GameView.java. W tym celu należy:
96
TWORZENIE GRY JEDNOOSOBOWEJ
1. Otworzyć nowy projekt Eclipse i nadać mu nazwę AllTogether. 2. Skopiować wszystkie pliki z projektu SoundsTest z rozdziału 4. Nie należy również zapominać o skopiowaniu obu plików źródłowych Javy z katalogu src oraz plików zasobów z katalogu resources. Zanim zaczniemy wprowadzać jakiekolwiek zmiany, spójrzmy na procedurę obsługi ruchu oraz kolizji.
Ulepszanie sprajtów gry Rozpoczniemy od ulepszenia sprajtów w taki sposób, aby można było lepiej kontrolować ich ruch oraz wykrywać kolizje pomiędzy sprajtami oraz granicami obszaru gry. Ta cecha sprajtów będzie wykorzystywana od tej chwili w każdym zadaniu.
Dodawanie bardziej precyzyjnej kontroli ruchu Szybkość, z jaką utworzyliśmy ostatnią aplikację, byłą z pewnością dla potrzeb nowej gry zbyt duża. Aby ulepszyć kontrolę nad postacią, zwiększmy w tym momencie precyzję zmiennych przechowujących położenie sprajta, a także wielkość każdego kroku. Cel ten możemy osiągnąć, przekształcając zmienne określające ruch oraz położenie sprajta do typu Javy o nazwie double. W tym momencie zamiast używać do zwiększania oraz zmniejszania szybkości sprajta jedynie wartości całkowitych, będziemy mogli użyć wartości rzeczywistych. Taka cecha programu będzie krytyczna w przypadku, gdy będziemy chcieli zmniejszyć szybkość poruszania się postaci. Do zmiany szybkości ruchu sprajta nowa gra używać będzie wielokrotności 0,5. Zastosowanie tej wartości nie było możliwe w rozdziale 4., gdyż najmniejszą wielokrotnością wartości ruchu była tam liczba 1. Aby użyć nowej wartości, zmieńmy funkcje w klasie sprajta, a także deklaracje zmiennych. Rozpocznijmy od otwarcia pliku SpriteObject.java i dodania do definicji klasy SpriteObject kodu umieszczonego na listingu 5.1. Listing 5.1. Zwiększanie dokładności wartości położenia oraz szybkości sprajta private private private private
double double double double
x; y; x_move = 0; y_move = 0;
Teraz potrzebować będziemy jeszcze nowego kodu wykrywającego kolizje pomiędzy obiektami. Wykrywanie kolizji jest kluczowym aspektem prawie każdej gry wideo.
Wykrywanie kolizji pomiędzy sprajtami Kolejna wielka zmiana w programie wymaga dodania do klasy SpriteObject prawie całkowicie nowej funkcji kontrolującej kolizje. Jeżeli Czytelnik uprzednio zajmował się już wykrywaniem kolizji w programach 2D, rozwiązanie będzie wyglądać znajomo. W celu wykrycia kolizji funkcja będzie sprawdzać współrzędne dwóch prostokątów. Jak sobie przypominamy, współrzędne początkowe ekranu w Androidzie zaczynają się w lewym górnym rogu, dlatego też w przypadku, gdy dół jednego ze sprajtów ma współrzędną niższą od góry innego sprajta, kolizja nie będzie zachodzić, ponieważ pierwszy sprajt znajdzie się ponad drugim. Jeżeli pomiędzy dwoma sprajtami wystąpi kolizja, nowa metoda zwróci wartość true. Co ciekawe, w trakcie badania warunków kolizji do odczytania szerokości sprajta używane są dane z reprezentującej go mapy bitowej. Klasa sprajta nie przechowuje bezpośrednio wartości określających wysokość oraz szerokość, ponieważ obie dane zawarte są w mapie bitowej, reprezentującej grafikę rastrową obiektu sprajta. Analogiczne podejście wykorzystamy później do rozpatrywania ewentualnej kolizji sprajta ze ścianami.
97
ROZDZIAŁ 5. TWORZENIE JEDNOOSOBOWEJ GRY Z UTRUDNIENIAMI
Podobnie jak w przypadku dowolnej innej funkcji wymagającej wielu instrukcji if, kod wykrywający kolizje jest pod względem kosztów obliczeniowych całkiem wymagający. W każdym możliwym przypadku będziemy więc chcieli pozbyć się niepotrzebnych procedur wykrywania kolizji. Niemniej zaprezentowany sposób jest i tak znacznie lepszy od sprawdzania występowania kolizji piksel po pikselu, co mogłoby spowodować niemal całkowite zamrożenie działania gry. Do klasy SpriteObject dodajmy teraz funkcję umieszczoną na listingu 5.2. Listing 5.2. Funkcja wykrywania kolizji z klasy SpriteObject public boolean collide(SpriteObject entity){ double left, entity_left; double right, entity_right; double top, entity_top; double bottom, entity_bottom; left = x; entity_left = entity.getX(); right = x + bitmap.getWidth(); entity_right = entity.getX() + entity.getBitmap().getWidth(); top = y; entity_top = entity.getY(); bottom = y + bitmap.getHeight(); entity_bottom = entity.getY() + entity.getBitmap().getHeight(); if (bottom < entity_top) { return false; } if (top > entity_bottom){ return false; } if (right < entity_left) { return false; } if (left > entity_right){ return false; } return true; }
W kodzie przedstawionym na listingu 5.2 pobierane są współrzędne każdego z wierzchołków obu sprajtów. Pamiętajmy przy tym, że jeden sprajt wywołuje funkcję i jako jej parametru używa obiektu drugiego sprajta. Nie ma przy tym żadnego znaczenia, który sprajt będzie tym wywołującym. Rezultat będzie zawsze ten sam, niezależnie od tego, czy funkcja zwróci wartość true, czy false. Po uzyskaniu danych o współrzędnych można wykonać cztery instrukcje if. Sprawdzą one, czy dół pierwszego sprajta znajduje się poniżej góry drugiego. W takiej sytuacji pierwszy sprajt znajdowałby się powyżej drugiego i kolizja pomiędzy nimi nie byłaby możliwa. Kolejne instrukcje if przypominają poprzednie, ale sprawdzają przesunięcie sprajtów względem siebie. Jeśli żaden z warunków instrukcji if nie będzie spełniony, oznaczać to będzie wystąpienie kolizji pomiędzy sprajtami.
98
TWORZENIE GRY JEDNOOSOBOWEJ
Dodawanie wielu sprajtów Zasadnicze zmiany wprowadzimy w klasie GameView, w której znacznie zmodyfikujemy kod uaktualniający położenie obiektów w grze. Prawdopodobnie najbardziej istotną zmianą kodu będzie wprowadzenie tablicy obiektów klasy SpriteObject o nazwie bomb[]. Ponieważ wszystkie bomby zachowują się tak samo, zdecydowanie wygodniej jest zgrupować je w ten sposób, zamiast obsługiwać każdą z nich oddzielnie. Dzięki temu pozbywamy się również niepotrzebnych powtórzeń kodu. Inicjalizacja każdego z nowych sprajtów oznaczających bomby jest również interesująca ze względu na ich położenie na ekranie. Pierwszy oraz ostatni sprajt znajdują się u dołu ekranu, podczas gdy drugi jest prawie na samej górze. Taki rozproszony ruch obiektów powoduje zwiększenie trudności gry. Kiedy spojrzymy na funkcję surfaceCreated, zauważymy, że pierwsza oraz ostatnia bomba przesuwają się do góry ekranu, podczas gdy środkowa w kierunku jego dołu. Podczas definiowania ruchu bomb zaczniemy używać nowych zmiennych z klasy sprajta obsługujących wartości dziesiętne. Po wykonaniu paru testów wartość 1 reprezentująca szybkość sprajta okazała się zbyt duża, dlatego zostanie zmniejszona do 0.5. W celu umieszczenia bomb na ekranie funkcja onDraw używa szybkiej pętli przeszukującej tablicę z trzema bombami. W funkcji update zawiera się cała magia gry. To w tej funkcji definiuje się związek pomiędzy bohaterem gry a bombami, jak również zachowanie tych ostatnich. Pierwsze dwie pętle dbają o to, aby bomby pozostawały w obszarze gry. W tym celu chcielibyśmy, aby bomby zmieniały kierunek poruszania się w sytuacji, gdy wartość ich współrzędnej y przekroczy 100 lub 500. Kolejna pętla sprawdza, czy nastąpiła kolizja głównego sprajta z dowolną z bomb. W takim przypadku położenie sprajta jest zerowane do położenia początkowego. Uaktualnijmy teraz kod funkcji update, kopiując do niej kod przedstawiony na listingu 5.3. Listing 5.3. Nowa postać funkcji update() nadzorującej bomby //sprawdzenie, czy bomby nie znajdują się zbyt nisko for(int i = 0; i < 3; i++){ if(bomb[i].getY() > 500){ bomb[i].setMoveY(-.5); } } //sprawdzenie, czy bomby nie znajdują się zbyt wysoko for(int i = 0; i < 3; i++){ if(bomb[i].getY() < 100){ bomb[i].setMoveY(.5); } } //sprawdzenie kolizji sprajta for(int i = 0; i < 3; i++){ if(spritecharacter.collide(bomb[i])){ charactersprite.setX(100); } } //wykonanie odpowiednich akcji for(int i = 0; i < 3; i++){ bomb[i].update(adj_mov); } spritecharacter.update(adj_mov);
Ostatecznie na koniec wywoływane są funkcję update dla obiektów bomb oraz sprajta. Pewne kluczowe zmiany zostały również wprowadzone do funkcji processMotionEvent przedstawionej na listingu 5.4. Umieszczone w niej dwie instrukcje if sprawdzają wystąpienie zdarzeń polegających
99
ROZDZIAŁ 5. TWORZENIE JEDNOOSOBOWEJ GRY Z UTRUDNIENIAMI
na dotknięciu przez użytkownika ekranu tabletu lub jego puszczeniu. W sytuacji gdy użytkownik dotknie ekranu, sprajt przesunie się naprzód. W przeciwnym razie zatrzyma się w swoim bieżącym położeniu. Ten sposób kontrolowania ruchu przypomina grę z helikopterem, w której użytkownik podróżuje po jaskini. Helikopter porusza się w stronę ziemi, a w chwili gdy gracz dotknie ekranu, leci do góry. Listing 5.4. Metoda processMotionEvent() zarządza obsługą zdarzeń ekranowych if(input.action == InputObjectinput .ACTION_TOUCH_DOWN){ spritecharacter.setMoveX(.5); } if(input.action == InputObjectinput .ACTION_TOUCH_UP){ charactersprite.setMoveX(0); }
W tym momencie zakończyliśmy modyfikację logiki gry. Teraz przyjrzyjmy się grafice.
Dodawanie grafiki do sprajtów Nasza ciężka praca zacznie za moment przynosić efekty. Zanim jednak skompilujemy projekt, musimy do niego dodać dwa zasoby: grafikę bomby oraz obrazek reprezentujący główną postać gry (lub gracza). Obie grafiki zapisane są w postaci plików .png, obrazek dla gracza używa przezroczystego koloru tła, aby nie zakłócał wyświetlania innych obiektów podczas poruszania się. Wymiary bomb to 30×30 pikseli, a bohatera 70×120 pikseli. Wskazówka W tym momencie nie przejmujmy się tym, że grafika nie jest doskonała. Celem tego zadania jest przygotowanie początkowego programu, z którym można będzie później dalej pracować. Dobrą strategią ulepszenia gry będzie narysowanie postaci na zwykłej kartce papieru, a następnie jej zeskanowanie. Rysunek będzie można później ulepszyć w programie graficznym. Inną metodą będzie poznanie obsługi programu graficznego do tworzenia grafiki wektorowej, co może okazać się także wielkim usprawnieniem procesu.
Teraz skompilujmy oraz uruchommy program w emulatorze w sposób analogiczny do każdej poprzedniej aplikacji. Jeżeli wszystko pójdzie pomyślnie, naciskając ekran, powinniśmy być w stanie spowodować ruch postaci do przodu. Po dotknięciu bomby gra rozpocznie się od nowa. Powodzenia! W kolejnym podrozdziale zajmiemy się zwiększaniem satysfakcji z gry, dodając nagrodę.
Dodawanie nagrody za ukończenie gry Istnieje kilka kluczowych elementów rozgrywki dodanych do tej prostej gry: • Dodaliśmy utrudnienia w postaci bomb. Co więcej, trudność gry zwiększa się przez niezbyt precyzyjne sterowanie. • Konsekwencją porażki jest powrót do początku gry. Jest on dodatkowo bardziej bolesny, jeżeli stanie się to przy ostatniej bombie. • Dodanie postaci przypominającej człowieka dodatkowo zwiększa zainteresowanie graczy. Zrobiliśmy to, aby nie przesuwać dłużej po ekranie obrazka gwiazdy, jak miało to miejsce w poprzednich rozdziałach. Grę można dodatkowo ulepszyć, dodając jakąś realną korzyść jako nagrodę za wygraną. W tym celu spróbujmy zmodyfikować sprajta w taki sposób, aby przypominał sprajta przedstawionego na rysunku 5.2, a następnie, w chwili gdy gracz osiągnie punkt o odpowiednio wysokiej współrzędnej x, wywołajmy jego funkcję draw(). Ustawmy też wartość jego zmiennej na true, aby napis był wciąż wyświetlany, co pozwoli
100
TWORZENIE GRY JEDNOOSOBOWEJ
Rysunek 5.2. Nagroda dla gracza graczowi cieszyć się pełnią chwały. Końcowa wersja kodu dla tego rozdziału nie zawiera obsługi tego zdarzenia, ponieważ nie jest ono istotnym zagadnieniem poruszanym w tym miejscu. Niemniej we własnym zakresie Czytelnik może do woli eksperymentować z kodem.
Śledzenie stanu sprajtów Ponieważ sprajt (lub też cała gra) może przyjmować różne wartości stanów, konieczne będzie stworzenie sposobu ich śledzenia. Aby wyobrazić sobie kolejne stany, przejrzyjmy się rysunkowi 5.3. Został na nim przedstawiony cykl następowania po sobie trzech różnych stanów.
Rysunek 5.3. Cykl następowania po sobie stanów Jak widać na rysunku 5.3, stany mogą w trakcie gry ulegać zmianie. Również gry przechodzą pewien cykl życia, począwszy od momentu rozpoczęcia, przez pętle, a skończywszy na fazie zakończenia gry. Podobnie jak w wielu innych środowiskach, także w systemie Android stany definiowane są za pomocą stałych o wartościach całkowitych, które to wartości mogą być odczytane z wielu różnych klas. Uwaga! W rzeczywistości stanów używaliśmy już w momencie, gdy usiłowaliśmy sprawdzić, jakiego rodzaju ruch miał miejsce. W instrukcji if następowało sprawdzenie, czy eventtype był akcją typu ACTION_KEY_UP, czy ACTION_KEY_DOWN, z których obie były wartościami liczbowymi całkowitymi, zdefiniowanymi w klasie InputObject.
101
ROZDZIAŁ 5. TWORZENIE JEDNOOSOBOWEJ GRY Z UTRUDNIENIAMI
Cały kod umieszczony w tym podrozdziale powinien zostać dodany do klasy SpriteObject, w której następuje obsługa stanów każdego ze sprajtów. Sprajty takie jak bomby niekoniecznie muszą mieć różne stany, dlatego też te funkcje nie będą miały do nich zastosowania. Czytelnik może również chcieć zdefiniować w swoich własnych grach oddzielne klasy sprajtów, dziedziczących proste funkcje z bardziej ogólnych obiektów, a następnie w podklasach sprajtów rozróżniać bardziej szczegółowe metody oraz zmienne. W tym momencie wykonajmy następujące czynności: 1. Na początku pliku SpriteObject.java zdefiniujmy cztery podstawowe stany o wartościach będących liczbami całkowitymi (patrz listing 5.5). Listing 5.5. Stałe reprezentujące stany sprajtów public public public public
int int int int
DEAD = 0; //nieżywy ALIVE = 1; //żywy JUMPING = 2; //skacze CROUCHING = 3; //skrada się
2. Zawsze istnieje pokusa, aby stan DEAD reprezentowany był przez wartość 0, ponieważ jest to często wartość dla stanu domyślnego i sensowne wydaje się, aby w celu ożywienia sprajta (np. podczas inicjalizacji danego poziomu), należało wykonać pewne czynności. 3. Kolejną ważną cechą stanów jest to, że powinny się wykluczać. Oznacza to, że bohater nie może znajdować się w danej chwili w więcej niż jednym stanie. Bohaterowie będą zawsze rozpoczynać od początkowego stanu DEAD, dopóki nie zostanie on jawnie zmieniony. Od tego czasu będą oni w stanie ALIVE do momentu, gdy zostaną zabici lub wykonają dodatkową akcję, jak chociażby podskok. 4. Aby obsługiwać stany sprajtów, należy stworzyć dwie krótkie funkcje. W tym celu w pliku SpriteObject.java wystarczy umieścić funkcje przedstawione na listingu 5.6. Listing 5.6. Funkcje getstate() oraz setstate() public int getstate(){ return state; } public void setstate(int s){ state = s; }
5. Powyższe funkcje powinny wyglądać znajomo, ponieważ właśnie w taki sposób odczytywane są współrzędne x oraz y sprajta. 6. Ponieważ zdefiniowane stany są reprezentowane przez publiczne wartości całkowitoliczbowe, można w bardzo prosty sposób sprawdzić, czy sprajt jest w stanie „nieżywy”, umieszczając w tym celu kod z listingu 5.7 w pliku GameView.java. Kod należy umieścić w funkcji update. Listing 5.7. Zerowanie stanu bohatera po jego śmierci if(character.getstate() == SpriteObject.DEAD){ character.setX(100); character.setY(400); }
7. Zauważmy, w jak prosty sposób możemy obsłużyć podstawowe informacje o sprajcie, jak chociażby jego aktualne zachowanie. Stanie się to jeszcze bardziej istotne, kiedy przyjrzymy się bardziej złożonym stanom, takim jak podskoki. W tym przypadku prędkość ma największą wartość, gdy sprajt odrywa się od ziemi. Następnie wartość ta zmniejsza się stopniowo do chwili, gdy sprajt
102
TWORZENIE GRY JEDNOOSOBOWEJ
osiągnie swoją maksymalną wysokość, po czym spadając, będzie znów nabierać prędkości. Zmieniająca się prędkość sprajta musi być kontrolowana wewnątrz funkcji update. W tym celu musimy dowiedzieć się, w jakim stanie znajduje się sprajt, aby następnie móc nadać zmiennej moveY odpowiednią wartość. 8. Zwykły skok na przykład trwa przewidywalną długość czasu. Co stanie się jednak, jeżeli zostanie on przerwany przez dotknięcie platformy? W takiej sytuacji konieczne będzie ponowne sprawdzenie stanu w celu dokonania oceny nowej sytuacji. 9. Aby całkowicie dołączyć obsługę stanów do gry, należy skopiować kod z listingu 5.8 do instrukcji if sprawdzającej wystąpienie kolizji pomiędzy bohaterem a bombą. Jest to kolejny (poza wykonaniem tego zadania w kodzie wykrywającym kolizję) sposób na wyzerowanie położenia bohatera po uderzeniu w bombę. Listing 5.8. Nadawanie początkowego stanu bohaterowi character.setState(SpriteObject.DEAD);
Cała opisana logika jest umieszczona w kodzie na listingu 5.9 oraz 5.10. Jeżeli w jakimś momencie Czytelnik poczuje się zagubiony, może wykorzystać ten kod w swoim projekcie i w ten sposób uzyskać w pełni działającą grę. Listing 5.9. Plik SpriteObject.java package com.gameproject.alltogether; import android.graphics.Bitmap; import android.graphics.Canvas; public class SpriteObject { public public public public
int int int int
DEAD = 0; //nieżywy ALIVE = 1; //żywy JUMPING = 2; //skacze CROUCHING = 3; //skrada się
private Bitmap bitmap; private double private double private double private double
x; y; x_move = 0; y_move = 0;
public SpriteObject(Bitmap bitmap, int x, int y) { this.bitmap = bitmap; this.x = x; this.y = y; } public double getX() { return x; } public double getY() { return y; } public Bitmap getBitmap() {
103
ROZDZIAŁ 5. TWORZENIE JEDNOOSOBOWEJ GRY Z UTRUDNIENIAMI
return bitmap; } public void setMoveX(double speedx){ x_move = speedx; } public void setMoveY(double speedy){ y_move = speedy; } public void setX(int x) { this.x = x; } public void setY(int y) { this.y = y; } public void setBitmap(Bitmap bitmap) { this.bitmap = bitmap; } public int getstate(){ return state; } public void setstate(int s){ state = s; }
public void draw(Canvas canvas) { canvas.drawBitmap(bitmap, (int)x - (bitmap.getWidth() / 2), (int)y ´- (bitmap.getHeight() / 2), null); } public void update(int adj_mov) { x += (adj_mov * x_move); y += (adj_mov * y_move); } public boolean double double double double
collide(SpriteObject entity){ left, entity_left; right, entity_right; top, entity_top; bottom, entity_bottom;
left = x; entity_left = entity.getX(); right = x + bitmap.getWidth(); entity_right = entity.getX() + entity.getBitmap().getWidth(); top = y; entity_top = entity.getY(); bottom = y + bitmap.getHeight(); entity_bottom = entity.getY() + entity.getBitmap().getHeight();
104
TWORZENIE GRY JEDNOOSOBOWEJ
if (bottom < entity_top) { return false; } if (top > entity_bottom){ return false; } if (right < entity_left) { return false; } if (left > entity_right){ return false; } return true; } }
A teraz spójrzmy na kod zawarty w pliku GameView.java, który wykorzystuje ulepszone sprajty. Listing 5.10. Kompletny kod w pliku GameView.java package com.gameproject.alltogether; import java.util.concurrent.ArrayBlockingQueue; import import import import import import import import import import import
android.content.Context; android.graphics.BitmapFactory; android.graphics.Canvas; android.graphics.Color; android.media.AudioManager; android.media.MediaPlayer; android.media.SoundPool; android.util.Log; android.view.MotionEvent; android.view.SurfaceHolder; android.view.SurfaceView;
public class GameView extends SurfaceView implements SurfaceHolder.Callback { private SpriteObject character; private SpriteObject[] bomb; private GameLogic mGameLogic; private ArrayBlockingQueue inputObjectPool; private private private private private private private
int sound_id; Context context; SoundPool soundPool; int ID_robot_noise; int ID_alien_noise; int ID_human_noise; MediaPlayer mp;
public GameView(Context con) {
105
ROZDZIAŁ 5. TWORZENIE JEDNOOSOBOWEJ GRY Z UTRUDNIENIAMI
super(con); context = con; getHolder().addCallback(this); character = new SpriteObject(BitmapFactory.decodeResource(getResources(), ´R.drawable.sprite), 100, 400); bomb = new SpriteObject[3]; bomb[0] = new SpriteObject(BitmapFactory.decodeResource(getResources(), ´R.drawable.bomb), 400, 500); bomb[1] = new SpriteObject(BitmapFactory.decodeResource(getResources(), ´R.drawable.bomb), 650, 100); bomb[2] = new SpriteObject(BitmapFactory.decodeResource(getResources(), ´R.drawable.bomb), 900, 500); mGameLogic = new GameLogic(getHolder(), this); createInputObjectPool(); soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 0); ID_robot_noise = soundPool.load(context, R.raw.robot_noise, 1); ID_alien_noise = soundPool.load(context, R.raw.alien_noise, 2); ID_human_noise = soundPool.load(context, R.raw.human_noise, 3); sound_id = ID_robot_noise;
setFocusable(true); } private void createInputObjectPool() { inputObjectPool = new ArrayBlockingQueue(20); for (int i = 0; i < 20; i++) { inputObjectPool.add(new InputObject(inputObjectPool)); } }
@Override public boolean onTouchEvent(MotionEvent event) { try { int hist = event.getHistorySize(); if (hist > 0) { for (int i = 0; i < hist; i++) { InputObject input = inputObjectPool.take(); input.useEventHistory(event, i); mGameLogic.feedInput(input); } } InputObject input = inputObjectPool.take(); input.useEvent(event); mGameLogic.feedInput(input); } catch (InterruptedException e) { } try {
106
TWORZENIE GRY JEDNOOSOBOWEJ
Thread.sleep(16); } catch (InterruptedException e) { } return true; } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceCreated(SurfaceHolder holder) { mGameLogic.setGameState(mGameLogic.RUNNING); mGameLogic.start(); bomb[0].setMoveY(-.5); bomb[1].setMoveY(.5); bomb[2].setMoveY(-.5); mp = MediaPlayer.create(context, R.raw.background_music); mp.setLooping(true); mp.start(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { soundPool.release(); mp.stop(); mp.release(); } @Override public void onDraw(Canvas canvas) { canvas.drawColor(Color.GRAY); character.draw(canvas); for(int i = 0; i < 3; i++){ bomb[i].draw(canvas); } } public void update(int adj_mov) { if(character.getstate() == SpriteObject.DEAD){ character.setX(100); character.setY(400); } // sprawdzenie, czy bomby nie znajdują się zbyt nisko for(int i = 0; i < 3; i++){ if(bomb[i].getY() > 500){ bomb[i].setMoveY(-.5); } } // sprawdzenie, czy bomby nie znajdują się zbyt wysoko for(int i = 0; i < 3; i++){ if(bomb[i].getY() < 100){ bomb[i].setMoveY(.5);
107
ROZDZIAŁ 5. TWORZENIE JEDNOOSOBOWEJ GRY Z UTRUDNIENIAMI
} } //sprawdzanie kolizji sprajta for(int i = 0; i < 3; i++){ if(character.collide(bomb[i])){ character.setState(SpriteObject.DEAD); } } //wykonanie odpowiednich akcji for(int i = 0; i < 3; i++){ bomb[i].update(adj_mov); } character.update(adj_mov); } public void processMotionEvent(InputObject input){ if(input.action == InputObject.ACTION_TOUCH_DOWN){ sprite.setMoveX(.5); } if(input.action == InputObject.ACTION_TOUCH_UP){ sprite.setMoveX(0); } } public void processKeyEvent(InputObject input){ } public void processOrientationEvent(float orientation[]){ float roll = orientation[2]; if (roll < -40) { character.setMoveX(2); } else if (roll > 40) { character.setMoveX(-2); } } public void playsound(int sound_id){ soundPool.play(sound_id, 1.0f, 1.0f, 1, 0, 1.0f); } }
Teraz mamy już wszystkie zmiany za sobą. Przećwiczyliśmy wykorzystanie stanów, a także obsługę kolizji i precyzyjnych ruchów.
108
PODSUMOWANIE
Podsumowanie Ostatecznie dotarliśmy do końcowego etapu tworzenia pierwszej gry. Gratulacje! Opracowaliśmy również kod, który może być wykorzystywany w przyszłych grach. Dodanie stanów sprajtów jest dokładnie tym, czego potrzebujemy do umożliwienia graczom sprawowania większej kontroli nad bohaterami gry. W tym momencie możemy stworzyć prawie dowolną grę 2D, a przyszłe projekty będą w znacznym stopniu korzystać z zaprezentowanej tu wydajnej metody wykrywania kolizji. W kolejnych kilku rozdziałach przyjrzymy się dokładniej różnym gatunkom gier, skorzystamy z dużych możliwości ekranów tabletów, ich mocy przetwarzania, a także możliwości komunikowania się z otoczeniem. W rozdziale 6. zostanie opisana bardziej złożona gra, w której gracz będzie mógł skorzystać z paletki, aby uderzać piłkę i kierować ją w mur zbudowany z klocków, czyli gra znana pod nazwą Breakout1. Głównym zagadnieniem, jakim się zajmiemy w kolejnym rozdziale, będzie obsługa praw fizyki.
1
Rozwinięciem gry Breakout, w którą grało się głównie na samodzielnych konsolach do gier w Stanach Zjednoczonych pod koniec lat 70., jest dobrze znana późniejszym właścicielom komputerów osobistych gra Arkanoid — przyp. tłum.
109
ROZDZIAŁ 5. TWORZENIE JEDNOOSOBOWEJ GRY Z UTRUDNIENIAMI
110
ROZDZIAŁ 6
Gra w odbijaną piłkę
Przypomnijmy, że w rozdziale 5. utworzyliśmy już prostą grę, w której głównym zadaniem gracza było unikanie poruszających się bomb. Dzięki temu mieliśmy powód, aby skorzystać z wielu funkcji oraz technik programowania istotnych podczas tworzenia gier na tablety z systemem Android. W tym rozdziale opracujemy już bardziej zaawansowaną grę. Naszym głównym zadaniem będzie stworzenie programu przypominającego grę Pong1, w której gracze będą odbijać piłkę rakietką, próbując jednocześnie trafić i za pomocą piłki zniszczyć umieszczony na ekranie kwadratowy blok. W zasadzie pierwsze doświadczenie autora z grami mobilnymi wiązało się ze starym urządzeniem Blackberry, w którym jedyną dostępną grą była wspomniana prosta gra Pong. Rakietka musiała być kontrolowana za pomocą topornego wskaźnika, a mały rozmiar ekranu oraz niska rozdzielczość czyniły to zadanie mało satysfakcjonującym. Zadziwiające, ale tamta gra była również napisana przy użyciu języka Java, tego samego, który w tym rozdziale wykorzystamy do stworzenia gry znacznie bardziej absorbującej i zabawnej. W trakcie programowania gry Czytelnik znów zdobędzie nowe umiejętności, które będzie mógł następnie ująć w swoim portfolio. Do plików z zasobami dodamy dodatkowe obrazki, natomiast postać oraz bomby z programu AllTogether z rozdziału 5. zastąpimy rakietką oraz blokami. Aby utrzymywać piłkę w stałym ruchu, będziemy musieli zarządzać interakcjami pomiędzy sprajtami, a także wykrywać większą liczbę kolizji. Co więcej, do gry będziemy musieli dodać pewne dodatkowe algorytmy fizyczne, wymagające wykonania zdecydowanie większej liczby jednoczesnych obliczeń. Za swoje działania gracz będzie też w większym stopniu wynagradzany dzięki zastosowaniu dźwięków oraz usuwaniu nieaktywnych bloków. Na koniec zobaczymy, w jaki sposób będzie można jednocześnie zainicjalizować dane dla wielu bloków, używając w tym celu pojedynczego pliku XML definiującego pożądany układ aplikacji. A więc do dzieła!
Początki Rozpocznijmy od zgromadzenia obrazków oraz innych zasobów, których będziemy używać w naszej grze, a następnie utwórzmy zupełnie nowy projekt.
1
Oryginalna gra Pong pamięta jeszcze czasy elektronicznych automatów do gier, popularnych w krajach zachodnich w latach 70., a w Polsce w latach 80. Została ona stworzona przez firmę Atari i obecnie należy do kanonu klasycznych gier komputerowych — przyp. tłum.
ROZDZIAŁ 6. GRA W ODBIJANĄ PIŁKĘ
Gromadzenie zasobów używanych w grze Ponieważ gra w Ponga korzysta z całkiem prostych kształtów oraz obiektów, stworzenie grafiki nie powinno przysparzać nam wielu kłopotów. Oczywiście najważniejszym problemem będzie ustalenie względnej skali oraz rozmiaru każdego z elementów. Rakietka musi być na tyle duża, aby mogła uderzać w piłkę, jednak wciąż na tyle mała, by stanowiło to dla gracza pewne wyzwanie. W dalszej części Czytelnik dowie się także, w jaki sposób dodawać inne obrazki, które będą mogły być później użyte do umieszczania na ekranie różnych bonusów. Na rysunku 6.1 przedstawione zostały wykorzystywane w grze grafiki wraz z wymiarami. Zauważmy, że każdy z obrazków jest oddzielnym plikiem .png. Dla celów tej implementacji obrazki zostały stworzone własnoręcznie przez autora książki w darmowym programie graficznym o nazwie GIMP (wspomnianym w rozdziale 2.), dostępnym na zasadach licencji open source.
Rysunek 6.1. Kwadratowy blok (u góry) ma rozmiar 30×50 pikseli, piłka (pośrodku) 30×30 pikseli, rakietka zaś (u dołu) 30×200 pikseli W rozdziale 7. Czytelnik dowie się ponadto, w jaki sposób układ kolejnych poziomów będzie mógł być definiowany za pomocą nowego pliku zasobu. Zamiast kodować pozycję każdego z poszczególnych bloków oddzielnie, dane o nich wszystkich umieścimy w pliku XML, określającym układ docelowy. Ponieważ jest to dość skomplikowana część tego projektu, zostawmy ją sobie do kolejnego rozdziału. W pierwszym przykładzie użyjemy tylko trzech bloków, bez wykorzystywania dodatkowych zasobów określających ich położenie. Czytelnik nie musi obawiać się zastosowania w programie piłki w czarnym kolorze. Mimo że taki kolor ma zazwyczaj również tło ekranu, nie powinien się tym zbytnio przejmować, ponieważ kolor tła można w bardzo prosty sposób zmienić. W rzeczywistości użycie jaśniejszego koloru tła będzie bardziej zachęcać gracza do gry. Wskazówka Zarówno obrazek rakietki, jak też piłki są częściowo przezroczyste. Taki efekt można uzyskać, wybierając w programie GIMP jako przezroczysty kolor biały. Szczególnie zachęcamy Czytelnika do zastosowania przezroczystych obrazków, ponieważ wtedy gra nabiera bardziej profesjonalnego wyglądu. Mamy to szczęście, że tego rodzaju obrazy w systemie Android mogą być używane bez żadnych problemów, podczas gdy w innych językach do ich obsługi konieczne jest stworzenie dodatkowego kodu.
112
POCZĄTKI
Gra będzie zdecydowanie bardziej inspirująca, jeżeli dodamy do niej nieco przyjemnych dźwięków. Ponieważ gra Pong nie narzuca użycia żadnych szczególnych zestawów dźwięków, Czytelnik może do woli z nimi eksperymentować. Do naszego przykładu wybrany został tylko jeden dźwięk — krótki plik MP3 z odgłosem uderzenia piłki o blok. Kod nie zawiera żadnych dodatkowych dźwięków lub muzyki, ale Czytelnik może w każdej chwili dodać je samodzielnie. Kiedy jednak rozpoczynamy tworzenie nowego projektu dla gry, wtedy im prostszy będzie kod, tym łatwiej będzie również wyszukiwać w nim ewentualne błędy.
Tworzenie nowego projektu Ponieważ nasza gra jest kompletnym programem (to znaczy obsługuje interakcję z użytkownikiem, ma cel, a także warunki określające zwycięstwo), powinniśmy traktować ją raczej jako profesjonalną aplikację, a nie jedynie prosty przykład. Z tego powodu do nazywania elementów oraz fragmentów kodu lepiej jest używać nazw opisowych. Nadajmy więc aplikacji nazwę TabletPaddle. Chociaż nazwa ta nie jest zbyt oryginalna, to jednak wskazuje, że będziemy tworzyć nową grę w stylu gry Pong. Aby rozpocząć programowanie, wykonajmy następujące czynności: 1. W programie Eclipse utwórzmy nowy projekt o wybranej nazwie, a następnie skopiujmy do nowego projektu cały kod z projektu AllTogether. W katalogu res utwórzmy nowy podkatalog, który będzie służył do przechowywania dźwięków, a następnie nazwijmy go raw. 2. Skopiujmy pliki zasobów do odpowiednich katalogów. Oczekiwany wygląd konfiguracji projektu przedstawiony jest na rysunku 6.2.
Rysunek 6.2. Prawidłowa konfiguracja projektu TabletPaddle 3. Jeżeli na tym wczesnym etapie otrzymamy komunikaty o błędach, będzie to spowodowane brakiem plików grafiki oraz dźwięków, które są używane w poprzednim kodzie. Błędy te poprawimy później, pracując nad docelowym kodem aplikacji.
113
ROZDZIAŁ 6. GRA W ODBIJANĄ PIŁKĘ
4. W panelu edycyjnym programu Eclipse wyświetlmy zawartość plików SpriteObject.java oraz GameView.java. Pozostałe pliki możemy w tej chwili pominąć. Teraz kiedy zgromadziliśmy już pliki zasobów, a także utworzyliśmy nowy projekt o nazwie TabletPaddle, możemy wygenerować potrzebne elementy gry. W tym celu przygotujmy obiekt powierzchni, na której elementy te będą umieszczane, oraz zmodyfikujmy pętlę gry.
Przygotowanie środowiska gry Zanim będziemy mogli rozpocząć modyfikację pętli gry, musimy zainicjalizować wszystkie nowe sprajty: rakietkę, piłkę oraz bloki, z których każdy będzie dysponować różnymi atrybutami oraz właściwościami. Konieczne będzie również przygotowanie środowiska, to znaczy powierzchni gry, na której będą wyświetlane sprajty. Rozpocznijmy od zmiany utworzonych poprzednio plików źródłowych w sposób adekwatny do ich zamierzonego użycia w nowej grze.
Modyfikacja pliku SpriteObject.java W pliku SpriteObject.java konieczne będzie dodanie funkcji, która zwracać będzie wartości MoveX oraz MoveY, będące zmiennymi przechowującymi poziomą oraz pionową prędkość sprajtów. W ten sposób będzie można łatwo odwrócić te wartości, powodując tym samym zmianę kierunku poruszania się piłki. W grach innego rodzaju funkcję tę będzie można wykorzystać do sprawdzenia prędkości sprajta, aby przekonać się, czy nie porusza się on zbyt szybko. Aby dodać tę funkcję, wykonajmy następujące czynności: 1. Do pliku SpriteObject.java dodajmy następujące dwie metody: public double getMoveY(){ return y_move; } public double getMoveX(){ return x_move; }
2. W pliku SpriteObject.java możemy wykonać kolejną zmianę ułatwiającą programowanie. Zamiast przejmować się zmienną adj_move przechowującą współczynnik szybkości gry, spróbujmy pozwolić grze działać tak szybko, jak to tylko możliwe. Pozwoli to pominąć obsługę bardzo małych wartości ruchu i zwiększy nieprzewidywalność tej całkiem prostej gry. Aby wprowadzić tę zmianę, w funkcji update() zmieńmy kod w ten sposób, żeby przypominał on ten umieszczony poniżej: public void update(int adj_mov) { x += x_move; y += y_move; }
Po wprowadzeniu tych małych modyfikacji będzie nam znacznie łatwiej przekształcać właściwą pętlę gry. W dalszej części tego rozdziału sprawdzimy, w jaki sposób te zmiany będą ze sobą współdziałać.
Modyfikacja pliku GameView.java Nasza gra nabierze ostatecznego kształtu dopiero po wprowadzeniu modyfikacji do pliku GameView.java. Należy pamiętać, że to właśnie ten plik jest miejscem, w którym znajduje się kod modyfikujący szybkość oraz funkcje gry. Oto czynności, jakie należy w tym celu wykonać: 1. Ponieważ gra nie używa dźwięków z poprzedniej gry, z kodu w pliku GameView.java należy usunąć następujące deklaracje zmiennych:
114
PRZYGOTOWANIE ŚRODOWISKA GRY
private private private private private
SoundPool soundPool; int sound_id; int ID_robot_noise; int ID_alien_noise; int ID_human_noise;
2. Należy przejrzeć kod i usunąć wszystkie inne odwołania do powyższych zmiennych, ponieważ w przeciwnym razie pojawią się komunikaty o błędach. 3. Konieczne będzie również zmodyfikowanie dwóch obiektów sprajtów używanych przez poprzednią grę. Im bardziej rozrastać się będzie nasza gra, tym większe będzie prawdopodobieństwo, że konieczne będzie skorzystanie z tablicy sprajtów. Tworzona gra również nie stanowi wyjątku od tej reguły i w dalszej części poszukamy sposobów na inicjalizację tablicy bloków przy użyciu jednego pliku XML. W tym momencie możemy usunąć sprajty z poprzedniego rozdziału, ponieważ w tej grze nie będziemy używać bomb! Nowe sprajty powinny być zadeklarowane w pliku GameView.java: private SpriteObject paddle; private SpriteObject[] block; private SpriteObject ball;
4. Do kodu należy jeszcze dodać następujące zmienne, których będzie można później użyć do określenia rozmiaru ekranu w trakcie sprawdzania warunków brzegowych zetknięcia się piłki z jego krańcami. private int game_width; private int game_height;
Uwaga! Jeżeli wszystkie powyższe modyfikacje są dla Czytelnika zbyt skomplikowane, może on pobrać pusty projekt Androida, korzystając w tym celu z witryny zawierającej kody z tej książki, znajdującej się pod adresem: http://code.google.com/p/android-tablet-games/. Na bazie tego kodu źródłowego Czytelnik może później utworzyć od podstaw swój własny kod gry.
5. Metoda konstruktora klasy GameView musi być całkowicie zmodyfikowana w taki sposób, aby mogła ona odpowiadać potrzebom nowej aplikacji. Nowa postać tej metody została przedstawiona na listingu 6.1. Zaraz po nim umieszczony został krótki opis jej działania. Czytelnik powinien sprawdzić, czy jego kod jest identyczny z tym przedstawionym na listingu 6.1. Listing 6.1. Konstruktor klasy GameView public GameView(Context con) { super(con); context = con; getHolder().addCallback(this); paddle = new SpriteObject(BitmapFactory.decodeResource(getResources(), ´R.drawable.paddle), 600, 600); block = new SpriteObject[3]; block[0] = new SpriteObject(BitmapFactory.decodeResource(getResources(), ´R.drawable.block), 300, 200); block[1] = new SpriteObject(BitmapFactory.decodeResource(getResources(), ´R.drawable.block), 600, 200); block[2] = new SpriteObject(BitmapFactory.decodeResource(getResources(), ´R.drawable.block), 900, 200);
115
ROZDZIAŁ 6. GRA W ODBIJANĄ PIŁKĘ
ball = new SpriteObject(BitmapFactory.decodeResource(getResources(), ´R.drawable.ball), 600, 300); mGameLogic = new GameLogic(getHolder(), this); createInputObjectPool(); setFocusable(true); }
6. Jeżeli przyjrzymy się ponownie poprzedniemu projektowi, powyższy kod powinien wydać się bardzo znajomy. Z kodu usunięty został obiekt soundPool, natomiast dodane zostały nowe współrzędne sprajtów, określające miejsce ich początkowego wyświetlania. Wyliczenie współrzędnych może być niekiedy skomplikowane, dlatego można sobie w tym pomóc, tworząc w GIMP-ie pusty rysunek o wymiarach ekranu (1280×1040). Na podstawie tego rysunku można następnie ustalić współrzędne właściwe dla danej gry. 7. W poprzedniej grze używane były trzy bomby, które w tym momencie zastąpione zostaną trzema blokami. Oczywiście w przyszłości konieczne może być wykorzystanie większej liczby bloków i wtedy do inicjalizacji kolejnych można wykorzystać pętle. Ponieważ w tym momencie jesteśmy już zaznajomieni z obiektami sprajtów, zwróćmy uwagę, że jedyne, co musi być w nich zmodyfikowane, to ich położenie oraz wykorzystywana przez nie grafika. 8. Teraz musimy jeszcze przesunąć piłkę. Kolejną funkcją, którą należy w tym celu zmodyfikować, będzie surfaceCreated(). Funkcja ta zostanie przez nas teraz uproszczona, a także dodanych zostanie do niej kilka zmian do obsługi piłki. Dołączymy również dwa wiersze, które przypiszą wysokość oraz szerokość ekranu do zmiennych wykorzystywanych później w funkcji update. W tym celu dodajmy do projektu kod przedstawiony na listingu 6.2. Listing 6.2. Modyfikacje funkcji surfaceCreated() @Override public void surfaceCreated(SurfaceHolder holder) { mGameLogic.setGameState(mGameLogic.RUNNING); mGameLogic.start(); ball.setMoveY(-10); ball.setMoveX(10); Canvas c = holder.lockCanvas(); game_width = canvas.getWidth(); game_height = canvas.getHeight(); holder.unlockCanvasAndPost(c); }
9. W tym momencie piłka rozpoczyna swój ruch w stronę górnego prawego rogu ekranu, co powinno dać graczowi dużo czasu, wystarczającego do prześledzenia jej ruchu i podjęcia odpowiedniej reakcji. Jeżeli ustawiona tu początkowa szybkość wydaje się Czytelnikowi zbyt duża lub mała, funkcja surfaceCreated() jest dobrym miejscem do jej zmiany. Później do tego rodzaju modyfikacji będziemy używać już samego obiektu piłki. 10. Konieczne będzie również zmodyfikowanie funkcji onDraw(), jednak nie będzie to zbyt skomplikowana zmiana. Pętla rysująca wszystkie klocki będzie identyczna z tą używaną poprzednio do uaktualniania stanu bomb. W tym celu zmodyfikujmy funkcję onDraw() za pomocą kodu przedstawionego na listingu 6.3. Listing 6.3. Zmodyfikowana funkcja onDraw() @Override public void onDraw(Canvas canvas) { canvas.drawColor(Color.WHITE); ball.draw(canvas);
116
DODAWANIE WYKRYWANIA KOLIZJI ORAZ OBSŁUGI ZDARZEŃ
paddle.draw(canvas); for(int i = 0; i < 3; i++){ block[i].draw(canvas); } }
To były dopiero podstawy. A teraz dodamy do utworzonego poprzednio kodu bardziej złożoną modyfikację obsługującą kolizje oraz zdarzenia.
Dodawanie wykrywania kolizji oraz obsługi zdarzeń Nie istnieje prawdopodobnie nic gorszego od włożenia dużego wysiłku w kodowanie programu, a następnie uświadomienia sobie, że w ogóle nie było to konieczne. Aby tego uniknąć, spędzimy sporo czasu, rysując diagramy i zastanawiając się, jak będzie działał program oraz jak będzie wyglądał. Na rysunku 6.3 przedstawiony jest diagram opisujący czynności niezbędne do wykonania oraz ostateczny widok pętli gry.
Rysunek 6.3. Zdarzenia, które muszą być obsłużone w pętli gry. Każdy element stanowi reprezentację od kilku wierszy do całej metody obsługującej zmiany Jeżeli nad stworzeniem aplikacji pracuje grupa osób, wtedy jeszcze bardziej istotne jest, aby każda z nich dzieliła ten sam obraz końcowego projektu. To właśnie w tym momencie można utworzyć przykładową grafikę, tak by każdy z członków zespołu mógł mieć pewien punkt odniesienia podczas pracy nad kodem i pozostałymi składnikami gry. W poprzedniej wersji gry sprawdzaliśmy warunki wystąpienia kolizji, a następnie zerowaliśmy grę do stanu początkowego. W aplikacji TabletPaddle dodany zostanie dodatkowy poziom złożoności, ponieważ będziemy musieli na szereg różnych sposobów reagować na kolizje. Co więcej, reakcje na zdarzenia muszą być natychmiastowe, aby uniknąć dziwnego zachowania gry w rodzaju piłki przechodzącej przez rakietkę lub wylatującej poza ekran.
117
ROZDZIAŁ 6. GRA W ODBIJANĄ PIŁKĘ
Dobra wiadomość jest taka, że w tej grze wszystkie kolizje ze ścianami, blokami oraz rakietką powodują odwrócenie ruchu piłki. Jeżeli na przykład rzucimy piłką o ścianę, odbije się ona od niej i powróci do nas. Jeżeli rzucimy tą samą piłką o stół, to również odbije się w naszym kierunku. Kiedy Czytelnik zrozumie już tę prawidłowość, będzie mógł dodać ją do wszystkich elementów gry. Nie wszystkie odbicia jednak są identyczne. Niekiedy trzeba odwrócić poziomy wektor ruchu, a innym razem pionowy. Modyfikacja ruchu piłki oznacza w rzeczywistości zmianę jej kierunku ruchu. Wartości MoveX oraz MoveY są w rzeczywistości wektorami, które po złożeniu ze sobą określają prędkość oraz kierunek ruchu piłki. Zmiana znaku którejkolwiek z tych wartości (z dodatniej na ujemną i na odwrót) zmienia kierunek, w którym zmierza piłka. Na rysunku 6.4 oraz 6.5 zobrazowany jest sposób działania opisanego powyżej algorytmu. Cała sztuka polega na właściwym rozpoznaniu sytuacji, kiedy konieczna jest zmiana poziomej, a kiedy pionowej wartości wektora ruchu. To właśnie tym spowodowana jest duża ilość kodu oraz liczba instrukcji if, których należy użyć w funkcji update().
Rysunek 6.4. Jeżeli piłka uderzy w blok z prawej strony, powinna odbić się w prawo. W takim przypadku zmianie ulega składowa pozioma ruchu, podczas gdy pionowa pozostaje bez zmian
Rysunek 6.5. W tym przypadku piłka uderza w blok od dołu i odbija się w dół. Ponieważ piłka wciąż przesuwa się w prawo, zmianie ulega jedynie pionowa składowa wartości wektora ruchu Dotychczas byliśmy w stanie wykryć kolizję sprajtów, jednak nigdy nie określaliśmy, z której strony obiekt był uderzany przez inny sprajt. Kod przedstawiony na listingu 6.4 rozwiązuje ten problem, porównując współrzędne x oraz y prawego i dolnego brzegu piłki z położeniem rakietki, ścian oraz bloków. Zwróćmy uwagę na to, że w dotychczasowym kodzie po uderzeniu bloków ich stan był zmieniany na DEAD, lecz same bloki nie były z gry usuwane. Zajmiemy się tym po przetestowaniu dotychczasowego wyniku pracy. Na listingu 6.4 przedstawiony jest kod wykorzystany do modyfikacji metody update w celu poprawnego wykrywania kolizji.
118
DODAWANIE WYKRYWANIA KOLIZJI ORAZ OBSŁUGI ZDARZEŃ
Listing 6.4. Funkcja update() z algorytmami fizycznymi wykrywania kolizji public void update(int adj_mov) { int int int int
ball_bottom = (int)(ball.getY() + ball.getBitmap().getHeight()); ball_right = (int)(ball.getX() + ball.getBitmap().getWidth()); ball_y = (int) ball.getY(); ball_x = (int) ball.getX();
//Zderzenie od dołu if(ball_bottom > game_height){ ball.setMoveY(-ball.getMoveY()); //gracz przegrywa } //zderzenie od góry if(ball_y < 0){ ball.setMoveY(-ball.getMoveY()); } //zderzenie z prawej strony if(ball_right > game_width){ ball.setMoveX(-ball.getMoveX()); } //zderzenie z lewej strony if(ball_x < 0){ ball.setMoveX(-ball.getMoveX()); } //zderzenie z rakietką if(paddle.collide(ball)){ if(ball_bottom > paddle.getY() && ball_bottom < paddle.getY() + 20){ ball.setMoveY(-ball.getMoveY()); } }
//sprawdzenie zderzenia z blokami for(int i = 0; i < 3; i++){ if(ball.collide(block[i])){ block[i].setstate(block[i].DEAD); int block_bottom = (int)(block[i].getY() + ´block[i].getBitmap().getHeight()); int block_right =(int)(block[i].getX() + ´block[i].getBitmap().getWidth()); //uderzenie w dół bloku if(ball_y > block_bottom - 10){ ball.setMoveY(ball.getMoveY()); } //uderzenie w górę bloku else if(ball_bottom < block[i].getY() + 10){ ball.setMoveY(-ball.getMoveY());
119
ROZDZIAŁ 6. GRA W ODBIJANĄ PIŁKĘ
} //uderzenie z prawej strony else if(ball_x > block_right - 10){ ball.setMoveX(ball.getMoveX()); } //uderzenie z lewej strony else if(ball_right < block[i].getX() + 10){ ball.setMoveX(-ball.getMoveX()); } } } //wykonanie określonych uaktualnień for(int i = 0; i < 3; i++){ block[i].update(adj_mov); } paddle.update(adj_mov); ball.update(adj_mov); }
Przed rozpoczęciem sprawdzania warunków kolizji określiliśmy wymiary piłki. Oszczędzi nam to czasu, w chwili gdy będziemy musieli odczytać szerokość oraz położenie piłki. Tego rodzaju praktyka powinna być stosowana wszędzie tam, gdzie jest to tylko możliwe, ponieważ czyni kod bardziej przejrzystym i zrozumiałym dla innych. Kolejne cztery instrukcje if wykonują raczej łatwe zadanie, sprawdzając, czy piłka uderzyła w którąkolwiek z krawędzi ekranu. Metody getMoveX() oraz getMoveY() utworzone w klasie SpriteObject są wykorzystywane wielokrotnie, ponieważ chcemy odwrócić poprzedni kierunek poruszania się piłki. Kolizje z krawędziami bocznymi ekranu zmieniają poziomą składową ruchu, podczas gdy kolizje z górną oraz dolną krawędzią powodują zmianę składowej pionowej. Czytelnik mógł już do tej pory przenikliwie zauważyć, że raczej rzadko będziemy odbijać piłkę od dołu ekranu, wręcz będziemy karać gracza za wystąpienie tego rodzaju sytuacji. Takie podejście uprości edycję kodu gry, ponieważ nie będziemy musieli ciągle przejmować się jej zerowaniem. Wskazówka Podczas tworzenia gry jej twórcy bardzo często pozostawiają pewnego rodzaju furtki, które zapobiegają konieczności przechodzenia całej gry w celu przetestowania jedynie jej fragmentów. Na przykład trudno sobie wyobrazić konieczność ukończenia 10 poziomów, aby przetestować jedynie ostateczne starcie. Konieczne jest raczej stworzenie możliwości pominięcia tej części gry.
Fragment kodu sprawdzający warunki kolizji piłki z rakietką może wydawać się zwodniczo prosty, ponieważ jedyną rzeczą, którą chcemy sprawdzić, będzie sytuacja, gdy piłka uderza o górną krawędź rakietki. Chociaż nie można wykluczyć, że piłka uderzy w bok rakietki, to w takiej sytuacji zmieniłaby się jedynie pozioma część wektora ruchu, co mogłoby prędzej czy później doprowadzić do uderzenia przez piłkę dolnej krawędzi ekranu, a tym samym zakończenia gry. Aby zapobiec niepotrzebnym obliczeniom, nie zajmujmy się teraz kolizjami z bokami rakietki. Zdecydowanie prościej będzie rozpatrywać ten problem bez jego dodatkowych aspektów. Kod obsługujący rakietkę sprawdza, czy piłka znajduje się w odległości nie większej niż 20 pikseli od górnego brzegu rakietki. Ponieważ w jednej sekwencji ruchu piłka może się przesunąć w każdą ze stron najwyżej o dziesięć jednostek, nigdy nie przekroczy tych warunków brzegowych. Należy zawsze upewnić się, czy badany obszar ma rozmiar większy od maksymalnego przesunięcia sprajta, tak aby nie trzeba było później zajmować się piłką lub innym obiektem zakleszczonym wewnątrz innego sprajta.
120
DODAWANIE OBSŁUGI DOTYKU, DŹWIĘKU ORAZ NAGRÓD
Kolizje z blokami to już całkiem inna historia. Aby obsłużyć zderzenia z blokami, które mogą nastąpić ze wszystkich stron, należy wykonać nieco więcej pracy. Na początku utworzyliśmy kilka zmiennych, do których przypisane zostały położenie oraz wymiary bloku, co eliminuje konieczność odczytywania tych wartości za każdym razem, gdy będą potrzebne. Następnie sprawdzane są warunki wystąpienia kolizji od góry oraz dołu bloku, ponieważ te zderzenia są najbardziej prawdopodobne. W dalszej kolejności sprawdzane są kolizje boczne. Zauważmy, że kolejność sprawdzania kolizji wpływa na ogólne zachowanie piłki. Kiedy tylko któryś z warunków wystąpienia kolizji przyjmie wartość true, kod gry przestanie poszukiwać kolejnych warunków wystąpienia kolizji. Cały algorytm przedstawiony jest na rysunku 6.6. Obszary sprawdzania kolizji bocznych są całkiem małe, ponieważ nie chcemy ryzykować ich negatywnego wpływu na testowanie zderzeń górnych oraz dolnych.
Rysunek 6.6. Miejsca potencjalnych kolizji z blokami
Dodawanie obsługi dotyku, dźwięku oraz nagród W tym momencie jesteśmy już gotowi do ukończenia tworzenia aplikacji. Musimy jeszcze jedynie dać użytkownikowi możliwość sprawowania kontroli nad rakietką, a także dodać dźwięk oraz pewne nagrody, które mogłyby zaciekawić gracza.
Dodawanie dotykowego sterowania rakietką W projekcie AllTogether do sterowania ruchem postaci używaliśmy zdarzeń polegających na dotknięciu oraz puszczeniu ekranu. W programie TabletPaddle rakietka poruszać się będzie poziomo w zależności od ruchów użytkownika przeciągającego ją w poprzek ekranu. Dla celów testowania kolizji można także pozwolić użytkownikowi przeciągać rakietkę po całym ekranie. Po skończeniu testów można zablokować współrzędną y rakietki, aby użytkownik nie był już w stanie swobodnie nią poruszać. Oto czynności, jakie należy wykonać w tym celu: 1. Poniżej znajduje się nowa funkcja processMotionEvent(), uaktualniająca położenie rakietki na podstawie ostatnich współrzędnych dotknięcia ekranu. Funkcję tę należy odpowiednio dołączyć do głównego kodu. public void processMotionEvent(InputObject input){ paddle.setX(input.x); paddle.setY(input.y); }
121
ROZDZIAŁ 6. GRA W ODBIJANĄ PIŁKĘ
2. Sam kod będzie też wymagać niewielkiego czyszczenia. Czy Czytelnik przypomina sobie funkcję playsound() oraz zdarzenie processOrientationEvent? Oba fragmenty kodu mogą być bezpiecznie umieszczone w komentarzu. 3. Kiedy mamy już możliwość sterowania rakietką, możemy wypróbować działanie programu TabletPaddle. W tym celu jak zwykle należy uruchomić program i rozpocząć grę. Sama gra może nie być pasjonująca, niemniej jest ona zaskakująco funkcjonalna, zważywszy na bardzo ograniczony rozmiar kodu. Na rysunku 6.7. przedstawiony jest wynik, jakiego moglibyśmy oczekiwać.
Rysunek 6.7. Wynik działania programu TabletPaddle
Dodawanie dźwięków Oczywiście w grę można już zagrać, jednak wciąż daleko jej do doskonałości. Kolejnym zadaniem będzie dodanie do gry dźwięków. Możemy w tym celu wykorzystać procedurę z poprzednich rozdziałów. Ponieważ potrzebujemy tylko jednego dźwięku, zamiast klasy SoundPool możemy użyć klasy MediaPlayer. 1. Dodajmy najpierw poniższą zmienną do listy zmiennych na początku programu: private MediaPlayer mp;
2. Następnie dodajmy poniższy kod do konstruktora klasy GameView. mp = MediaPlayer.create(context, R.raw.bounce);
3. Na listingu 6.5 przedstawiona jest część funkcji update(), w której umieszczane są instrukcje odgrywające dźwięki. Przypomnijmy jeszcze, że będziemy odgrywać dźwięk niezależnie od tego, w którą stronę bloku uderzy piłka.
122
DODAWANIE OBSŁUGI DOTYKU, DŹWIĘKU ORAZ NAGRÓD
Listing 6.5. Funkcja update() z obsługą dźwięku // sprawdzenie zderzenia z blokami for(int i = 0; i < 3; i++){ if(ball.collide(block[i])){ block[i].setstate(block[i].DEAD); mp.start(); int block_bottom = (int)(block[i].getY() + ´block[i].getBitmap().getHeight()); int block_right =(int)(block[i].getX() + block[i].getBitmap().getWidth());
//uderzenie w dół bloku if(ball_y > block_bottom - 10){ ball.setMoveY(ball.getMoveY()); } //uderzenie w górę bloku else if(ball_bottom < block[i].getY() + 10){ ball.setMoveY(-ball.getMoveY()); } //uderzenie z prawej strony else if(ball_x > block_right - 10){ ball.setMoveX(ball.getMoveX()); } //uderzenie z lewej strony else if(ball_right < block[i].getX() + 10){ ball.setMoveX(-ball.getMoveX()); } }
Inicjalizacja bloków Po pojawiających się już wcześniej sygnałach można się domyślić, w jaki sposób można dodać wiele bloków, a tym samym uczynić grę zdecydowanie bardziej interesującą. Zamiast przechodzić przez cały żmudny proces zapisywania pozycji każdego z bloków z osobna, ich współrzędne można zachować w pliku XML. System Android jest bardzo sprytny, jeżeli brać pod uwagę umieszczanie danych w pliku XML. To ćwiczenie będzie niezwykle przydatne, ponieważ sprawi, że kod stanie się bardziej czytelny i łatwiejszy do modyfikacji, a nowy sposób inicjalizacji wprowadzać będzie jedynie niewielkie opóźnienie, które zazwyczaj w trakcie działania programu nie będzie zauważalne. A oto czynności, jakie w tym celu należy wykonać: 1. W pierwszej kolejności utwórzmy plik o nazwie blockposition.xml, klikając prawym klawiszem podkatalog values wewnątrz katalogu res, a następnie wybierzmy z menu pozycje New (nowy) i File (plik). Jako nazwę pliku należy wprowadzić blockposition.xml. Poniżej znajduje się początkowy kod, jaki trzeba umieścić w nowym pliku. Ma on na celu pozostawienie bloków w tym samym położeniu co poprzednio oraz, w przypadku gdy zajdzie taka potrzeba, umożliwienie dołączenia dodatkowych bloków:
3
300 600 900
123
ROZDZIAŁ 6. GRA W ODBIJANĄ PIŁKĘ
200 200 200
2. Na początku powyższego kodu definiowana jest wartość całkowitoliczbowa 3, określająca liczbę bloków, które będą zdefiniowane w pliku. Następne dwie tablice przechowują współrzędne x oraz y odpowiednich bloków. Kiedy Czytelnik będzie dodawać więcej bloków, powinien pamiętać o uaktualnieniu wartości blocknumber oraz dodatkowych wpisów ze współrzędnymi. 3. Aby odczytać dane zapisane w pliku XML, na początku pliku GameView.java należy umieścić deklaracje następujących zmiennych: private private private private
Resources res; int[] x_coords; int[] y_coords; int block_count;
4. Ponieważ korzystamy z klasy Resources, na początku zestawu instrukcji import należy dodać następujący wiersz: import android.content.res.Resources;
5. W konstruktorze klasy GameView, umieszczonym w pliku GameView.java, należy usunąć wszystkie wiersze, które dotyczą konfiguracji bloków. Ta część pliku zostanie stworzona całkowicie od podstaw. Poniżej znajduje się nowa oraz ulepszona wersja kodu odczytującego dane z utworzonego przed chwilą dokumentu XML: res = getResources(); block_count = res.getInteger(R.integer.blocknumber); x_coords = res.getIntArray(R.array.x); y_coords = res.getIntArray(R.array.y); block = new SpriteObject[block_count]; for(int i = 0; i < block_count; i++){ block[i] = new SpriteObject(BitmapFactory.decodeResource(getResources(), ´R.drawable.block), x_coords[i], y_coords[i]); }
6. Najprościej rzecz biorąc, res będzie naszym obiektem pozwalającym na wywołanie funkcji getInteger() oraz getIntArray() czytających dane z pliku XML. Odczytane dane, zarówno tablice, jak też stałe, są następnie zachowywane w pamięci i w kolejnej pętli wykorzystywane do utworzenia każdego z nowych bloków. W tym momencie liczba bloków nie jest nigdzie w kodzie zapisywana na stałe, co pozwala ją w bardzo prosty sposób zmienić. 7. Niestety ponieważ początkowo liczba bloków została zdefiniowana jako 3, konieczna będzie zmiana w funkcjach onDraw() oraz update(). Dlatego należy odnaleźć wszystkie miejsca, w których w tych funkcjach liczba 3 została użyta w pętlach, a następnie wstawić w nich zmienną block_count. W metodzie update() występują dwa takie miejsca, w których należy dokonać wspomnianej zmiany, ponieważ w jednej pętli umieszczone jest wywołanie dla każdego sprajta funkcji update(), a w drugiej sprawdzenie, czy żaden z bloków nie zderzył się z piłką. Uwaga! Jednym z powodów, dla których warto przechowywać położenia bloków w pliku XML, jest możliwość szybkiego sprawdzenia ich pozycji. Na przykład trzy bloki, które zdefiniowaliśmy początkowo, mają wszystkie współrzędną y = 200. Ponieważ taki trend łatwo jest nam zauważyć, dla każdego z bloków możemy od razu zwiększyć nieco wartość współrzędnej x. Co więcej, ponieważ bloki mają wysokość 30 pikseli, kolejny ich rząd mógłby mieć współrzędną y o wartości 230.
124
DODAWANIE OBSŁUGI DOTYKU, DŹWIĘKU ORAZ NAGRÓD
Usuwanie nieaktywnych bloków Zanim grę będzie można potraktować poważnie, musimy poradzić sobie z jeszcze jednym problemem — po uderzeniu przez piłkę bloki muszą znikać. Właściwie ich stan został już przez nas zmieniony na DEAD, jednak zmiana tego stanu nie została w żaden sposób obsłużona. Aby temu zaradzić, musimy dokonać pewnych modyfikacji w pliku SpriteObject.java. Najprościej mówiąc, każda funkcja musi mieć dodaną początkową instrukcję if sprawdzającą stan obiektu. Jeżeli blok ma stan ALIVE, funkcja powinna kontynuować swoje działanie. W przeciwnym razie funkcja powinna zwrócić wartość null i nie zajmować się już więcej obsługą nieaktywnego sprajta. Aby to zapewnić, należy wykonać następujące czynności: 1. Do konstruktora obiektu SpriteObject należy dodać poniższą instrukcję, zapewniającą poprawną inicjalizację stanu każdego nowego sprajta. Nie ma bowiem żadnego sensu inicjalizowanie nieaktywnego sprajta. state = ALIVE;
2. Spójrzmy na kod funkcji draw(), update() oraz collide() umieszczony na listingu 6.6. Prosta instrukcja if zapewnia wykonanie kodu funkcji jedynie w przypadku właściwego stanu sprajta. Listing 6.6. Funkcje draw(), update() oraz collide() public void draw(Canvas canvas) { if(state == ALIVE){ canvas.drawBitmap(bitmap, (int)x - (bitmap.getWidth() / 2), (int)y ´- (bitmap.getHeight() / 2), null); } } public void update(int adj_mov) { if(state == ALIVE){ x += x_move; y += y_move; } } public boolean collide(SpriteObject entity){ if(state == ALIVE){ double left, entity_left; double right, entity_right; double top, entity_top; double bottom, entity_bottom; left = x; entity_left = entity.getX(); right = x + bitmap.getWidth(); entity_right = entity.getX() + entity.getBitmap().getWidth(); top = y; entity_top = entity.getY(); bottom = y + bitmap.getHeight(); entity_bottom = entity.getY() + entity.getBitmap().getHeight(); if (bottom < entity_top) { return false; } else if (top > entity_bottom){ return false; } else if (right < entity_left) {
125
ROZDZIAŁ 6. GRA W ODBIJANĄ PIŁKĘ
return false; } else if (left > entity_right){ return false; } else{ return true; } } else{ return false; } }
Jedyną sztuczką w kodzie jest instrukcja else w funkcji collide(), której użycie jest w tym miejscu konieczne, ponieważ ta metoda musi zawsze zwrócić pewną wartość. W ten sposób zaimplementowaliśmy bardzo prostą procedurę usuwającą trafione bloki. Oczywiście wciąż możemy odczytać wartości współrzędnych x, y, obrazek sprajta oraz stan bloków, ale dla tych nieaktywnych nie ma już takiej konieczności.
Podsumowanie W tym rozdziale osiągnęliśmy bardzo wiele. Oczywiście program TabletPaddle jest prostą grą, w której wciąż jest wiele miejsca na ulepszenia i dalszy rozwój. Utworzyliśmy już jednak najbardziej skomplikowaną i podstawową część funkcjonalności, algorytmy fizyczne obsługują płynnie kolizje, a cała gra reaguje na polecenia w sposób szybki i poprawny. Poniżej umieszczona jest lista dodatkowych pomysłów, które mogą zaciekawić Czytelnika, a jednocześnie odpowiednio zaimplementowane uatrakcyjnić całą grę. Żaden z tych pomysłów nie dotyczy kwestii obsługi dotyku, jednak każdy dotyka logiki gry i wymaga kreatywności. • Zerowanie gry w momencie uderzenia piłki w podłogę. W obecnej chwili piłka najzwyczajniej odbije się od niej. A może warto byłoby wyświetlić planszę „Game Over”? • Rejestrowanie wyników. Ponieważ możemy wykryć sytuację, w której piłka zostanie uderzona, czy nie moglibyśmy śledzić liczby tych uderzeń? Użytkownicy mogliby zobaczyć, jak dobrze sobie radzą. • Dodawanie poziomów. To zadanie może być nieco wymagające, pamiętajmy jednak, że w tej grze jedyną różnicą pomiędzy kolejnymi poziomami byłoby położenie bloków. W pliku brickposition.xml można byłoby na przykład utworzyć odpowiednie zestawy stałych oraz tablic, które przechowywałyby położenie bloków wewnątrz każdego z poziomów. Po przeczytaniu tego rozdziału Czytelnik jest już na dobrej drodze do tworzenia naprawdę niesamowitych aplikacji. Nowe umiejętności opanował na przykład w trakcie pracy nad obsługą kolizji występujących pomiędzy piłką, rakietką oraz blokami. Dodaliśmy także dźwięki jako sposób reagowania na polecenia użytkownika oraz nagrodziliśmy go znikającymi blokami. W przyszłości jeszcze bardziej zwiększymy złożoność akcji wykonywanych przez tablet. W szczególności w kolejnym rozdziale zwiększymy niezależność działania tabletu. Zamiast jedynie biernie odpowiadać na akcje użytkownika, aplikacja będzie mogła tworzyć swoje własne zdarzenia, czym zmusi użytkownika do reagowania na nieprzewidywalne zachowanie programu.
126
ROZDZIAŁ 7
Tworzenie gry dwuosobowej
Tworząc gry na tablety z systemem Android, wykonaliśmy już do tej chwili całkiem sporo fantastycznej pracy. Teraz jesteśmy gotowi, aby do naszego dzieła dodać kolejny poziom zaawansowania, umożliwiając graczowi współzawodnictwo z innymi osobami znajdującymi się w pobliżu. Będzie to następny, mający duże konsekwencje krok milowy w programowaniu gier. Jeżeli przyjrzymy się wielu popularnym obecnie grom, przekonamy się, że większość z nich jest wybierana przez graczy głównie ze względu na możliwość przeprowadzania z własnego domu rozgrywek z przyjaciółmi oraz zupełnie obcymi osobami. Dodawanie funkcji łączenia wielu urządzeń jest całkiem skomplikowanym procesem. Na szczęście dokumentacja systemu Android zawiera przykłady, które można dostosować do swoich potrzeb w taki sposób, aby uzyskać pożądany efekt. Musimy więc jedynie zrozumieć sposób działania kodu, a następnie dołączyć go do własnych gier. W tym rozdziale zajmiemy się szeregiem różnych zagadnień związanych z grami wieloosobowymi, omawiając jednocześnie wiele różnych ich typów oraz implementacji. Następnie skoncentrujemy się na systemie Android. Pod koniec rozdziału Czytelnik będzie już rozumiał, w jaki sposób zmodyfikować własne gry, aby umożliwić w nich rozgrywkę wieloosobową. Zanim jednak zagłębimy się w ten temat, przyjrzyjmy się różnym rodzajom trybów wieloosobowych oraz ich typowej implementacji. Uwaga! Jeżeli jakikolwiek umieszczony w tym rozdziale fragment kodu będzie w pierwszej chwili dla Czytelnika niezrozumiały, powinien on czytać dalej, a po pewnym czasie wszystko ułoży się w spójną całość. Jeśli jednak pewne fragmenty wciąż pozostaną dla niego niezrozumiałe, powinien sprawdzić rozwiązania dostępne w internecie lub też uruchomić programy, modyfikując jedynie ich niezbędne elementy. Dobrym miejscem do rozpoczęcia poszukiwań jest zawsze dokumentacja systemu Android, dostępna pod adresem: http://developer. android.com/guide/index.html. Jeżeli tylko Czytelnik zrozumie, w jaki sposób działa przykładowy kod, bardzo często nie będzie musiał nawet umieć odtwarzać go od podstaw.
Podstawy gier wieloosobowych Czy Czytelnik grał kiedykolwiek na komputerze lub konsoli gier wideo z innymi graczami w strzelankę typu FPS? Tego rodzaju gry przynoszą swoim twórcom corocznie setki milionów dolarów zysku, ponieważ ich główną cechą jest włączanie do rozgrywki innych graczy, a nie tylko postaci kreowanych przez komputer. Bardzo popularne są również gry sieciowe udostępniające całe światy (spójrzmy na przykład na grę World of Warcraft). Także tablety oraz telefony powoli wkraczają do tego świata połączeń.
ROZDZIAŁ 7. TWORZENIE GRY DWUOSOBOWEJ
Prawdopodobnie najnowszym rodzajem gier wieloosobowych są gry społecznościowe. Aplikacje w rodzaju Farmville, Mafia Wars oraz wiele innych łączą się z witrynami społecznościowymi (głównie ze stroną Facebook) i przesyłają tam informację o postępie gracza oraz informują o nim przyjaciół tego gracza.
Gry wieloosobowe wykorzystujące serwer gier Wszystkie gry wspomniane powyżej używają do łączenia graczy serwera pośredniczącego. Oznacza to, że ani urządzenia, ani gracze nie są połączeni ze sobą bezpośrednio, ale raczej poprzez dodatkową warstwę. W rzeczywistości strony internetowe wykorzystują tę samą metodę. Użytkownik (klient) pobiera ze strony informację umieszczoną na serwerze WWW (serwer). Na rysunku 7.1 przedstawiony jest prosty schemat ilustrujący sytuację, w której kilku graczy łączy się z serwerem, aby grać w grę wieloosobową.
Rysunek 7.1. Grupa graczy łączy się z centralnym serwerem z różnych miejsc, co pozwala im grać ze sobą Zanim jednak przyjrzymy się wadom i zaletom gier wieloosobowych korzystających z serwera gier, pomocne może okazać się porównanie tej metody z inną. Spójrzmy więc teraz na metodę połączeń równorzędnych (ang. peer-to-peer).
Gry wieloosobowe z połączeniami równorzędnymi W sytuacji gdy gracze łączą się ze sobą bezpośrednio, używają sieci połączeń równorzędnych (ang. P2P network). Gry umożliwiające tego rodzaju połączenia, używane przez graczy znajdujących się blisko siebie, wykorzystują zazwyczaj połączenia typu Bluetooth. Obsługa tego standardu dostępna jest w większości tabletów z systemem Android. Połączenia tego rodzaju nie mają żadnej warstwy kontrolującej proces komunikacji. Jeżeli Czytelnik używał w przeszłości do wymiany plików sieci P2P (na przykład pobierając duże pliki z sieci torrent), wie, że był wtedy połączony z innymi komputerami w sposób równorzędny. Oznacza to, że nie był mu potrzebny żaden duży serwer łączący wszystkich użytkowników. Wiele dużych gier wideo przeznaczonych na konsole gier nie używa połączeń równorzędnych, ponieważ gdyby ich używały, umożliwiałyby rozgrywkę jedynie kilku graczom jednocześnie. Różnica pomiędzy grą wykorzystującą połączenia klient-serwer a grą używającą połączeń równorzędnych przedstawiona jest na rysunku 7.2.
Rysunek 7.2. Dwóch graczy w celu przeprowadzenia rozgrywki łączy się bezpośrednio ze sobą Oczywiście te dwie strategie gier wieloosobowych w sposób znaczący różnią się od siebie. Czytelnik może się zastanawiać, która z nich jest lepsza. Na to pytanie nie ma jednoznacznej odpowiedzi. Natomiast istnieją przykłady sytuacji, w których jedna metoda ma przewagę nad drugą.
128
PODSTAWY GIER WIELOOSOBOWYCH
Wybór metody rozgrywki wieloosobowej W tabelach 7.1 oraz 7.2 przedstawionych jest kilka głównych zalet oraz wad każdej z dwóch metod rozgrywek wieloosobowych. Nie jest to oficjalna lista (a niektórzy z czytelników mogą mieć nawet własne zdanie, czy pewne cechy wymienione w tabelach są zaletami, czy w istocie wadami), jednak daje ona pewne bardzo ważne podstawy wyboru właściwego rozwiązania. Tabela 7.1. Wady oraz zalety gier wieloosobowych z udziałem serwera Zaleta
Wada
Umożliwia jednoczesny dostęp do gry wielu graczom.
Wymaga dodatkowego sprzętu i prawdopodobnie opłat za dostęp do serwera.
Zmniejsza obciążenie obliczeniowe poszczególnych urządzeń.
Awaria serwera dotyka wszystkich graczy.
Ułatwia dystrybucję uaktualnień oraz poprawek.
Programista musi utworzyć dodatkowy kod obsługujący operacje na serwerze.
Gracze mogą być rozmieszczeni na całym świecie.
Gracze nie mogą się łatwo ze sobą porozumiewać, o ile nie używają komunikatora.
Tabela 7.2. Wady oraz zalety gier wieloosobowych z użyciem połączeń równorzędnych Zaleta
Wada
Programista nie musi tworzyć zbyt wiele kodu.
W przypadku wykrycia błędu każdy z graczy osobno musi pobrać uaktualnienie.
Usterka pojedynczego urządzenia nie powstrzymuje działania gry na innych urządzeniach.
Liczba graczy jest zazwyczaj ograniczona.
Nie jest konieczny dodatkowy serwer (wszystkie urządzenia zawierają niezbędne technologie).
Urządzenia graczy muszą same wykonywać wszystkie niezbędne obliczenia.
Podczas gry użytkownicy znajdują się zazwyczaj blisko siebie i mogą komunikować się nawzajem.
Prawie niemożliwe jest prowadzenie rozgrywek z graczami na całym świecie.
Jeżeli Czytelnik przyjrzał się dokładnie powyższym tabelom, powinien zauważyć, że kolumna opisująca zalety metody z użyciem serwera jest podobna do kolumny opisującej wady metody z wykorzystaniem połączeń równorzędnych, podobnie jest w przypadku drugiej pary kolumn. Niemniej zgromadzenie w jednym miejscu wad oraz zalet każdej z metod nie prowadzi wcale do podjęcia właściwego wyboru. To Czytelnik musi mieć świadomość tego, jaki efekt chce osiągnąć, a następnie musi wybrać tę metodę, która w największym stopniu mu to umożliwia. W dalszej części tego rozdziału dostosujemy grę TabletPaddle, utworzoną przez nas w rozdziale 6., do rozgrywki dwuosobowej. Każdy z graczy będzie kontrolował na swoim tablecie jedną z rakietek. Ponieważ temat programowania gier wieloosobowych może być dość złożony, w tym rozdziale zajmiemy się przedstawieniem jedynie jego głównych zagadnień. Pełny kod aplikacji dostępny jest pod adresem: http://code.google.com/p/android-tablet-games/. Ponieważ chcemy umożliwić rozgrywkę jedynie dwóm graczom jednocześnie, a przy tym użyć w tym celu najbardziej efektywnego z dostępnych sposobów, wykorzystamy metodę połączeń równorzędnych. Dodatkowo zamiast łączyć graczy poprzez połączenie internetowe w sieci 3G lub WIFI, połączymy ich urządzenia bezpośrednio za pomocą interfejsu Bluetooth, dostępnego w większości urządzeń z systemem Android. W ten sposób oszczędzimy sobie znaczną ilość czasu, który musielibyśmy poświęcić na konfigurację architektury serwera oraz zapewnienie poprawnych połączeń za jego pośrednictwem.
129
ROZDZIAŁ 7. TWORZENIE GRY DWUOSOBOWEJ
Wskazówka Początkujący programiści gier powinni trzymać się z dala od gier wieloosobowych typu klient-serwer, ponieważ prawie zawsze są one bardziej złożone. Nie powinno to jednak zniechęcać Czytelnika, ponieważ i tak będzie w stanie utworzyć wiele wspaniałych gier używających połączeń Bluetooth. W tym przypadku dodatkową zaletą dla graczy będzie to, że będą przebywać w niewielkiej odległości od siebie. Dzięki temu będą w stanie wymieniać między sobą komentarze na temat skomplikowanych poziomów lub też oddawać się swobodnej rozmowie.
Gra dwuosobowa z połączeniami równorzędnymi Czytelnik może być słusznie przekonany, że większość tabletów z systemem Android obsługuje standard Bluetooth1. Prawie wszystkie nowoczesne telefony używają go do połączeń z bezprzewodowymi zestawami słuchawkowymi. Również tablety implementują tę technologię, umożliwiając zastosowanie tych samych zestawów, jak również klawiatur i wielu innych urządzeń zewnętrznych. Chociaż niektórzy ludzie używają terminu Bluetooth do określenia zestawów słuchawkowych oraz urządzeń służących do podłączenia do telefonów, to jednak w rzeczywistości Bluetooth jest systemem transmisji radiowej, używanym przez różnego rodzaju urządzenia do wymiany zdjęć, muzyki, wideo, a także prawie każdego innego typu danych. Największą zaletą tego standardu jest jednak jego niesamowita szybkość2. Jeżeli może on być wykorzystywany do prowadzenia ciągłych rozmów telefonicznych przy użyciu słuchawek bezprzewodowych, możemy być pewni, że będzie się nadawać również do większości typów gier3. W kolejnych podrozdziałach dostosujemy grę TabletPaddle z poprzedniego rozdziału do rozgrywki dwuosobowej. Najpierw dodamy kod umożliwiający połączenie dwóch tabletów z Androidem przy użyciu ich wbudowanych nadajników Bluetooth, a następnie dołączymy dodatkową rakietkę oraz kod pozwalający graczom na współzawodnictwo o kontrolę nad piłką. Zacznijmy więc od utworzenia nowego projektu w środowisku Eclipse i nazwania go TwoPlayerPaddleGame.
Dodawanie połączeń Bluetooth Ponieważ łączenie wielu urządzeń jest zadaniem złożonym, kod obsługujący tego rodzaju interakcje na tabletach z Androidem będzie znacznie trudniej objaśnić. Fragmenty kodu umieszczone w tym przykładzie stanowią część dostarczonego z przykładami Androida większego projektu o nazwie BluetoothChat, obsługującego standard Bluetooth. W tym miejscu użyjemy ich do omówienia głównych zagadnień. Nie wszystkie zmienne z kodu zostały już wcześniej zainicjalizowane, niemniej wciąż mogą posłużyć do omówienia istoty zagadnienia. Zanim jednak zajmiemy się przedstawieniem przykładu, spójrzmy na główne elementy aplikacji korzystającej ze standardu Bluetooth. Na początek musimy w tablecie zainicjalizować łącze do urządzenia Bluetooth. W tym celu wykonajmy następujące czynności: 1
Tanie urządzenia spotykane na polskim rynku w cenie ok. 500 zł prawie zawsze pozbawione są interfejsu Bluetooth. Można założyć, że stwierdzenie autora jest w większości przypadków prawdziwe dla urządzeń w cenie od 1000 zł wzwyż — przyp. tłum.
2
Autor ma na myśli ostatnie standardy Bluetooth 3.X oferujące teoretyczne transfery na poziomie kilkudziesięciu Mb/s. Należy jednak pamiętać, że poprzednie wersje standardu Bluetooth oferowały transfery o rząd wielkości niższe i o ile nadawały się do transmisji głosowej, o tyle transmisja danych była dla nich wyzwaniem. Protokoły używane do transmisji danych głosowych nie wymagają aż tak dużej przepustowości. Dane głosowe poddają się dość dobrej kompresji i mogą być kompresowane stratnie. Natomiast dane aplikacji muszą być zawsze przesyłane bezstratnie, co też stanowi pewne wyzwanie dla medium używanego do transmisji — przyp. tłum.
3
Istotną wadą standardu Bluetooth jest jego podatność na złamanie i podsłuchanie. Co prawda wadę tę ogranicza niewielki zasięg transmisji, niemniej decydując się na wykorzystanie standardu Bluetooth do transmisji poufnych danych należy zaimplementować dodatkową warstwę szyfrowania — przyp. tłum.
130
GRA DWUOSOBOWA Z POŁĄCZENIAMI RÓWNORZĘDNYMI
1. Do funkcji onCreate() w pliku MainActivity.java dodajmy kod przedstawiony na listingu 7.1. Listing 7.1. Funkcja onCreate() BlueAdapter = BluetoothAdapter.getDefaultAdapter(); if (BlueAdapter == null) { Toast.makeText(this, "Interfejs Bluetooth nie jest dostępny", Toast.LENGTH_LONG).show(); return; }
Obiekt BlueAdapter stanie się uchwytem do funkcji urządzenia Bluetooth. Instrukcja if wykorzystana została do sprawdzenia dostępności interfejsu w danym urządzeniu. W przypadku jego nieobecności użytkownik jest powiadamiany stosownym komunikatem o braku możliwości użycia programu. 2. Kolejny fragment inicjalizujący umieszczony jest w metodzie, z którą nie mieliśmy dotychczas do czynienia — w funkcji onState() znajdującej się w pliku MainActivity.java tuż po metodzie onCreate(). Jej zawartość przedstawiona jest na listingu 7.2. Do poprawnego działania kodu potrzebne jest również dołączenie klasy android.intent.Intent, umożliwiającej wysyłanie komunikatów. Listing 7.2. Metoda onStart() @Override public void onStart() { super.onStart(); if (!BlueAdapter.isEnabled()) { Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableIntent, REQUEST_ENABLE_BT); } else { if (game_running == null) startgame(); } }
Kod na listingu 7.2 sprawdza najpierw, czy urządzenie Bluetooth jest włączone, czy wyłączone. W przypadku gdy jest niedostępne, zostaje utworzona aktywność z żądaniem włączenia urządzenia (niedługo dowiemy się, co ta aktywność wykonuje). Jeżeli urządzenie jest włączone, następuje sprawdzenie stanu gry. W przypadku gdy gra nie została uruchomiona, wywoływana jest funkcja inicjalizująca nową grę. Zauważmy, jak duża ilość kodu wykorzystywana jest najpierw do sprawdzania dostępności poprawnego połączenia Bluetooth. 3. Kod na listingu 7.3 używany jest po wysłaniu przez aktywność żądania. Listing 7.3. Metoda onActivityResult() public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case REQUEST_CONNECT_DEVICE: if (resultCode == Activity.RESULT_OK) { String address = data.getExtras() .getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS); BluetoothDevice device = BlueAdapter.getRemoteDevice(address);
131
ROZDZIAŁ 7. TWORZENIE GRY DWUOSOBOWEJ
mGameView.connect(device); } break; case REQUEST_ENABLE_BT: if (resultCode == Activity.RESULT_OK) { startgame(); } else { Toast.makeText(this, “Błąd inicjalizacji interfejsu Bluetooth”, ´Toast.LENGTH_SHORT).show(); finish(); } } }
Kod umieszczony powyżej wykonuje dwie proste czynności. Jeżeli zostanie wywołany z żądaniem połączenia do innego urządzenia, kod pobierze jego adres i utworzy link do jego interfejsu Bluetooth. Następnie wywoływana jest funkcja obiektu mGameView łącząca ze sobą dwa urządzenia. 4. A teraz przyszła pora na bardzo krótką i prostą funkcję startgame(). Listing 7.4 przedstawia sposób uruchomienia gry. Listing 7.4. Metoda startgame() private void startgame() { mGameView = new GameView(this, mHandler); setContentView(mGameView); }
Powyższa metoda nie jest zbyt ciekawa. Istotne jest jednak, aby zauważyć, w jaki sposób do konstruktora klasy GameView przekazywany jest nowy argument. Uchwyt do procedury obsługi komunikatów pozwala na przesłanie do gry danych z kanału Bluetooth. Zrozumienie sposobu działania tej procedury jest prawdopodobnie najważniejszym aspektem programowania tego interfejsu. 5. Kod na listingu 7.5 zajmuje się obsługą uchwytu, używanego w różnych zadaniach wysyłania oraz odbierania danych. Listing 7.5. Obsługa uchwytu do procedury obsługi komunikatów private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_STATE_CHANGE: switch (msg.arg1) { case BluetoothChatService.STATE_CONNECTED: break; case BluetoothChatService.STATE_CONNECTING: Toast.makeText(this, "Łączenie z Bluetooth", Toast.LENGTH_SHORT).show(); break; case BluetoothChatService.STATE_LISTEN:
132
GRA DWUOSOBOWA Z POŁĄCZENIAMI RÓWNORZĘDNYMI
case BluetoothChatService.STATE_NONE: Toast.makeText(this, "Brak połączenia z Bluetooth", ´Toast.LENGTH_SHORT).show(); break; } break; case SEND_DATA: byte[] writeBuf = (byte[]) msg.obj; String writeMessage = new String(writeBuf); break; case RECEIVE_DATA: byte[] readBuf = (byte[]) msg.obj; String readMessage = new String(readBuf, 0, msg.arg1); break; case MESSAGE_DEVICE_NAME: mConnectedDeviceName = msg.getData().getString(DEVICE_NAME); Toast.makeText(getApplicationContext(), "Podłączony do " + mConnectedDeviceName, Toast.LENGTH_SHORT).show(); break; case MESSAGE_TOAST: Toast.makeText(getApplicationContext(), msg.getData().getString(TOAST), Toast.LENGTH_SHORT).show(); break; } } };
Ponieważ w trakcie zaprezentowanej powyżej inicjalizacji uchwytu wykonywanych jest wiele czynności, poniżej przedstawiona zostanie lista różnych wykorzystywanych aktywności. Powrócimy do niej, kiedy będziemy tworzyć własny rzeczywisty projekt. Mówiąc najogólniej, do uchwytu przekazywany jest określony komunikat lub zdarzenie, które musi on obsłużyć albo zignorować. Lista odpowiedzi na zdarzenia wymagająca dodatkowego kodowania jest długa. Należy pamiętać również, że zdarzenia są wysyłane z wewnątrz klasy GameView. • MESSAGE_STATE_CHANGE: Pierwsza instrukcja case sprawdza zmianę stanu połączenia Bluetooth. W większości przypadków konieczne jest poinformowanie użytkownika o zmianie stanu na brak połączenia. Na przykład możemy poinformować użytkownika o tym, że urządzenie próbuje nawiązać połączenie, a następnie, w przypadku gdy ta próba zakończyła się niepowodzeniem, wyjaśnić użytkownikowi przyczynę tej sytuacji. Takie założenie jest przydatne podczas testowania programu. • SEND_DATA: Kolejne zdarzenie obsługuje wysyłanie danych do innego urządzenia. W tym miejscu tworzony jest bufor danych i następuje przygotowanie do ich wysłania. W rzeczywistości w tym momencie żadne dane nie są jeszcze wysyłane. Ta funkcja zostanie dodana w tym miejscu w późniejszym czasie. • RECEIVE_DATA: Analogicznie do fragmentu obsługującego wysyłanie komunikatu występuje tu również część kodu odbierająca dane nadchodzące z innego urządzenia. Podobnie jak poprzednio więcej kodu do tego fragmentu zostanie dodane później, gdy będziemy już wiedzieli, co dokładnie chcemy osiągnąć.
133
ROZDZIAŁ 7. TWORZENIE GRY DWUOSOBOWEJ
• MESSAGE_DEVICE_NAME: Przedostatni komunikat jest wywołaniem informującym użytkownika o podłączeniu do konkretnego urządzenia. O tym fakcie użytkownik jest informowany za pomocą małego okna dialogowego. • MESSAGE_TOAST: Ostatecznie natrafiamy na ogólny kod wysyłający do użytkownika komunikat z wewnątrz klasy GameView.
Zarządzanie połączeniami Bluetooth A teraz wrócimy na bardziej znane terytorium i dodamy kilka zmian do pliku GameView.java. Pamiętajmy, że większość umieszczonego w tym podrozdziale kodu jest niezbędna, ponieważ to w tej klasie będziemy mogli zmienić położenie sprajtów na podstawie danych przesyłanych tam i z powrotem pomiędzy tabletami. Na listingach 7.6, 7.7 oraz 7.8 przedstawiony jest kod trzech miniwątków, który musi zostać dodany do klasy GameView w celu zapewnienia obsługi różnych operacji Bluetooth zachodzących w trakcie interakcji pomiędzy dwoma graczami. Należeć do nich będą wątki AcceptThread, ConnectThread oraz ConnectedThread. Wątek AcceptThread obsługuje początkowe połączenie, ConnectThread obsługuje niuanse związane z parowaniem urządzeń, a ConnectedThread jest zwykłą procedurą obsługiwaną w chwili połączenia urządzeń ze sobą. Listing 7.6. Wątek AcceptThread private class AcceptThread extends Thread { // Gniazdo serwera lokalnego private final BluetoothServerSocket mmServerSocket; public AcceptThread() { BluetoothServerSocket tmp = null; // Utwórz nowe nasłuchujące gniazdo serwera try { tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID); } catch (IOException e) { Log.e(TAG, "błąd funkcji listen()", e); } mmServerSocket = tmp; } public void run() { if (D) Log.d(TAG, "START mAcceptThread" + this); setName("AcceptThread"); BluetoothSocket socket = null; // Dopóki brak jest połączenia, nasłuchuj na gnieździe sieciowym while (mState != STATE_CONNECTED) { try { // To jest wywołanie blokujące, które zwróci wartość jedynie w przypadku // nawiązania poprawnego połączenia lub wystąpienia wyjątku socket = mmServerSocket.accept(); } catch (IOException e) { Log.e(TAG, "błąd funkcji accept()", e); break; } // Po zaakceptowaniu połączenia if (socket != null) {
134
GRA DWUOSOBOWA Z POŁĄCZENIAMI RÓWNORZĘDNYMI
synchronized (BluetoothChatService.this) { switch (mState) { case STATE_LISTEN: case STATE_CONNECTING: // Sytuacja poprawna. Uruchom wątek połączenia connected(socket, socket.getRemoteDevice()); break; case STATE_NONE: case STATE_CONNECTED: // Połączenie aktywne lub brak gotowości. Usuń nowe gniazdo try { socket.close(); } catch (IOException e) { Log.e(TAG, "Błąd zamykania zbędnego gniazda ", e); } break; } } } } if (D) Log.i(TAG, "KONIEC mAcceptThread"); } public void cancel() { if (D) Log.d(TAG, "anulowanie " + this); try { mmServerSocket.close(); } catch (IOException e) { Log.e(TAG, "błąd funkcji close()", e); } } }
Klasa AcceptThread jest złożonym fragmentem kodu, jednak w rzeczywistości oczekuje ona jedynie na zaakceptowanie połączenia. Zwróćmy uwagę na to, jak często pojawia się słowo kluczowe socket (gniazdo). Gniazda są standardowym sposobem nawiązywania połączeń pomiędzy urządzeniami i umożliwiają przesyłanie informacji. Ten kod został zaczerpnięty z jednego z przykładów z dokumentacji systemu Android. Kilkanaście zawartych w nich metod oraz bloków kodu jest, jak się okazało, niesamowicie wydajnie napisanych i nie wymagało żadnych poprawek. Listing 7.7. Wątek ConnectThread private class ConnectThread extends Thread { private final BluetoothSocket mmSocket; private final BluetoothDevice mmDevice; public ConnectThread(BluetoothDevice device) { mmDevice = device; BluetoothSocket tmp = null; // Pobierz gniazdo Bluetooth dla połączenia // z danym urządzeniem Bluetooth try { tmp = device.createRfcommSocketToServiceRecord(MY_UUID); } catch (IOException e) { Log.e(TAG, "błąd funkcji create()", e); } mmSocket = tmp;
135
ROZDZIAŁ 7. TWORZENIE GRY DWUOSOBOWEJ
} public void run() { Log.i(TAG, "START mConnectThread"); setName("ConnectThread"); // Należy zawsze unikać fazy discovery, ponieważ zdecydowanie spowalnia ona połączenie mAdapter.cancelDiscovery(); // Nawiąż połączenie z gniazdem Bluetooth try { // To jest wywołanie blokujące, które zwróci wartość jedynie w przypadku // nawiązania poprawnego połączenia lub wystąpienia wyjątku mmSocket.connect(); } catch (IOException e) { connectionFailed(); // Zamknij gniazdo try { mmSocket.close(); } catch (IOException e2) { Log.e(TAG, "błąd zamykania gniazda spowodowany błędem połączenia", e2); } // Ponownie uruchom usługę w trybie nasłuchu GameView.this.start(); return; } // Wyzeruj zmienną mConnectThread synchronized (BluetoothChatService.this) { mConnectThread = null; } // Uruchom wątek połączenia connected(mmSocket, mmDevice); } public void cancel() { try { mmSocket.close(); } catch (IOException e) { Log.e(TAG, "błąd funkcji close()", e); } } }
Pod względem obsługi próby połączenia z innym urządzeniem ten wątek przypomina poprzedni. Ten fragment kodu także zawarty był w przykładach Androida, dlatego został on pozostawiony bez zmian. Jeżeli takie szczegóły interesują Czytelnika, na początku kod podejmuje jedną próbę nawiązania połączenia z innym urządzeniem. W przypadku błędu kolejne próby mogą być kontynuowane w bloku try, w którym wystąpienie błędu spowoduje powtórne przejście w tryb nasłuchu. Na całe szczęście jesteśmy jedynie zainteresowani wysyłaniem danych tam i z powrotem i nie musimy zmieniać sposobu nawiązywania połączenia. Klasa ConnectedThread wykonuje nadzwyczajną ilość pracy. Ten kod uruchamiany jest zawsze wtedy, gdy urządzenia są w stanie połączenia. Zwróćmy uwagę, że na początku pobierane są strumienie wejściowy oraz wyjściowy, aby możliwe było odbieranie danych z innego urządzenia lub wysyłanie własnych informacji.
136
GRA DWUOSOBOWA Z POŁĄCZENIAMI RÓWNORZĘDNYMI
Listing 7.8. Wątek ConnectedThread private class ConnectedThread extends Thread { private final BluetoothSocket mmSocket; private final InputStream mmInStream; private final OutputStream mmOutStream; public ConnectedThread(BluetoothSocket socket) { Log.d(TAG, "utwórz ConnectedThread"); mmSocket = socket; InputStream tmpIn = null; OutputStream tmpOut = null; // Pobierz strumienie wejściowe i wyjściowe gniazda try { tmpIn = socket.getInputStream(); tmpOut = socket.getOutputStream(); } catch (IOException e) { Log.e(TAG, "błąd tworzenia gniazd tymczasowych", e); } mmInStream = tmpIn; mmOutStream = tmpOut; } public void run() { Log.i(TAG, "START mConnectedThread"); byte[] buffer = new byte[1024]; int bytes; // Dopóki jest połączenie, nasłuchuj strumień wejściowy while (true) { try { // Czytaj ze strumienia wejściowego bytes = mmInStream.read(buffer); // Wyślij otrzymane bajty do głównej aktywności mHandler.obtainMessage(MainActivity.MESSAGE_READ, bytes, -1, buffer) .sendToTarget(); } catch (IOException e) { Log.e(TAG, "rozłączony", e); connectionLost(); break; } } } /** * Zapisz do podłączonego strumienia OutStream. * @param buffer Bajty do zapisu */ public void write(byte[] buffer) { try { mmOutStream.write(buffer); // Udostępnij wysłany komunikat głównej aktywności mHandler.obtainMessage(MainActivity.MESSAGE_WRITE, -1, -1, buffer)
137
ROZDZIAŁ 7. TWORZENIE GRY DWUOSOBOWEJ
.sendToTarget(); } catch (IOException e) { Log.e(TAG, "Wyjątek podczas zapisu ", e); } } public void cancel() { try { mmSocket.close(); } catch (IOException e) { Log.e(TAG, "błąd funkcji close()", e); } } }
W dalszej kolejności w metodzie run() umieszczona jest pętla, w której następuje stałe sprawdzanie obecności nowych danych do przetworzenia. Większość danych przesyłana jest w postaci liczb całkowitych, jednak użycie łańcuchów znakowych w charakterze łącznika pomiędzy urządzeniami ma też pewne zalety. Po pierwsze, w złożonych grach może występować wiele liczb koniecznych do przesłania (np. poziom życia lub amunicji, położenie czy wyposażenie). Wysyłanie samych liczb nie jest zbyt zrozumiałe. Natomiast łańcuch w postaci "a:10" może zostać bardzo szybko odnaleziony i rozdzielony na wartość liczbową oraz jej opis przed znakiem dwukropka. Na tej podstawie można szybko podjąć decyzję, czy konieczna jest zmiana. Za pętlą występuje metoda wysyłająca komunikat w buforze do innego urządzenia. Metoda jest łatwa do zrozumienia i wysyła komunikat w takiej postaci jak w buforze. Przed dodaniem wątków konieczne jest jeszcze zdefiniowanie pewnych metod używanych do wysyłania danych i wywoływania tych wątków podczas wykonywania określonych akcji. Pamiętajmy przy tym, że wątki nie zostały jeszcze w żaden sposób zainicjalizowane ani użyte. Wszystkie wymienione metody przedstawione są na listingu 7.9. Listing 7.9. Połączenie z urządzeniem Bluetooth public synchronized void start() { if (D) Log.d(TAG, "start"); // Usuń dowolny wątek próbujący nawiązać połączenie if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;} // Usuń dowolny wątek obsługujący połączenie if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;} // Uruchom wątek nasłuchujący na gnieździe Bluetooth if (mAcceptThread == null) { mAcceptThread = new AcceptThread(); mAcceptThread.start(); } setState(STATE_LISTEN); } public synchronized void connect(BluetoothDevice device) { if (D) Log.d(TAG, "połącz z: " + device); // Usuń dowolny wątek próbujący nawiązać połączenie if (mState == STATE_CONNECTING) { if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;} }
138
GRA DWUOSOBOWA Z POŁĄCZENIAMI RÓWNORZĘDNYMI
// Usuń dowolny wątek obsługujący połączenie if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;} // Uruchom wątek w celu nawiązania połączenia z danym urządzeniem mConnectThread = new ConnectThread(device); mConnectThread.start(); setState(STATE_CONNECTING); } public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) { if (D) Log.d(TAG, "połączony"); // Usuń wątek, który ukończył połączenie if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;} // Usuń dowolny wątek obsługujący połączenie if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;} // Usuń wątek AcceptThread, ponieważ chcemy połączyć się tylko z jednym urządzeniem if (mAcceptThread != null) {mAcceptThread.cancel(); mAcceptThread = null;} // Uruchom wątek w celu zarządzania połączeniem i transmisją mConnectedThread = new ConnectedThread(socket); mConnectedThread.start(); Message msg = mHandler.obtainMessage(MainActivity.MESSAGE_DEVICE_NAME); Bundle bundle = new Bundle(); bundle.putString(BluetoothChat.DEVICE_NAME, device.getName()); msg.setData(bundle); mHandler.sendMessage(msg); setState(STATE_CONNECTED); } public synchronized void stop() { if (D) Log.d(TAG, "stop"); if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;} if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;} if (mAcceptThread != null) {mAcceptThread.cancel(); mAcceptThread = null;} setState(STATE_NONE); } public void write(byte[] out) { // Utwórz obiekt tymczasowy ConnectedThread r; // Synchronizuj kopię wątku ConnectedThread synchronized (this) { if (mState != STATE_CONNECTED) return; r = mConnectedThread; } // Wykonaj niesynchronizowany zapis r.write(out); } private void connectionFailed() { setState(STATE_LISTEN);
139
ROZDZIAŁ 7. TWORZENIE GRY DWUOSOBOWEJ
// Wyślij komunikat o błędzie z powrotem do aktywności Message msg = mHandler.obtainMessage(MainActivity.MESSAGE_TOAST); Bundle bundle = new Bundle(); bundle.putString(BluetoothChat.TOAST, "Błąd połączenia z urządzeniem"); msg.setData(bundle); mHandler.sendMessage(msg); } private void connectionLost() { setState(STATE_LISTEN); // Wyślij komunikat o błędzie z powrotem do aktywności Message msg = mHandler.obtainMessage(MainActivity.MESSAGE_TOAST); Bundle bundle = new Bundle(); bundle.putString(MainActivity.TOAST, "Utracono połączenie z urządzeniem"); msg.setData(bundle); mHandler.sendMessage(msg); }
Ponieważ wcześniej widzieliśmy już kod wątków, łatwo zrozumiemy, że głównym zadaniem tych funkcji jest uruchomienie wątków. Pierwsze trzy funkcje uruchamiają trzy wątki (AcceptThread, ConnectThread oraz ConnectedThread). Kiedy gra dobiega końca (to znaczy postać umiera), wywoływana jest funkcja stop(), zapewniająca, że żaden z wątków nie będzie działał nieskończenie. W przypadku gdybyśmy chcieli wysłać coś do innego urządzenia, użyjemy metody write(). Na koniec dwie inne metody wykorzystują uchwyt do wyświetlania komunikatów w chwili utraty połączenia lub wystąpienia błędu.
Modyfikacja kodu gry dla dwóch graczy Dysponujemy już większością kodu do nawiązywania oraz utrzymywania połączenia. Teraz musimy zastanowić się, w jaki sposób gra będzie współpracować z interfejsem Bluetooth. Cały kod dla tej prostej gry okazał się zbyt duży, aby zmieścił się na stronach tej książki. Można go jednak pobrać ze strony: http://code.google.com/p/android-tablet-games/. Cały dodatkowy kod pominięty w tym miejscu obsługuje wybór urządzenia do podłączenia (co nie jest istotne w tym momencie). Kontynuując już bez zbędnej zwłoki nasz opis, podczas gry chcielibyśmy mieć na ekranie dwie rakietki: jedną na górze ekranu i jedną na dole. Cały ważny kod z metody update() w klasie GameView umieszczony jest na listingu 7.10. Zauważmy, że w istniejących funkcjach konieczne jest zainicjalizowanie sprajta o nazwie paddle_other, a także dodanie go do funkcji draw(). Sprajt ten zostanie umieszczony u góry ekranu i będzie używać tego samego obrazka co druga rakietka. Listing 7.10. Dodawanie rakietki, wykrywanie kolizji oraz uaktualnianie stanu gry //dane wejściowe dla rakietki int val=0; for (int i=latest_input.length-1, j = 0; i >= 0; i--,j++) { val += (latest_input[i] & 0xff) 97 && boat_count < 12){ int previous_boat = boat_count - 1; if(boat_count == 0 || boat[previous_boat].getX() > 150){ boat[boat_count] = new SpriteObject(BitmapFactory.
149
ROZDZIAŁ 8. JEDNOOSOBOWA GRA STRATEGICZNA
´decodeResource(getResources(),R.drawable.boat), 100, 150); boat[boat_count].setMoveX(3); boat_count++; } }
5. Najpierw utworzony zostaje generator liczb pseudolosowych. Wywoływana jest metoda nextInt() wybierająca liczbę całkowitą z przedziału pomiędzy wartością 0 a podanym parametrem. Następnie sprawdzana jest wartość zmiennej check_boat, co pozwala na tworzenie łodzi w losowych przedziałach. Uwaga! Utworzenie generatora liczb pseudolosowych i wykorzystanie go do pobrania liczby całkowitej z przedziału od zera do zdefiniowanej przez siebie wartości jest doskonałym sposobem zwiększenia losowości gry. Nie trzeba się przy tym dłużej martwić o wartości dziesiętne, ponieważ z liczbami całkowitymi pracuje się znacznie łatwiej. Należy przy tym pamiętać, że konieczne jest wielokrotne przetestowanie gry używającej generatora, ponieważ może się ona zachowywać w sposób nieprzewidziany wcześniej, w sytuacji gdy wylosowane zostaną liczby, których autor gry nie wziął pod uwagę.
6. Pierwsza instrukcja if zostanie wykonana jedynie w przypadku, gdy wylosowana liczba jest większa od 97, co jest bardzo mało prawdopodobne, ale dzięki temu utrzymuje się potok łodzi na minimalnym poziomie. Następnie wartość zmiennej boat_count musi być mniejsza od 12. To porównanie z kolei zapobiega jednoczesnemu pojawieniu się zbyt wielu łodzi w obszarze gry. Oczywiście jeżeli te ustawienia okażą się zbyt proste dla gracza, mogą zostać zmienione, co sprawi, że gra stanie się bardziej wymagająca. 7. Druga instrukcja if sprawdza, czy tworzona łódź jest pierwszą z kolei, a jeśli nie, to czy zachowana zostanie pewna odległość od poprzednio utworzonej łodzi. W tym celu wartość boat_count jest zmniejszana o 1 i sprawdzane jest, czy współrzędna x łodzi o takim indeksie jest większa od 150. Gdyby ten warunek nie został spełniony, łodzie mogłyby zachodzić na siebie, co z pewnością zmniejszyłoby atrakcyjność gry pod względem wizualnym (chociaż zwiększyłoby poziom trudności!). 8. W przypadku gdy oba warunki są spełnione, wtedy współrzędna x obiektu łodzi jest inicjalizowana początkową wartością 100. Łódź będzie poruszać się raczej z niską prędkością trzech pikseli na każde wykonanie funkcji update(). W tym miejscu kryje się kolejna możliwość zwiększenia trudności gry dzięki stopniowemu zwiększaniu szybkości łodzi w przypadku, gdy gracz osiągnie pewien określony wynik lub też uzyska innego rodzaju osiągnięcie. 9. Na koniec wartość zmiennej boat_count jest zwiększana, aby funkcja draw() mogła obsłużyć również dodaną przed chwilą łódź. W ten sposób nasza flota uległa powiększeniu. 10. Konieczna będzie jeszcze zmiana kierunku poruszania się łodzi w ten sposób, aby mogły one wykonać właściwy zwrot, kierując się do swojego celu, jakim jest zamek. Tę funkcję spełnia kod przedstawiony na listingu 8.10. Należy go dodać do metody update(). Listing 8.10. Zmiana kierunku poruszania się łodzi for(int i = 0; i < boat_count; i++){ if((int)boat[i].getX() > 950){ boat[i].setMoveX(0); boat[i].setMoveY(3); boat[i].setBitmap(BitmapFactory.decodeResource(getResources(), ´R.drawable.boatdown)); } }
150
SKŁADANIE ELEMENTÓW GRY
11. W chwili gdy łódź osiągnie punkt o współrzędnej x wynoszącej 950 pikseli, przestanie poruszać się w prawo, a skieruje się w dół. Zwróćmy uwagę na ostatni wiersz kodu — zmieniliśmy w nim obrazek sprajta, ponieważ bardzo rzadko statki poruszają się bez zmiany kierunku. W tym celu oryginalny rysunek łodzi został obrócony o 90 stopni i zapisany w nowym pliku zasobów o nazwie boatdown. I to już wszystko. Kiedy dodamy działo, zobaczymy statki poruszające się losowo w kierunku zamku.
Dodawanie dział Podobnie jak w przypadku łodzi także liczba dział zmienia się w czasie gry. W tej chwili zajmiemy się jedynie udowodnieniem słuszności tego pomysłu. W tym celu wykonajmy następujące czynności: 1. Kod umieszczony na listingu 8.11 skopiujmy do konstruktora klasy GameView. Wartość zmiennej cannon_count może być zmieniona w celu utworzenia większej liczby dział. Nie będziemy czekać na początkowe rozstawienie dział przez gracza — zostaną one umieszczone automatycznie na trzech kolejnych blokach falochronu w odległości 100 jednostek od siebie. Listing 8.11. Zmiana liczby dział //sprajty dział cannon = new SpriteObject[cannon_count]; for(int i = 0; i < cannon_count; i++){ cannon[i] = new SpriteObject(BitmapFactory.decodeResource(getResources(), ´R.drawable.cannonup), (580 + i * 100), 200); }
2. Aby przygotować się do tworzenia dodatkowych sprajtów dział, nazwijmy oryginalny rysunek cannonup. Ułatwi to wprowadzanie modyfikacji, w przypadku gdy użytkownik zażyczy sobie, by zmienił się kierunek działa. 3. Dodajmy teraz kod przedstawiony na listingu 8.12 do funkcji onDraw(), co spowoduje wyświetlenie dział po rozpoczęciu gry. Listing 8.12. Rysowanie dział for(int i = 0; i < cannon_count; i++){ cannon[i].draw(canvas); }
W tej chwili pozostało nam do obsłużenia już tylko kilka przypadków. Podstawowy szkielet gry jest ukończony.
Dodawanie obrazów Obrazki użyte w grze są dostępne pod adresem: http://code.google.com/p/android-tablet-games/. Czytelnik może też wykonać swoje własne. Na rysunkach od 8.3 do 8.7 przedstawione są rysunki wykorzystane w grze Harbor Defender. Ich wymiary znajdują się w podpisach pod rysunkami. W dalszej części książki przedstawionych zostanie kilka pomysłów na utworzenie własnych rysunków. Należy pamiętać, że niekiedy trzeba obrócić rysunki lub wykonać ich lustrzane odbicie, aby wyświetlić je w innym stanie.
Rysunek 8.3. Obrazek zamku o wymiarach 200×100 pikseli
151
ROZDZIAŁ 8. JEDNOOSOBOWA GRA STRATEGICZNA
Rysunek 8.4. Rysunki łodzi w dwóch położeniach o wymiarach odpowiednio 50×30 oraz 30×50
Rysunek 8.5. Rysunek gruntu o wymiarach 800×250 pikseli
Rysunek 8.6. Rysunek falochronu o wymiarach 100×100 pikseli
Rysunek 8.7. Rysunek działa o wymiarach 100×100 pikseli
Testowanie gry Skoro mamy już podstawowy obraz wyglądu gry, możemy przystąpić do jej testowania. Wczytajmy ją w taki sam sposób jak każdą inną grę, a ujrzymy łodzie pojawiające się powoli na ekranie i kierujące się w stronę zamku. Jeżeli poczekamy odpowiednio długo, zobaczymy, że łodzie przechodzą przez zamek i znikają poza ekranem. Jeżeli gra nie zadziała w opisany powyżej sposób, otrzymamy komunikat o błędzie zaprezentowany na rysunku 8.8 lub też gra zawiesi swoje działanie tuż po uruchomieniu i oznaczać to będzie konieczność wykonania dodatkowej pracy. W tym podrozdziale zajmiemy się naprawą częstych problemów, jakie można napotkać podczas tworzenia gier dla systemu Android. Nie będziemy tu jednak opisywać konkretnych problemów, ponieważ nie sposób przewidzieć błędu każdego rodzaju. Błędy wychwycone przez środowisko Eclipse powinny być całkiem łatwe do naprawienia, zdecydowanie trudniej jest poradzić sobie z problemami występującymi w czasie działania programu.
152
TESTOWANIE GRY
Rysunek 8.8. Błąd w trakcie działania programu Harbor Defender A oto w jaki sposób przebiega proces testowania: 1. Upewnijmy się, czy do pobrania informacji o emulatorze używamy polecenia LogCat. Jego wykorzystanie będzie niezbędne w przypadku, gdy do wyświetlania komunikatów o konkretnych zdarzeniach wewnątrz programu używamy instrukcji Log.d. Instrukcja LogCat wyświetla także całkiem szczegółowe raporty o błędach. 2. Jeśli wystąpi błąd, nie zamykajmy emulatora. Spójrzmy na rysunek 8.8. Gdy pojawi się podobny błąd, możemy być skłonni do natychmiastowego zamknięcia emulatora, jednak takie zachowanie spowodowałoby usunięcie wszystkich informacji dla instrukcji LogCat. Poczekajmy więc z zamknięciem programu do czasu zdiagnozowania problemu. 3. Kiedy przewiniemy w górę informacje wyświetlone przez instrukcję LogCat, przedstawione na rysunku 8.9, powinniśmy ujrzeć wypisane na czerwono komunikaty sygnalizujące miejsce wystąpienia błędu. Na całe szczęście informacja o błędzie zawiera numer konkretnego wiersza, w którym wystąpił problem.
Rysunek 8.9. Informacja w programie LogCat o wystąpieniu błędu NullPointerException 4. W większości przypadków konieczne jest jedynie zwrócenie uwagi na kilka wcześniejszych wierszy przed wystąpieniem błędu. W przedstawionej sytuacji błąd wystąpił w funkcji onDraw() podczas rysowania dział. Przyczyną błędu było umieszczenie miejsca inicjalizacji sprajtów dział
153
ROZDZIAŁ 8. JEDNOOSOBOWA GRA STRATEGICZNA
w komentarzu. To dość powszechny problem w przypadku gry, która tworzy oraz usuwa sprajty w trakcie działania programu. Aby uniknąć błędu, należy upewnić się, czy obiekty, do których odwołujemy się w trakcie rysowania oraz uaktualniania widoku, rzeczywiście istnieją. 5. Ostatnią podpowiedzią przy rozwiązywaniu problemów z błędami będzie zmniejszenie emulatora. Jeżeli Czytelnik dysponuje komputerem z niewielką rozdzielczością ekranu, wtedy emulator może zająć jego większą część. Uniemożliwi to obserwowanie okna z instrukcją LogCat w trakcie działania programu. Aby zmniejszyć ekran emulatora, należy wybrać menu Run (uruchom), a w nim pozycję Run configuration (konfiguracja uruchomieniowa). Następnie należy przejść do zakładki Target (konfiguracja docelowa) i przewinąć jej zawartość. W polu o nazwie command line options (parametry wiersza poleceń) należy wpisać scale .81. Wpisanie tej wartości spowoduje zmniejszenie ekranu emulatora do 80% jego pierwotnego rozmiaru. Uwaga! Jeżeli pomimo najlepszych starań, by wyeliminować błąd, będzie on nadal występował, dobrym źródłem informacji jest repozytorium StackOverFlow (http://stackoverflow.com/). W przyszłości jednak autor zaleca Czytelnikowi wykonywanie częstych testów pomiędzy niewielkimi zmianami kodu. W ten sposób łatwo można powrócić do poprzedniego działającego stanu aplikacji. Należy zawsze być przygotowanym na konieczność powrotu do stanu, w którym gra na pewno działała.
W kolejnym rozdziale wprowadzimy do gry wiele różnych poprawek. Jedną z najbardziej istotnych będzie dodanie możliwości sterowania działami przez użytkownika. W poprzednich grach gracz nigdy nie miał tylu możliwości sterowania. W tym przypadku będzie to stanowić niepowtarzalne doświadczenie. Kolejną modyfikacją będzie dodanie systemu punktacji, który zostanie użyty do nagradzania gracza za każdą zniszczoną łódź. Także algorytmy fizyczne muszą zostać uaktualnione, ponieważ po uderzeniu zamku przez łódź powinno nastąpić zakończenie gry, a nie, jak to ma miejsce obecnie, przeniknięcie łodzi przez zamek i wyjście poza ekran. Będziemy musieli również zająć się nowym czynnikiem, jaki stanowi niewłaściwe sterowanie przez użytkownika. Na przykład całkiem rozsądne jest kliknięcie elementu falochronu w celu umieszczenia na nim działa. Co się jednak stanie, jeżeli gracz nie trafi w falochron i kliknie obrazek oceanu? Konieczna będzie szybka i efektywna obsługa każdego kliknięcia, aby możliwa była natychmiastowa reakcja na polecenie użytkownika i zapobiegnięcie pojawianiu się dział na polach, na których nie mogą one zostać umieszczone. Ostatecznie dodamy pewną logikę oraz komunikaty poprawiające ogólny wygląd gry.
Podsumowanie W tym rozdziale przeszliśmy przez proces konfiguracji prawdziwej gry. Skoro mamy już wszystkie elementy na właściwych miejscach, możemy dodać funkcje, które zwiększą satysfakcję użytkownika. Grając w grę, gracz powinien czuć się komfortowo, a używanie sprajtów oraz obiektów przez nią wykorzystywanych nie powinno sprawiać mu kłopotu. W przyszłości Czytelnik będzie skupiał się raczej na zwiększeniu doznań użytkownika programu, a nie będzie się już tak bardzo zastanawiał nad ograniczeniami technicznymi wynikającymi z niskich umiejętności programowania. Bardzo często dużym ograniczeniem podczas tworzenia gier jest strona wizualna. Jeśli jednak gra jest zabawna oraz oryginalna, może w ten sposób nadrobić wiele innych braków. W tym momencie spróbujmy przygotować grę do publikacji.
1
W analogiczny sposób można zwiększyć rozmiar okna emulatora w sytuacji, gdy Czytelnik dysponuje dużym monitorem. Parametr scale przyjmuje wartości od 0,1 do 3. Pełny opis wartości parametrów podawanych z wiersza poleceń znajduje się na stronie http://developer.android.com/guide/developing/tools/emulator.html#startup-options — przyp. tłum.
154
ROZDZIAŁ 9
Jednoosobowa gra strategiczna Część II. Programowanie gry
Teraz gdy mamy już zdefiniowany szkielet aplikacji, możemy dodać kod, który będzie tworzył w pełni funkcjonującą grę. Oczywiście zawsze głównym celem jest zaprojektowanie jak najbardziej wydajnego kodu. Wraz ze wzrostem poziomu skomplikowania gier oraz w miarę dodawania większej liczby sprajtów działanie gier może ulegać spowolnieniu, ponieważ procesor będzie starał się nadążyć za przetwarzaniem kodu. Można temu zapobiec, stosując pewne sprytne metody programowania, które mogą zmniejszyć obciążenie procesora. Oczywiście wciąż niezbędne będzie pamiętanie o zakładanym celu tworzenia gry. Zanim dodamy pewne ozdobniki wyróżniające grę spośród innych, musimy przede wszystkim zapewnić jej poprawne działanie. Z doświadczenia autora książki wynika, że w rzeczywistości zawsze najtrudniejszą decyzją do podjęcia jest określenie momentu, w którym należy przestać pracować nad grą i wprowadzić ją na rynek. Istnieje cienka granica pomiędzy zbyt prostą grą a taką, w którą trudno jest grać z powodu jej przeładowania w funkcjonalności oraz dodatki, jakich zwykły użytkownik nie będzie miał czasu nawet poznać. Uwaga! W trakcie lektury kodu umieszczonego w tym rozdziale należy pamiętać o tym, że stosowanie instrukcji Log.d może zdecydowanie ułatwić zrozumienie działania kodu, a także wskazać aktualnie wykonywane funkcje. Ponieważ niektóre części kodu mogą być całkiem złożone, wykorzystanie tej techniki pomaga analizować stosowane metody, szczególnie w sytuacji, gdy otrzymany rezultat ich działania nie jest zgodny z oczekiwaniami.
A oto lista funkcjonalności, które w tym rozdziale należy dodać do gry, aby uzyskać w pełni działający program: • rozszerzenie obiektów sprajtów, • strzelanie kulami z dział, • usuwanie łodzi po ich trafieniu, • rozpoczęcie gry od początku po trafieniu zamku przez łódź. Niektóre z powyższych funkcjonalności można zapewnić w prosty sposób (jak na przykład zmniejszenie poziomu „energii” łodzi po trafieniu jej przez kulę). Inne natomiast wymagają pewnych przemyśleń i sprytnego programowania. Dla ułatwienia pracy Czytelnika w tym rozdziale zaprezentowane zostaną kompletne ciała metod. W ten sposób Czytelnik będzie mógł sprawdzić, czy wykonana przez niego wcześniej praca skutkowała otrzymaniem kodu niezbędnego dla docelowej gry. Takie podejście ułatwi również analizę wzajemnego wywoływania funkcji, a także informacji przez nie współdzielonych.
ROZDZIAŁ 9. JEDNOOSOBOWA GRA STRATEGICZNA
W kolejnym podrozdziale zajmiemy się ulepszeniami w pliku SpriteObject.java. Wprawdzie wprowadzimy tam tylko kilka modyfikacji, lecz będą one ułatwiać przeprowadzenie dalszych koniecznych zmian w pliku GameView.java.
Rozszerzenie sprajtów używanych w grze W tej grze od sprajtów będziemy sporo wymagać. Aby obsłużyć tę dodatkową funkcjonalność, potrzebujemy kilku nowych metod oraz zmiennych używanych przez wszystkie sprajty. Chociaż z danej funkcjonalności może w rzeczywistości skorzystać tylko jeden sprajt, nie będziemy dla niej tworzyć nowych klas, a w zamian sprawimy, że każdy ze sprajtów używanych w grze będzie dziedziczył z klasy SpriteObject. Ponieważ wszystkie sprajty są w dużej mierze podobne, nie istnieje konieczność, aby rozdrabniać projekt na mniejsze elementy. Niemniej jednak, jeżeli Czytelnik zdecyduje się rozszerzyć grę i umożliwić łodziom prowadzenie ostrzału, zmianę kierunku poruszania się lub wypuszczanie mniejszych łodzi, wtedy może zastanowić się nad utworzeniem oddzielnej, specjalnej klasy łodzi, która obejmowałaby wszystkie nowe możliwości. Zazwyczaj nową klasę tworzymy dla sprajta lub obiektu, który używa dwóch lub więcej specyficznych funkcji. Aby zmodyfikować plik SpriteObject.java, należy wykonać następujące czynności: 1. Na listingu 9.1 przedstawione są nowe niezbędne zmienne oraz wartości, które należy im przypisać. Ten kod powinien zostać dodany na początku klasy SpriteObject. Listing 9.1. Zmienne w klasie SpriteObject private int health = 3; private int Orientation = -1; public int LEFT = 0; public int RIGHT = 1; public int UP = 2; public int DOWN = 3; private boolean stack = false;
2. Wykorzystanie zmiennych z listingu 9.1 stanie się widoczne w funkcjach przedstawionych na listingu 9.2. Cały ten kod powinien zostać dodany pod koniec klasy SpriteObject. Nowe metody będą wykorzystywane przez sprajty. Listing 9.2. Nowe funkcje w klasie SpriteObject public boolean cursor_selection(int cursor_x, int cursor_y){ int sprite_right = (int)(getBitmap().getWidth() + getX()); int sprite_bottom = (int)(getBitmap().getHeight() + getY()); if(cursor_x > getX() && cursor_x < sprite_right && cursor_y > getY() && cursor_y < ´sprite_bottom){ return true; } else{ return false; } } public void setStacked(boolean s){ stack = s; } public boolean getStacked(){ return stack;
156
PROJEKTOWANIE STEROWANIA GRĄ
} public void diminishHealth(int m){ health -= m; } public int getHealth(){ return health; } public void setOrientation(int o){ Orientation = o; } public int getOrientation(){ return Orientation; }
Funkcja cursor_selection() jest bardzo potężną metodą, która zwraca wartość true w przypadku, gdy użytkownik dotknie sprajta, natomiast w przeciwnym razie zwraca wartość false. Właściwie jest ona prostą wersją metody collide(), jednak w tym przypadku analizuje jedynie polecenia użytkownika. W prezentowanej implementacji funkcji tej użyjemy do wyboru działa, które zostanie dodane na falochronie. Funkcje setStacked() oraz getStacked() wykorzystamy do ustalenia, czy na danym fragmencie falochronu umieszczone jest już działo. Jeżeli takie działo już istnieje, użytkownik nie będzie mógł umieścić na nim drugiego. Ponieważ niektóre fragmenty falochronu są lepszymi punktami obserwacji od innych, nie byłoby to zbyt uczciwe, gdyby użytkownik mógł tworzyć w nich warstwy z dział. Do obsługi poziomu uszkodzeń łodzi dodamy dwie nowe funkcje. Jedynym sprajtem w grze, który będzie mieć poziom energii, będą łodzie. Po trzykrotnym trafieniu łodzi będą one usuwane z gry. 3. Do sprawdzenia, czy dana łódź straciła całą swoją energię, będziemy używać zmodyfikowanej funkcji update() z klasy SpriteObject. Istniejący kod powinien zostać zastąpiony tym przedstawionym na listingu 9.3. Listing 9.3. Modyfikacja metody update() public void update(int adj_mov) { if(state == ALIVE){ x += x_move; y += y_move; if(health 950){ boat[i].setMoveX(0); boat[i].setMoveY(3); boat[i].setBitmap(BitmapFactory.decodeResource(getResources(), ´R.drawable.boatdown)); } } Random random_boat = new Random(); int check_boat = random_boat.nextInt(100); if(check_boat > 97 && boat_count < 12){ int previous_boat = boat_count - 1; if(boat_count == 0 || boat[previous_boat].getX() > 150){ boat[boat_count] = new SpriteObject(BitmapFactory.decodeResource ´(getResources(), R.drawable.boat), 100, 150); boat[boat_count].setMoveX(3); boat_count++; } }
163
ROZDZIAŁ 9. JEDNOOSOBOWA GRA STRATEGICZNA
Kod zaprezentowany na listingu 9.12 był już ukończony w rozdziale 8. Pierwsza pętla for określa, czy łódź nie przesunęła się zanadto w prawą stronę. Jeżeli tak się stało, wtedy zostaje wykorzystany nowy rysunek sprajta, a łódź zaczyna przesuwać się do dołu w kierunku zamku. W kolejnej części bloku kodu obsługiwane jest losowe wyświetlanie łodzi. Najważniejszą częścią jest instrukcja if, sprawdzająca, czy poprzednia łódź jest już odpowiednio daleko od nowej. Jeżeli tak, zwiększana jest wartość zmiennej służącej do przechowywania liczby łodzi, a następna łódź rozpoczyna swą podróż. Kod funkcji znajduje się na listingu 9.12. A teraz sprawdzimy, czy nastąpiło zderzenie z zamkiem, co skutkowałoby przegraną gracza. 2. Do metody update() należy dodać pętlę for z listingu 9.13. Listing 9.13. Sprawdzanie zderzenia z zamkiem oraz zerowanie stanu gry for(int i = 0; i < boat_count; i++){ if(boat[i].collide(castle)){ reset(); } }
W przypadku gdy użytkownik przegra i łódź uderzy w zamek, wywołana zostanie nowa funkcja o nazwie reset(). Za chwilę przyjrzymy się temu, co ta prosta funkcja robi (moglibyśmy umieścić w tym miejscu cały kod, jednak zdecydowanie lepiej pod względem wizualnym prezentować się będzie dodanie kilku funkcji obsługujących oddzielne zadania). Kiedy już łodzie pływają, a kule są gotowe do odpalenia, koniecznie trzeba się zająć działami. Bez nich nie będzie możliwe zorganizowanie obrony przed łodziami. W kolejnym podrozdziale opiszemy sposób obsługi oraz używania dział.
Strzelanie z dział Zagadnienie obsługi kul jest zaraz po przetwarzaniu poleceń użytkownika najbardziej złożoną częścią gry. Śledzenie torów 50 sprajtów, mogących poruszać się w czterech różnych kierunkach, a także potencjalnie będących w danym momencie w różnych stanach, jest czynnością skomplikowaną. Działa staną się jeszcze bardziej interesujące. W tym podrozdziale dodamy do programu kule oraz napiszemy kod obsługujący sposób, w jaki działa będą wypluwać z siebie serie pocisków. W tym celu wykonajmy następujące czynności: 1. Do konstruktora klasy GameView dodajmy kod umieszczony na listingu 9.14. Ten kod obsługuje nowe kule, którymi będzie strzelać działo. Aby uprościć zagadnienie, liczba kul na ekranie jest ograniczona do 50. W kodzie umieszczone są dwie tablice. Pierwsza z nich zawiera sprajty reprezentujące kule (bullets[]), natomiast druga przechowuje listę kul, które nie są aktualnie używane (available_bullet[]). Listing 9.14. Rozszerzenie metody onCreate() o obsługę kul available_bullet = new int[50]; for(int i = 0; i < 50; i++){ available_bullet[i] = i; } bullets = new SpriteObject[50]; for(int i = 0; i < 50; i++){ bullets[i] = new SpriteObject(BitmapFactory.decodeResource(getResources(), ´R.drawable.bullet), 10, 10); bullets[i].setState(bullets[i].DEAD); }
164
STRZELANIE Z DZIAŁ
W tym miejscu deklarujemy tablicę liczb całkowitych, w której wszystkie kule będą oznaczone jako dostępne, ponieważ wiemy, że żadna z nich nie została jeszcze wystrzelona. Również sprajty reprezentujące kule są inicjalizowane w tym miejscu. Ich stan jest ustawiany jako DEAD (ang. martwy), ponieważ nie chcemy, aby żadna z jeszcze niewystrzelonych kul była wyświetlana na ekranie. 2. Do metody update() dodajmy kod umieszczony na listingu 9.15. Na początku zmiennej available_bullet przypiszmy wartość zero. Ułatwi to zdecydowanie późniejsze obliczenia. Następnie utwórzmy bardzo ważną zmienną g=0. Zmienna ta będzie używana do wskazania, które z kul są dostępne, a które już nie. Listing 9.15. Zerowanie listy dostępnych kul for(int f = 0; f < 50; f++){ available_bullet[f] = 0; } int g = 0;
3. Zaraz po wyczyszczeniu tablicy kod z listingu 9.16 dodajmy do metody update(). Listing 9.16. Obsługa zmian w kulach for(int i = 0; i < 50; i++){ if(bullets[i].getY() > 800 || bullets[i].getX() > 1280 || bullets[i].getY() < 0 || bullets[i].getX() < 0){ bullets[i].setstate(bullets[i].DEAD); } for(int b = 0; b < boat_count; b++){ if(bullets[i].collide(boat[b])){ boat[b].diminishHealth(1); bullets[i].setstate(bullets[i].DEAD); } } bullets[i].update(adj_mov); if(bullets[i].getstate() == bullets[i].DEAD){ available_bullet[g] = i; g++; } }
Powyższa pętla przeszukuje każdy ze sprajtów reprezentujących kule. Pierwsza instrukcja if sprawdza, czy kula pozostała na ekranie. Jeżeli tak, jej stan jest zmieniany na DEAD. Oznacza to, że dana kula może być w następnej iteracji pętli wykorzystana jako dostępna. Pętla for obsługuje również zderzenia z łodziami. W przypadku gdy łódź zostanie trafiona przez kulę, poziom energii tej łodzi zmniejsza się o jeden, a kula zostaje usunięta, czyli może być ponownie wykorzystana. Proste wywołanie metody update() zmienia położenie kuli na podstawie wartości zmiennych moveX oraz moveY. Jeżeli kula jest w stanie DEAD, wtedy zostaje dodana do listy dostępnych kul. Jeżeli przyjrzymy się dokładnie instrukcji if, zauważymy, że pierwsza kula w tym stanie jest umieszczana w pierwszym wolnym miejscu tablicy available_bullet, wartość zmiennej g zwiększa się o jeden, a następne wolne miejsce wypełniane jest przez kolejną kulę. 4. Kiedy dysponujemy już kulami gotowymi do wystrzelenia, pora zająć się mechanizmem strzału. Pięćdziesiąt wywołań funkcji update() zwalania kulę z każdego działa znajdującego się na polu 165
ROZDZIAŁ 9. JEDNOOSOBOWA GRA STRATEGICZNA
gry. Czynność tę wykonuje kod znajdujący się na listingu 9.17, wywołujący nową funkcję createBullet() przyjmującą cztery argumenty. Kod powinien zostać umieszczony w metodzie update() bezpośrednio za kodem, który dodaliśmy do niej w poprzednim punkcie. Listing 9.17. Obliczanie momentu wystrzelenia kul shooting_counter++; if(shooting_counter >= 50){ shooting_counter = 0; int round = 0; for(int i = 0; i < cannon_count; i++){ if(cannon[i].getOrientation() == cannon[i].LEFT){ int x = (int)(cannon[i].getX()); int y = (int)(cannon[i].getY() + ´cannon[i].getBitmap().getHeight()/2); createBullet(x,y,cannon[i].LEFT, round); round++; } if(cannon[i].getOrientation() == cannon[i].RIGHT){ int x = (int)(cannon[i].getX() + cannon[i].getBitmap().getWidth()); int y = (int)(cannon[i].getY() + ´cannon[i].getBitmap().getHeight()/2); createBullet(x,y,cannon[i].RIGHT, round); round++; } if(cannon[i].getOrientation() == cannon[i].UP){ int x = (int)(cannon[i].getX() + ´cannon[i].getBitmap().getWidth()/2); int y = (int)(cannon[i].getY()); createBullet(x,y,cannon[i].UP, round); round++; } if(cannon[i].getOrientation() == cannon[i].DOWN){ int x = (int)(cannon[i].getX() + ´cannon[i].getBitmap().getWidth()/2); int y = (int)(cannon[i].getY() + cannon[i].getBitmap().getHeight()); createBullet(x,y,cannon[i].DOWN, round); round++; } } }
Powyższy fragment kodu tworzy zmienną o nazwie round, która służy do śledzenia numeru wystrzelonej kuli. Pierwsze działo wystrzeliwuje kulę w pierwszej rundzie, drugie w drugiej i tak dalej. Szereg instrukcji if używa nowej funkcji o nazwie getOrientation(), utworzonej w pliku SpriteObject.java. Współrzędne x oraz y wskazujące na koniec lufy każdego z dział są następnie przekazywane do metody o nazwie createBullet(). Uzyskanie tych współrzędnych wymaga pewnych obliczeń, ponieważ wiemy, że lufa znajduje się w centrum działa. Mechanizm działania kul nabierze jeszcze większego sensu po przeanalizowaniu funkcji createBullet(), którą utworzymy w następnym podrozdziale. W tym momencie załóżmy, że kod na listingu 9.17 po prostu wysyła do tej metody wszystkie niezbędne informacje. Ponieważ do tego momentu zostały zainicjalizowane już wszystkie sprajty reprezentujące kule, czynność ta nie będzie więcej obciążać procesora, skoro sprajty będą wymagać jeszcze jedynie uaktualnienia stanów. 5. Aby zakończyć uaktualnianie metody update(), upewnijmy się, że w kodzie występują już wywołania szeregu metod update() każdego ze sprajtów, tak jak zostało to przedstawione na listingu 9.18.
166
WYNIK DZIAŁANIA GRY
Listing 9.18. Dołączanie prostych funkcji update() castle.update(adj_mov); ground.update(adj_mov); for(int i = 0; i < boat_count; i++){ boat[i].update(adj_mov); } }
W kolejnym podrozdziale połączymy te luźne fragmenty kodu poprzez dodanie obsługi zerowania gry oraz mechanizmu wystrzeliwania kul.
Wynik działania gry Kiedy gracz przegra grę i łódź trafi w zamek, wtedy wywoływana jest funkcja reset(). Jest ona prostą i szybką funkcją. Aby utworzyć tę funkcję, należy wykonać następujące czynności: 1. Dodajmy kod umieszczony na listingu 9.19 do klasy GameView. Listing 9.19. Metoda reset() private void reset(){ for(int i = 0; i < boat_count; i++){ boat[i].setstate(boat[i].DEAD); } boat_count = 0; }
W powyższym kodzie jedyną wykonywaną czynnością jest zniszczenie łodzi. To w rzeczywistości powoduje rozpoczęcie gry od nowa, ponieważ łodzie są w sposób losowy tworzone od początku. Działa nie są usuwane, gdyż nie musimy się nimi zajmować. Jeżeli istniałaby taka potrzeba, gracz mógłby je usunąć samodzielnie. Jeżeli Czytelnik chciałby wyświetlić komunikat dla gracza, może w tym momencie utworzyć nowy sprajt i wyświetlić go na ekranie, następnie w funkcji update() odczekać 30 sekund lub więcej i na koniec usunąć komunikat. 2. Metoda createBullet() jest nieco bardziej skomplikowana, jak zobaczymy na listingu 9.20. Niemniej jednak jest ona całkowicie możliwa do zrozumienia. Powinna zostać umieszczona tuż za metodą reset(). Listing 9.20. Metoda createBullet() private void createBullet(int x, int y, int direction, int r){ if(r >= 0){ int index = available_bullet[r]; if(direction == bullets[index].RIGHT){ bullets[index].setMoveX(10); bullets[index].setMoveY(0); bullets[index].setX(x); bullets[index].setY(y); bullets[index].setstate(bullets[index].ALIVE); } if(direction == bullets[index].LEFT){ bullets[index].setMoveX(-10); bullets[index].setMoveY(0);
167
ROZDZIAŁ 9. JEDNOOSOBOWA GRA STRATEGICZNA
bullets[index].setX(x); bullets[index].setY(y); bullets[index].setstate(bullets[index].ALIVE); } if(direction == bullets[index].UP){ bullets[index].setMoveY(-10); bullets[index].setMoveX(0); bullets[index].setX(x); bullets[index].setY(y); bullets[index].setstate(bullets[index].ALIVE); } if(direction == bullets[index].DOWN){ bullets[index].setMoveY(10); bullets[index].setMoveX(0); bullets[index].setX(x); bullets[index].setY(y); bullets[index].setstate(bullets[index].ALIVE); } } }
Sprajty reprezentujące kule są symetryczne, dlatego nie musimy przejmować się ich obracaniem, a jedynie kierunkiem ich poruszania się. Nie zapomnijmy o ostatnim wierszu każdego z bloków instrukcji if, który powoduje zmianę stanu kuli na ALIVE (żywy). W przeciwnym razie kule nigdy nie zostaną wyświetlone, a Czytelnik będzie się zastanawiał, co poszło nie tak, jak powinno. W tym momencie ukończyliśmy już całkowicie nasz projekt gry. W kolejnym podrozdziale pojawi się kilka pomysłów na plany w przyszłości.
Analiza gry Jeżeli Czytelnik jeszcze tego nie zrobił, niech teraz uruchomi grę. Kiedy łodzie zaczną wypływać, Czytelnik będzie mógł umieścić działa, które będą bronić zamku. Autor życzy mu powodzenia w tej bitwie. A oto lista funkcji oraz metod programistycznych, których użyliśmy do stworzenia gry Harbor Defender; Czytelnik może być dumny z niesamowitego wysiłku, w którym trwał niezależnie od poziomu skomplikowania kodu, błędów oraz ogromu pracy do wykonania: • pętla gry, • wielokrotne sprajty, • rysowanie obrazów na ekranie, • modyfikacje map bitowych, • komunikacja z użytkownikiem, • pewne elementy sztucznej inteligencji, • wykrywanie zderzeń, • przetwarzanie danych z plików XML, • i wiele więcej. Dysponujemy już całą skończoną grą, możemy więc w tej chwili odprężyć się i zmodyfikować ją zgodnie ze swoimi pomysłami. Jeżeli Czytelnik doda wystarczająco dużo zmian, może będzie mógł zarabiać na niej w sklepie Android Market. Taka możliwość zostanie omówiona w ostatnim rozdziale tej książki.
168
PODSUMOWANIE
Dysponowanie grą, którą można dalej rozszerzać, jest bardzo istotne. Jeżeli programiści gier musieliby każdą z gier zaczynać od początku, prawdopodobnie nigdy nie stworzyliby wystarczającej liczby gier, aby opłacić czynsz. W rzeczywistości przekształcają oni jeden szkielet aplikacji w wiele indywidualnych i zadziwiająco różnych tworów. Program, który utworzyliśmy w tej książce, można na wiele sposobów przekształcać w grę labiryntową, platformową, turową grę strategiczną, a także w wiele innych rodzajów gier. Klasę SpriteObject można w całości ponownie wykorzystać, a klasę GameView można bez problemu przekształcić do zastosowania w innych rodzajach gier. Jeżeli Czytelnik potrzebuje dodatkowych pomysłów, pomocne może być zapoznanie się z innymi książkami opisującymi temat tworzenia gier, a następnie przekształcenie zawartych w nich przykładów do postaci umożliwiającej uruchomienie ich w systemie Android. Każda gra w dowolnym języku programowania może być z dużym prawdopodobieństwem utworzona również w systemie Android. Pewnym wyzwaniem mogą być gry projektowane pierwotnie na komputer osobisty i wykorzystujące klawiaturę. Jeżeli Czytelnik wykaże się pewną kreatywnością, prawie pewne jest, że będzie mógł stworzyć różne programy. Ukończona gra została przedstawiona na rysunku 9.2. Czy Czytelnik może wyobrazić sobie, na ile różnych projektów mogłaby zostać przekształcona?
Rysunek 9.2. Ukończony projekt
Podsumowanie Nasza ciężka praca, podczas której wiele się nauczyliśmy, została ukończona. W tym rozdziale Czytelnik dowiedział się, w jaki sposób utworzyć obiekt macierzy, którego można użyć do obracania mapy bitowej. Sprawdziliśmy również, w jaki sposób śledzić tor poruszania się 50 sprajtów, a także utrzymywać listę sprajtów wyeliminowanych z gry, a przez to gotowych do powtórnego użycia. W tym rozdziale uczyniliśmy też pierwszy krok na drodze do stworzenia interfejsu użytkownika z wieloma ikonami oraz wskaźnika pokazującego aktualny wybór dokonany przez użytkownika.
169
ROZDZIAŁ 9. JEDNOOSOBOWA GRA STRATEGICZNA
Jeżeli Czytelnik jest już zmęczony tworzeniem kodu, autor ma dla niego wspaniałą wiadomość. W kolejnym rozdziale zajmiemy się kwestiami publikacji gry, wprowadzania uaktualnień oraz warstwą biznesową. Przyjrzymy się temu, które z gier sprzedają się dobrze, oraz temu, w jaki sposób tablety zmieniają przestrzeń związaną z komputerami. Kiedy zrozumiemy już biznesowe aspekty tworzenia gier, zadaniem Czytelnika będzie utworzenie swojego własnego wielkiego dzieła.
170
ROZDZIAŁ 10
Publikacja gry
Nasza gra jest już gotowa do dystrybucji dla masowego użytkownika, zanim jednak będzie można ją nabyć, aplikacja musi przejść przez jeszcze kilka etapów. Do wykończenia programu będzie niezbędne przeprowadzenie kilku dodatkowych modyfikacji. W dalszej części tego rozdziału zajmiemy się również przejrzeniem czynności koniecznych do wykonania przed sprzedażą gry lub oddaniem jej za darmo. Na koniec sprawdzimy, jakie są sposoby na osiągnięcie sukcesu na bardzo konkurencyjnym rynku aplikacji mobilnych. Stworzenie dobrej jakościowo gry jest dopiero pierwszym krokiem na drodze do osiągnięcia statusu najlepszego sprzedawcy w sklepie z aplikacjami Androida1. Wszystko, co zrobiliśmy do tej pory, może składać się na prezentację efektu końcowego ostatecznego produktu. Zarówno grafika, jak też dźwięki oraz ogólny wygląd aplikacji decydują o tym, w jaki sposób będzie ona sprzedawana potencjalnym klientom.
Poprawianie aplikacji Chociaż gra w obecnym stanie jest już grywalna, może wymagać jeszcze pewnych poprawek. Miłym dodatkiem mógłby być na przykład ekran startowy, z którego gracze przed rozpoczęciem gry mogliby dowiedzieć się o niej nieco więcej. Oczywiście ekran taki można stworzyć na wiele sposobów. W przypadku podstawowego ekranu jest to relatywnie proste, a w każdej z tworzonych przez siebie gier Czytelnik może go później rozbudowywać. W tym podrozdziale dodamy do gry ekran startowy oraz przycisk służący do rozpoczęcia rozgrywki.
Dodawanie ekranu początkowego Ponieważ nadzorowaniem właściwej rozgrywki zajmuje się klasa GameView umieszczona w pliku GameView.java, strona startowa będzie obsługiwana przez kod umieszczony w pliku MainActivity.java. Zamiast konfigurować ekran w taki sposób, aby od razu pokazywał widok z klasy GameView, utworzymy nowy prosty widok i damy użytkownikowi możliwość zdecydowania o samodzielnym rozpoczęciu
1
Na początku roku 2012 firma Google zmieniła politykę dotyczącą sprzedaży aplikacji mobilnych. Dotychczasowy sklep Android Market został zastąpiony usługą Google Play, w ramach której firma Google planuje zintegrować wszystkie usługi z dziedziny rozrywki. W Google Play będą również rozpowszechniane aplikacje dla systemu Android. Docelowo będzie to tylko jedna z usług rozrywkowych oferowanych przez Google obok filmów, muzyki oraz książek. Strona dostępna jest pod adresem: https://play.google.com/store — przyp. tłum.
ROZDZIAŁ 10. PUBLIKACJA GRY
rozgrywki. Dzięki temu sprawimy, że nasza praca będzie wyglądać bardziej profesjonalnie, a użytkownikowi będzie łatwiej obsługiwać program. Idąc dalej, na ekranie startowym mógłby być odtwarzany krótki plik wideo wprowadzający do gry, jednak to pozostawimy już Czytelnikowi. Aby sprawdzić, jak wygląda nasz ekran startowy, spójrzmy na rysunek 10.1. W dalszej części tego podrozdziału zajmiemy się sposobami dołączania dodatkowych funkcji do ekranu startowego, w razie gdyby w przyszłości zaszła taka potrzeba.
Rysunek 10.1. Wprowadzenie do gry Aby uzyskać wygląd ekranu przedstawiony na rysunku 10.1, powróćmy na chwilę do tematów omówionych w rozdziale 1. Wygląd aplikacji generowany jest na podstawie zawartości pliku main.xml, w którym można stworzyć interfejs użytkownika, przeciągając na ekran przyciski oraz umieszczając na nim tekst. Następnie zarówno przyciski, jak też tekst można poddawać modyfikacjom. Poniżej przedstawione są czynności konieczne do wykonania w tym celu: 1. Odnajdźmy plik o nazwie main.xml znajdujący się w projekcie Harbor Defender. W tym celu przejdźmy do katalogów res, a następnie layout. 2. Otwórzmy plik main.xml i z rozwijanego menu u góry ekranu wybierzmy pozycję 10.1 in WXGA (tablet). Kolejną czynnością będzie przyjrzenie się zawartości pliku main.xml. 3. Wybierzmy plik main.xml wyświetlany na małej zakładce u dołu ekranu. 4. Istniejący kod zastąpmy tym przedstawionym na listingu 10.1. Listing 10.1. Plik main.xml
Istniejący dotychczas układ elementów typu LinearLayout zastąpiliśmy układem AbsoluteLayout. Oba są pewnego rodzaju szablonami, do których można dołączać dodatkowe elementy. Układ typu AbsoluteLayer pozwala jednak określić dokładne położenie elementów, podczas gdy w układzie LinearLayout zostają one umieszczone liniowo i wyrównane do lewej strony. Decydowanie o położeniu elementów będzie istotną cechą w przypadku tworzenia ekranu startowego. 5. Powróćmy do okna układu graficznego strony, wybierając u dołu ekranu małą zakładkę o nazwie Graphical Layout (układ graficzny). 6. Z lewej strony umieszczona jest paleta z elementami koniecznymi do stworzenia układu graficznego. Jej wygląd przedstawiony jest na rysunku 10.2. Z tej palety należy przeciągnąć na ekran obiekt Button reprezentujący przycisk, a także TextView, czyli etykietę z tekstem. W tym momencie oba obiekty będą zawierać przykładowy tekst, który zostanie za chwilę zmieniony.
Rysunek 10.2. Z palet po lewej stronie ekranu można przeciągnąć obiekty TextView oraz Button 7. W tym momencie nadszedł czas, aby powrócić do edycji kodu. W tym celu u dołu ekranu wybierzmy zakładkę o nazwie main.xml. W elemencie AbsoluteLayout powinny pojawić się dwa nowe elementy (Button, czyli przycisk, oraz TextView, czyli etykietka z tekstem). 8. Zmieńmy teraz tekst wyświetlany na ekranie, a także identyfikator przycisku. Sprawdźmy wyróżniony kod na listingu 10.2. Oczywiście możemy użyć innych słów, ale najważniejsze jest, aby zapamiętać nazwę przypisaną do identyfikatora przycisku. Wiersze ze zmiennymi layout_x oraz layout_y określają położenie elementów. Czytelnik może zmienić te wartości w przypadku, gdyby chciał precyzyjnie określić położenie przycisku oraz tekstu. Jak zobaczymy w kolejnym podrozdziale, identyfikatory id przypisane do obiektów będą wykorzystywane w celu odwołania się do tych elementów w kodzie programu. 173
ROZDZIAŁ 10. PUBLIKACJA GRY
Listing 10.2. Zawartość pliku main.xml
Reakcja na wciśnięcie przycisku W tej chwili, gdy dysponujemy już ładnym ekranem witającym użytkownika, musimy jeszcze sprawić, aby gracz mógł go użyć. Istotne jest, by użytkownik mógł szybko rozpocząć grę. Dotyczy to szczególnie graczy wznawiających rozgrywkę. Pamiętajmy, że jeżeli gracz powraca do gry, oczekuje możliwości szybkiego rozpoczęcia rozgrywki i nie chce czytać instrukcji ani też być niepokojony żadnym filmem. Aby wyświetlić nowy układ ekranu, a następnie umożliwić użytkownikowi przejście do rzeczywistej rozgrywki, powróćmy na chwilę do pliku MainActivity.java. W tym pliku zostanie wykonane szybkie sprawdzenie poleceń użytkownika, a następnie przejście do gry. Najpierw jednak musimy sprawić, aby plik Main.xml stał się głównym widokiem gry i zastąpił GameView.java. W tym celu wykonajmy następujące czynności: 1. W panelu edycyjnym programu Eclipse otwórzmy plik MainActivity.java. 2. U góry pliku dodajmy następującą instrukcję import: import android.widget.Button;
3. Zmieńmy metodę onCreate umieszczoną w pliku MainActivity.java w taki sposób, aby przypominała tę na listingu 10.3. Miejsca w kodzie zmienione w stosunku do poprzedniej wersji pliku zostały wyróżnione pogrubieniem. Aby kod w pliku zadziałał poprawnie, należy dodać instrukcję: import android.view.View;
Listing 10.3. Zawartość pliku MainActivity.java @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
174
OPAKOWYWANIE GRY
mGameView = new GameView(this); setContentView(R.layout.main); mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE); mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); final Button button = (Button) findViewById(R.id.startgame ); button.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { setContentView(mGameView); } }); }
Pierwsza instrukcja setContentView() nakazuje aplikacji wczytać plik Main.xml jako podstawowy układ widoku. W dalszej części rejestrowana jest klasa nasłuchująca zdarzenia wciśnięcia przycisku. Gdy takie zdarzenie wystąpi, ponownie wywoływana jest metoda setContentView(), tym razem w celu wyświetlenia na ekranie widoku zdefiniowanego przez klasę GameView. W ten prosty sposób zainicjalizujemy grę. W sytuacji gdy przyciskowi nadawana jest pewna wartość, używana jest funkcja findViewById. Funkcja ta przyjmuje w charakterze argumentu identyfikator przycisku. Właśnie dlatego identyfikator ten powinien być łatwo kojarzony z obiektem rozpoczynającym grę. 4. Po uruchomieniu gry ujrzymy ekran powitalny, natomiast po wciśnięciu przycisku pojawi się widok aplikacji działający tak jak poprzednio. Gratulacje! W tym momencie Czytelnik ukończył już całkowicie opracowywanie kodu analizowanego w tej książce. W kolejnym podrozdziale zajmiemy się utworzeniem ostatecznej kompilacji gry i przygotowaniem jej do rozpowszechniania. Czytelnik jest już coraz bliżej chwili, w której będzie mógł podzielić się swoją pracą z innymi użytkownikami.
Opakowywanie gry Zanim gra będzie gotowa do dystrybucji, musimy zająć się jeszcze kilkoma kwestiami. W tym podrozdziale omówimy sposób czyszczenia kodu oraz jego ostatecznej kompilacji do postaci pliku APK, który będzie gotów do rozpowszechniania. Plik APK jest pakietem zawierającym cały kod gry, obrazki oraz zasoby. W celu utworzenia tego pliku należy wykonać następujące czynności: 1. Po pierwsze, z kodu należy usunąć wszystkie polecenia Log.d. W tym celu najłatwiej jest wykonać na całym kodzie wyszukanie wraz z zamianą znalezionego tekstu na pusty łańcuch znakowy. Z pewnością nie chcielibyśmy, aby wersja przeznaczona do sprzedaży marnowała moc obliczeniową urządzenia, wysyłając komunikaty testowe na konsolę. 2. W dalszej kolejności konieczne będzie poprawienie wersji kodu umieszczonej w pliku manifestu systemu Android. Plik ten możemy odnaleźć, przechodząc do głównego katalogu projektu Harbor Defender, w którym jest umieszczony plik AndroidManifest.xml. Kod zawarty w tym pliku powinien przypominać ten przedstawiony na listingu 10.4. Listing 10.4. Plik AndroidManifest.xml
Zwróćmy uwagę na wyróżnione pogrubieniem części pliku. Oczywiście Czytelnik może podać własny numer wersji kodu oraz nazwę, jednak zazwyczaj w przypadku pierwszej wersji gry używa się wartości 1.0. Należy także upewnić się, czy w przypadku minimalnej wersji SDK podana jest wartość 11. 3. W programie Eclipse wybierzmy menu File (plik), a następnie pozycję Export (eksportuj). 4. Jako rodzaj eksportu, jaki chcemy wykonać, wybierzmy Export Android Application (eksport aplikacji Android). 5. Na kolejnej stronie wprowadźmy nazwę ostatecznego projektu, czyli Harbor Defender. 6. W następnym kroku musimy stworzyć plik z certyfikatem, który będzie wymagany do ochrony bezpieczeństwa aplikacji, a także używany w charakterze identyfikatora w sklepie Google Play. W tym celu w sposób przedstawiony na rysunku 10.3 wybierzmy opcję Create New Keystore (utwórz nowy plik z kluczami). Następnie skorzystajmy z przycisku Browse (przeglądaj) i otwórzmy okno, które pozwoli umieścić wygenerowany plik w odpowiednim katalogu. Jako nazwę pliku podajmy coś w rodzaju harbordefenderkey, a następnie zaakceptujmy domyślne położenie zasugerowane przez program. 7. Dla własnego bezpieczeństwa Czytelnik powinien utworzyć niepowtarzalne oraz trudne do odgadnięcia hasło w sposób przedstawiony na rysunku 10.3. 8. Następnie należy wypełnić formularz umieszczony na stronie Key Creation (tworzenie klucza) w sposób przedstawiony na rysunku 10.4. Trzeba w nim podać wszystkie stosowne informacje (wspomniany rysunek przedstawia, jak powyższy formularz został wypełniony przez autora książki). Hasło może być identyczne z tym użytym w poprzednim kroku. 9. Kolejna strona będzie już ostatnią. Należy na niej kliknąć przycisk Browse (przeglądaj) i jako docelową nazwę APK podać Harbor Defender. Zamknięcie okna dialogowego spowoduje zakończenie procesu. I to już wszystko. Czytelnik właśnie skończył projekt. W kolejnym podrozdziale zajmiemy się tym, w jaki sposób projekt może zostać dodany do sklepu Google Play, a tym samym trafić w ręce potencjalnych klientów. Pobieżnie opowiemy również, jak najlepiej sprzedać swoją pracę i zaistnieć w przepełnionym sklepie.
Rozpowszechnianie gry Mam nadzieję, że Czytelnik jest zadowolony ze swojej gry i przekonany, że inni użytkownicy również ją polubią. W tym podrozdziale opiszemy, w jaki sposób używać sklepu Google Play. Dowiemy się, jak wysłać do niego aplikację, a także prześledzimy podstawy marketingu oraz sprzedaży. Dysponując tą wiedzą, Czytelnik będzie mógł stworzyć więcej gier do sprzedaży.
176
ROZPOWSZECHNIANIE GRY
Rysunek 10.3. Okno z prośbą o wygenerowanie klucza
Rysunek 10.4. Podawanie informacji o programiście 177
ROZDZIAŁ 10. PUBLIKACJA GRY
Aby rozpocząć, spójrzmy na rysunek 10.5 przedstawiający stronę startową sklepu Google Play (https://play.google.com/store?hl=pl).
Rysunek 10.5. Sklep Google Play z sekcją poświęconą aplikacjom Android Na powyższej stronie posiadacze urządzeń mobilnych z systemem Android i tabletów mogą kupować oraz pobierać aplikacje. Należy zwrócić uwagę szczególnie na zakładkę o nazwie Staff picks for tablets (warto zobaczyć — na tablety). Ponieważ firma Google stara się jak najbardziej zainteresować nabywców tabletów, aplikacje przeznaczone na ten typ urządzeń zostały wyróżnione w stosunku do tych przeznaczonych na telefony. Jest to wspaniała informacja, ponieważ w zasadzie oznacza mniejszą liczbę konkurentów. Jeżeli chodzi o sposób dystrybucji programów, to istnieje dość duża swoboda. Cena za aplikację może zostać ustalona w przedziale pomiędzy 1 a 200 dolarami (lub ich równowartością w lokalnej walucie — przyp. tłum.). Aplikacje mogą być też rozdawane za darmo. W przypadku zakupu aplikacji przez użytkownika jej twórca otrzymuje 70% ceny sprzedaży, natomiast reszta przeznaczana jest na koszty obsługi sklepu oraz wysłania aplikacji do urządzenia. Firma Google nie bierze opłat za pośrednictwo w procesie, ale twórcy urządzeń oraz dystrybutorzy są wynagradzani za obsługę transakcji w sposób analogiczny do firm rozliczających płatności kartami kredytowymi, które obciążają sprzedawców za każdą wykonaną transakcję. Twórcy aplikacji na urządzenia iPhone oraz iPad otrzymują jedynie 60% przychodów. Pod tym względem więc system Android ma po raz kolejny przewagę nad sklepem App Store firmy Apple.
178
ROZPOWSZECHNIANIE GRY
Ponad połowa aplikacji w sklepie Google Play jest darmowa. Rywalizujące sklepy z aplikacjami mają zdecydowanie niższy odsetek darmowych aplikacji. Konsekwencją dla Czytelnika będzie konieczność zrozumienia, że programy wymagające od użytkowników zapłaty muszą demonstrować zdecydowanie wyższy poziom wykonania od aplikacji darmowych, a także zapewniać wiele godzin rozrywki. W tym momencie znamy już podstawy sklepu z aplikacjami. Aby móc rozpowszechniać swoje programy, Czytelnik musi utworzyć konto w usłudze Google Play. Zajmiemy się tym w kolejnym podrozdziale.
Otwieranie konta w usłudze Google Play Nic nie satysfakcjonuje twórcy aplikacji bardziej niż ujrzenie swojego programu używanego przez innych ludzi. Poniżej omówimy sposób utworzenia konta developerskiego w usłudze Google Play, a następnie udostępnienia programu całemu światu. 1. Najpierw otwórzmy stronę usługi Google Play (https://play.google.com/store?hl=pl). Na samym dole ekranu znajduje się łącze o nazwie Developers (programiści), które należy w tej chwili kliknąć. 2. Na kolejnej stronie należy kliknąć rysunek o nazwie Publish (publikuj), co spowoduje przekierowanie do strony logowania. 3. Następnie należy zalogować się do własnego konta Google lub utworzyć nowe. Do celów dystrybucji aplikacji zalecane jest utworzenie nowego konta, aby możliwe było oddzielenie sprzedaży aplikacji od korespondencji mailowej czy też wpisów w usłudze Google+. 4. Kolejny ekran przedstawiony jest na rysunku 10.6. W dostępnym na stronie formularzu należy podać dokładne informacje o sobie. Jeżeli Czytelnik nie ma w danym momencie strony WWW, będzie mógł w tej chwili pozostawić to pole puste. Niemniej jednak na pewno w przyszłości będzie chciał taką stroną dysponować.
Rysunek 10.6. Tworzenie konta w usłudze Google Play 5. Po rejestracji konieczne będzie dokonanie opłaty rejestracyjnej. Opłata wynosi 25 dolarów i musi być uiszczona za pomocą usługi Google Checkout.
179
ROZDZIAŁ 10. PUBLIKACJA GRY
Po ukończeniu procesu rejestracji Czytelnik będzie dysponować swoim kontem. Przy jego użyciu będzie mógł zrobić wiele różnych rzeczy, począwszy od dodania konta w usłudze Google Checkout, a skończywszy na możliwości wysyłania aplikacji. W tym momencie będzie też gotowy, aby swoją grę zaoferować w usłudze Google Play.
Wysyłanie aplikacji do sklepu Google Play Pomimo że większość programistów chciałaby sprzedawać swoje aplikacje, to jednak w tym podrozdziale omówimy, w jaki sposób upowszechnić swoją aplikację za darmo. Gdyby jednak Czytelnik chciał otrzymać za swoją aplikację gratyfikację, powinien zapoznać się z następującym przewodnikiem o sklepie: http://developer.android.com/guide/publishing/publishing.html. Zanim Czytelnik będzie w stanie ukończyć ten prosty proces udostępniania gry, będzie musiał przygotować jeszcze kilka elementów: • plik APK z aplikacją, • dwa ładne zrzuty z ekranów aplikacji przedstawiające jej główne cechy, • ikonę aplikacji w dużej rozdzielczości, której gracz będzie używał w celu uruchomienia gry. Wysyłanie gry do sklepu jest prostą czynnością. Z konsoli dostępnej na koncie developerskim należy wybrać pozycję Upload App (wczytaj aplikację). Następnie zostanie wyświetlony kreator, który zapyta o kilka elementów wymienionych powyżej. Należy w nim wskazać położenie docelowych plików. Bardzo istotne jest, aby mieć atrakcyjny zrzut ekranu aplikacji, jej opis, a także wszelkie dodatkowe rysunki, którymi Czytelnik chciałby podzielić się z potencjalnym klientem. Powodzenie sprzedaży gry będzie zależeć od tego, jak bardzo użytkownicy zostaną zachęceni do jej zakupu. W kolejnym podrozdziale przyjrzymy się, w jaki sposób przygotować się na największy możliwy sukces na rynku.
Reklamowanie gry W celu jak najlepszego zareklamowania gry należy przekazać informacje o niej jak największej liczbie potencjalnych użytkowników. Jeżeli Czytelnik stworzył przyzwoitą grę, wtedy ludzie nabędą ją, o ile tylko będą mieli szansę ją zobaczyć. Pierwszym problemem jest to, w jaki sposób wyróżnić się spośród tłumu. Inaczej niż w przypadku aplikacji dla urządzeń typu iPad oraz iPhone dystrybuowanych przez sklep App Store programy dla systemu Android mogą być pobierane z dowolnej witryny, a nie tylko z oficjalnego sklepu firmy Google. Oznacza to, że programiści dysponujący własnymi witrynami mają o wiele bardziej ułatwioną sprzedaż swoich produktów, ponieważ nie muszą przejmować się wieloma podobnymi aplikacjami w sklepie. Użytkownicy mogą wyświetlić tego rodzaju stronę bezpośrednio i oglądać filmy, rysunki, jak też objaśnienia działania programu, co nie jest możliwe w sklepie Google’a, biorąc pod uwagę krótki opis tam dostępny. Czytelnik powinien z tego skorzystać i utworzyć własną stronę, a następnie zachęcić potencjalnych klientów do jej odwiedzenia. Utworzenie kont w usługach Facebook lub Twitter może także potencjalnie zwiększyć zainteresowanie stroną. Zamiast wskazywać na stronę w sklepie Google Play, należy klientów zachęcić do odwiedzenia własnej strony, która będzie sprawiać im mniej kłopotów. Jeżeli Czytelnik kiedykolwiek zajmował się marketingiem w internecie, z pewnością zdaje sobie sprawę, jak przydatne mogą być listy dystrybucyjne. Na swojej własnej stronie Czytelnik może umożliwić gościom zapisanie się do listy dystrybucyjnej przekazującej wiadomości o uaktualnieniach aplikacji oraz różnych darmowych dodatkach. W ten sposób Czytelnik może dodatkowo zainteresować klientów i przekonać ich do zakupu programów w przyszłości, nawet jeżeli nie zdecydują się na to od razu. Autor zachęca do przyjrzenia się stronie AWeber (www.aweber.com), która oferuje fantastyczny system pocztowy, możliwy do zastosowania do wysyłania listów z informacjami do użytkowników. Wspomniana powyżej usługa jest płatna, niemniej jednak wielu programistów szybko przekona się, że klienci pozyskani z takiego newslettera wygenerują więcej przychodów, niż będzie konieczne do pokrycia kosztu tej usługi. 180
PODSUMOWANIE
Ostatecznie Czytelnik może rozważyć umieszczenie nazwy firmy lub gry w bardziej tradycyjnych i zaufanych mediach. Może na przykład poprosić magazyny zajmujące się nowymi technologiami, aby ją oceniły, lub też wysłać informacje o grze do portali branżowych. Przed podjęciem takiego kroku Czytelnik powinien upewnić się, że jego gra oferuje coś specjalnego. Może to być na przykład całkowicie innowacyjny sposób sterowania lub też umieszczenie akcji gry w przestrzeni bez grawitacji. Aplikacja powinna być warta wspomnienia w mediach. Powyższe założenie dotyczy również całej firmy. Jeżeli na przykład grafika we wszystkich grach Czytelnika tworzona jest przez znanego artystę, będzie to z pewnością ciekawa informacja do zacytowania w mediach. Wszystkie powyższe techniki sprzedażowe nawiązują do podstawowego podejścia tunelowania strumienia klientów używanego w reklamie. Ta metoda została już zaprezentowana w szeregu podstawowych książek o marketingu oraz public relations. Niemniej jednak warto o niej wspomnieć również w tym miejscu. Im więcej potencjalnych klientów uda się zainteresować produktem, a także im dłużej to zainteresowanie będzie trwało, tym wyższych wyników sprzedaży można oczekiwać. Sposób działania powyższej techniki przedstawia rysunek 10.7.
Rysunek 10.7. Technika tunelowania gości strony w nabywców produktów To tyle, jeżeli chodzi o sztuczki marketingowe. Po pewnym okresie prób i błędów Czytelnik i tak sam znajdzie najlepszy dla siebie sposób. Z doświadczeń wynika, że w przypadku gier sprzedawanych w sklepie sukces jest bardzo rzadko osiągany dla pierwszej lub nawet drugiej gry. Czytelnik musi się do tego przyzwyczaić i zanim zacznie zarabiać słuszne pieniądze, musi przejść przez okres budowania na rynku atmosfery wyczekiwania, a nawet rozgorączkowania w oczekiwaniu na grę.
Podsumowanie Gratulacje! Właśnie ukończyłeś, Czytelniku, lekturę tej książki. Rozpoczęliśmy ją od odkrywania, czym jest system Android oraz w jaki sposób programuje się w tym systemie, a skończyliśmy na utworzeniu kompletnej gry i umieszczeniu jej w sklepie Google Play.
181
ROZDZIAŁ 10. PUBLIKACJA GRY
Pisanie tej książki było zajęciem ciekawym i interesującym. Autor ma nadzieję, że jej lektura sprawiła przyjemność także Czytelnikowi. Praca z szybko rozwijającą się technologią może być zarówno zniechęcająca, jak również inspirująca. W najlepszym razie ta książka dała Czytelnikowi pewien pogląd na temat tworzenia własnych gier dla tabletów z systemem Android. Autor jest pewien, że wraz z ostatnimi sukcesami, jakie odnosi system Android, oraz jego świetlaną przyszłością popyt na lepsze gry na tablety będzie rósł jeszcze przez długi czas. Czytelnik powinien postarać się skorzystać z tego trendu.
182
DODATEK A
Testowanie gier dla systemu Android na prawdziwym urządzeniu Jeśli Czytelnik będzie chciał tworzyć gry dla tabletu działającego pod kontrolą systemu Android, z pewnością będzie musiał przetestować je na prawdziwym urządzeniu. Android dysponuje wieloma wbudowanymi mechanizmami wspierającymi tego rodzaju testowanie. W ten sposób wyeliminowanych zostaje wiele przeszkód, które w przeszłości napotykali programiści pragnący testować swoje programy na konsolach do gier lub innych platformach mobilnych. Zarządcy sklepu z aplikacjami Android nie są zbyt wyrozumiali, jeśli chodzi o programy z błędami oraz problemami, które z łatwością mogłyby zostać usunięte w fazie testowania przedprodukcyjnego. W tym dodatku przyjrzymy się szybkiemu procesowi konfiguracji prawdziwego tabletu pod kątem testowania programów. Do tego zadania potrzebny będzie tablet działający w systemie Android 3.0 (lub nowszym). W trakcie pisania tej książki na rynku znajdowało się wiele tego rodzaju urządzeń i każdego tygodnia przybywały nowe. Dlatego niemożliwe byłoby wymienienie ich wszystkich w tym miejscu. Wybierając urządzenie, powinniśmy mniej kierować się jego zaawansowaniem technicznym, a bardziej jego popularnością na rynku. Jeżeli tego samego urządzenia będzie używać dużo użytkowników, wtedy uzyskane wyniki będą podobne dla większości z nich. Radzimy wybrać tablet znanego producenta z dużą liczbą ocen. Jeżeli Czytelnik ma przyjaciół posiadających tablety, wtedy powinien przetestować swoje aplikacje na wszystkich tych urządzeniach. Ponieważ proces opisany w tym miejscu nie zajmuje dużo czasu, Czytelnik nie powinien mieć z tym kłopotu. Ponieważ będziemy szukać błędów, interfejs sprzętowy będzie wymagał, aby aplikacja była skonfigurowana jako debuggable (umożliwiająca testowanie błędów). W tym celu zmodyfikujemy odpowiedni parametr w pliku AndroidManifest.xml. Kiedy spojrzymy na główny katalog projektu w eksploratorze programu Eclipse, nie ujrzymy pliku z manifestem. Na rysunku A.1 przedstawione jest miejsce umieszczenia tego pliku. Aby zdefiniować program jako skonfigurowany do testowania, należy do pliku XML dodać prosty parametr. Na listingu A.1 przedstawiony jest kod całego pliku z manifestem wraz z wyróżnionym pogrubieniem odpowiednim fragmentem, który należy dodać. Listing A.1 Plik z manifestem systemu Android
DODATEK A TESTOWANIE GIER DLA SYSTEMU ANDROID NA PRAWDZIWYM URZĄDZENIU
Rysunek A.1. Plik z manifestem systemu Android
Kolejna czynność będzie różnić się w zależności od używanego urządzenia. Firma Google zaleca, aby najpierw przejść na tablecie do katalogu Application (aplikacje), następnie do katalogu Development (dla programistów) i ostatecznie zaznaczyć opcję USB Debugging (debugowanie USB). Jeżeli ten opis nie będzie właściwy dla tabletu Czytelnika, należy poszukać w internecie ustawień włączających ten typ testowania. W tym momencie będziemy jeszcze potrzebować odpowiedniego sterownika dla danego urządzenia USB. Może on różnić się od tego, który był zainstalowany przy podłączeniu tabletu do komputera. Pełna lista sterowników USB umieszczona jest na stronie dla programistów systemu Android: http://developer.android.com/sdk/oem-usb.html. Proces instalacji sterowników jest bardzo prosty. Weźmy na przykład tablet firmy Motorola, który posiada autor tej książki. W celu skonfigurowania tabletu należy najpierw kliknąć odnośnik kierujący do strony domowej firmy Motorola ze sterownikami dla programistów. Ponieważ autor książki używa 64-bitowej wersji systemu Windows, wybrana została ostatnia wersja sterownika USB dla zestawów głośnomówiących (to, czy tablet jest urządzeniem głośnomówiącym, może być przedmiotem sporu, jednak sterownik jest w obu przypadkach ten sam). Po wykonaniu poleceń wyświetlanych w trakcie instalacji sterownika tablet zostanie skonfigurowany. Uwaga! Jeżeli Czytelnik używa do programowania komputera Macintosh, nie musi się martwić o sterowniki USB, ponieważ wszystko jest już odpowiednio skonfigurowane. Natomiast użytkownicy systemu Linux będą musieli się nieco napracować. Więcej informacji na ten temat przedstawionych jest w oficjalnej dokumentacji systemu Android opisującej proces konfiguracji urządzenia do programowania: http://developer.android.com/ guide/developing/device.html#setting-up.
184
TESTOWANIE GIER DLA SYSTEMU ANDROID NA PRAWDZIWYM URZĄDZENIU
Po poprawnym wypełnieniu powyższych instrukcji Czytelnik będzie mógł testować swoje programy bezpośrednio na urządzeniu. W tym celu należy przejść do programu Eclipse i tak jak zwykle uruchomić program. Zamiast jednak od razu testować go na emulatorze, Czytelnik będzie mógł wybrać urządzenie spośród opcji, które pojawią się automatycznie. Po wybraniu z listy podłączonego do komputera urządzenia Czytelnik będzie mógł używać programu w dokładnie taki sam sposób, w jaki używają go inni użytkownicy. Trzeba mieć na uwadze, że niektóre aplikacje działają jedynie na fizycznym urządzeniu. Należą do nich aplikacje używające danych pochodzących z akcelerometru lub też przekazywanych przez połączenia Bluetooth.
185
DODATEK A TESTOWANIE GIER DLA SYSTEMU ANDROID NA PRAWDZIWYM URZĄDZENIU
186
Skorowidz A akcelerometr, 56 algorytmy wykrywania kolizji, 119 analiza gry, 168 Android, 11 Android 3.0, 13 AVD, Android Virtual Device, 31
B barometr, 56 błąd w programie, 153
C cechy systemu Android 3.0, 13 czujnik oświetlenia, 56 zbliżeniowy, 56
D dane z akcelerometru, 72 dodawanie dział, 151 dźwięku, 78, 122 ekranu początkowego, 171 grafiki do sprajtów, 100 łodzi, 163 muzyki, 85 obrazów, 151 połączeń Bluetooth, 130 rakietek, 140 sekwencji filmowych, 86 sprajtów, 99
E edytor graficzny, 34 typu WYSIWYG, 34 efekty dźwiękowe, 84 ekrany opornościowe, 55 pojemnościowe, 55 elementy falochronu, 146 sterujące, 163 emulator Androida, 141
F FLAC, Free Lossless Audio Codec, 78 funkcja collide(), 125 draw(), 49, 125 getOrientation(), 75 getstate(), 102 onCreate(), 131, 164 onDraw(), 50, 116, 162 playsound(), 84 setstate(), 102 surfaceCreated(), 116 update(), 49, 99, 119, 123, 157, 160 funkcje w klasie SpriteObject, 156 wykrywania kolizji, 98 funkcjonalności dodane do gry, 155
G generator liczb pseudolosowych, 149 generowanie klucza, 177 GPS, 56
SKOROWIDZ
gra AllTogether, 95 Breakout, 109 Harbor Defender, 144 Pong, 112 grafika rastrowa, 42 gry dwuosobowe, 130 jednoosobowe, 95 strategiczne, 143 wieloosobowe, 128
I IDE, Integrated Development Environment, 16 ikonki dział, 158 implementacja zarządzania czasem, 52 informacje o programiście, 177 inicjalizacja bloków, 123 obiektów czujników, 71 obiektów w projekcie, 147 instalacja pakietu Java JDK, 16 pakietu SDK dla systemu Android, 20 sterowników, 184 środowiska Eclipse, 17 klasy GameView, 73 integracja Eclipse z SDK, 24 interfejs sprzętowy, 183 wielodotykowy, multitouch, 55
J JDK, Java Development Kit, 16 język Java, 14 XML, 15
K katalog Res, 33 values, 33 klasa Asteroid, 89 AudioManager, 83 Explosion, 89 GameLogic, 46, 49 GameView, 43, 80, 105, 115, 133 Gesture, 61 InputObject, 65
JetBoyView, 89 JetPlayer, 87, 89 Main, 30 MainActivity, 174 MediaPlayer, 79 MotionEvent, 59 SpriteObject, 48, 53, 103 View, 40 kod wykrywający kolizje, 53 kolejki wejścia, 64 kolizje pomiędzy sprajtami, 97 komentarz z nagłówkiem, 51 komunikat o błędzie, 153 komunikaty, 133 konfiguracja emulatora, 62 ikon, 158 łodzi, 163 narzędzi Androida, 23 środowiska programistycznego, 16 konstruktor klasy GameView, 115 kontrola ruchu, 97
M MESSAGE_DEVICE_NAME, 134 MESSAGE_STATE_CHANGE, 133 MESSAGE_TOAST, 134 metoda createBullet(), 167 draw(), 49, 125 initializeJetPlayer, 89 load(), 83 onActivityResult(), 131 onCreate(), 131, 164 onDraw, 50, 116, 162 onPause(), 71 onResume(), 71 onSensorChanged, 72 onStart(), 131 onTouchEvent, 67 processMotionEvent(), 100 queueJetSegment(), 92 reset(), 167 startgame(), 132 update(), 49, 99, 119, 123, 157, 160 updateGameState(), 93 metody do obsługi czujników, 71 klasy GameLogic, 47 obsługujące kolizję, 54 tworzące pule obiektów, 67 mikrofon, 56
SKOROWIDZ
modyfikacja GameView.java, 114 SpriteObject.java, 114 muzyka sterowana zdarzeniami, 89
N nagroda dla gracza, 101
O obiekt BlueAdapter, 131 Canvas, 42 inputObjectPool, 66 obiekty klasy SpriteObject, 158 obliczanie momentu wystrzelenia kul, 166 obsługa dotyku, 121 dźwięku, 121 komunikatów, 132 kul, 164 muzyki, 87 nagród, 121 poleceń użytkownika, 160 zdarzeń, 117 zmian w kulach, 165 odtwarzanie dźwięków, 78–80 okno tworzenia projektu, 39 opakowywanie gry, 175 osie współrzędnych, 75
P P2P, peer-to-peer, 128 pakiet JDK, 16 SDK, 16 parametry queueJetSegment(), 92 pętla gry, 58 pętla gry programu JetBoy, 92 plik AndroidManifest.xml, 175, 183 GameLogic.java, 46 GameView.java, 43, 45, 80, 105 InputObject.java, 65 JetBoy.zip, 89 Level1.jtc, 89 Main.java, 30 main.xml, 34, 172, 174 MainActivity.java, 174 SpriteObject.java, 48, 103 strings.xml, 33
pliki FLAC, 78 JET, 87 MIDI, 78 mp3, 78 XML, 123 płótno, 42 pobieranie danych, 55 połączenia równorzędne, P2P network, 128 połączenie z urządzeniem Bluetooth, 134, 138 proces gry, 57 program Eclipse, 16 FirstApp, 33 GIMP, 112 JET Creator, 87 LogCat, 153 projekt programu w Eclipse, 29 TabletPaddle, 113 TwoPlayerPaddleGame, 130 przetwarzanie danych, 60 danych wejściowych, 68, 69 danych z sensora, 73 poleceń, 59 zdarzeń, 68 publikacja gry, 171
R reagowanie na dane z czujników, 70 dotyk, 59 gesty, 61 wciśnięcie przycisku, 174 RECEIVE_DATA, 133 reklamowanie gry, 180 rozmieszczanie elementów, 162 rozpowszechnianie gry, 176 rozszerzenie sprajtów, 156 rysowanie dział, 151 falochronu, 148 łodzi, 149 obrazu, 42 zamku oraz gruntu, 149
S sekwencje filmowe, 77 SEND_DATA, 133 serwer gier, 128
189
SKOROWIDZ
sieć P2P, 128 torrent, 128 sklep Android Market, 15 App Store firmy Apple, 178 Google Play, 178 sprajt, sprite, 37 sprajt star.png, 61 sprawdzanie zderzenia, 164 stała liczba klatek animacji, 52 stan początkowy, 103 stany sprajtów, 101, 102 sterowanie grą, 157 sterowanie rakietką, 121 sterowniki USB, 184 strzelanie z dział, 164
Ś śledzenie stanu sprajtów, 101 środowisko gry, 114 środowisko programistyczne, 15
T testowanie gry, 141, 152 gry na fizycznym urządzeniu, 183 narzędzi programistycznych, 27 programów, 15 projektu JetBoy, 90, 91 tworzenie falochronu, 145, 147 gestu, 63 gruntu oraz zamku, 148 gry jednoosobowej, 96 konta w Google Play, 179 łodzi, 149 obiektów nowych klas, 50 obiektów wejściowych, 67 pętli gry, 46 projektu, 27 projektu TabletPaddle, 113 sprajta, 48 wirtualnego urządzenia z Androidem, 31
U uaktualnianie stanu gry, 140 ukrywanie paska zadań, 51 uruchamianie aplikacji, 33 emulatora, 63 gry, 49 USB Debugging, 184 usługa Google Play, 179 usuwanie bloków, 125 uwolnienie sprajta, 74
W warstwy obrazu, 148 wątek, 46 AcceptThread, 134 ConnectedThread, 137 ConnectThread, 135 wielowątkowość, 46 wprowadzenie do gry, 172 wykrywanie kolizji, 53, 98, 117, 119, 140 wysyłanie aplikacji, 180 wysyłanie danych czujnika, 73 wyświetlanie grafiki, 42 obrazów, 38 sprajtów, 45
Z zachowywanie wskazań użytkownika, 159 zarządzanie sprajtem, 60 zasoby, resources, 33 zdarzenia, 133 zdarzenia wejściowe, input events, 57 zdarzenie dotknięcia ekranu, 159 zerowanie stanu gry, 102, 164 zmiana kierunku poruszania, 150 liczby dział, 151 zmienne przechowujące wybór użytkownika, 159 w klasie SpriteObject, 156
Ż żyroskop, 56
NOTATKI