Burchard E. - Tworzenie gier internetowych. Receptury

...

47 downloads 26 Views 9MB Size


Nazwałem (a raczej moja siostrzenica nazwała) tę grę Przygody ludzika i tę nazwę wpisałem w tytule strony. W dalszej części kodu znajduje się element ze zdefiniowanymi podstawowymi własnościami CSS, który będzie służył jako ekran gry. W nim jest umieszczony element o identyfikatorze jsapp zawierający elementy dołączające do strony skrypty melon.js, resources.js, screens.js oraz main.js. Elementem tym zajmiemy się niebawem.

Receptura. Uruchamianie gry  117

Wszystkie te pliki można by było połączyć w jeden, tak jak robimy w innych grach opisanych w tej książce. Z drugiej strony warto znać różne sposoby robienia tego samego. Na przykład w rozdziale 4. nie utworzyliśmy osobnego pliku na kod JavaScript, a w tym przeciwnie — utworzyliśmy nawet kilka takich plików. Co one zawierają? Plik melon.js jest silnikiem gry. Dalej w tym rozdziale poznamy znaczną część API tej biblioteki, ale jeśli lubisz podczas pracy korzystać z dokumentacji, znajdziesz ją pod adresem http://www. melonjs.org/docs/index.html (w dodatku C możesz zapoznać się z listą stron wszystkich opisanych w tej książce silników). W kodzie samego silnika nie będziemy niczego zmieniać, chociaż podobnie jak wszystkie pozostałe, jest to skrypt open source. Jeśli więc uznasz, że czegoś w nim brakuje, albo znajdziesz jakiś błąd, możesz zaimplementować poprawkę i pomóc w rozwijaniu biblioteki. W pliku resources.js są zapisane informacje dotyczące potrzebnych w grze obrazów, plików dźwiękowych oraz plików poziomów (utworzonych w programie Tiled). Na razie zawartość tego pliku jest bardzo prosta. Wpisz w nim kod widoczny na listingu 5.2 dodający zasoby dla poziomu i sprite’y użyte do jego budowy. Listing 5.2. Zawartość pliku resources.js var resources = [{ name: "levelSprites", type: "image", src: "levelSprites.png" }, { name: "level1", type: "tmx", src: "level1.tmx" }];

OSTRZEŻENIE UWAŻAJ NA PRZECINKI. Pracując w JavaScripcie z tablicami i obiektami, uważaj na to, jak używasz przecinków. W niektórych przeglądarkach występuje błąd dotyczący znajdującego się za ostatnią instrukcją przecinka. Opuszczanie przecinków nie jest dobrym pomysłem niezależnie od przeglądarki.

Zawartość pliku screens.js również jest nieskomplikowana. Zawiera informacje o ekranach, które można traktować jako stany gry, takie jak Play, Menu czy GameOver. Na razie potrzebujemy tylko ekranu PlayScreen, który dziedziczy po obiekcie me.ScreenObject i powoduje wczytanie poziomu level1. Wklej kod znajdujący się na listingu 5.3 do pliku screen.js. Listing 5.3. Dodanie obiektu PlayScreen do pliku screen.js var PlayScreen = me.ScreenObject.extend({ onResetEvent: function() { me.levelDirector.loadLevel("level1"); } });

118  Rozdział 5. GRY PLATFORMOWE Przedrostek me to skrót od słów Melon Engine. Jest to przestrzeń nazw, w której znajdują się wszystkie obiekty biblioteki melonJS. Wprawdzie ryzyko, że we własnym kodzie użyjesz nazwy levelDirector, jest niewielkie, ale bardziej pospolite nazwy mogą się duplikować i wówczas przestrzenie nazw pomagają zapanować nad odwołaniami do obiektów. Użycie słowa kluczowego var do definiowania zmiennych również jest nieprzypadkowe. W języku JavaScript zmienne zdefiniowane bez tego słowa znajdują się w globalnej przestrzeni nazw, co oznacza, że są dostępne wszędzie. Wracamy do kodu. Na listingu 5.4 znajduje się zawartość pliku main.js. Jest to wysokopoziomowa logika gry i jest ona nieco bardziej skomplikowana. Najpierw została utworzona zmienna o nazwie jsApp. Zastosowany został obiektowy wzorzec do utworzenia dwóch funkcji. Funkcja onload jest wywoływana po załadowaniu okna. W funkcji tej element div z identyfikatorem jsapp jest zadeklarowany jako obiekt kanwy gry. Funkcja init() przyjmuje cztery parametry, określające szerokość, wysokość, podwójne buforowanie oraz skalowanie. Jako że nasze sprite’y mają wymiary 16×16 pikseli, skalowanie zostało ustawione na 2.0 (powiększenie) w porównaniu z domyślnym ustawieniem oczekiwanym przez bibliotekę melonJS. Zastosowanie skalowania zmusza nas też do ustawienia parametru podwójnego buforowania na true. Listing 5.4. Inicjowanie aplikacji i załadowanie zasobów var jsApp = { onload: function() { if (!me.video.init('jsapp', 320, 240, true, 2.0)) { alert("Ta przeglądarka nie obsługuje kanwy HTML5."); return; } me.loader.onload = this.loaded.bind(this); me.loader.preload(resources); me.state.change(me.state.LOADING); }, loaded: function() { me.state.set(me.state.PLAY, new PlayScreen()); me.state.change(me.state.PLAY); } }; window.onReady(function() { jsApp.onload(); });

Następnie funkcja me.loader.onload ustawia funkcję loaded jako funkcję zwrotną wywoływaną po zakończeniu działania funkcji onload. Instrukcja bind.this() zapewnia, że funkcja zwrotna działa w kontekście jsApp. Funkcja preload ładuje obrazy i mapę poziomu z plików zasobów. Funkcja zwrotna loaded wiąże utworzony w pliku screens.js obiekt PlayScreen z wbudowanym stanem PLAY (przy użyciu funkcji game.set), a następnie zmienia stan gry na PLAY za pomocą funkcji state.change. Na koniec funkcja window.OnReady() uruchamia funkcję jsApp.onload(), gdy zostanie załadowane okno. Teraz plik index.html w przeglądarce powinien wyglądać jak na rysunku 5.4. Widać część naszej mapy, ale to jeszcze nie gra. Czego jeszcze potrzebujemy? Dowiesz się w następnej recepturze.

Receptura. Dodawanie postaci  119

Rysunek 5.4. Mapa załadowana w przeglądarce

Receptura. Dodawanie postaci Czas dodać do naszej gry ludzika. Nazwiemy go Guy i pozwolimy mu przeżyć niezapomnianą przygodę. Najpierw musimy za pomocą programu Tiled ustawić pozycję startową. W tym celu trzeba dodać warstwę obiektu. Otwórz menu Layer (warstwa) i kliknij opcję Add Object Layer (dodaj warstwę obiektu). Jeśli nie widzisz u siebie warstw, kliknij View/Layers. Po prawej stronie okienka Layers (warstwy) znajdź nazwę nowo utworzonej warstwy i zmień ją na player. Ponadto dodaj obraz player.png jako zestaw kafelków w taki sam sposób jak poprzednio. Ludzika możesz umieścić gdzieś w bezpiecznym miejscu blisko ziemi. W tym celu kliknij ikonę wstawiania obiektu (rysunek 5.5) albo naciśnij klawisz O, a następnie kliknij mapę w miejscu, w którym chcesz umieścić postać. W odróżnieniu od sprite’ów pierwszego planu, tego sprite’a nie zobaczysz. Kliknij prawym przyciskiem myszy szare pole, które zostało dodane, i wybierz opcję Object Properties (właściwości obiektu). W polu Name (nazwa) wpisz player. Dodatkowo na dole pola trzeba ustawić jeszcze dwie własności o nazwach image i spritewidth i wartościach odpowiednio player i 16 — rysunek 5.6.

Rysunek 5.5. Przycisk dodawania obiektu

Jeśli teraz spróbujesz załadować grę, to niewiele zobaczysz. Dodawanie ludzika do świata nie zostało jeszcze ukończone. Najpierw trzeba dodać jego obraz do tablicy w pliku resources.js, jak pokazano na listingu 5.5. Pamiętaj, aby uważać na przecinki. Listing 5.5. Dodawanie gracza do pliku resources.js { name: "player", type: "image", src: "player.png" }

120  Rozdział 5. GRY PLATFORMOWE

Rysunek 5.6. Obiekt player

Następnie dodamy ludzika do puli jednostek biblioteki melonJS w funkcji main.js — listing 5.6.

loaded

w pliku

Listing 5.6. Dodawanie postaci gry do puli jednostek w pliku resources.js me.entityPool.add("player", PlayerEntity);

Musimy też utworzyć ostatni z potrzebnych nam plików — entities.js. W programowaniu gier ważne obiekty często nazywa się jednostkami (ang. entity). Mogą nimi być wrogowie gracza, sam gracz albo pociski. Systemy jednostkowe mają mniej restrykcyjną hierarchię od typowych systemów obiektowych. Potrzeba trochę czasu, aby przyzwyczaić się do tego stylu programowania — będziesz mieć ku temu okazję w rozdziale 10. Na razie jednostki traktuj, jakby były złożone z logicznych składników opisujących ich właściwości, takie jak możliwości ruchu i to, co się dzieje, gdy zderzą się ze sobą. Ponadto zapamiętaj, że są to nie tylko obiekty w kodzie, ale również obiekty w grze. W związku z tym wyrażenie „jednostka gracza” oznacza zarówno kod reprezentujący gracza, jak i pojęcie „przedmiotu” w świecie gry. Na listingu 5.7 znajduje się kod pliku entities.js, który należy dołączyć do pliku index.html w pobliżu innych plików JavaScript. Listing 5.7. Ładowanie pliku entities.js do pliku index.html

Kod pliku entities.js znajdujący się na listingu 5.8 jest bardzo prosty. Inicjujemy zmienną Player Entity dziedziczącą po obiekcie ObjectEntity i sprawiamy, aby widoczny obszar (viewport) zawsze podążał za postacią gracza. Następnie wyznaczamy funkcję update do obsługi animacji podczas ruchu gracza.

Receptura. Budowa mapy kolizji  121

Listing 5.8. Dodawanie obiektu PlayerEntity var PlayerEntity = me.ObjectEntity.extend({ init: function(x, y, settings) { this.parent(x, y, settings); me.game.viewport.follow(this.pos, me.game.viewport.AXIS.BOTH); }, update: function() { this.updateMovement(); if (this.vel.x!=0 || this.vel.y!=0) { this.parent(this); return true; } return false; } });

Jeśli teraz uruchomisz grę, to zobaczysz już coś bardziej zachęcającego, choć dramatycznego. Bohater pojawi się na ekranie w wyznaczonym miejscu, ale od razu zniknie poza ekranem. Dlaczego? Nie utworzyliśmy jeszcze twardego podłoża, na którym mógłby stać.

Receptura. Budowa mapy kolizji Dodamy nową warstwę kafelkową. Podobnie jak poprzednio (rysunek 5.2), w programie Tiled kliknij opcję Layer/Add Tile Layer. Następnie nazwij utworzoną warstwę collision. Niektóre warstwy można nazywać w dowolny sposób, ale biblioteka melonJS rozpoznaje warstwy kolizji, jeśli w nazwie mają słowo collision. Następnie kliknij opcję Map/New Tileset, zaimportuj plik collision.png i tak jak poprzednio, nie dodawaj marginesów oraz użyj kafelków o wymiarach 16×16. Później kliknij prawym przyciskiem myszy pierwszy kafelek w nowym wyświetlonym po prawej stronie zestawie (jeśli nie widzisz paska narzędzi zestawów kafelków, kliknij opcję View/Tilesets). Może być konieczne wybranie zestawu kolizyjnego. Dodaj własność type o wartości solid. Na warstwie kafelków kolizji narysuj ziemię przy użyciu kafelka o nazwie solid. Powinieneś otrzymać wynik podobny do pokazanego na rysunku 5.7. Czarne kafelki to dodane przed chwilą kafelki kolizji. Warstwę kolizji widać, ponieważ znajduje się na wierzchu. Jeśli chcesz, za pomocą narzędzi dostępnych na pasku narzędzi warstw możesz zmienić kolejność, filtry i przezroczystość warstw, aby zobaczyć różne sposoby prezentacji mapy. Zapisz i odśwież w przeglądarce plik index.html, aby przekonać się, że teraz ludzik stoi na twardym podłożu. To już jest jakieś osiągnięcie. Niewykluczone, że w przyszłości gatunek gier „stanie bez ruchu” stanie się bardzo popularny, ale na razie niewiele za tym przemawia. Dlatego stworzymy jednak klasyczną platformówkę i pozwolimy naszemu ludzikowi przeżyć trochę przygód.

Receptura. Chodzenie i skakanie Aby umożliwić chodzenie i skakanie ludzikiem, musimy dokonać dwóch zmian. Po pierwsze, zwiążemy przyciski skakania oraz chodzenia w prawo i w lewo z klawiszami na klawiaturze. Na listingu 5.9 jest przedstawiony odpowiednio zmodyfikowany kod z pliku screen.js.

122  Rozdział 5. GRY PLATFORMOWE

Rysunek 5.7. Warstwa kolizji nałożona na pierwszy plan Listing 5.9. Wiązanie klawiszy ruchu var PlayScreen = me.ScreenObject.extend({ onResetEvent: function() { me.levelDirector.loadLevel("level1"); me.input.bindKey(me.input.KEY.LEFT, "left"); me.input.bindKey(me.input.KEY.RIGHT, "right"); me.input.bindKey(me.input.KEY.SPACE, "jump"); } });

Następnie zmiany należy wprowadzić w funkcjach init i update obiektu PlayerEntity w pliku entities.js. W funkcji init trzeba ustawić domyślną prędkość skakania i chodzenia, a w funkcji update musimy obsłużyć aktualizację ruchu na podstawie przychodzących danych oraz sprawdzać, czy występują kolizje. Odpowiedni kod jest pokazany na listingu 5.10. Listing 5.10. Obsługa ruchu gracza init: function(x, y, settings) { this.parent(x, y, settings); me.game.viewport.follow(this.pos, me.game.viewport.AXIS.BOTH); this.setVelocity(3, 12); }, update: function() { if (me.input.isKeyPressed('left')) { this.doWalk(true); } else if (me.input.isKeyPressed('right')) { this.doWalk(false); } else { this.vel.x = 0; }; if (me.input.isKeyPressed('jump')) { this.doJump(); } me.game.collide(this); this.updateMovement(); if (this.vel.x!=0 || this.vel.y!=0) { this.parent(this); return true;

Receptura. Ekran tytułowy  123

} return false; }

W bibliotece melonJS są dostępne bardzo wygodne w użyciu i pomocne funkcje doJump() i doWalk(), które doskonale nadają się na początek. Warto jednak mieć na uwadze, że mimo iż opracowanie własnej procedury obsługi ruchu jest o wiele bardziej czasochłonne, może pozwolić nadać grze całkiem inny charakter. Przykładowo gra Sonic the Hedgehog swoją popularność w znacznym stopniu zawdzięcza niepowtarzalnemu sposobowi, w jaki tytułowy bohater powoli się rozpędza, aby w końcu osiągnąć dużą prędkość maksymalną. Istnieje nawet silnik gier HTML5 umożliwiający badanie ruchu tego zwierzaka w trójwymiarowej przestrzeni. Jeśli teraz otworzysz plik index.html w przeglądarce, odkryjesz, że można już ruszać ludzikiem za pomocą klawiszy strzałek w lewo i prawo oraz spacji! Na razie wszystko idzie nam bardzo dobrze. Zwróć też uwagę, że w czasie chodu ruszają się stopy Guya. To prezent od biblioteki melonJS. Wystarczyło tylko załadować zestaw dwóch sprite’ów player.png, a skrypt sam wiedział, co z nimi zrobić. Fantastycznie! Co teraz? Mimo wysiłków gracza ludzik od czasu do czasu wpadnie w jakąś dziurę. Wówczas gracz powinien odświeżyć stronę, prawda? Nie. Gdy coś złego stanie się postaci w grze, gra powinna być resetowana.

Receptura. Ekran tytułowy Najpierw utworzymy obiekt TitleScreen reprezentujący ekran tytułowy, który będziemy wyświetlać na początku gry oraz gdy Guy wpadnie w dziurę. Kod przedstawiony na listingu 5.11 dopisz na końcu pliku screens.js. Listing 5.11. Tworzenie obiektu TitleScreen var TitleScreen = me.ScreenObject.extend({ init: function() { this.parent(true); me.input.bindKey(me.input.KEY.SPACE, "jump", true); }, onResetEvent: function() { if (this.title == null) { this.title = me.loader.getImage("titleScreen"); } }, update: function() { if (me.input.isKeyPressed('jump')) { me.state.change(me.state.PLAY); } return true; }, draw: function(context){ context.drawImage(this.title, 50, 50); } });

Zobaczmy, do czego ten kod służy. Najpierw została utworzona zmienna TitleScreen dziedzicząca po obiekcie me.ScreenObject. Następnie w funkcji init znajduje się wywołanie

124  Rozdział 5. GRY PLATFORMOWE this.parent(true) powodujące ustawienie ekranu TitleScreen na widoku oraz pozwalające się upewnić, że funkcje update i draw działają. Ponadto spacja została związana z klawiszem jump.

W funkcji onResetEvent ładujemy obraz titleScreen, jeśli jeszcze nie został ustawiony. Funkcja update czeka na naciśnięcie spacji i gdy to nastąpi, przechodzi do głównej pętli gry. Funkcja draw rysuje obraz (pierwszy parametr) w wyznaczonym miejscu (drugi i trzeci parametr). Osoby, które nie przeczytały wcześniejszych rozdziałów zawierających opis gier działających na kanwie, mogą zastanawiać się, co znaczy parametr context. Określa on tzw. kontekst renderowania kanwy, który jest zdefiniowany przez bibliotekę melonJS. W tym przypadku jest używany kontekst dwuwymiarowy, ale za pomocą tego samego API można też zainicjalizować kontekst trójwymiarowy WebGL. Nie ma sensu dwa razy wiązać klawisza jump, a więc otwórz plik screens.js i w obiekcie PlayScreen usuń wiersz me.input.bindKey(me.input.KEY.SPACE, "jump", true);. Następnie załaduj obraz ekranu w pliku resources.js w sposób pokazany na listingu 5.12. Listing 5.12. Ładowanie obrazu ekranu jako zasobu { name: "titleScreen", type: "image", src: "titleScreen.png" }

Kolejne trzy zmiany są potrzebne w funkcji loaded w pliku main.js. Najpierw przypiszemy obiekt TitleScreen do standardowego stanu MENU. Później zmienimy stan ładowany na początku gry z PLAY na MENU. Na koniec zdefiniujemy efekt przejścia między ekranami. Kod powinien teraz wyglądać tak jak na listingu 5.13. Listing 5.13. Obsługa stanu MENU loaded: function() { me.entityPool.add("player", PlayerEntity); me.state.set(me.state.PLAY, new PlayScreen()); me.state.set(me.state.MENU, new TitleScreen()); me.state.transition("fade", "#2FA2C2", 250); me.state.change(me.state.MENU); }

Prawie skończyliśmy. Przy uruchamianiu gry pojawia się ekran powitalny, ale nie włączyliśmy jeszcze automatycznego resetowania gry, gdy użytkownik wpadnie w dziurę. Aby zaimplementować taką funkcję, musimy wprowadzić kilka drobnych zmian w obiekcie PlayerEntity w pliku entities.js. Przejdź do pliku entities.js i w obiekcie PlayerEntity za funkcją update dodaj pokazaną na listingu 5.14 funkcję gameOver. Nie zapomnij dodać przecinka za klamrą nad tą funkcją. Nie dodawaj nowej klamry, a jedynie przecinek. Potrzebny jest też warunek powodujący wywołanie funkcji gameOver, którego kod znajduje się na listingu 5.15. Na każdej mapie warunek ten może być trochę inny, ale ogólnie rzecz biorąc,

Receptura. Dodawanie przedmiotów do zbierania  125

Listing 5.14. Funkcja gameOver w pliku entities.js },

// Nie zapomnij dodać tu przecinka

gameOver: function() { me.state.change(me.state.MENU); } });

// Koniec pliku

fakt wpadnięcia postaci w dziurę wykrywa się poprzez sprawdzenie jej położenia na osi pionowej. Jeśli jest zbyt nisko, następuje koniec gry. Kod ten można umieścić pod wywołaniem funkcji updateMovement. Listing 5.15. Zakończenie gry, gdy Guy wpadnie w dziurę this.updateMovement(); if (this.bottom > 490){ this.gameOver(); }

Teraz gdy gracz przegra, zobaczy ekran tytułowy. Jest to o wiele lepsze rozwiązanie niż konieczność odświeżenia strony. Wszystko świetnie, ale nadal jest jeden problem. Na razie jedyną przygodą naszego ludzika jest unikanie wpadnięcia w dziurę. Przydałoby się dać mu lepsze powody do wyjścia z domu.

Receptura. Dodawanie przedmiotów do zbierania Za co gracze kochają gry platformowe? Oczywiście za możliwość zbierania większych od ich postaci przedmiotów, które mogą nosić ze sobą w dalszej części gry. Zaczniemy od edytowania po raz kolejny mapy w programie Tiled. Dodaj nową warstwę obiektową (Layer/Add Object Layer) o nazwie coin. Następnie dodaj nowy zestaw kafelków dla monet (Map/New Tileset) i również nazwij go coin. Aby przypomnieć sobie, jak się dodaje obiekty na ekran, spójrz na rysunek 5.5. Każdą dodaną do ekranu monetę kliknij prawym przyciskiem myszy i wybierz opcję Object Properties, aby ustawić nazwę na coin oraz dodać atrybuty image o wartości coin i spritewidth o wartości 16. W menu pojawiającym się po kliknięciu ikony prawym przyciskiem myszy jest dostępna opcja utworzenia duplikatu obiektu, a za pomocą myszy można przesuwać monety po ekranie. Jeśli utworzysz kopię monety, to zostanie ona umieszczona bezpośrednio na oryginale, przez co można pomyśleć, że kopia wcale nie została utworzona. Teraz musimy dodać trochę kodu źródłowego. Zaczniemy od dodania monet do puli jednostek w funkcji loaded w pliku main.js zaraz pod kodem dodającym do puli jednostkę gracza, jak widać na listingu 5.16. Listing 5.16. Dodanie monet do puli jednostek me.entityPool.add("player", PlayerEntity); me.entityPool.add("coin", CoinEntity);

Następnie dodamy plik obrazu monety jako zasób w pliku resources.js — listing 5.17.

126  Rozdział 5. GRY PLATFORMOWE Listing 5.17. Dodanie sprite’a monet do zasobów gry },

// Przypomnienie: dodaj przecinki w tym miejscu także do poprzednich obiektów!

{ name: "coin", type: "image", src: "coin.png" }

Teraz utworzymy obiekt CoinEntity na końcu pliku entities.js — listing 5.18. Listing 5.18. Tworzenie CoinEntity var CoinEntity = me.CollectableEntity.extend({ init: function(x, y, settings) { this.parent(x, y, settings); }, onCollision : function (res, obj) { this.collidable = false; me.game.remove(this); } });

Zdefiniowaliśmy obiekt CoinEntity dziedziczący po CollectableEntity (który z kolei dziedziczy po ObjectEntity). Następnie wywołaliśmy konstruktor rodzica, aby mieć dostęp do pewnych metod. Na koniec dodaliśmy logikę obsługi kolizji, by żadnej monety nie dało się zebrać dwa razy. Otwórz w przeglądarce plik index.html i zobacz, ile już zostało zrobione. Teraz Guy może zbierać monety, aby zapłacić za wszystko, co jest mu potrzebne w przygodach. Ale teraz nasz ludzik ma chyba zbyt lekkie życie. Trochę mu je skomplikujemy.

Receptura. Wrogowie Utwórz nową warstwę obiektową w programie Tiled (Layer/Add Object Layer) i nazwij ją Enemy Tiles. Następnie dodaj do mapy nowy obiekt (nie trzeba dodawać nowego zestawu kafelków) i kliknij go prawym przyciskiem myszy, aby nadać mu nazwę EnemyEntity. Teraz będzie najtrudniejsza część zadania. W oknie właściwości obiektu znajdują się pola do ustawiania położenia obiektu na osi pionowej i poziomej (sekcja Position) oraz szerokości i wysokości (sekcja Size). Liczby, które wpisuje się w tych polach, to mnożniki wartości 16 określające położenie i rewir wroga na planszy. Jeśli wróg ma wysokość 16 pikseli, to w polu wysokości należy wpisać liczbę 1. Co ciekawe, szerokość (width) nie dotyczy samej postaci wroga, tylko określa szerokość poziomego obszaru, po którym może się on poruszać w tę i z powrotem. Kolejną czynnością jest dodanie wroga do puli jednostek w pliku main.js. Kod przedstawiony na listingu 5.19 można wpisać za kodem CoinEntity. Listing 5.19. Dodanie obiektu EnemyEntity do puli me.entityPool.add("coin", CoinEntity); me.entityPool.add("EnemyEntity", EnemyEntity);

Następnie dodajemy naszego złoczyńcę badGuy do pliku resources.js, jak pokazano na listingu 5.20. Ponownie przypominam o przecinkach.

Receptura. Wrogowie  127

Listing 5.20. Dodanie obrazu badGuy do zasobów gry { name: "badGuy", type: "image", src: "badGuy.png" }

Pewnie domyślasz się już, że trzeba jeszcze zdefiniować obiekt EnemyEntity w pliku entities.js. Ta jednostka jest dość skomplikowana, ale mimo to ogólna struktura kodu powinna już wyglądać znajomo. Jedną z najważniejszych różnic w stosunku do wcześniejszych definicji tego typu jest definicja niektórych własności (ustawień) bezpośrednio w kodzie melonJS zamiast w programie Tiled. Zwróć też uwagę na oznaczenie ścieżki przy użyciu ustawienia settings.width. Ostatnią ważną rzeczą, na którą należy zwrócić uwagę, jest dodanie nowego warunku zakończenia gry, gdy ludzik dotknie wroga. Kod znajdujący się na listingu 5.21 można umieścić na końcu pliku entities.js. Listing 5.21. Jednostka EnemyEntity var EnemyEntity = me.ObjectEntity.extend({ init: function(x, y, settings) { settings.image = "badguy"; settings.spritewidth = 16; this.parent(x, y, settings); this.startX = x; this.endX = x + settings.width - settings.spritewidth; this.pos.x = this.endX; this.walkLeft = true; this.setVelocity(2); this.collidable = true; }, onCollision: function(res, obj) { obj.gameOver(); }, update: function() { if (!this.visible){ return false; } if (this.alive) { if (this.walkLeft && this.pos.x <= this.startX) { this.walkLeft = false; } else if (!this.walkLeft && this.pos.x >= this.endX){ this.walkLeft = true; } this.doWalk(this.walkLeft); } else { this.vel.x = 0; } this.updateMovement(); if (this.vel.x!=0 || this.vel.y!=0) { this.parent(this); return true; } return false; } });

128  Rozdział 5. GRY PLATFORMOWE Teraz Guy nie ma już tak lekko jak jeszcze niedawno. Gdybym był okrutny i utworzył mapę tylko z lawą i wrogiem, to jego życie stałoby się jeszcze trudniejsze. Jednak nie jestem i pozwolę mu utrzymać przewagę nad oponentem.

Receptura. Zwiększanie mocy postaci Umieściłeś jakieś monety poza zasięgiem ludzika? Jeśli tak, to teraz damy mu do dyspozycji buty ze skrzydłami, aby mógł wyżej skakać. W tym celu w programie Tiled należy dodać nową warstwę o nazwie boots oraz dodać obiekty w taki sam sposób jak wcześniej monety, deklarując nazwę boots i ustawiając własności image na boots, a spritewidth na 16. Kolejną czynnością jest dodanie zasobu boots do pliku resources.js, jak pokazano na listingu 5.22. Nie zapomnij dodać przecinka między obiektami. Listing 5.22. Dodanie obrazu boots do zasobów gry { name: "boots", type: "image", src: "boots.png" }

Teraz dodamy buty do puli jednostek w pliku main.js, jak pokazano na listingu 5.23. Listing 5.23. Dodanie butów do puli jednostek me.entityPool.add("EnemyEntity", EnemyEntity); me.entityPool.add("boots", BootsEntity);

Następnie zadeklarujemy jednostkę listingu 5.24.

BootsEntity

na końcu pliku entity.js, jak pokazano na

Listing 5.24. Tworzenie obiektu BootsEntity var BootsEntity = me.CollectableEntity.extend({ init: function(x, y, settings) { this.parent(x, y, settings); }, onCollision : function (res, obj) { this.collidable = false; me.game.remove(this); obj.gravity = obj.gravity/4; } });

Ten kod powinien wyglądać bardzo znajomo, bo jest prawie identyczny z kodem dla jednostki CoinEntity. Jedyna różnica znajduje się w trzecim wierszu od końca odpowiedzialnym za zwiększenie mocy ludzika. Gdy gracz zdobędzie specjalne buty, grawitacja dla jego postaci zmniejszy się do jednej czwartej dotychczasowej wartości. Teraz nie powinien mieć problemu z dosięgnięciem wszystkich monet! Gra jest już ukończona. W ostatniej recepturze zobaczymy, co można zrobić, aby jeszcze lepiej się prezentowała.

Receptura. Przegrywanie, wygrywanie oraz informacje  129

Receptura. Przegrywanie, wygrywanie oraz informacje Ludzie czasami lubią, gdy wprowadza się ich w błąd, poza tym łamigłówki są zabawne. Ale czy na pewno rozwiązywanie rebusów typu „Którym przyciskiem się skacze?” albo „Wygrałem czy nie?” to również dobra zabawa? Oczywiście można zrobić grę, w której przycisk skakania trzeba odkryć, ale musiałoby to być jakoś sprytnie rozwiązane. Naszym celem w tym rozdziale nie jest jednak realizowanie nowatorskich pomysłów. Chcemy utworzyć typową grę platformową, więc jej zasady i informacje o obsłudze powinny być jasne i łatwe do znalezienia. Zaczniemy od zdefiniowania kontenerów na informacje w pliku index.html. Na listingu 5.25 widać, że kontenery te należy dodać za elementem div z identyfikatorem jsapp. Listing 5.25. Dodanie kontenerów na wiadomości i instrukcje


Instrukcje dla gracza dodamy w pliku screens.js w funkcji onResetEvent obiektu PlayScreen — listing 5.26. Listing 5.26. Instrukcje dotyczące gry me.input.bindKey(me.input.KEY.RIGHT, "right"); document.getElementById('game_state').innerHTML = "Zbierz wszystkie monety!"; document.getElementById('instructions').innerHTML = "Do chodzenia służą klawisze strzałek, a skacze się spacją.";

W tym samym pliku usuniemy te wiadomości z funkcji onResetEvent obiektu TitleScreen, jak widać na listingu 5.27. Listing 5.27. Usunięcie starych wiadomości this.title = me.loader.getImage("titleScreen"); document.getElementById('game_state').innerHTML = ""; document.getElementById('instructions').innerHTML = "";

Teraz w pliku entities.js dodamy trochę kodu do funkcji gameOver, jak na listingu 5.28. Listing 5.28. Utworzenie stanu gameOver gameOver: function() { me.state.change(me.state.MENU); document.getElementById('game_state').innerHTML = "Koniec gry"; document.getElementById('instructions').innerHTML = ""; }

130  Rozdział 5. GRY PLATFORMOWE Obecnie w grze znajduje się tylko funkcja obsługująca zakończenie gry przez przegraną gracza. Ale przecież nasz ludzik zasługuje też, aby mieć szansę wygrać. Dlatego zdefiniujemy także stan wygrywający — listing 5.29. Nie zapomnij dodać przecinka na końcu obiektu gameOver. Listing 5.29. Utworzenie stanu youWin },

// To jest końcówka funkcji gameOver. Za klamrą powinien znajdować się przecinek.

youWin: function() { me.state.change(me.state.MENU); document.getElementById('game_state').innerHTML = "Wygrałeś!"; document.getElementById('instructions').innerHTML = ""; }

Powiedzmy, że aby wygrać, trzeba zebrać wszystkie monety. Jak to zaimplementować? Należy dodać coins i totalCoins do funkcji onload zmiennej jsApp w pliku main.js, jak na listingu 5.30. Listing 5.30. Dodanie własności coins i totalCoins me.gamestat.add("coins", 0); me.gamestat.add("totalCoins", 2);

Pamiętaj, że wartość totalCoins należy ustawić zgodnie z liczbą monet dodanych do poziomu w programie Tiled. Następnie dodaj widoczną na listingu 5.31 funkcję onDestroyEvent do obiektu PlayScreen w pliku screens.js. Jej zadaniem jest resetowanie zebranych monet. Funkcję tę umieść przed funkcją onResetEvent i uważaj na przecinki. Listing 5.31. Resetowanie monet po zakończeniu gry onDestroyEvent: function() { me.gamestat.reset("coins"); },

Kolejną czynnością jest dodanie do jednostki CoinEntity w pliku entities.js pogrubionego kodu widocznego na listingu 5.32. Jego zadaniem jest zwiększanie wartości zmiennej coins, gdy użytkownik weźmie monetę, oraz wyświetlenie wiadomości o wygranej, gdy zostaną zebrane wszystkie monety. Listing 5.32. Definicja wygranej, gdy zostaną zebrane wszystkie monety var CoinEntity = me.CollectableEntity.extend({ init: function(x, y, settings) { this.parent(x, y, settings); }, onCollision : function (res, obj) { me.gamestat.updateValue("coins", 1); this.collidable = false; me.game.remove(this); if(me.gamestat.getItemValue("coins") === me.gamestat.getItemValue("totalCoins")){ obj.youWin();

Podsumowanie  131

} } });

Oczywiście zakończenie gry nie musi polegać tylko na wyświetleniu tekstu pod grą. Dla każdego przypadku zakończenia, tj. wygranej i przegranej, można nawet utworzyć osobne ekrany.

Podsumowanie Mam nadzieję, że podobała Ci się budowa gry platformowej przy użyciu biblioteki melonJS i programu Tiled. Korzystając z tych narzędzi, można w krótkim czasie utworzyć prostą grę z możliwością zwiększania mocy postaci, wrogami, monetami oraz ekranami informującymi o wygranej bądź przegranej. Grę opisaną w tym rozdziale można rozbudować na wiele sposobów. Można dodać ogniste kule, wyświetlić poziom zdrowia postaci sterowanej przez gracza i wroga, nadać wrogowi inteligentne zachowania, dodać animacje skakania i zabijania, stworzyć więcej poziomów, wyświetlić stoper, zapisywać punktację graczy itd. A jeśli chcesz użyć większej liczby narzędzi dostępnych w bibliotece melonJS, to masz w czym wybierać. Dostępne są czasomierze, dźwięki, niesynchroniczne przewijanie (ang. parallax scrolling), wyświetlacze HUD i czcionki bitmapowe. W tym rozdziale wykorzystaliśmy standardowe metody języka JavaScript do wyświetlania informacji dla gracza. Pamiętaj, że tworzysz gry internetowe, a więc możesz bez problemu używać drzewa DOM, pobierać treść z innych stron, a nawet przekierowywać graczy pod inne adresy.

132  Rozdział 5. GRY PLATFORMOWE

6 Bijatyki

W porównaniu z fikcją interaktywną i strzelankami bijatyki to dość młody gatunek gier, ale na podstawie długotrwałej popularności takich tytułów jak Street Fighter można przewidywać, że szybko nie zniknie. Nieważne, czy jesteś w pracy, czy akurat siedzisz w swoim pokoju, bijatyka na pięści, kopniaki i ogniste kule z przeciwnikiem jest bardzo intensywnym przeżyciem. Do budowy gry w tym rozdziale wykorzystamy silnik game.js i będzie to nasza pierwsza dwuosobowa gra.

134  Rozdział 6. BIJATYKI

Receptura. Podstawowe wiadomości o bibliotece game.js Biblioteka game.js powstała na bazie biblioteki do tworzenia gier napisanej w Pythonie o nazwie pygame. Jeśli znasz bibliotekę pygame, to rozpoznasz wiele metod i klas używanych w tym rozdziale. Nie można jednak powiedzieć, że znając jeden z tych skryptów, jest się automatycznie także ekspertem od drugiego. Nawet jeśli uważasz, że JavaScript od Pythona różni się tylko powierzchownie i elementami składni, to dla początkującego programisty któregokolwiek z tych języków ma to i tak niewielkie znaczenie. Ponadto środowiska pracy bardzo się różnią. Podczas gdy przy użyciu biblioteki pygame tworzy się gry działające w systemie operacyjnym (Windows, OS X i Linux), game.js służy do tworzenia gier przeglądarkowych będących tematem tej książki. W tym rozdziale utworzymy prostą bijatykę dla dwóch graczy. Zaczniemy od utworzenia pliku index.html o zawartości widocznej na listingu 6.1. Listing 6.1. Początkowy plik index.html Bijatyka
Wczytywanie...


Większość tego kodu wygląda już znajomo. Na dole znajduje się definicja elementu canvas o identyfikatorze gjs-canvas. Niektóre biblioteki nie wymagają obecności gotowego elementu canvas na stronie, ale ta tak. Powyżej znajduje się element div o identyfikatorze gjs-loader zawierający tekst Wczytywanie..., który będzie wyświetlony podczas ładowania gry. W jego miejsce można by było wstawić jakąś grafikę albo kompletny ekran startowy z paskiem postępu wczytywania gry, aby gracz wiedział, że coś się dzieje i za chwilę będzie mógł się zabawić. Niektóre silniki gier udostępniają gotowe paski postępu. Wyżej znajduje się element script ładujący plik main.js jako moduł. Wstępnie wzorzec modularyzacji został już omówiony w poprzednim rozdziale, ale tutaj dzięki skryptowi yabble.js dołączonemu w pierwszym elemencie script stosujemy go na całkiem inną skalę. W omówionych do tej pory bibliotekach podczas działania gry w konsoli mieliśmy dostęp do głównego obiektu przestrzeni nazw biblioteki. Przykładem takiej przestrzeni jest me w bibliotece melonJS. Zaletą takiego rozwiązania jest minimalizacja ryzyka zdublowania nazw funkcji albo zmiennych. Natomiast wadą jest konieczność wstawiania instrukcji diagnostycznych do kodu (np. console.log(coChcęObejrzeć)) zamiast, jak jest w przypadku globalnych zmiennych, używania w konsoli podczas działania programu instrukcji typu coChcę Obejrzeć.

Receptura. Podstawowe wiadomości o bibliotece game.js  135

Teraz obejrzymy zawartość pliku main.js przedstawioną na listingu 6.2. W większości gier opisanych w tej książce główny skrypt ma nazwę game.js, ale w tym przypadku ta nazwa jest zajęta przez samą bibliotekę. Dlatego tym razem tworzymy plik o innej nazwie niż zwykle. Listing 6.2. Wstępna zawartość pliku main.js var gamejs = require('gamejs'); function main() { var display = gamejs.display.setMode([1200, 600]); var sprites = gamejs.image.load('sprites.png'); display.blit(sprites); }; gamejs.preload(['sprites.png']); gamejs.ready(main);

W pierwszym wierszu zmienna o nazwie gamejs została ustawiona na bibliotekę game.js załadowaną w pliku index.html. Funkcja main ustawia szerokość i wysokość kanwy oraz ładuje i składa na kanwie plik sprite’ów. Zwróć uwagę, że funkcja main zostaje uruchomiona dopiero w ostatnim wierszu tego kodu. Przedtem silnik wczytuje plik sprite’a. Obrazy są wczytywane z wyprzedzeniem po to, aby nie było przeszkadzających i nieprzyjemnych przestojów przy ich ładowaniu, gdy będą potrzebne. Jeśli teraz otworzysz plik index.html w przeglądarce, zobaczysz stronę widoczną na rysunku 6.1.

Rysunek 6.1. Wyrenderowane sprite’y

UWAGA W bibliotece game.js i innych silnikach gier można spotkać słowo blit będące skrótem od angielskich słów block image transfer (blokowe przesyłanie obrazu). W przypadku biblioteki game.js oznacza to nałożenie jednej powierzchni (zawierającej informacje graficzne) na inną. Nie jest to równoznaczne z wyrenderowaniem ani wyświetleniem obrazu, ponieważ nie każda utworzona powierzchnia musi zostać wyświetlona. Jeśli znasz się na tworzeniu stron internetowych i wiesz, jak działa własność z-index w CSS, to blit jest czymś w rodzaju wzbogaconej wersji tej własności, umożliwiającej nakładanie na siebie elementów i wykonywanie działań arytmetycznych na ich wartościach kolorów. Poza kontekstem kanwy HTML5 słowo blit może mieć różne znaczenia, ale w tej książce należy je rozumieć w podobny sposób do pojęcia składania w kontekście kanwy. Więcej informacji na temat składania (ang. compositing) znajduje się na stronie http://dev.w3.org/ html5/2dcontext/#compositing.

Wyświetlenie sprite’ów to dobry początek, ale zanim zakończymy tę recepturę, dodamy jeszcze kod pogrubiony na listingu 6.3, aby je trochę powiększyć. Listing 6.3. Powiększenie sprite’ów var gamejs = require('gamejs'); var screenWidth = 1200; var screenHeight = 600;

136  Rozdział 6. BIJATYKI var scale = 8; var spriteSheetWidth = 64; var spriteSheetHeight = 16; function main() { var display = gamejs.display.setMode([screenWidth, screenHeight]); var sprites = gamejs.image.load('sprites.png'); sprites = gamejs.transform.scale(sprites, [spriteSheetWidth*scale, spriteSheetHeight*scale]); display.blit(sprites); }; gamejs.preload(['sprites.png']); gamejs.ready(main);

Ponieważ wartości ustawionych w programie będziemy używać wielokrotnie, zamiast pozostawiać je wpisane bezpośrednio w logikę, zdefiniowaliśmy je jako zmienne. W istocie nowością jest tylko trzeci wiersz funkcji main. Kod ten skaluje obrazy o współczynnik 8. Teraz strona powinna wyglądać tak jak na rysunku 6.2.

Rysunek 6.2. Przeskalowane sprite’y

Receptura. Wybieranie poszczególnych sprite’ów z zestawu W tej recepturze dowiesz się, jak wybierać każdego sprite’a osobno. Najpierw zmień niektóre zmienne, aby łatwiej się z nimi pracowało — listing 6.4. Listing 6.4. Zmodyfikowane zmienne var var var var var var

gamejs = require('gamejs'); screenWidth = 1200; screenHeight = 600; scale = 8; spriteSize = 16; numSprites = 4;

Następnie zdefiniuj funkcję skalującą, aby przy jej użyciu wykonywać skalowanie. Kod przedstawiony na listingu 6.5 można wpisać bezpośrednio pod kodem z listingu 6.4. Listing 6.5. Funkcja scaleUp function scaleUp(image){ return gamejs.transform.scale(image, [spriteSize*scale, spriteSize*scale]); };

Receptura. Odbieranie danych od dwóch graczy  137

Ostatnią zmianę potrzebną do tego, aby można było używać pojedynczych sprite’ów, trzeba wprowadzić w funkcji main. Funkcję tę należy zmodyfikować tak, jak pokazano na listingu 6.6. Listing 6.6. Funkcja main renderująca sprite’y pojedynczo function main() { var display = gamejs.display.setMode([screenWidth, screenHeight]); var sprites = gamejs.image.load('sprites.png'); var surfaceCache = []; for (var i = 0; i < numSprites; i++){ var surface = new gamejs.Surface([spriteSize, spriteSize]); var rect = new gamejs.Rect(spriteSize*i, 0, spriteSize, spriteSize); var imgSize = new gamejs.Rect(0, 0, spriteSize, spriteSize); surface.blit(sprites, imgSize, rect); surfaceCache.push(surface); }; for (var i=0; i < surfaceCache.length; i++) { var sprite = scaleUp(surfaceCache[i]); display.blit(sprite, [i*spriteSize*scale, spriteSize*i*scale]); }; };

Najpierw została zainicjowana pusta tablica o nazwie surfaceCache. Za nią znajdują się dwie pętle, które powodują wyświetlenie sprite’ów na linii ukośnej. Pierwsza z tych pętli przygotowuje sprite’a, tworząc obiekty surface, rect oraz imgSize. Wszystkie mają rozmiar sprite’a. Zadaniem obiektu rect jest kontrolowanie przesunięcia sprite’a podczas jego nakładania na powierzchnię (surface). Na końcu pierwszej pętli obiekt surface zostaje dodany jako element do tablicy surfaceCache. Druga pętla tylko skaluje obrazy i wyświetla je na linii skośnej. WSKAZÓWKA Kilka słów do początkujących: jeśli da się konkretnie określić przeznaczenie pętli albo kilku wierszy kodu, to często dobrym pomysłem jest zapisanie ich w postaci funkcji. Kod podzielony na logiczne moduły jest znacznie łatwiejszy w utrzymaniu. Jednak na początkowych etapach pracy nad programem zazwyczaj lepiej jest się wstrzymać z tworzeniem struktury. Umiejętność rozpoznania momentu, w którym najlepiej jest popracować nad strukturą i refaktoryzacją kodu, jest wspaniała, ale trzeba czasu, żeby ją posiąść. Na razie zapamiętaj, że odpowiedzi „zawsze” i „nigdy” na pytanie, czy refaktoryzować, są zwykle błędne, jeśli chodzi o ważne i duże projekty.

Teraz strona index.html w przeglądarce wygląda tak, jak widać na rysunku 6.3.

Receptura. Odbieranie danych od dwóch graczy Na razie w grze brakuje elementu interaktywności. Zmienimy to w tej recepturze, dodając obiekt Player, który będzie mógł naciskać przyciski w celu zmiany sprite’a. Najpierw dodaj kod przedstawiony na listingu 6.7 do pliku main.js. Można go wpisać za funkcją scaleUp. Listing 6.7. Obiekt Player function Player(placement, sprite){ this.placement = placement; this.sprite = sprite;

138  Rozdział 6. BIJATYKI

Rysunek 6.3. Sprite’y wyświetlone pojedynczo wzdłuż ukośnej linii }; Player.prototype.draw = function(display) { sprite = scaleUp(this.sprite); display.blit(sprite, [spriteSize+this.placement, spriteSize]); };

Na listingu znajdują się dwie funkcje. Pierwsza jest konstruktorem, co można poznać po wielkiej pierwszej literze jej nazwy. Funkcja ta inicjuje obiekt Player, gdy zostanie zastosowane wywołanie new Player. Metoda draw wykonuje zadanie podobne do zadania drugiej pętli for przedstawionej na listingu 6.6. OSTRZEŻENIE PRACUJĄC Z OBIEKTAMI, TRZYMAJ SIĘ KONWENCJI. Przeglądarce nazwy konstruktorów pisane małą literą nie sprawiają żadnego problemu. Mimo to rozpoczynanie nazw tego rodzaju funkcji wielką literą w przyszłości pomoże zrozumieć kod innym programistom, jak również Tobie. Ponadto konstruktor można wywoływać bez użycia słowa kluczowego new, ale wtedy otrzyma się zaskakujące rezultaty (w szczególności this zostanie związane z globalnym kontekstem). To właśnie ze względu na różne tego typu pułapki niektórzy wolą tworzyć obiekty przy użyciu metody create.

Funkcja main bardzo urosła i dlatego podzieliłem ją na cztery części przedstawione na listingach 6.8 – 6.11. Listingi te obejmują cały kod tej funkcji. Nie zapomnij usunąć drugiej pętli for, jak pokazano na listingu 6.8.

Receptura. Odbieranie danych od dwóch graczy  139

Listing 6.8. Definicje nazw sprite’ów function main() { var display = gamejs.display.setMode([screenWidth, screenHeight]); var sprites = gamejs.image.load('sprites.png'); var surfaceCache = []; for (var i = 0; i < numSprites; i++){ var surface = new gamejs.Surface([spriteSize, spriteSize]); var rect = new gamejs.Rect(spriteSize*i, 0, spriteSize, spriteSize); var imgSize = new gamejs.Rect(0, 0, spriteSize, spriteSize); surface.blit(sprites, imgSize, rect); surfaceCache.push(surface); };

// Usuń to: for (var i=0; i < surfaceCache.length; i++) { // Usuń to: var sprite = scaleUp(surfaceCache[i]); // Usuń to: display.blit(sprite, [i*spriteSize*scale, spriteSize*i*scale]); // Usuń to: }; var var var var

rock = surfaceCache[0]; paper = surfaceCache[1]; scissors = surfaceCache[2]; person = surfaceCache[3];

Nowy na tym listingu jest tylko kod oznaczony pogrubieniem. Zawiera on definicje nazw sprite’ów znajdujących się w tablicy surfaceCache. Na listingu 6.9 jest przedstawiona dalsza część funkcji main. Wpisz ten kod bezpośrednio pod kodem z listingu 6.8. Listing 6.9. Obsługa naciśnięć klawiszy function handleEvent(event) { if(gamejs.event.KEY_DOWN){ if(event.key === gamejs.event.K_UP){ player2.sprite = person; }else if(event.key === gamejs.event.K_DOWN){ player2.sprite = paper; }else if(event.key === gamejs.event.K_RIGHT){ player2.sprite = scissors; }else if(event.key === gamejs.event.K_LEFT){ player2.sprite = rock; }else if(event.key === gamejs.event.K_w){ player1.sprite = person; }else if(event.key === gamejs.event.K_a){ player1.sprite = rock; }else if(event.key === gamejs.event.K_s){ player1.sprite = paper; }else if(event.key === gamejs.event.K_d){ player1.sprite = scissors; } } };

Ten kod jest w całości nowy, ale jest też bardzo prosty. Gdy zostanie wywołana ta funkcja i ktoś naciśnie jakiś klawisz (funkcja ta jest wywoływana wiele razy na sekundę, dzięki czemu jest w stanie

140  Rozdział 6. BIJATYKI przechwycić takie zdarzenie), to nastąpi odpowiednia dla tego klawisza zmiana sprite’a (tylko atrybutu, nic w tym miejscu nie jest renderowane). Na listingu 6.10 została przedstawiona ostatnia funkcja zdefiniowana w funkcji main. Należy ją wkleić za kodem z listingu 6.9. Listing 6.10. Funkcja gameTick function gameTick(msDuration) { gamejs.event.get().forEach(function(event) { handleEvent(event); }); display.clear(); player1.draw(display); player2.draw(display); };

Funkcja gameTick w silnikach gier występuje pod różnymi nazwami, ale to nieistotne. Ważne jest, aby wiedzieć, że w tej funkcji można wpisać kod dotyczący odbierania danych oraz aktualizowania obiektów i tego, co widać na ekranie. Zwróć też uwagę na to, czego nie wymieniłem, czyli ładowanie zasobów, definiowanie obiektów, inicjowanie funkcji pomocniczych itp. Druga ważna kwestia związana z tą funkcją dotyczy przekazywania parametru określającego, ile milisekund minęło od ostatniego wywołania. W tej funkcji gameTick tylko obsługujemy naciśnięcia niektórych klawiszy i odpowiednio modyfikujemy sprite’y. W końcu na listingu 6.11 znajduje się pozostała część kodu źródłowego funkcji main. Należy go wpisać bezpośrednio za kodem z listingu 6.10, a przed instrukcją gamejs.preload(['sprites.png']);. Listing 6.11. Ostatnia część funkcji main var player1 = new Player(0, person); var player2 = new Player(200, person); gamejs.time.fpsCallback(gameTick, this, 60); };

// Koniec funkcji main

W tym kodzie są inicjowane obiekty graczy. Pierwszym parametrem tych inicjacji jest wartość przesunięcia na osi poziomej, a drugim pierwszy sprite. Ostatni wiersz uruchamia funkcję gameTick w nieskończonej pętli. Wartość 60 oznacza, że pętla powinna próbować działać z prędkością 60 klatek na sekundę. Na rysunku 6.4 jest pokazana strona index.html po otwarciu w przeglądarce i zmianie sprite’ów za pomocą klawiszy wymienionych w kodzie na listingu 6.9.

Rysunek 6.4. Zmiana sprite’ów

Receptura. Poruszanie się i zmienianie formy  141

W ten sposób utworzyliśmy klawiaturowy interfejs do klasycznej gry Kamień, papier, nożyce. W dalszej części rozdziału przekształcimy go w typową grę bijatykę, chociaż pozostaniemy w tej samej konwencji.

Receptura. Poruszanie się i zmienianie formy W tej recepturze do możliwości zmiany sprite’a dodamy możliwość poruszania się w poprzek ekranu. Przestaniemy też posługiwać się określeniem sprite, które zastąpimy pojęciem formy, jaką gracz może przyjąć. Ponadto zmienimy nieco interfejs, aby klawisze w prawo i w lewo powodowały zmianę kierunku ruchu, a w górę i w dół — zmianę formy. Najpierw zajmiemy się renderowaniem tekstu, w związku z czym musimy zaimportować moduł czcionek ze skryptu game.js. W tym celu wystarczy na początku pliku main.js dodać pogrubiony wiersz kodu widoczny na listingu 6.12. Listing 6.12. Ładowanie modułu czcionek var gamejs = require('gamejs'); var font = require('gamejs/font');

Następnie zmodyfikujemy konstruktor Player, tak jak widać na listingu 6.13. Teraz będziemy tworzyć graczy na bazie form, a nie sprite’ów. Listing 6.13. Konstruktor z formami zamiast sprite’ów function Player(placement, form, forms){ this.placement = placement; this.form = form; this.forms = forms; };

Kolejną czynnością jest dodanie dwóch funkcji przedstawionych na listingu 6.14 za konstruktorem z listingu 6.13. Funkcje te umożliwiają zmienianie form. Listing 6.14. Funkcje zmieniające formy Player.prototype.nextForm = function() { this.form = this.forms[this.form["next"]]; }; Player.prototype.previousForm = function() { this.form = this.forms[this.form["previous"]]; };

Potrzebna jest też zmiana w funkcji Player jemy się do obrazu formy, a nie sprite’a.

draw

przedstawiona na listingu 6.15. Teraz odwołu-

Także nazwy sprite’ów zdefiniowane w funkcji main trzeba teraz wstawić do obiektu forms, jak pokazano na listingu 6.16.

142  Rozdział 6. BIJATYKI Listing 6.15. Zmieniona funkcja Player draw Player.prototype.draw = function(display) { sprite = scaleUp(this.form.image); display.blit(sprite, [spriteSize+this.placement, spriteSize]); };

Listing 6.16. Obiekt forms zastępujący nazwy sprite’ów // Usuń poniższe cztery wiersze kodu //var rock = surfaceCache[0]; //var paper = surfaceCache[1]; //var scissors = surfaceCache[2]; //var person = surfaceCache[3]; var forms = { rock: {image: surfaceCache[0], next: 'paper', previous: 'scissors'}, paper: {image: surfaceCache[1], next: 'scissors', previous: 'rock'}, scissors: {image: surfaceCache[2], next: 'rock', previous: 'paper'}, person: {image: surfaceCache[3], next: 'rock', previous: 'scissors'} };

Następnie zamienimy funkcję handleEvent na kod znajdujący się na listingu 6.17. Umożliwia on ciągłe zmienianie form za pomocą klawiszy w górę i w dół oraz poruszanie postaciami w grze przy użyciu klawiszy w prawo i w lewo. Klawisze pozostały te same, ale zmienił się ich sposób działania, co widać po pogrubionych wierszach w tym kodzie. Listing 6.17. Nowy sposób obsługi klawiszy function handleEvent(event) { if(gamejs.event.KEY_DOWN){ if(event.key === gamejs.event.K_UP){ player2.previousForm(); }else if(event.key === gamejs.event.K_DOWN){ player2.nextForm(); }else if(event.key === gamejs.event.K_RIGHT){ player2.placement = player2.placement + 25; }else if(event.key === gamejs.event.K_LEFT){ player2.placement = player2.placement - 25; }else if(event.key === gamejs.event.K_w){ player1.previousForm(); }else if(event.key === gamejs.event.K_a){ player1.placement = player1.placement - 25;

Receptura. Poruszanie się i zmienianie formy  143

}else if(event.key === gamejs.event.K_s){ player1.nextForm(); }else if(event.key === gamejs.event.K_d){ player1.placement = player1.placement + 25; } } };

Następnie w funkcji gameTick dodamy instrukcje rysujące tekst na ekranie. Wyróżniono je pogrubieniem na listingu 6.18. Listing 6.18. Dodanie tekstu do gry function gameTick(msDuration) { gamejs.event.get().forEach(function(event) { handleEvent(event); }); display.clear(); var defaultFont = new font.Font("40px Arial"); var textSurface = defaultFont.render("PAPIER KAMIEŃ NOŻYCE", "#000000"); display.blit(textSurface, [0, 160]); player1.draw(display); player2.draw(display); };

Pod koniec funkcji main, przed funkcją zwrotną gameTick tworzymy egzemplarze obiektów gracza, przekazując konstruktorowi Player wartość przesunięcia w poziomie, formę początkową oraz zmienną forms — listing 6.19. Ostatnia klamra na tym listingu została dodana po to, aby podkreślić, że jest to koniec funkcji main. Listing 6.19. Tworzenie obiektów graczy var player1 = new Player(0, forms['person'], forms); var player2 = new Player(1000, forms['person'], forms); gamejs.time.fpsCallback(gameTick, this, 60); };

Na rysunku 6.5 widać ekran gry po przesunięciu graczy i zmianie formy jednego z nich na papier.

Rysunek 6.5. Prawie bijatyka

144  Rozdział 6. BIJATYKI

Receptura. Przyjmowanie danych od obu graczy naraz Gra zaczyna nabierać kształtu i pewnie już nie możesz się doczekać dodania pasków życia, punktów zdrowia i specjalnych ruchów. Zanim jednak przejdziemy do implementacji tych ciekawych elementów gry, musimy rozwiązać jeden problem. Teraz czynności jednego gracza blokują drugiego. W innych grach takie blokowanie może być nawet wskazane, np. w pojedynku wyciągnięcie pistoletu i strzał oddany przez jednego gracza powoduje, że drugi zamiera w bezruchu. Ale nawet w takim przypadku nasza gra w obecnym stanie nie byłaby idealnym rozwiązaniem. W tej recepturze oddzielimy pobieranie danych (które musi następować jak najszybciej) od aktualizowania graczy (które może trwać dłużej). Pierwszą zmianą, jaką wprowadzimy, będzie zapisanie informacji o zdarzeniach naciśnięcia klawiszy w rejestrze w obiekcie Player. W tym celu zmodyfikujemy konstruktor Player w sposób pokazany na listingu 6.20, aby przechowywał nowe potrzebne nam atrybuty. Listing 6.20. Rejestr naciśnięć klawiszy w obiekcie Player function Player(placement, form, forms){ this.placement = placement; this.form = form; this.forms = forms; this.up = false; this.down = false; this.left = false; this.right = false; this.canChange = true; };

W tych nowych atrybutach będą przechowywane informacje dotyczące tego, czy został naciśnięty klawisz odpowiadający jednemu z czterech kierunków. Pewnie zastanawiasz się, do czego służy atrybut canChange. Zawiera on informację, czy gracz może zmieniać formę. Uniemożliwia to zmienianie formy w nieskończoność, co można zaobserwować w poprzedniej recepturze, gdy naciśnie się jeden z klawiszy zmiany formy. Aby nie mnożyć zbyt wielu funkcji, funkcje previousForm i nextForm można połączyć w jedną funkcję changeForm pobierającą jako parametr łańcuch next lub previous. Możesz usunąć te dwie funkcje i wstawić w ich miejsce przedstawioną na listingu 6.21 funkcję changeForm. Listing 6.21. Funkcja changeForm Player.prototype.changeForm = function(next_or_previous) { this.form = this.forms[this.form[next_or_previous]]; };

Następnie w obiekcie Player utworzymy funkcję update odczytującą wartości up, down, left, right oraz canChange i wykonującą odpowiednie czynności. Jej kod znajduje się na listingu 6.22 i można go wkleić za kodem z listingu 6.21. Listing 6.22. Funkcja update obiektu Player Player.prototype.update = function(msDuration) { if(this.up){ if (this.canChange) {

Receptura. Przyjmowanie danych od obu graczy naraz  145

this.changeForm('previous'); this.canChange = false; } } if(this.down){ if (this.canChange) { this.changeForm('next'); this.canChange = false; } }; if(this.left){ this.placement = this.placement - 14; }else if(this.right){ this.placement = this.placement + 14; } };

Pozostały jeszcze dwie zmiany do wprowadzenia w tej recepturze. Pierwsza jest dość długa i polega na zamianie funkcji handleEvent (w funkcji main) na przedstawiony na listingu 6.23 kod rejestrujący dane wejściowe poprzez ustawianie wartości logicznych w obiektach graczy zamiast w pętli rysującej. Listing 6.23. Rejestrowanie danych wejściowych w obiektach graczy function handleEvent(event) { if(event.type === gamejs.event.KEY_DOWN){ if(event.key === gamejs.event.K_UP){ player2.up = true; }else if(event.key === gamejs.event.K_DOWN){ player2.down = true; }else if(event.key === gamejs.event.K_RIGHT){ player2.right = true; player2.left = false; }else if(event.key === gamejs.event.K_LEFT){ player2.left = true; player2.right = false; }else if(event.key === gamejs.event.K_w){ player1.up = true; }else if(event.key === gamejs.event.K_a){ player1.left = true; player1.right = false; }else if(event.key === gamejs.event.K_s){ player1.down = true; }else if(event.key === gamejs.event.K_d){ player1.right = true; player1.left = false; } }else if(event.type === gamejs.event.KEY_UP){ if(event.key === gamejs.event.K_UP){ player2.up = false; player2.canChange = true; }else if(event.key === gamejs.event.K_DOWN){ player2.down = false; player2.canChange = true; }else if(event.key === gamejs.event.K_RIGHT){ player2.right = false; }else if(event.key === gamejs.event.K_LEFT){

146  Rozdział 6. BIJATYKI player2.left = false; }else if(event.key === gamejs.event.K_w){ player1.up = false; player1.canChange = true; }else if(event.key === gamejs.event.K_a){ player1.left = false; }else if(event.key === gamejs.event.K_s){ player1.down = false; player1.canChange = true; }else if(event.key === gamejs.event.K_d){ player1.right = false; } } };

W tym kodzie należy zwrócić uwagę na fakt, że teraz procedura obsługi zdarzeń rozpoznaje następujące po sobie zdarzenia naciśnięcia klawiszy w górę i w dół. Ogólnie rzecz biorąc, zdarzenie naciśnięcia klawisza w dół powoduje ustawienie atrybutu na true, a w górę na false. To samo dotyczy klawiszy w lewo i prawo, tzn. naciśnięcie jednego powoduje anulowanie drugiego. Zdarzenia klawiszy w górę i w dół ustawiają własność canChange na true. Dzięki temu, jak również dzięki ustawieniu canChange na false po wykonaniu funkcji update, zdarzenie zmiany ma miejsce tylko raz po każdym naciśnięciu klawisza. Ostatnia zmiana w tej recepturze jest pokazana na listingu 6.24. W tym kodzie wywołujemy funkcję update dla każdego z graczy przed funkcją draw w funkcji gameTick. Listing 6.24. Wywołania funkcji update dla graczy player1.update(); player2.update(); player1.draw(display); player2.draw(display);

Receptura. Implementacja masek bitowych W tej recepturze do obsługi niektórych atrybutów graczy wykorzystamy maski bitowe, zamiast przechowywać wartości true i false. Jeśli nie znasz operatorów bitowych i nie umiesz posługiwać się maskami bitowymi, to ta receptura może być trudna do zrozumienia. Dlatego zanim przejdziemy do właściwej części tego podrozdziału, najpierw porozmawiamy sobie o liczbach. Dla osób z wykształceniem informatycznym (zarówno formalnym, jak i dla samouków) informacje zawarte w tej części nie są nowe. Są one jednak niezbędne do zrozumienia opisanych technik. Jeśli więc chcesz, to możesz pominąć tę część i ewentualnie wrócić do niej, gdy nie będziesz mógł czegoś zrozumieć. Liczby (jak również wszystkie inne rodzaje informacji) w komputerach są przechowywane w postaci szeregów zer i jedynek. Każdą liczbę można wyrazić zarówno w systemie binarnym (o podstawie 2) zawierającym tylko cyfry 0 i 1, jak i dziesiętnym (o podstawie 10) zawierającym cyfry 0 – 9. Jako że w systemie binarnym każda cyfra reprezentuje mnożenie przez 2 zamiast 10, do zapisu liczb binarnych z reguły potrzeba więcej cyfr niż do zapisu dziesiętnych. W tabeli 6.1 przedstawiono dla porównania zestawienie kilku liczb zapisanych w tych dwóch formatach.

Receptura. Implementacja masek bitowych  147

Tabela 6.1. Liczby binarne i ich dziesiętne odpowiedniki Binarne

Dziesiętne

Małe liczby

0, 1, 10, 11, 100, 101, 110, 111, 1000, 1001, 1010

0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

Duża liczba

10000001

129

Wiemy już, jak wyglądają liczby binarne. Ale co mają one wspólnego z naszą grą? Powiedzmy, że chcemy śledzić cztery wartości, które mogą być true lub false. W pierwszym momencie możesz pomyśleć, że najlepszym sposobem na ich zapisywanie może być utworzenie czterech zmiennych (np. var1, var2, var3 oraz var4). Następnie można ustawiać wartości tych zmiennych na true i false w zależności od potrzeby za pomocą operatora przypisania = albo sprawdzać, czy zawierają wartość true lub false, za pomocą operatora porównawczego ===. Zmierzam do tego, że wszystkie te cztery wartości logiczne można także zaprezentować przy użyciu czterech bitów, przyjmując, że 1 oznacza true, a 0 false. Na przykład odpowiednikiem ustawienia zmiennych na true, false, false oraz true byłaby binarna liczba 1001 (9 w systemie dziesiętnym). Pozostaje do rozwiązania problem sprawdzania i ustawiania wartości bitów. Dałoby się zastosować prymitywne rozwiązanie polegające na wykorzystaniu operatorów = i ===, ale nie musimy tego robić. Zamiast tego skorzystamy z arytmetyki logicznej. W tabeli 6.2 przedstawiono kilka przykładów obu rodzajów operacji. Tabela 6.2. Operacje logiczne wykorzystywane w tej recepturze Cel

Użycie zmiennej

Operacja na bitach

Ustawienie na true

var1 = true

mask |= var1

Ustawienie na false

var1 = false

mask &= ~var1

Sprawdzenie, czy wartość to true, czy false

if (var1){}

if(var1 & mask){}

W metodzie bitowej jest używana maska bitowa będąca kontenerem wartości 0 i 1 reprezentujących fałsz i prawdę. Zmienna var1 powinna mieć wartość równą 2 do potęgi odpowiadającej pozycji bitu. Jeśli więc zdecydujemy, że pierwszy bit (od prawej) reprezentuje własność left, to var1 możemy zapisać jako 1 (2^0). Następny bit miałby wartość 2^1 (2), kolejny 2^2 (4). UWAGA Osoby, które wcześniej nie miały styczności z operatorami bitowymi, pewnie zastanawiają się, czy ich stosowanie jest w ogóle warte zachodu. Wszystko zależy od celu programisty. Jest to jeden z przykładów poświęcenia klarowności kodu na rzecz potencjalnego zysku na wydajności. Programy są ograniczone przestrzennie i czasowo, a metoda bitowa pozwala uzyskać lepszy wynik w obu tych kategoriach. Jeśli chodzi o przestrzeń, to zapisywanie bitów w liczbach całkowitych wymaga o wiele mniej miejsca niż utworzenie dużej liczby zmiennych logicznych. Co się zaś tyczy czasu, to zwiększenie wydajności uzyskuje się poprzez zastąpienie czasochłonnych deklaracji i operacji porównywania wartości logicznych szybszymi operacjami na bitach. Należy też pamiętać, że szybkość działania programów w JavaScripcie w dużym stopniu zależy od przeglądarki, w której są uruchamiane. Jeśli nie wiesz, którą technikę zastosować, przetestuj swoją aplikację we wszystkich interesujących Cię przeglądarkach.

148  Rozdział 6. BIJATYKI Przejdźmy do konkretnego kodu. Pierwsza zmiana będzie polegała na zdefiniowaniu pomocniczych zmiennych dla każdego bitu w masce. Dodaj do kodu wiersz oznaczony pogrubieniem na listingu 6.25. Zwróć uwagę na zastosowanie przecinków w deklaracjach zmiennych. Wszystkie te zmienne są zdefiniowane tak, jakby przed każdą z nich stało słowo kluczowe var. Listing 6.25. Definicje zmiennych pomocniczych var var var var var var

screenWidth = 1200; screenHeight = 600; scale = 8; spriteSize = 16; numSprites = 4; up = 1, down = 2, left = 4, right = 8, canChange = 16;

Następnie wyrzucimy niektóre atrybuty z obiektu Player i dodamy atrybut mask. Kod powinien teraz wyglądać jak na listingu 6.26. Maska została ustawiona na 16, ponieważ powinna mieć wartość równą sumie wartości wszystkich zmiennych pomocniczych ustawionych na prawdę. W naszym przypadku dotyczy to tylko zmiennej canChange, która ma właśnie wartość 16. Gdyby żadna zmienna nie miała prawdziwej wartości, to maskę ustawilibyśmy na 0. Gdyby zmienne down i left były prawdziwe, maska miałaby wartość 6. Listing 6.26. Obiekt Player z atrybutem mask function Player(placement, form, forms){ this.placement = placement; this.form = form; this.forms = forms; this.mask = 16; };

Kolejną czynnością będzie modyfikacja metody update obiektu Player, aby dostosować ją do nowego sposobu sprawdzania i zmieniania atrybutu mask. Zmodyfikuj kod metody update tak, jak pokazano na listingu 6.27. Listing 6.27. Bitowa metoda update Player.prototype.update = function() { if(this.mask & up){ if (this.mask & canChange) { this.changeForm('previous'); this.mask &= ~canChange; } } if(this.mask & down){ if (this.mask & canChange) { this.changeForm('next'); this.mask &= ~canChange; } }; if(this.mask & left){ this.placement = this.placement - 14; }else if(this.mask & right){

Receptura. Implementacja masek bitowych  149

this.placement = this.placement + 14; } };

Ostatnia zmiana w tej recepturze dotyczy funkcji maski — listing 6.28.

handleEvent,

Listing 6.28. Obsługa zdarzeń przy użyciu maski function handleEvent(event) { if(event.type === gamejs.event.KEY_DOWN){ if(event.key === gamejs.event.K_UP){ player2.mask |= up; }else if(event.key === gamejs.event.K_DOWN){ player2.mask |= down; }else if(event.key === gamejs.event.K_LEFT){ player2.mask |= left; player2.mask &= ~right; }else if(event.key === gamejs.event.K_RIGHT){ player2.mask |= right; player2.mask &= ~left; }else if(event.key === gamejs.event.K_w){ player1.mask |= up; }else if(event.key === gamejs.event.K_s){ player1.mask |= down; }else if(event.key === gamejs.event.K_a){ player1.mask |= left; player1.mask &= ~right; }else if(event.key === gamejs.event.K_d){ player1.mask |= right; player1.mask &= ~left; } } else if(event.type === gamejs.event.KEY_UP){ if(event.key === gamejs.event.K_UP){ player2.mask &= ~up; player2.mask |= canChange; }else if(event.key === gamejs.event.K_DOWN){ player2.mask &= ~down; player2.mask |= canChange; }else if(event.key === gamejs.event.K_RIGHT){ player2.mask &= ~right; }else if(event.key === gamejs.event.K_LEFT){ player2.mask &= ~left; }else if(event.key === gamejs.event.K_w){ player1.mask &= ~up; player1.mask |= canChange; }else if(event.key === gamejs.event.K_a){ player1.mask &= ~left; }else if(event.key === gamejs.event.K_s){ player1.mask &= ~down; player1.mask |= canChange; }else if(event.key === gamejs.event.K_d){ player1.mask &= ~right; } } };

która powinna ustawiać bity

150  Rozdział 6. BIJATYKI Po tej recepturze gra powinna działać tak samo jak poprzednio.

Receptura. Maskowanie kolizji Jeśli nadal zastanawiasz się, jaki jest pożytek ze stosowania masek, to w tej recepturze znajdziesz odpowiedź na nurtujące Cię pytanie. Niektóre silniki wykrywają kolizje poprzez wykrywanie nakładania się otaczających obiekty prostokątnych lub eliptycznych obszarów, tzw. pól kolizyjnych (ang. hitbox). Silnik game.js jest jednak wyjątkowy pod tym względem, ponieważ zawiera bibliotekę masek do wykrywania kolizji na podstawie wartości koloru związanych z obiektem sprite’ów. Co ciekawe, game.js zapisuje informacje o pikselach jako prawdę i fałsz, a nie jako liczby całkowite, w których zera i jedynki oznaczają kolizje oraz braki kolizji. Aby poprawnie obsłużyć kolizje, trzeba rozwiązać dwa problemy. Po pierwsze, powiększone obrazy mogą mieć rozmazaną obwódkę powodującą zatarcie wartości kolorów używanych do wykrywania kolizji. Dlatego należy jakoś przeskalować obrazy, aby utworzyć większe wersje sprite’ów. Można pieczołowicie odtworzyć każdy obraz i pokolorować go albo rozciągnąć obraz. Można też pozostawić mały ekran gry. Jeśli zdecydujesz się na opcję skalowania, to musisz na koniec oczyścić krawędzie. W programie Gimp (http://www.gimp.org) narzędzie do znajdowania krawędzi między kolorami nazywa się Magiczna różdżka (ang. Magic wand). Dla ułatwienia w folderach bijatyki/po_recepturze7 i bijatyki/gotowe znajduje się już odpowiednio przygotowany plik sprites.png pod nazwą sprites_big.png. Po zmianie rozmiaru obrazu niepotrzebne stają się zmienna scale i funkcja scaleUp. Skoro sprite jest teraz osiem razy szerszy i wyższy przy zachowaniu zdefiniowanego wcześniej współczynnika sprite’ów, to wartość zmiennej spriteSize należy teraz zmienić z 16 na 128. Aby wszystko było jasne, zmianę tę przedstawiłem na listingu 6.29. Listing 6.29. Zmiana rozmiaru sprite’a var var var var

screenWidth = 1200; screenHeight = 600; spriteSize = 128; numSprites = 4;

Trzeba także uprościć metodę draw w obiekcie Player, usuwając z niej części dotyczące skalowania. Nowa wersja metody draw jest przedstawiona na listingu 6.30. Listing 6.30. Metoda draw bez skalowania Player.prototype.draw = function(display) { display.blit(this.form.image, [this.placement, 0]); };

Następnie zmienimy parametr przekazywany do funkcji load i preload na nazwę nowego sprite’a, czyli sprites_big.png — listing 6.31. Listing 6.31. Zmiana obrazu w funkcjach load i preload function main() { var display = gamejs.display.setMode([screenWidth, screenHeight]);

// Zamień nazwę sprites na sprites_big

Receptura. Maskowanie kolizji  151

// var sprites = gamejs.image.load('sprites.png'); var sprites = gamejs.image.load('sprites_big.png'); ...

// Zamień nazwę sprites na sprites_big //gamejs.preload(['sprites.png']); gamejs.preload(['sprites_big.png']); gamejs.ready(main);

Następnie dodamy tablicę maskCache do przechowywania sprite’ów. Najpierw dodamy bibliotekę mask na początku pliku, jak pokazano na listingu 6.32. Domyślnie samo dołączenie do strony biblioteki game.js nie powoduje załadowania wszystkich narzędzi. Trzeba samodzielnie dodać te, które są potrzebne. Listing 6.32. Dołączenie narzędzia mask var gamejs = require('gamejs'); var font = require('gamejs/font'); var mask = require('gamejs/mask');

Teraz napełnimy tablicę maskCache w funkcji main i dodamy element maskCache jako własność do obiektu forms, który z kolei sam później stanie się własnością obiektu Player. Kod z tymi modyfikacjami jest przedstawiony na listingu 6.33. Zwróć uwagę na użycie funkcji mask.fromSurface na zmiennej maski niezwiązanej z żadnym graczem. Listing 6.33. Budowa tablicy maskCache function main() { var display = gamejs.display.setMode([screenWidth, screenHeight]); var sprites = gamejs.image.load('sprites_big.png'); var surfaceCache = []; var maskCache = []; for (var i = 0; i < numSprites; i++){ var surface = new gamejs.Surface([spriteSize, spriteSize]); var rect = new gamejs.Rect(spriteSize*i, 0, spriteSize, spriteSize); var imgSize = new gamejs.Rect(0, 0, spriteSize, spriteSize); surface.blit(sprites, imgSize, rect); surfaceCache.push(surface); var maskCacheElement = mask.fromSurface(surface); maskCache.push(maskCacheElement); }; var forms = { rock: {image: surfaceCache[0], mask: maskCache[0], next: 'paper', previous: 'scissors'}, paper: {image: surfaceCache[1], mask: maskCache[1], next: 'scissors', previous: 'rock'},

152  Rozdział 6. BIJATYKI scissors: {image: surfaceCache[2], mask: maskCache[2], next: 'rock', previous: 'paper'}, person: {image: surfaceCache[3], mask: maskCache[3], next: 'rock', previous: 'scissors'} };

Ostatnia zmiana, jakiej musimy dokonać, aby maskowanie zaczęło działać, to dodanie kodu przedstawionego na listingu 6.34 przed zakończeniem funkcji gameTick. Ten kod będzie wykrywał kolizje i jeśli jakąś wykryje, będzie drukował informacje w konsoli. Listing 6.34. Zmiana końcówki funkcji gameTick player1.draw(display); player2.draw(display); var hasMaskOverlap = player1.form.mask.overlap(player2.form.mask, [player1.placement - player2.placement, 0]); if (hasMaskOverlap) { console.log(hasMaskOverlap); }

Mechanizm wykrywania kolizji jest już zaimplementowany, ale na razie w żaden sposób z niego nie korzystamy. Zajmiemy się tym niedopatrzeniem w ostatniej recepturze.

Receptura. Niszczenie z wzajemnością Umiemy już poruszać postaciami w grze, zmieniać ich formy oraz zderzać je ze sobą. W tej recepturze dokończymy grę, aby przypominała prawdziwą bijatykę. Na początek potrzebujemy kilku nowych zmiennych. Dodamy je pod wartościami maski, jak pokazano na listingu 6.35. Listing 6.35. Kilka nowych zmiennych var var var var var var

up = 1, down = 2, left = 4, right = 8, canChange = 16; forms = []; timeBetweenHits = 300; timeSinceHit = 0; activeGame = true; defaultFont = new font.Font("40px Arial");

Następnie wprowadzimy w konstruktorze Player zmiany pokazane na listingu 6.36. Teraz zapamiętujemy formy graczy poza obiektami graczy oraz zapisujemy informacje o zdrowiu i uderzeniach, a formę określamy na podstawie parametru formIndex.

Receptura. Niszczenie z wzajemnością  153

Listing 6.36. Zapisywanie dodatkowych informacji o graczu function Player(placement, formIndex){ this.placement = placement; this.form = forms[formIndex]; this.mask = 16; this.hit = false; this.health = 30; };

Po nazwie formIndex można odgadnąć, że teraz formy są przechowywane w tablicy indeksowanej liczbowo, a nie łańcuchowo. Zmianę tę wprowadziłem, aby uprościć interakcje w czasie walki, ale miłym skutkiem ubocznym jest skrócenie kodu funkcji changeForm do jednego wiersza pokazanego na listingu 6.37. Listing 6.37. Zmieniona funkcja changeForm Player.prototype.changeForm = function(index) { this.form = forms[index]; };

Kolejną potrzebną nam rzeczą jest funkcja registerHit w obiekcie Player. Jej kod znajduje się na listingu 6.38. Funkcję tę można wpisać pod funkcją changeForm. Za jej pomocą można określić, przy użyciu indeksu formy, który gracz otrzymuje cios. Kamień bije nożyczki, nożyczki biją papier, a papier bije kamień. Wszystko bije pierwszą formę człowieka, włącznie z formą człowieka (w takim przypadku punkty zdrowia są odejmowane obu graczom). Listing 6.38. Rejestrowanie ciosów Player.registerHit = function(player1, player2){ player1Index = player1.form.index; player2Index = player2.form.index; if(player1Index === 0){ if (player2Index === 1) { player1.hit = true; }else if (player2Index === 2) { player2.hit = true; }; }else if (player1Index === 1){ if (player2Index === 0) { player2.hit = true; }else if (player2Index === 2) { player1.hit = true; }; }else if (player1Index === 2){ if (player2Index === 0) { player1.hit = true; }else if (player2Index === 1) { player2.hit = true; }; }else{ player1.hit = true; } if(player2Index === 3){ player2.hit = true;

154  Rozdział 6. BIJATYKI } if(player2Index !== player1Index || player1Index === 3){ timeSinceHit = 0; }; };

UWAGA Zmienna hit jest logiczna, więc można ją było dodać do maski, a jako że jej wartość początkowa to false, jej dodanie nie miałoby wpływu na początkową wartość maski, która wynosi 16. Trzeba było tylko w instrukcjach sprawdzania i ustawiania wartości zamienić operatory = i === na operatory bitowe. Należy jednak pamiętać, że w liczbie całkowitej można przechowywać ograniczoną liczbę wartości logicznych. W najnowszych wersjach przeglądarek Firefox i Chrome można zapisać maksymalnie 32 bity. Może nigdy nie będziesz potrzebować więcej, ale czasami dobrze jest napisać coś dziwnego, aby dojść do granic możliwości sprzętu. To wyrabia charakter.

Teraz spójrzmy na funkcję update obiektu Player przedstawioną na listingu 6.39. Znajdują się w niej trzy ważne zmiany. Pierwsza dotyczy zmieniania formy poprzez przekazywanie wartości liczbowej względnej do bieżącej formy. Druga to aktualizacja zdrowia (health) gracza na końcu funkcji. Trzecia to sprawdzanie położenia graczy, aby uniemożliwić im wychodzenie poza ekran. Listing 6.39. Funkcja update obiektu Player Player.prototype.update = function(msDuration) { if(this.mask & up){ if (this.mask & canChange) { this.changeForm((this.form.index+3-1)%3); this.mask &= ~canChange; } } if(this.mask & down){ if (this.mask & canChange) { this.changeForm((this.form.index+1)%3); this.mask &= ~canChange; } }; if(this.mask & left){ if(this.placement > 0){ this.placement = this.placement - 14; } }else if(this.mask & right){ if(this.placement < 1000){ this.placement = this.placement + 14; } } if(this.hit===true){ this.health = this.health -3; this.hit = false; }; };

Receptura. Niszczenie z wzajemnością  155

W funkcji draw znajduje się tylko drobna zmiana powodująca utworzenie niewielkiej ilości miejsca na tytuł gry poprzez przesunięcie obiektów graczy w dół — listing 6.40. Listing 6.40. Przesunięcie obiektów graczy w dół Player.prototype.draw = function(display) {

// display.blit(this.form.image, [this.placement, 0]); display.blit(this.form.image, [this.placement, 80]); };

Jeśli chodzi o definicje form, to można zastąpić oryginalną implementację indeksowaną liczbowo tablicą pokazaną na listingu 6.41. Listing 6.41. forms jako normalna tablica // Niepogrubione wiersze pozostawiono, aby pokazać kontekst var maskCacheElement = mask.fromSurface(surface); maskCache.push(maskCacheElement); }; forms = [ {index: 0, image: surfaceCache[0], mask: maskCache[0]}, {index: 1, image: surfaceCache[1], mask: maskCache[1]}, {index: 2, image: surfaceCache[2], mask: maskCache[2]}, {index: 3, image: surfaceCache[3], mask: maskCache[3]} ];

Sporo zmian jest w funkcji gameTick, która jest w całości widoczna na listingu 6.42. Nie jest jednak tak źle, jeśli spojrzy się tylko na pogrubione wiersze. Zmienna activeGame określa, czy gra została zakończona, czy nie. Jeśli tak, kod funkcji gameTick nie jest wykonywany. Drugi pogrubiony fragment kodu sprawdza, czy od ostatniego uderzenia minęło wystarczająco dużo czasu i czy nastąpiło uderzenie, oraz wywołuje w razie potrzeby funkcję registerHit. Funkcja update obiektu Player przyjmuje teraz jako parametr czas trwania w milisekundach. Ponadto przybyło tekstu do wydrukowania na ekranie. W końcu jeśli poziom zdrowia gracza spadnie do 0, następuje koniec gry, ustawienie zmiennej activeGame na false oraz wyświetlenie informacji o przegranej jednego z graczy. Listing 6.42. Modyfikacje funkcji gameTick function gameTick(msDuration) { if(activeGame){ gamejs.event.get().forEach(function(event) { handleEvent(event); }); display.clear(); if(timeSinceHit > timeBetweenHits){ var hasMaskOverlap = player1.form.mask.overlap(player2.form.mask,

156  Rozdział 6. BIJATYKI [player1.placement - player2.placement, 0]); if (hasMaskOverlap) { Player.registerHit(player1, player2); }; }else{ timeSinceHit +=msDuration; }; player1.update(msDuration); player2.update(msDuration); display.blit(defaultFont.render("KAMIEŃ PAPIER NOŻYCE", "#000000"), [300, 0]); display.blit(defaultFont.render("Gracz 1: ", "#000000"), [0, 240]); display.blit(defaultFont.render(player1.health, "#000000"), [170, 240]); display.blit(defaultFont.render("Sterowanie: W A S D", "#000000"), [0, 280]); display.blit(defaultFont.render("Gracz 2: ", "#000000"), [600, 240]); display.blit(defaultFont.render(player2.health, "#000000"), [770, 240]); display.blit(defaultFont.render("Sterowanie: \u2191 \u2193 \u2190 \u2192", "#000000"), [600, 280]); player1.draw(display); player2.draw(display); if(player1.health === 0 || player2.health === 0){ activeGame = false; if (player1.health === 0){ display.blit(defaultFont.render("Wygrał gracz 2", "#000000"), [0, 320]); } if (player2.health === 0){ display.blit(defaultFont.render("Wygrał gracz 1", "#000000"), [600, 320]); } }; }; };

Ostatni fragment kodu w tej recepturze dotyczy zastąpienia obiektu formy liczbowym identyfikatorem formy w instrukcji tworzenia obiektu gracza — listing 6.43. Listing 6.43. Wywołanie konstruktora Player z identyfikatorem formy 3 (człowiek) var player1 = new Player(0, 3); var player2 = new Player(1000, 3); gamejs.time.fpsCallback(gameTick, this, 60);

Jeśli wszystko poszło zgodnie z planem, to masz gotową prostą bijatykę dla dwóch graczy, której zasady dzięki wykorzystaniu znanej na całym świecie gry w kamień, papier i nożyce są zrozumiałe dla każdego. Gdy otworzysz plik index.html w przeglądarce, możesz zagrać w grę. Na rysunku 6.6 pokazano ekran po wygraniu gry przez jednego z graczy.

Podsumowanie Gratulacje. Właśnie skończyłeś pracę nad dwuwymiarową bijatyką na bazie silnika game.js. Ponadto w tym rozdziale dowiedziałeś się, czym są zestawy sprite’ów, maski bitowe oraz jak pobierać dane od dwóch użytkowników naraz i jak zaimplementować obsługę kolizji obiektów z pikselową dokład-

Podsumowanie  157

Rysunek 6.6. Skończona gra

nością. Przy okazji poznałeś sporą część biblioteki game.js. Jeśli chcesz dowiedzieć się więcej, poszukaj biblioteki A* Pathfinding, a jeżeli lubisz PRAWDZIWE wyzwania, spróbuj przenieść tę grę na komputery przy użyciu biblioteki pygame. Jeśli chodzi o możliwości rozbudowy tej gry, to jest ich wiele. Można spróbować zoptymalizować wydajność, rysując tylko tę część kanwy, na której coś się dzieje. Można zastąpić kamień, nożyce i papier specjalnymi ruchami ludzika, np. kopnięciami, uderzeniami ręką, ognistymi kulami itp. Można też dodać możliwość skakania i blokowania ataków przeciwnika. Od strony interfejsu natomiast można np. dodać paski zdrowia, które są o wiele atrakcyjniejsze od zwykłych liczb, oraz stoper, aby rozgrywka odbywała się na czas.

158  Rozdział 6. BIJATYKI

7 Strzelanka

Niewykluczone, że strzelanki powstały z inspiracji flipperami, w których także używa się jakiegoś przedmiotu do strzelania w inne przedmioty. Jednak mimo zdobień zarówno wewnątrz, jak i na zewnątrz maszyny do flippera, gra pozostaje grą. Podczas gdy w grze we flippera można usłyszeć okrzyki typu „uderz kulką w zderzak”, w strzelance przeważają okrzyki typu „wystrzel rakietę w kierunku samolotu wroga”. Ta różnica może się wydawać drobna, ale dzięki niej można stworzyć coś niepowtarzalnego. Wrogowie stają się liczbami, a różne rodzaje broni funkcjami matematycznymi.

160  Rozdział 7. STRZELANKA

Trochę podstawowych informacji o renderowaniu Biblioteka gameQuery różni się od wszystkich pozostałych pod dwoma względami. Po pierwsze, działa w oparciu o manipulowanie elementami DOM biblioteki jQuery. A po drugie, gameQuery opiera się na strukturze DOM, a nie na kanwie, co jest konsekwencją pierwszej cechy. Wymienione cechy architektury mają wpływ na wewnętrzną budowę silnika oraz sposób, w jaki pracuje z nim programista. Ma to związek z tym, że kanwa zna tylko piksele. Każemy jej coś narysować, a kanwa posłusznie rysuje kształty, linie i obrazy. A potem wszystko to zapomina. Kanwa nie zapamiętuje niczego, co robiła. Trzeba zapisywać referencje do wszystkich obiektów, aby móc je narysować więcej razy. Można zmienić albo zapisać w buforze wybrane fragmenty kanwy, ale jeśli obejrzy się jej obiekt w strukturze DOM, to można się dowiedzieć, że nie wie ona nic na swój temat. Jest opakowana w interfejsie JavaScript do kontekstu kanwy. Na zwykłej stronie HTML (albo w grze opartej na DOM) wszystkie elementy znają swoje style, jak położenie i kolor tła, oraz „wiedzą”, co zawierają, tekst czy inne elementy. Jeśli zastosuje się pozycjonowanie bezwzględne, można te elementy umieścić, gdzie się chce. Ale czy nowoczesnych gier nie tworzy się na kanwie? Gry internetowe tworzy się na wiele sposobów. Aktualnie modne jest renderowanie na kanwie, ale w zależności od wymagań planowanej gry i tego, w jakich przeglądarkach powinna działać, należy wziąć pod uwagę kilka możliwości: kanwa z trójwymiarowym kontekstem (WebGL), kanwa z dwuwymiarowym kontekstem, DOM, CSS3 oraz SVG. Czasami trzeba też użyć kilku technologii naraz. Dlatego warto znać wady i zalety każdej z nich (tabela 7.1) oraz pamiętać, aby po pojawieniu się nowej wersji którejkolwiek z przeglądarek internetowych powtórnie przeprowadzić wszystkie testy. Wszystko tu zmienia się bardzo szybko. WSKAZÓWKA Na temat tego, kiedy i dlaczego powinno się używać określonych technologii, ciągle toczą się dyskusje. Jednak najmądrzejszy jest ten, kto zamiast tracić czas na jałowe sprzeczki, po prostu działa i gromadzi wiedzę. Jeśli chodzi o przeglądarkowe technologie renderowania, przykłady tego typu zachowań można znaleźć w takich projektach jak silnik gier Quintus Pascala Rettiga udostępniający API do pracy z DOM, kanwą i SVG oraz domvas Paula Bakausa, który umożliwia renderowanie dowolnego fragmentu drzewa DOM na kanwie przy użyciu obrazu SVG jako kanału.

Receptura. Wstęp do gameQuery Przepraszam za wcześniejszą dygresję. Przecież nie kupiłeś tej książki, aby czytać o renderowaniu w przeglądarkach. Chcesz tworzyć gry, prawda? W takim razie bierzemy się do pracy. Utworzymy kosmiczną strzelankę z widokiem z boku, która będzie miała jedną wyjątkową cechę: zamiast statków kosmicznych i pocisków użyjemy liczb i funkcji. Na listingu 7.1 jest pokazany potrzebny kod HTML. Listing 7.1. Początkowy kod HTML Strzelanka

Receptura. Wstęp do gameQuery  161

Tabela 7.1. Technologie renderowania, które można wykorzystać w grach Technologia

Zalety

Wady

Kanwa z kontekstem dwuwymiarowym

Pikselowy interfejs z prostą siatką. Duża szybkość działania w porównaniu z DOM w renderowaniu dużej liczby elementów

Trzeba pod każdym względem zajmować się wszystkimi utworzonymi obiektami

Kanwa z kontekstem trójwymiarowym

Jest trójwymiarowa! Trochę prostszy (ale to nie znaczy, że prosty) interfejs do pracy z WebGL udostępnia też biblioteka three.js

Jest skomplikowana! Trzeba mieć wiedzę na temat obsługi kamer, tworzenia programów cieniujących, wielokątów, tekstur oraz znać trochę matematyki i fizyki

SVG (Scalable Vector Graphics)

Pozwala na bezproblemowe przewijanie oraz nie ma utraty jakości obrazu przy powiększaniu (dobre do tworzenia map). Obrazy można tworzyć programowo przy użyciu JavaScriptu (np. Raphael.js) albo edytorów grafiki, takich jak svg-edit czy Inkscape. Dostępność spójnego interfejsu do sprite’ów bez względu na etap cyklu renderowania

Niezbyt dobra wydajność przy dynamicznym tworzeniu i usuwaniu wielu obiektów (nie nadaje się do tworzenia gier z ogromną liczbą wrogów do zniszczenia naraz). Brak obsługi w przeglądarce IE do wersji 8 (za pomocą narzędzia Rafael.js można utworzyć analogiczne pliki VML)

DOM (Document Object Model)

Łatwe manipulowanie obiektami DOM przy użyciu różnych API i bibliotek JavaScript, takich jak jQuery. Mimo że są potencjalnie bardziej dynamiczne, obiekty gry przypominają inne elementy strony internetowej, a więc wystarczy znać tylko jeden interfejs. Przy użyciu własności z-index można łatwo kontrolować nakładanie się na siebie elementów

Podobne problemy z wydajnością przy dużej liczbie obiektów jak w SVG. Złożoność układów i problemy ze spójnością w różnych przeglądarkach. Domyślne style elementów (np. dopełnienie i marginesy), jeśli nie zastosuje się resetu CSS

CSS (Cascading Style Sheets)

Szybsze działanie niż JavaScript w przypadku prostych zmian. Względnie niewielkie API udostępniające interfejs do definiowania stylów. Przeglądarka automatycznie decyduje, kiedy zastosować zmiany, więc nie trzeba zajmować się takimi kwestiami jak szybkość zmiany klatek. Renderowanie (przynajmniej teoretycznie i dosłownie, jeśli przekształcenia CSS3 są przyspieszane sprzętowo i przerzucone na GPU) może być wykonywane poza pętlą gry JavaScript

Ograniczenie praktycznie tylko do efektów wizualnych, przez co i tak trzeba używać JavaScriptu. Występują problemy z domyślnymi stylami elementów i brakiem spójności w interpretacji arkuszy stylów przez przeglądarki. Utrudnione wielokrotne wykorzystanie kodu bez sprytnego użycia klas i identyfikatorów oraz narzędzi typu SASS i LESS umożliwiających stosowanie przestrzeni nazw i zmiennych



162  Rozdział 7. STRZELANKA

Nie ma tu nic niezwykłego. Ładujemy pliki skryptów i definiujemy tytuł. A do czego służy element div znajdujący się na początku elementu body? Ma on identyfikator background, i to właśnie do niego będą dodawane wszystkie elementy DOM. W niektórych z wcześniej opisanych gier podobną rolę kontenera pełnił element canvas. I podobnie jak w przypadku renderowania na kanwie, większość kodu gry będzie napisana nie w HTML-u, ale w JavaScripcie. Utwórz więc plik game.js i wpisz w nim kod widoczny na listingu 7.2. Listing 7.2. Podstawowe elementy gry var var var var var

PLAYGROUND_WIDTH = 700; PLAYGROUND_HEIGHT = 250; REFRESH_RATE = 15; farParallaxSpeed = 1; closeParallaxSpeed = 3;

var background1 = new $.gQ.Animation({imageURL: "background1.png"}); var background2 = new $.gQ.Animation({imageURL: "background2.png"}); var background3 = new $.gQ.Animation({imageURL: "background3.png"}); var background4 = new $.gQ.Animation({imageURL: "background4.png"}); $("#playground").playground({height: PLAYGROUND_HEIGHT, width: PLAYGROUND_WIDTH, keyTracker: true}); $.playground().addGroup("background", {width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT}) .addSprite("background1", {animation: background1, width: PLAYGROUND_ WIDTH, height: PLAYGROUND_HEIGHT}) .addSprite("background2", {animation: background2, width: PLAYGROUND_ WIDTH, height: PLAYGROUND_HEIGHT, posx: PLAYGROUND_WIDTH}) .addSprite("background3", {animation: background3, width: PLAYGROUND_ WIDTH, height: PLAYGROUND_HEIGHT}) .addSprite("background4", {animation: background4, width: PLAYGROUND_ WIDTH, height: PLAYGROUND_HEIGHT, posx: PLAYGROUND_WIDTH}) $.playground().registerCallback(function(){ $("#background1").x(($("#background1").x() - farParallaxSpeed PLAYGROUND_WIDTH) % (-2 * PLAYGROUND_WIDTH) + PLAYGROUND_WIDTH); $("#background2").x(($("#background2").x() - farParallaxSpeed PLAYGROUND_WIDTH) % (-2 * PLAYGROUND_WIDTH) + PLAYGROUND_WIDTH); $("#background3").x(($("#background3").x() - closeParallaxSpeed PLAYGROUND_WIDTH) % (-2 * PLAYGROUND_WIDTH) + PLAYGROUND_WIDTH); $("#background4").x(($("#background4").x() - closeParallaxSpeed PLAYGROUND_WIDTH) % (-2 * PLAYGROUND_WIDTH) + PLAYGROUND_WIDTH); }, REFRESH_RATE); $.playground().startGame();

Najpierw ustawiliśmy wysokość i szerokość planszy (playground) na wartości odpowiadające wymiarom obrazów tła. Następnie określiliśmy częstotliwość odświeżania, czyli liczbę milisekund między każdym odświeżeniem głównej pętli gry. Zmienne ze słowem parallax określają szyb-

Receptura. Dodawanie wrogów  163

kość przewijania teł. Następnie przypisujemy tła do zmiennych, jakby były animacjami, mimo że na tym etapie nie będzie jeszcze żadnej animacji. Gdybyśmy chcieli zastosować prawdziwe animacje zamiast przewijanych teł, to opisalibyśmy je właśnie w tych deklaracjach zmiennych. Dalej inicjujemy planszę jako element z identyfikatorem playground. Po jej zainicjowaniu tworzymy nową warstwę i dodajemy do niej sprite’y za pomocą metod addGroup i addSprite. W kolejnym bloku kodu za pomocą funkcji registerCallbacks aktualizujemy pozycje przewijanych teł. Polega to na uruchamianiu pętli co 15 milisekund. Aby określić szybkość odświeżania, należy podzielić liczbę milisekund na klatkę przez 1000, co daje wynik 0,015 sekundy na klatkę. Następnie obliczamy odwrotność tej liczby, czyli dzielimy 1 przez 0,015, otrzymując liczbę 66,667 klatek na sekundę. UWAGA W interfejsie playground gameQuery wykorzystuje metody pobierające i ustawiające w stylu biblioteki jQuery i dlatego są dwie funkcje o nazwie playground. W tym przypadku, podobnie jak w jQuery (np. funkcja text() jQuery), metoda pobierająca jest wersją nieprzyjmującą parametrów (playground()), a metoda ustawiająca przyjmuje parametry — playground( {height: PLAYGROUND_HEIGHT, width: PLAYGROUND_WIDTH, keyTracker: true}).

Pozostało już tylko uruchomić funkcję startGame na elemencie playground, aby wyświetlić kosmiczną scenerię zawierającą matematyczne symbole zamiast gwiazd. Jeśli otworzysz w przeglądarce internetowej plik index.html, zobaczysz widok pokazany na rysunku 7.1. Większe, czyli znajdujące się bliżej nas, symbole poruszają się szybciej, a mniejsze, znajdujące się dalej, poruszają się wolniej.

Rysunek 7.1. Przestrzeń matematyczna

Receptura. Dodawanie wrogów Takie niesynchroniczne tło doskonale nadaje się do zastosowania w tego typu strzelance, ale mało który gracz zadowoli się tylko jego oglądaniem. Dlatego dodamy do gry trochę wrogo nastawionych obiektów, aby ją nieco urozmaicić. Zaczniemy od zdefiniowania konstruktora wrogów i kilku zmiennych. Odpowiedni kod jest pokazany na listingu 7.3. Można go wpisać na początku pliku game.js.

164  Rozdział 7. STRZELANKA Listing 7.3. Konstruktor Enemy i nowe zmienne var closeParallaxSpeed = 3; var enemyHeight = 30; var enemyWidth = 60; var enemySpawnRate = 1000; function Enemy(node, value){ this.value = value; this.speed = 5; this.node = node; this.update = function(){ this.node.x(-this.speed, true); }; };

Pod zmiennymi dodanymi do pliku z listingu 7.2 dopisaliśmy zmienne enemyHeight i enemyWidth. Następna jest zmienna enemySpawnRate określająca, co ile milisekund mają się pojawiać nowi wrogowie. Za nią znajduje się definicja konstruktora Enemy. Zmienna value służy do przechowywania liczby przyjętych ciosów, czyli określa liczbę „żyć”. Zmienna speed określa szybkość poruszania się wroga. Własność node przechowuje element div (opakowany w obiekt jQuery) zawierający dane DOM dotyczące wroga (enemy). Funkcja update zmniejsza wartość speed (przesuwa węzeł w lewo). Pamiętaj, że funkcje pozycjonujące gameQuery, takie jak x, bez drugiego parametru true działają całkiem inaczej. Szczegółowe informacje na ten temat znajdują się w dokumentacji biblioteki pod adresem gamequeryjs.com/documentation/api/. Następnie dodamy drugą warstwę dla wrogów, która będzie umiejscowiona na obrazach tła. Odpowiedni kod jest oznaczony pogrubieniem na listingu 7.4. Listing 7.4. Dodanie nowej warstwy .addSprite("background4", {animation: background4, width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT, posx: PLAYGROUND_WIDTH}) .end() .addGroup("enemies", {width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT}) $.playground().registerCallback(function(){ ... $("#background4").x(($("#background4").x() - closeParallaxSpeed - PLAYGROUND_WIDTH) % (-2 * PLAYGROUND_WIDTH) + PLAYGROUND_WIDTH); $(".enemy").each(function(){ this.enemy.update(); if(($(this).x()+ enemyWidth) < 0){ $(this).remove(); } }); }, REFRESH_RATE);

W pierwszym pogrubionym wierszu dodajemy do planszy nową grupę (warstwę) o nazwie enemies. Funkcja end sprawia, że warstwa zostaje dodana do warstwy bazowej planszy, a nie do warstwy tła. Krótko mówiąc, oznacza to dodanie elementu div, który później będzie zawierał sprite’y. Inaczej niż w przypadku warstwy background, sprite’y wrogów będą dodawane automatycznie i dlatego nie dodajemy ich tutaj. W pozostałych pogrubionych wierszach na tym listingu (znaj-

Receptura. Dodawanie wrogów  165

dujących się w głównej pętli gry) przeglądamy wrogów za pomocą pętli i aktualizujemy ich pozycje przy użyciu funkcji update. Sprawdzamy też, czy obiekt nie znalazł się za lewą krawędzią ekranu, i jeśli tak, to usuwamy go z drzewa DOM. UWAGA Może zastanawiasz się, na czym są wywoływane metody addGroup i addSprite. Zastosowana w ich przypadku technika to łączenie wywołań metod w łańcuchy. Wykorzystuje się w niej fakt, że każda z metod zwraca obiekt jQuery, dzięki czemu na jej wartości zwrotnej można wywołać kolejną metodę. Można to porównać do wykonywania wielu działań dodawania w jednej linii: 3+2+6. Należy jednak pamiętać, że mimo iż w wyniku działania metody jest otrzymywany obiekt jQuery, to nie zawsze musi to być dokładnie ten sam obiekt. Jeśli używasz funkcji find albo addGroup, to aby ponownie odwołać się do oryginalnego obiektu, należy najpierw wywołać funkcję end. W jakim celu stosuje się takie rozwiązanie? Śledzenie elementów DOM jest czasochłonne, a zapisywanie wszystkich mogących się przydać wyników w zmiennych spowodowałoby duży bałagan w kodzie. A opisana technika modyfikowania obiektów jQuery jest wygodna i szybka. Więcej informacji na temat przeglądania elementów w jQuery można znaleźć w dokumentacji na stronie http://api.jquery.com/category/traversing/.

Teraz musimy zarejestrować inną funkcję zwrotną dla wolniejszej pętli dodającej wrogów. Kod przedstawiony na listingu 7.5 należy wpisać pod kodem z listingu 7.4. Listing 7.5. Dynamiczne dodawanie wrogów $.playground().registerCallback(function(){ var enemyValue = Math.ceil(Math.random()*21) - 11; var name = "enemy_"+(new Date).getTime(); $("#enemies").addSprite(name, {animation: '', posx: PLAYGROUND_WIDTH,posy: Math.random()*PLAYGROUND_HEIGHT*0.9,width: enemyWidth, height: enemyHeight}); var enemyElement = $("#"+name); enemyElement.addClass("enemy"); enemyElement[0].enemy = new Enemy(enemyElement, enemyValue); enemyElement.text(enemyValue); }, enemySpawnRate);

Najważniejsze w tym kodzie są pierwszy i ostatni wiersz. Deklarujemy pętlę dodającą wrogów z prędkością co 1000 milisekund wg zmiennej enemySpawnRate. Łatwe tworzenie takich niezależnych pętli jest jedną z dużych zalet opisywanej biblioteki i warto o tym pamiętać. W pętli najpierw ustawiamy zmienną enemyValue na wartość z przedziału od –10 do 10. Następnie tworzymy wrogowi niepowtarzalną nazwę zawierającą liczbę milisekund, które upłynęły od pierwszej sekundy roku 1970 (wybór akurat tego roku może się wydawać dziwny, ale jest to tzw. czas Uniksa mierzony jako liczba milisekund, które upłynęły od 1 stycznia 1970 r.). Później dodajemy sprite’a enemy do warstwy enemies. W pierwszym parametrze przekazujemy nazwę (name), która będzie identyfikatorem elementu. Animację można ustawić na pusty łańcuch,

166  Rozdział 7. STRZELANKA ponieważ nie będziemy używać żadnego obrazu. Pozycję początkową elementu na osi x ustawiamy na prawą krawędź ekranu, a na osi y na losową wartość (wartość 0.9 uniemożliwia pojawienie się wroga zbyt nisko i wyświetlenie tylko jego górnej części). Szerokość i wysokość ustawiamy na enemyWidth i enemyHeight. Później dodajemy klasę enemy. Jako że selektory jQuery zawsze zwracają obiekty jako tablice, aby dodać atrybut, należy konkretnie określić pierwszy element przy użyciu indeksu [0]. Atrybut ten przypisujemy do nowego obiektu Enemy, który inicjujemy przy użyciu węzła wybranego przez jQuery i wcześniej wylosowanej wartości. Zwróć uwagę, że działamy tu trochę chaotycznie w porównaniu z tym, jak byśmy działali, korzystając z biblioteki używającej elementu canvas. Zamiast zapisać wszystkie informacje dotyczące pozycji w obiekcie JavaScript, robimy to w obiekcie DOM, do którego odwołujemy się poprzez obiekt JavaScript. W zależności od punktu widzenia można to uważać za wygodniejsze lub mniej wygodne podejście. Niezależnie od tego warto na to zwrócić uwagę. Ostatnia część tego listingu ustawia wartość wroga na jego węzeł tekstowy przy użyciu funkcji text biblioteki jQuery. Aby móc zdefiniować style, trzeba dokonać jeszcze dwóch drobnych zmian. Po pierwsze, należy utworzyć plik o nazwie game.js i wpisać w nim kod przedstawiony na listingu 7.6. Po drugie, należy dodać ten wiersz kodu w części head pliku index.html: . Listing 7.6. Style dla wrogich kosmicznych statków .enemy{ color:red; background-color: black; font-size:22px; border:1px solid red; text-align:center; };

Jeśli wszystko poszło zgodnie z planem, na ekranie powinieneś widzieć planszę podobną do pokazanej na rysunku 7.2.

Rysunek 7.2. Wrogie obiekty z numerami

Receptura. Tworzenie pojazdu  167

Receptura. Tworzenie pojazdu Mamy już wrogów, ale nie mamy jak z nimi walczyć. Tworzenie własnego statku kosmicznego rozpoczniemy od zdefiniowania kilku zmiennych pokazanych na listingu 7.7. Można je wpisać pod ostatnim wierszem funkcji Enemy. Listing 7.7. Zmienne statku kosmicznego gracza var playerHeight = 60; var playerWidth = 120; function Player(){ this.value = 10; this.number = 1; };

Kod ten niczym szczególnym się nie wyróżnia. Zadeklarowaliśmy w nim wysokość, szerokość oraz dwie inne domyślne własności konstruktora Player. WSKAZÓWKA Atrybuty width i height można też umieścić w funkcji Player, ale zostawiłem je na zewnątrz, bo ich wartości nie będą się zmieniać. Dzięki temu nie będzie trzeba później dokopywać się do nich w obiekcie. Projektując architekturę swojego kodu, sami decydujemy, jak skomplikowane mają być nasze obiekty i jaki ma być zakres dostępności różnych zmiennych. Niektóre wartości powinny być głęboko ukryte, a inne lepiej, żeby były łatwo dostępne i żeby nie trzeba było wykonywać skomplikowanych operacji, aby je wydobyć.

Kolejną czynnością jest dodanie warstwy i sprite’a do planszy — pogrubiony kod na listingu 7.8. Listing 7.8. Dodanie gracza do planszy .addGroup("enemies", {width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT}) .end() .addGroup("player", {posx: 0, posy: PLAYGROUND_HEIGHT/2, width: playerWidth, height: playerHeight}) .addSprite("playerBody",{animation: '', posx: 0, posy: 0, width: playerWidth, height: playerHeight}) $("#player")[0].player = new Player(); $("#playerBody").html(""+$("#player")[0].player.value+"
"+$("#player")[0].player.number+""); $.playground().registerCallback(function(){

Najpierw wywołujemy funkcję end(), aby warstwa player, którą zamierzamy dodać, nie została zagnieżdżona w warstwie enemies. Początkowe położenie warstwy player zostało ustawione w pobliżu połowy wysokości ekranu przy lewej krawędzi. Po ustawieniu pozycji dodajemy sprite’a i, podobnie jak w przypadku wrogów, jako animację podajemy pusty łańcuch, ponieważ nie planujemy używać żadnego sprite’a ani animacji.

168  Rozdział 7. STRZELANKA Pewnie zauważyłeś, że warstwa player jest nieco bardziej specyficzna niż warstwa wrogów. W dalszej części rozdziału będziemy używać głównie elementu player, a nie playerBody. Następnie inicjujemy nowy obiekt player i przypisujemy go do atrybutu wskazywanego przez pierwszy element wyniku selektora jQuery #player. Ostatnia część listingu dotyczy budowy kodu HTML pozwalającego wizualnie sformatować element playerBody i znajdujący się w nim tekst. Nie ma w tym nic specjalnie ciekawego. Trzeba teraz jednak dodać do pliku game.css parę własności CSS. Pokazano je na listingu 7.9. Listing 7.9. Style statku kosmicznego .enemy{ color:red; background-color: black; font-size:24px; border:1px solid red; text-align:center; } #player{ color:white; background-color:black; font-size:24px; border: 1px solid white; text-align:center; }

Jeśli teraz uruchomimy grę, to zauważymy, że nasz pojazd jest już widoczny, ale nie da się nim sterować. Można to zmienić, dodając kod pokazany na listingu 7.10. Jako że obsługujemy w nim dane wejściowe, powinien on działać jak najszybciej. Dodaj ten kod na końcu funkcji zwrotnej wykorzystującej REFRESH_RATE, aby był często wywoływany. Listing 7.10. Kod sterowania pojazdem $.playground().registerCallback(function(){ ... if(jQuery.gameQuery.keyTracker[37]){ var nextpos = $("#player").x()-5; if(nextpos > 0){ $("#player").x(nextpos); } } if(jQuery.gameQuery.keyTracker[39]){ var nextpos = $("#player").x()+5; if(nextpos < PLAYGROUND_WIDTH - playerWidth){ $("#player").x(nextpos); } } if(jQuery.gameQuery.keyTracker[38]){ var nextpos = $("#player").y()-5; if(nextpos > 0){ $("#player").y(nextpos); } } if(jQuery.gameQuery.keyTracker[40]){ var nextpos = $("#player").y()+5; if(nextpos < PLAYGROUND_HEIGHT - playerHeight){

Receptura. Kolizje z wrogami  169

$("#player").y(nextpos); } } }, REFRESH_RATE);

Kod ten należy dodać nad ostatnim wierszem głównej pętli gry. W bibliotece gameQuery nie ma żadnych wymyślnych dodatków do mapowania klawiszy i dlatego trzeba samodzielnie dowiedzieć się, jaki numer odpowiada któremu klawiszowi. Liczby 37 – 40 to numery strzałek, w związku z czym zdefiniowane w tym kodzie procedury dotyczą kolejno obsługi strzałki w lewo, prawo, w górę oraz w dół. W każdym bloku znajduje się kod utrzymujący statek na ekranie. Listę numerów wszystkich klawiszy można znaleźć, wpisując w wyszukiwarce frazę „JavaScript keycodes”. Aktualnie najlepszym źródłem informacji na temat kodów klawiszy jest strona https://developer. mozilla.org/en-US/docs/DOM/KeyboardEvent#Key_location_constants. Po dodaniu tego kodu powinno dać się sterować statkiem kosmicznym, jak widać na rysunku 7.3.

Rysunek 7.3. Sterowalny statek kosmiczny

Receptura. Kolizje z wrogami Mając pojazd kosmiczny gracza i pojazdy wrogów, możemy zająć się obsługą ich interakcji, czyli określić, co się stanie, gdy się ze sobą zetkną. Wrogów potraktujemy jak przedmioty do zbierania, których dotknięcie powoduje zwiększenie licznika. Skoro jednak obiekty te nazwaliśmy wrogami, to można się spodziewać, że ich dotknięcie będzie powodować uszkodzenie naszego statku. To już jest kwestia filozoficzna. Później będziemy do nich strzelać, więc nie powinniśmy się niepotrzebnie zaprzyjaźniać. Zastąp kod aktualizacji wroga (w pętli głównej) kodem obsługi kolizji przedstawionym na listingu 7.11. Listing 7.11. Obsługa kolizji $(".enemy").each(function(){ this.enemy.update(); if(($(this).x() + enemyWidth) < 0){ $(this).remove(); } else { var collided = $(this).collision("#playerBody,."+$.gQ.groupCssClass); if(collided.length > 0){ $("#player")[0].player.value += $(this)[0].enemy.value; $("#player")[0].player.number = $(this)[0].enemy.value;

170  Rozdział 7. STRZELANKA $("#player .value").html($("#player")[0].player.value); $("#player .number").html($("#player")[0].player.number); $(this).remove(); } } });

Teraz zetknięcie z wrogim statkiem powoduje trzy zdarzenia. Najpierw do atrybutu value gracza zostaje dodany atrybut value wroga oraz następuje aktualizacja odpowiedniej części kodu HTML. W ten sposób gracz zdobywa punkty. Po drugie, atrybut number wroga zostaje przypisany do atrybutu number gracza i następuje aktualizacja kodu HTML. Będzie to amunicja gracza. Po trzecie, statek wroga zostaje usunięty z planszy.

Receptura. Strzelanie Mamy statki i amunicję. Aby gra była strzelanką, musimy jeszcze dodać możliwość strzelania. W tym celu najpierw dodamy kolejną globalną zmienną dla prędkości pocisku — pogrubiony wiersz kodu na listingu 7.12. Listing 7.12. Prędkość pocisku function Player(){ this.speed = 5; this.value = 10; this.number = 1; }; var missileSpeed = 10;

Musimy też dodać kolejną warstwę w kodzie inicjacji warstw, jak widać na listingu 7.13. Listing 7.13. Warstwa pocisku .addSprite("playerBody",{animation: '', posx: 0, posy: 0, width: playerWidth, height: playerHeight}) .end() .addGroup("playerMissileLayer",{width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT}).end()

Kod dodający pociski jest przedstawiony na listingu 7.15. Ale zanim do niego dojdziemy, dodamy kod widoczny na listingu 7.14 obsługujący kolizje pocisków ze statkami wroga. Należy go wpisać za kodem obsługi kolizji z wrogami i przed kodem obsługi klawiszy w funkcji, na której końcu znajduje się REFRESH_RATE. Listing 7.14. Wykrywanie kolizji z pociskami $(".enemy").each(function(){ ... }); $(".playerMissiles").each(function(){ var posx = $(this).x(); if(posx > PLAYGROUND_WIDTH){

Receptura. Strzelanie  171

$(this).remove(); }else{ $(this).x(missileSpeed, true); var collided = $(this).collision(".enemy,."+$.gQ.groupCssClass); if(collided.length > 0){ collided.each(function(){ var possible_value = $(this)[0].enemy.value + $('#player')[0].player.number; if(possible_value < 10000 && possible_value > -10000){ var thisEnemy = $(this)[0].enemy; thisEnemy.value = possible_value; $(thisEnemy.node[0]).text(thisEnemy.value) }; }) $(this).remove(); }; }; });

Najpierw sprawdzamy, czy pocisk minął wszystkie wrogie statki i wyleciał za prawą krawędź ekranu. Jeśli tak, to można go usunąć. Jeśli pocisk wciąż jest na ekranie, to przesuwamy go w prawo. Następnie sprawdzamy, czy jakiś pocisk trafił w statek wroga. Przeglądamy za pomocą pętli te, które trafiły, i dodajemy wartość każdego pocisku do wartości statku wroga, ale z jednym wyjątkiem. Jeśli wartość statku po dodaniu do niego wartości wroga byłaby mniejsza niż –10 000 lub większa niż 10 000, to nie zmieniamy jej. Na koniec usuwamy pocisk, który uległ kolizji. Teraz musimy dodać kod generujący pociski (uruchamiany za pomocą spacji). Można by było umieścić go w głównej pętli aktualizującej, ale wówczas reakcja programu na naciśnięcia spacji byłaby zbyt wolna. Ponadto w pętli tej nie powinno być niczego, co nie jest absolutnie niezbędne, ponieważ to powodowałoby spowolnienie gry. Potrzebny kod jest przedstawiony na listingu 7.15. Można go wpisać na dole pliku nad wywołaniem funkcji startGame. Listing 7.15. Tworzenie pocisków $(document).keydown(function(e){ if(e.keyCode === 32){ var playerposx = $("#player").x(); var playerposy = $("#player").y(); var name = "playerMissile_"+(new Date()).getTime(); $("#playerMissileLayer").addSprite(name, {posx: playerposx + playerWidth, posy: playerposy, width: playerWidth/2,height: playerHeight/2}); $("#"+name).addClass("playerMissiles, player"); $("#"+name).html("
"+$("#player")[0].player.number+"
"); } }); $.playground().startGame();

Najpierw za pomocą funkcji jQuery (nie gameQuery) keydown sprawdzamy, czy została naciśnięta spacja (klawisz o numerze 32). Zapisujemy położenie statku, aby wykorzystać tę informację później, a następnie generujemy nazwę name (przy użyciu instrukcji (new Date()).getTime()), która zostaje użyta jako identyfikator sprite’a. Szerokość i wysokość ustawiamy na połowę wartości statku gracza, a pozycję na jego prawą krawędź. Dodaliśmy też klasę playerMissiles potrzebną w kodzie przedstawionym na listingu 7.14, aby było co przeglądać za pomocą pętli w celu wykrycia kolizji. W ostatnim wierszu dodajemy kod HTML. Jak się pewnie domyślasz, dla przycisków

172  Rozdział 7. STRZELANKA trzeba jeszcze zdefiniować style. W tym celu kod pokazany na listingu 7.16 możesz dodać na końcu pliku game.css. W podobny sposób sformatujemy klasę number, ponieważ liczby są pociskami załadowanymi i gotowymi do wystrzału. Listing 7.16. Formatowanie pocisków .playerMissiles{ text-align:center; border: solid 1px green; font-size:24px; color:green; background-color:black; } .number{ color:green; }

Receptura. Uzupełnianie mocy Zdefiniowane w poprzedniej recepturze operacje dodawania i odejmowania może nie wprawiły Cię w zachwyt, ale to nie szkodzi. W tej recepturze dodamy bardziej złożone procedury obsługi zdarzeń zetknięcia wrogiego statku z pociskiem. Dodaj pod zmienną missileSpeed dwie kolejne zmienne, jak pokazano na listingu 7.17. Listing 7.17. Zmienne dla tablicy symboli var missileSpeed = 10; var symbols = ["+", "-", "*", "/", "%", "|", "&", "<<", ">>"]; var symbolIndex = 0;

Symbole zdefiniowane w zmiennej symbols to używane w języku JavaScript i wielu innych językach programowania operatory dodawania, odejmowania, mnożenia, dzielenia, dzielenia modulo (pobierania reszty z dzielenia), binarnego lub, binarnego i, bitowego przesunięcia w lewo oraz bitowego przesunięcia w prawo. Plan jest taki, aby „zaatakować” przy użyciu wszystkich tych funkcji, a nie tylko dodawania. Kolejną czynnością jest zamienienie pierwotnego kodu HTML elementu playerBody na nieco dłuższy kod przedstawiony na listingu 7.18. Nowy kod jest wyróżniony pogrubieniem. Listing 7.18. Dodanie symbolu do elementu playerBody $("#player")[0].player = new Player(); $("#playerBody").html(""+$("#player")[0].player.value+"
("+symbols[symbolIndex]+") "+$("#player")[0].player.number+"");

W tym kodzie po prostu dodaliśmy symbol w nawiasie. Teraz musimy zmienić kod wykrywania kolizji z pociskami, aby nie tylko dodawał wartości, ale również wykonywał działania wskazywane przez symbole. Odpowiednie poprawki są przedstawione na listingu 7.19.

Receptura. Uzupełnianie mocy  173

Listing 7.19. Udoskonalenie obsługi kolizji z pociskami $(".playerMissiles").each(function(){ var posx = $(this).x(); ... collided.each(function(){ var possible_value = Math.round(eval($(this)[0].enemy.value + " " + symbols[symbolIndex] + " " + $('#player')[0].player.number)); if(possible_value < 10000 && possible_value > -10000){ ... }; });

Zmienił się tylko jeden wiersz kodu. Teraz wartość jest przepuszczana przez funkcję Math.round, dzięki czemu nie powstaną bardzo małe ułamki. Ponadto cały wiersz jest umieszczony w funkcji eval, która wykonuje przekazany jej łańcuch jako kod JavaScript. Moglibyśmy dla każdego symbolu utworzyć po funkcji i przekazywać tym funkcjom każdy z argumentów działania, ale na nasze potrzeby w tym przypadku wystarczy funkcja eval. OSTRZEŻENIE NIGDY NIE UŻYWAJ FUNKCJI EVAL! Tak przynajmniej się mówi. Jest kilka dobrych powodów, aby posłuchać tej rady. Jeśli chodzi o strukturę kodu, łączenie w jeden łańcuch kilku zmiennych i łańcuchów powoduje powstanie trudnych w utrzymaniu i diagnozowaniu ciągów. Ponadto funkcja eval niekorzystnie wpływa na wydajność programu, ponieważ powoduje wywoływanie interpretera w sposób uniemożliwiający wykorzystanie wbudowanych w przeglądarki mechanizmów optymalizacyjnych. Jednak najgorsze jest często opisywane narażenie aplikacji na ataki. Jakiego rodzaju niebezpieczeństwa należy się spodziewać? Głównie ataków typu XSS (ang. cross-site scripting), które łatwo jest przeprowadzić, gdy aplikacja generuje instrukcje do wykonania z łańcuchów przy użyciu adresu URL, danych z formularza albo z innych danych wysyłanych przez użytkownika.

No dobrze, nie czuję się z tym komfortowo. Jeśli nie chcesz używać funkcji eval, to zamiast wcześniej pokazanego kodu użyj kodu przedstawionego na listingu 7.20. Wklej funkcję function Eval w pobliżu początku pliku (pod deklaracją zmiennej symbolIndex) i wywołaj ją w wierszu przypisania zmiennej possible_value, w którym wcześniej znajdowało się wywołanie funkcji eval. Listing 7.20. Kod pozbawiony funkcji eval var symbols = ["+", "-", "*", "/", "%", "|", "&", "<<", ">>"]; var symbolIndex = 0; function functionEval(enemyValue, symbol, missileValue){ switch(symbol){ case "+": return enemyValue + missileValue; case "-": return enemyValue - missileValue; case "*": return enemyValue * missileValue;

174  Rozdział 7. STRZELANKA case "/": return enemyValue case "%": return enemyValue case "|": return enemyValue case "&": return enemyValue case "<<": return enemyValue case ">>": return enemyValue };

/ missileValue; % missileValue; | missileValue; & missileValue; << missileValue; >> missileValue;

}; ... var possible_value = Math.round(functionEval($(this)[0].enemy.value, symbols[symbolIndex], $('#player')[0].player.number));

Pozostało już tylko dodać jeden fragment kodu do przeglądania możliwych funkcji. Kod ten znajduje się na listingu 7.21. Listing 7.21. Przeglądanie funkcji $(document).keydown(function(e){ if(e.keyCode === 32){ var playerposx = $("#player").x(); var playerposy = $("#player").y(); var name = "playerMissile_"+(new Date()).getTime(); $("#playerMissileLayer").addSprite(name, {posx: playerposx + playerWidth, posy: playerposy, width: playerWidth/2,height: playerHeight/2}); $("#"+name).addClass("playerMissiles"); $("#"+name).html("
"+$("#player")[0].player.number+"
"); } else if(e.keyCode === 70){ symbolIndex = (symbolIndex+1)%symbols.length; $("#player .symbol").text(symbols[symbolIndex]); }; });

Ten kod zostanie wykonany, gdy naciśniemy klawisz F. Jego działanie polega na dodawaniu 1 do zmiennej symbolIndex, która po osiągnięciu maksymalnej wartości dzięki użyciu operatora dzielenia modulo wraca do wartości 0. (Jeśli trudno Ci zrozumieć, jak to działa, pograj trochę w grę i wszystko powinno stać się jasne). Na koniec następuje aktualizacja kodu HTML gracza, aby pokazać aktualnie wybrany symbol. Teraz gra powinna wyglądać podobnie jak na rysunku 7.4.

Podsumowanie W tym rozdziale została opisana budowa matematycznej strzelanki w oparciu o silnik gameQuery działający na strukturze DOM. Poznaliśmy kilka technik renderowania w przeglądarce (kanwa trójwymiarowa, kanwa dwuwymiarowa, SVG, DOM oraz CSS3) oraz dowiedzieliśmy się, dlaczego nikt nie lubi funkcji eval (ze względów bezpieczeństwa, wydajności i z uwagi na strukturę kodu).

Podsumowanie  175

Rysunek 7.4. Ukończona gra

W większości strzelanek jest wykonywane tylko odejmowanie. Gdy gracz strzela do wroga, liczba punktów wroga maleje, aż dochodzi do zera, co powoduje, że wróg eksploduje i znika. Podobnie dzieje się w drugą stronę. Nasza gra jest nieco bardziej elastyczna i można wykorzystać ją nawet do dokładniejszego zbadania, jak działają operatory binarne, jeśli masz taką potrzebę. Jeśli chodzi o możliwości biblioteki gameQuery, to ma ona jeszcze sporo ciekawych, a nieomówionych w tym rozdziale funkcji. Warto na przykład zapoznać się z API Sound Wrapper, animacjami, mapami kafelkowymi oraz funkcjami manipulacji sprite’ami np. do skalowania i obracania. Jeśli chodzi o możliwości rozbudowy samej gry, to nasuwa się kilka pomysłów. Wystarczy na przykład zamienić własności x i y oraz obrócić niektóre obrazy o 90 stopni, aby otrzymać grę o pionowej orientacji. Można ustalić cele do osiągnięcia, zdefiniować warunki wygrania i przegrania oraz utworzyć poziomy. W trakcie trwania rozgrywki można stopniowo zwiększać poziom trudności. Liczba punktów może służyć jako jeden z celów do osiągnięcia. Można też dodać różne rodzaje broni, które trzeba odblokować, wykonując określone zadania. Pociski z różnych typów broni mogą różnie się zachowywać i poruszać z różnymi prędkościami. To samo dotyczy wrogów. Na koniec można dodać wielkiego paskudnego bossa do przejścia. Ogólnie rzecz biorąc, w obecnym stanie gra jest bardzo abstrakcyjna i przypomina udziwnioną wersję flippera.

176  Rozdział 7. STRZELANKA

8 Gry FPS

Zanim nastało panowanie wielokątów i ustawionych pod różnymi kątami kamer w trójwymiarowych grach, prym wiodły gry tworzone przy użyciu techniki polegającej na analizie przecięć promienia światła przez obiekty na scenie (ang. ray casting). W grach typu FPS gracz ma możliwość oglądania świata oczami żołnierza, tajnego agenta albo człowieka, którego życiowym celem jest ucieczka przed żywymi trupami. Zastosowana tu technika widoku znad ramienia pozwala też utworzyć perspektywę wykorzystywaną w grach wyścigowych i first person RPG.

178  Rozdział 8. GRY FPS

Receptura. Wprowadzenie do biblioteki Jaws W poprzednim rozdziale przemyciłem elementy trójwymiarowości w postaci niesynchronicznie przewijających się teł. Ta tzw. technika parallax scrolling jest często wykorzystywana przy budowie gier dwuwymiarowych w celu imitowania głębi ruchomego świata gry. W tym rozdziale poznasz inną technikę imitacji trójwymiarowego świata o nazwie raycasting. Nie należy jej mylić ze znacznie bardziej skomplikowaną techniką tworzenia efektów trójwymiarowych o nazwie śledzenie promieni (ang. ray tracing). Posługiwanie się techniką raycastingu polega na utworzeniu dwuwymiarowej mapy i „rzucaniu” ze swojej perspektywy promienia na ściany, aby określić, w jakiej są odległości, i w odpowiedni sposób je wyrenderować. UWAGA Jedną z bardzo popularnych technik imitacji efektów trójwymiarowych jest tzw. rzutowanie izometryczne. Może pamiętasz ze szkoły (albo będziesz się uczyć, jeśli jesteś jeszcze tak młody i piękny), że aby narysować sześcian w trzech wymiarach, trzeba go przekrzywić i przesunąć kąt widoku trochę do góry. To jest właśnie perspektywa izometryczna. Techniki tej będziemy używać w rozdziale 10. podczas tworzenia gry strategicznej. Jest wiele gier utworzonych przy użyciu tej techniki. Należą do nich wczesne wersje gry Q*bert na Atari, a także Marble Madness, Snake Rattle ‘n Roll oraz Solsticefor na NES. O uniwersalności i wszechstronności tej techniki świadczą natomiast takie produkcje jak Final Fantasy Tactics na PlayStation i MMO Sim Habbo Hotel. Jeśli nie rozpoznajesz w tych grach rzutowania izometrycznego na oko, to możesz poznać, że zostało zastosowane, po tym, że naciśnięcie przycisku ruchu do góry oznacza do góry i w prawo (czy w lewo), naciśnięcie przycisku ruchu w prawo oznacza ruch w prawo i w dół itd. Miej się na baczności, jeśli zdecydujesz się zastosować tę technikę do budowy gry ze sterowaniem kierunkowym zamiast poprzez wskazywanie i klikanie.

W poprzednich rozdziałach wiele pracy wykonywała za nas sama biblioteka. Ponieważ jednak mało która biblioteka ma wbudowaną standardową obsługę raycastingu, odpowiedni kod musimy napisać samodzielnie. Skoro tak, to może zastanawiasz się, czy w ogóle jest sens używać silnika gier. Mimo trudności, jakie sprawia konieczność pisania sporej ilości kodu całkiem od podstaw, pozwalam sobie twierdzić, że i tak warto użyć biblioteki. Organizacja kodu, zarządzanie pętlą gry, wczytywanie sprite’ów oraz odbieranie danych mogą się wydawać prostymi czynnościami, ale mimo wszystko zawsze warto jest dążyć do tworzenia jak najmniej skomplikowanego kodu. Z drugiej strony możesz chcieć zlikwidować zależności swojego programu od zewnętrznych bibliotek, korzystać z bardziej wyspecjalizowanych narzędzi albo zoptymalizować kod. W czym w takim razie może pomóc biblioteka Jaws? Aby się o tym przekonać, utwórz na początek plik index.html o zawartości przedstawionej na listingu 8.1. Listing 8.1. Podstawowy plik HTML FPS

Receptura. Tworzenie mapy dwuwymiarowej  179



Nic niezwykłego tu nie ma. Wczytujemy arkusz stylów, zdefiniowaliśmy element canvas o identyfikatorze canvas, załadowaliśmy silnik gier i plik JavaScript gry oraz uruchomiliśmy silnik za pomocą funkcji start, której przekazaliśmy parametr wywołania Game. Aby dowiedzieć się, jakie jest działanie instrukcji jaws.start(Game);, utwórz plik game.js i wklej w nim kod przedstawiony na listingu 8.2. Listing 8.2. Zawartość pliku game.js var Game = function(){ this.setup = function() { alert("Uruchomiono instalatora"); }; }

Wkrótce będziemy też używać innych funkcji biblioteki Jaws, takich jak draw i update, ale na razie zwróć uwagę, jak łatwo można zainicjować grę. Zanim przejdziemy dalej, utworzymy podstawowy plik CSS z kodem przedstawionym na listingu 8.3. Listing 8.3. Zawartość pliku game.css #canvas{ position:absolute; width:600px; height:300px; z-index:-1; }

Teraz w oknie przeglądarki powinien wyświetlać się ekran ładowania gry i okienko alertu, jak widać na rysunku 8.1. Warto tu zwrócić uwagę na dwie rzeczy. Po pierwsze, biblioteka Jaws pobrała pierwszy znaleziony element canvas. Nie trzeba było w tym celu wywoływać żadnej metody ani przekazywać nigdzie identyfikatora. Po drugie, funkcja setup jest wywoływana automatycznie (i tylko raz) dzięki funkcji start.

Receptura. Tworzenie mapy dwuwymiarowej Pracę z techniką raycastingu zaczniemy od utworzenia dwuwymiarowej mapy. Na listingu 8.4 pokazano, jakie zmiany należy wprowadzić w kodzie w pliku game.js.

180  Rozdział 8. GRY FPS

Rysunek 8.1. Ładowanie gry przy użyciu Jaws Listing 8.4. Dodawanie mapy var Game = function(){ var map = [ [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], [1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,1], [1,-1,-1,-1,-1,-1,-1,0,-1,2,3,1,-1,-1,-1,1], [1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,1], [1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,1], [1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,1], [1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,1], [1,-1,3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,1], [1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,2,-1,-1,-1,1], [1,-1,-1,1,-1,-1,-1,-1,-1,3,-1,-1,-1,-1,-1,1], [1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,1], [1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,1], [1,-1,-1,-1,-1,-1,1,-1,-1,2,-1,-1,-1,-1,-1,1], [1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,1], [1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,1], [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]]; var minimap = { init: function(){ this.element = document.getElementById('minimap'); this.context = this.element.getContext("2d"); this.element.width = 300; this.element.height = 300; this.width = this.element.width; this.height = this.element.height; this.cellsAcross = map[0].length; this.cellsDown = map.length; this.cellWidth = this.width/this.cellsAcross; this.cellHeight = this.height/this.cellsDown; this.colors = ["#ffff00", "#ff00ff", "#00ffff", "#0000ff"]; this.draw = function(){ for(var y = 0; y < this.cellsDown; y++){ for(var x = 0; x < this.cellsAcross; x++){ var cell = map[y][x]; if (cell===-1){ this.context.fillStyle = "#ffffff" }else{ this.context.fillStyle = this.colors[map[y][x]];

Receptura. Tworzenie mapy dwuwymiarowej  181

}; this.context.fillRect(this.cellWidth*x, this.cellHeight*y, this.cellWidth, this.cellHeight); }; }; }; } }; this.draw = function(){ minimap.draw(); }; this.setup = function() { minimap.init(); }; }

W tym kodzie znajduje się definicja dwuwymiarowej tablicy o nazwie map. Wartość -1 reprezentuje pustą przestrzeń, a wartości 0 – 3 różne rodzaje ścian. W funkcji setup usunęliśmy instrukcję alert, bo teraz inicjujemy obiekt minimap przy użyciu funkcji init(). Funkcja draw, którą biblioteka uprzejmie wywołuje w pętli, wywołuje funkcję draw obiektu minimap. WSKAZÓWKA Może martwisz się, że nie możesz sprawdzić, czy mapa została poprawnie załadowana, oraz zastanawiasz się, co ona w ogóle zawiera. Jeśli chcesz złamać hermetyczność obiektu, aby móc z nim pokombinować w konsoli, możesz na chwilę usunąć słowo kluczowe var, by sprawdzić, co się stanie. W złożonym programie wykorzystującym kod z bibliotek efekty tego mogą być nieprzewidywalne. Jeżeli jednak nazwy zmiennych nie będą się powtarzać ani powodować konfliktów, to praca z nimi może być o wiele wygodniejsza, niż gdyby trzeba było w konsoli wpisywać polecenia typu console.log(mojaZmienna), aby wyświetlać wartości zmiennych. Gdyby nazwy zmiennych się powtórzyły, możesz spróbować przypisania innej niepowtarzalnej globalnej zmiennej, np. var map = [[…], […], …]; THE_MAP = map;. Później w konsoli można używać nazwy THE_MAP, którą po zakończeniu eksperymentowania można usunąć.

Teraz szczegółowo przeanalizujemy obiekt minimap. Jest to element canvas przechowujący dwuwymiarowy szkic mapy. Wywoływana jest funkcja init, która sprawia, że atrybuty zadeklarowane jako this.nazwaAtrybutu są dostępne poza kontekstem. Najpierw pobieramy element canvas i jego kontekst. Następnie ustawiamy szerokość i wysokość elementu canvas. Możesz pomyśleć, że wysokość i szerokość kontekstu kanwy zostaną odziedziczone z atrybutów stylu elementu canvas, które zadeklarujemy w CSS. Tak jednak się NIE stanie i dlatego trzeba te parametry ustawić bezpośrednio. Później dla wygody nadajemy tym własnościom krótsze nazwy. Następnie znajdujemy liczbę komórek na osiach x i y oraz obliczamy rozmiar komórek. W ostatnim wierszu przed funkcją draw tworzymy tablicę kolorów, aby odróżnić ściany różnych rodzajów. W metodzie draw przeglądamy zewnętrzne elementy tablicy map (oś y), przeglądając za pomocą pętli wszystkie podtablice (oś x), rysując białą komórkę dla pustej przestrzeni lub kolor mapowany na indeks tablicy kolorów. Do funkcji fillRect są przekazywane pozycja x (od lewej) i y (od góry), szerokość oraz wysokość.

182  Rozdział 8. GRY FPS Teraz dodamy brakujący kod CSS i HTML. Zaczniemy od nowego elementu canvas pokazanego na listingu 8.5. Listing 8.5. Dodanie elementu minimap do pliku index.html

Na koniec dodamy style dla tego elementu. Wklej kod z listingu 8.6 do pliku game.css. Listing 8.6. Arkusz stylów dla elementu minimap ... #minimap{ border:1px solid black; position:absolute; top:350px; width:300px; height:300px; }

Jako że ten element jest pozycjonowany bezwzględnie, jego położenie na stronie określa atrybut top. Własności width i height ustawiają szerokość i wysokość elementu, ale pamiętaj, że nie przekładają się one na atrybuty JavaScript o tych samych nazwach z kontekstu tego elementu. Teraz plik index.html w przeglądarce powinien wyglądać tak jak na rysunku 8.2.

Rysunek 8.2. Minimapa

Receptura. Dodawanie postaci gracza Mamy już dwuwymiarową mapę. Teraz potrzebujemy sposobu na poruszanie się po niej. Aby umieścić obiekt gracza na ekranie, zmodyfikujemy zawartość pliku game.js tak, jak widać na listingu 8.7.

Receptura. Dodawanie postaci gracza  183

Listing 8.7. Umieszczanie postaci gracza na mapie var Game = function(){ var player = { init: function(){ this.x = 10; this.y = 6; this.draw = function(){ var playerXOnMinimap = this.x * minimap.cellWidth; var playerYOnMinimap = this.y * minimap.cellHeight; minimap.context.fillStyle = "#000000"; minimap.context.beginPath(); minimap.context.arc(minimap.cellWidth*this.x, minimap.cellHeight*this.y, minimap.cellWidth/2, 0, 2*Math.PI, true); minimap.context.fill(); }; } } var map = [[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], ... var minimap = { ... }; this.draw = function(){ minimap.draw(); player.draw(); }; this.setup = function() { minimap.init(); player.init(); }; }

Niepogrubione wiersze kodu można zignorować. Na tym listingu zostały dodane trzy ważne części. Zaczynając od dołu, teraz obiekt player inicjujemy tak samo jak minimap i rejestrujemy funkcję player.draw, aby była wywoływana w funkcji draw, która jest nieskończoną pętlą. Jeśli chodzi o znajdujący się na górze obiekt player, definiujemy w nim współrzędne początkowej pozycji gracza oraz rysujemy w tym miejscu czarne koło przy użyciu atrybutów minimap. Następnie zajmiemy się poruszaniem graczem. Strzałki powinny powodować ruch w kierunkach przez nie wskazywanych. Perspektywa będzie dla nas ważna trochę później, a więc musimy przyjąć inne podejście do ruchu. Zdefiniujemy klawisze do obracania gracza i do chodzenia w przód i w tył. Na listingu 8.8 jest przedstawiona funkcja update. Podobnie jak było w przypadku funkcji draw, ta funkcja będzie wykonywana przez Jaws w nieskończoność. Funkcję tę można wpisać za funkcją draw. Listing 8.8. Rejestrowanie danych wejściowych this.update = function(){ if(jaws.pressed("left")) { player.direction = -1 }; if(jaws.pressed("right")) { player.direction = 1 }; if(jaws.pressed("up")) { player.speed = 1 }; if(jaws.pressed("down")) { player.speed = -1 };

184  Rozdział 8. GRY FPS if(jaws.on_keyup(["left", "right"], function(){ player.direction = 0; })); if(jaws.on_keyup(["up", "down"], function(){ player.speed = 0; })); player.move(); }; }

// Koniec pliku

Powyższy kod rejestruje, czy gracz próbuje obrócić lub przesunąć swoją postać. Następnie wywołujemy funkcję move w celu zarejestrowania wyniku. Oprócz dodania metody move na początku tego pliku także dokonaliśmy pewnych zmian. Zostały pokazane na listingu 8.9. Listing 8.9. Poruszanie postacią w grze var Game = function(){ var player = { init: function(){ this.x = 10; this.y = 6; this.direction = 0; this.angle = 0; this.speed = 0; this.movementSpeed = 0.1; this.turnSpeed = 4 * Math.PI / 180; this.move = function(){ var moveStep = this.speed * this.movementSpeed; this.angle += this.direction * this.turnSpeed; var newX = this.x + Math.cos(this.angle) * moveStep; var newY = this.y + Math.sin(this.angle) * moveStep; if (!containsBlock(newX, newY)){ this.x = newX; this.y = newY; }; }; this.draw = function(){ var playerXOnMinimap = this.x * minimap.cellWidth; var playerYOnMinimap = this.y * minimap.cellHeight; minimap.context.fillStyle = "#000000"; minimap.context.beginPath(); minimap.context.arc(minimap.cellWidth*this.x, minimap.cellHeight*this.y, minimap.cellWidth/2, 0, 2*Math.PI, true); minimap.context.fill(); var projectedX = this.x + Math.cos(this.angle); var projectedY = this.y + Math.sin(this.angle); minimap.context.fillRect(minimap.cellWidth*projectedX - minimap.cellWidth/4, minimap.cellHeight*projectedY - minimap.cellHeight/4, minimap.cellWidth/2, minimap.cellHeight/2); }; } }; function containsBlock(x,y) { return (map[Math.floor(y)][Math.floor(x)] !== -1); };

Receptura. Dodawanie postaci gracza  185

Najpierw ustawiliśmy zmienną direction na 0, co oznacza, że nie chcemy obrócić gracza. Ustawienie angle na 0 oznacza, że grę rozpoczynamy, patrząc w prawo. Wartość 0 zmiennej speed znaczy, że nie próbujemy iść do przodu ani do tyłu. Zmienna movementSpeed określa szybkość poruszania się gracza, a turnSpeed — szybkość, z jaką gracz się obraca. Te dziwnie wyglądające działania matematyczne z Math.PI dotyczą zamiany czterech stopni na radiany (0,0698). Później liczby pi będziemy jeszcze używać do konwersji na radiany w celu wykorzystania funkcji trygonometrycznych takich jak sinus i cosinus. OSTRZEŻENIE PRZED NAMI TRYGONOMETRIA. Nie panikuj, jeśli nie znasz się na trygonometrii. W tym rozdziale będziemy jedynie wykonywać proste obliczenia dotyczące kątów i boków trójkąta. Wszystko jest wyjaśnione, ale nawet jeśli jedyne, na co Cię dzisiaj stać, to skopiowanie funkcji, to i tak będziesz w stanie dokończyć tę grę. Zawsze można się czegoś nauczyć, więc nie przejmuj się, jeśli czegoś chwilowo nie zrozumiesz albo będziesz potrzebować dodatkowych objaśnień na jakiś temat.

Pierwszy przypadek użycia funkcji trygonometrycznych w naszej grze przypada na metodę move. Naszym celem jest aktualizacja współrzędnych x i y gracza. Zaczynamy od utworzenia zmiennej moveStep, która określa odległość, na jaką gracz chce się przemieścić. Ale to nie wystarczy do określenia współrzędnych x i y. Następnie aktualizujemy kąt patrzenia gracza, jeśli akurat jesteśmy w trakcie obracania postaci. Teraz wiemy już, na jaką odległość i w którym kierunku gracz chce się przemieścić. Przy użyciu funkcji trygonometrycznych (sinus, cosinus i tangens) możemy określić proporcje boków trójkąta. W tym trójkącie kierunek patrzenia gracza jest przeciwprostokątną (najdłuższym bokiem trójkąta), oś x jest przyprostokątną przyległą, a oś y — przyprostokątną przeciwległą. Dla x użyjemy funkcji cosinus służącej do obliczania stosunku przylegającej przyprostokątnej i przeciwprostokątnej (moveStep). Gdy pomnożymy wartość kąta przez długość przeciwprostokątnej, otrzymamy odległość na osi x. Dodając ten wynik do bieżącej pozycji x, znajdujemy wartość newX, czyli miejsce, w którym gracz chce być. W podobny sposób można obliczyć nową pozycję y, tylko zamiast funkcji cosinus użyjemy funkcji sinus służącej do obliczania stosunku przyprostokątnej przeciwległej (osi y) do przeciwprostokątnej (moveStep). Tak jak poprzednio, pomnożymy współczynnik przez długość przeciwprostokątnej, aby dowiedzieć się, na jaką odległość gracz chce się poruszyć. Następnie otrzymaną wartość dodajemy do bieżącej pozycji i w ten sposób otrzymujemy wartość newY. Aby gracz nie przechodził przez ściany, przekazujemy te wartości do funkcji containsBlock, która sprawdza, czy gracz nie próbuje wejść w miejsce zajmowane przez blok. Jednym ze sposobów na zablokowanie aktualizacji do przestrzeni zajmowanej przez blok (tzn. pozycja x od 5 do 5.999 i pozycja y od 4 do 4.999) jest użycie funkcji floor. Jeśli blok jest pusty, gracz może zostać przesunięty. W funkcji draw dodajemy wskaźnik, w którym kierunku patrzy gracz. Używamy funkcji trygonometrycznych podobnie jak w funkcji move, ale nie interesujemy się, czy gracz może przejść w określone miejsce. Po prostu rysujemy. W funkcji fillRect określamy niewielki prostokąt i dlatego dzielimy cellWidth oraz cellHeight przez dwa w trzecim i czwartym parametrze. Jeśli chodzi o dzielenie przez cztery w dwóch pierwszych parametrach, robimy to, aby wskaźnik kierunku obracał się tak samo jak koło gracza.

186  Rozdział 8. GRY FPS

Receptura. Raycasting widoku z góry Mamy już gotową mapę i możemy zająć się raycastingiem. Na razie jeszcze nie uzyskamy takiej imitacji trójwymiarowości, o jaką nam chodzi, ale utworzymy obiekt raycaster, który będzie zawierał większość logiki gry. Najpierw zamienimy aktualnie posiadane funkcje setup i draw na funkcje pokazane na listingu 8.10. Nie pomyl tej funkcji draw z funkcją o tej samej nazwie obiektów player i minimap. To jest główna funkcja rysująca delegująca zadania do dwóch pozostałych. Listing 8.10. Konfiguracja raycastera this.draw = function(){ minimap.draw(); player.draw(); raycaster.castRays(); }; this.setup = function() { minimap.init(); player.init(); raycaster.init(); };

Nie ma tu nic nowego. Konfigurujemy obiekt raycaster w funkcji setup i wywołujemy funkcję castRays w pętli draw. Teraz utworzymy ten obiekt z tymi dwiema metodami, jak pokazano na listingu 8.11. Kod ten można wpisać pod pierwszym wierszem definicji funkcji Game. Listing 8.11. Obiekt raycaster var Game = function(){ var raycaster = { init: function(){ var numberOfRays = 300; var angleBetweenRays = .2 * Math.PI /180; this.castRays = function() { for (var i=0;i
Przez chwilę poudawajmy, że nie ma ostatniego wiersza wywołującego drugą funkcję raycaster. Dodamy ją w następnym listingu. Jak w przypadku wszystkich pozostałych obiektów w tej grze, wszystkie metody umieściliśmy w funkcji init. Chcemy, aby nasz gracz emitował 300 promieni w równych odstępach czasu. By otrzymać dostatecznie szerokie pole widoku, ustawiliśmy zmienną angleBetweenRays na 0,2 stopnia, którą to wartość przekonwertowaliśmy na radiany poprzez pomnożenie jej przez pi i podzielenie przez 180. W funkcji castRays wykonujemy pętlę 300 razy oraz określamy kąt, który wykorzystujemy w funkcji castRay. Jako że chcemy, aby promienie były rzucane z obu stron gracza, odejmujemy

Receptura. Raycasting widoku z góry  187

połowę promieni od indeksu pętli i zapisujemy tę liczbę w zmiennej rayNumber. Następnie mnożymy rayNumber przez angleBetweenRays, aby otrzymać kąt (dodatni lub ujemny) środkowy koła. Iloczyn ten dodajemy do player.angle, by otrzymać kąt rayAngle, który przekazujemy jako argument do funkcji castRay. Implementacja tej funkcji znajduje się na listingu 8.12. Listing 8.12. Rzucanie promieni var raycaster = { init: function(){ this.castRays = function() { ... } this.castRay = function(rayAngle){ var twoPi = Math.PI * 2; rayAngle %= twoPi; if (rayAngle < 0) rayAngle += twoPi; var right = (rayAngle > twoPi * 0.75 || rayAngle < twoPi * 0.25); var up = rayAngle > Math.PI; var slope = Math.tan(rayAngle); var distance = 0; var xHit = 0; var yHit = 0; var wallX; var wallY; var dX = right ? 1 : -1; var dY = dX * slope; var x = right ? Math.ceil(player.x) : Math.floor(player.x); var y = player.y + (x - player.x) * slope; while (x >= 0 && x < minimap.cellsAcross && y >= 0 && y < minimap.cellsDown) { wallX = Math.floor(x + (right ? 0 : -1)); wallY = Math.floor(y); if (map[wallY][wallX] > -1) { var distanceX = x - player.x; var distanceY = y - player.y; distance = Math.sqrt(distanceX*distanceX + distanceY*distanceY); xHit = x; yHit = y; break; } x += dX; y += dY; } slope = 1/slope; dY = up ? -1 : 1; dX = dY * slope; y = up ? Math.floor(player.y) : Math.ceil(player.y); x = player.x + (y - player.y) * slope; while (x >= 0 && x < minimap.cellsAcross && y >= 0 && y < minimap.cellsDown) { wallY = Math.floor(y + (up ? -1 : 0)); wallX = Math.floor(x); if (map[wallY][wallX] > -1) { var distanceX = x - player.x; var distanceY = y - player.y; var blockDistance = Math.sqrt(distanceX*distanceX + distanceY*distanceY); if (!distance || blockDistance < distance) { distance = blockDistance; xHit = x;

188  Rozdział 8. GRY FPS yHit = y; } break; } x += dX; y += dY; } this.draw(xHit, yHit); }; this.draw = function(rayX, rayY){ minimap.context.beginPath(); minimap.context.moveTo(minimap.cellWidth*player.x, minimap.cellHeight*player.y); minimap.context.lineTo(rayX * minimap.cellWidth, rayY * minimap.cellHeight); minimap.context.closePath(); minimap.context.stroke(); } } };

W przypadku tak długich funkcji zawsze dobrze jest najpierw spojrzeć na sam koniec, aby sprawdzić, co zwracają, jakie inne funkcje wywołują oraz jakie mają skutki uboczne (tzn. jakie zmienne ustawiają). Jeśli spojrzysz nad deklarację funkcji draw, to dowiesz się, że najważniejszą czynnością tej funkcji jest znajdowanie wartości zmiennych xHit i yHit reprezentujących współrzędne x i y na mapie bloku, na który trafia promień. Później współrzędne te są przekazywane do funkcji draw. Pamiętaj o tym celu, przeglądając tę funkcję od początku. Wiele razy będziemy używali radianowego odpowiednika 360 stopni (Math.PI*2), a więc zapisaliśmy tę wartość w zmiennej o nazwie twoPi. Następnie dzielimy modulo (%) wartość kąta przez twoPi, aby nie stała się większa od 360 stopni (wyrażonych w radianach). Następnie dodajemy do niej twoPi, jeśli jest zbyt mała, dzięki czemu nie będziemy musieli pracować z liczbami ujemnymi. Potem określamy kwadrant. To znaczy sprawdzamy, czy promień biegnie np. w górę i w prawo, a nie w dół i w lewo. Następnie obliczamy tangens kąta. Funkcja tangens opisuje stosunek długości przyprostokątnej przeciwstawnej (y) do przyprostokątnej przyległej (x). Gdy pomnożymy go przez dX (zmiana x), znajdziemy zmianę y, czyli dY. Wartości te są nam potrzebne, aby wiedzieć, gdzie szukać nowego bloku, jeśli najbliższa przestrzeń jest pusta, co zobaczymy za chwilę. Po ustawieniu zmiennej slope inicjujemy kilka dodatkowych zmiennych. Zmienna distance określa odległość gracza od końcowego bloku, czyli zera. Zmienne xHit i yHit służą do przechowywania lokalizacji, w których następuje kolizja promienia z blokiem. Zmienne wallX i wallY służą do przechowywania bardziej ogólnych współrzędnych na mapie bloku, który został trafiony. Zmienna dX określa jednostkę w prawo (+1) albo w lewo (–1), w zależności od kierunku promienia. Zmienna dY oznacza zmianę na osi y, która jest proporcjonalna do zmiany na osi x (dX). Zmienne x i y definiują bieżące położenie na mapie komórki, przez którą przechodzi promień. W pętli while szukamy uderzenia i gdy je wykryjemy, wychodzimy z pętli za pomocą instrukcji break. Warunki pętli wskazują także, że koniec jej działania nastąpi w przypadku osiągnięcia krawędzi mapy. Wewnątrz pętli ustawiamy zmienne wallX i wallY na komórkę, którą aktualnie badamy. Instrukcja if sprawdza, czy w komórce jest blok. Jeśli tak, mamy zderzenie, więc ustawiamy distanceX i distanceY na odległości od gracza. Następnie obliczamy odległość w prostej linii od gracza, stosując twierdzenie Pitagorasa. Później aktualizujemy najważniejsze zmienne

Receptura. Raycasting widoku z góry  189

(ponieważ potrzebujemy ich w funkcji draw), xHit i yHit, aby zapisać dokładną lokalizację kolizji. Na koniec przerywamy instrukcję while, jeśli wystąpi kolizja. Jeżeli zderzenie nie wystąpi, zwiększamy x i y, aby sięgnąć promieniem nieco dalej, i próbujemy jeszcze raz. Zwróć uwagę, że gdyby mapa była bardzo duża, powinno się ograniczyć zasięg wzroku. Promień biegnący do końca mapy mógłby pochłonąć dużo mocy obliczeniowej i spowolnić inne promienie. W kolejnych wierszach robimy w zasadzie to samo, tylko w odniesieniu do zderzeń pionowych. Jedną z ważnych różnic w stosunku do poprzedniego kodu jest dodatkowy warunek sprawdzający, czy odległość w poziomie była większa, i zastępowanie tej odległości jedynie w sytuacji, jeśli była większa. Cała reszta jest taka sama, tylko z lekkim przesunięciem. Kod ten może wyglądać jak dobry kandydat do refaktoryzacji, aby wszystko ścisnąć w jednej funkcji, ale trzeba by było zdecydować, ile zmiennych będzie wspólnych, oraz uwzględnić drobne różnice. W efekcie kod może uda się skompresować, ale będzie znacznie bardziej skomplikowany i trudny w utrzymaniu. Na końcu zostaje wywołana funkcja draw. Rysuje ona linię od gracza do punktu, w którym promień zderzył się z blokiem. Pamiętaj, że lineTo to funkcja rysująca, a moveTo to funkcja, której działanie przypomina przystawienie ołówka w wybranym miejscu (w tym przypadku tam, gdzie znajduje się gracz). Potem rzucamy pozostałe 299 promieni, po jednym w każdej iteracji pętli, tworząc nową ulepszoną minimapę widoczną na rysunku 8.3.

Rysunek 8.3. Rzucanie promieni w dwóch wymiarach

W zasadzie na tym można by było skończyć. Wystarczy dodać możliwość strzelania (i trochę więcej geometrii) i mamy gotowy bilard, który łatwo zamienić w strzelankę podobną do czołgów Combat na Atari. Jednak my mierzymy trochę wyżej i marzy nam się coś bliższego grze Golden Eye na Nintendo 64.

190  Rozdział 8. GRY FPS

Receptura. Imitacja trójwymiarowości przy użyciu raycastingu W poprzedniej recepturze przygotowaliśmy prawie wszystko, czego potrzebujemy do imitowania trzech wymiarów, ale nadal nic nie renderujemy na głównej kanwie. Dlatego teraz rozpoczniemy od zmodyfikowania funkcji draw i setup, jak pokazano na listingu 8.13. Listing 8.13. Modyfikacja funkcji draw i setup this.draw = function(){ minimap.draw(); player.draw(); canvas.blank(); raycaster.castRays(); }; this.setup = function() { minimap.init(); player.init(); raycaster.init(); canvas.init(); };

W tym kodzie znalazły się dwa nowe wywołania funkcji. Mamy obiekt kanwy, który będzie inicjowany w taki sam sposób, w jaki do tej pory inicjowaliśmy inne obiekty. Dlatego właśnie wywołujemy funkcję canvas.init() w funkcji setup. Pamiętaj, że funkcja ta jest uruchamiana tylko raz na początku gry. W funkcji draw, która jest wykonywana wielokrotnie w ciągu sekundy, wywołujemy funkcję blank rysującą ziemię i niebo. Obiekt kanwy dodamy za moment, a na razie musimy wprowadzić kilka zmian w obiekcie raycaster. Dodamy do niego wiersze kodu oznaczone pogrubieniem na listingu 8.14. Listing 8.14. Modyfikacja obiektu raycaster var raycaster = { init: function(){ this.castRays = function() { ... for (var i=0;i
// Zastępuje this.castRay = function(rayAngle);

... this.draw(xHit, yHit, distance, i, rayAngle);

// Zastępuje this.draw(xHit, yHit);

};

// Następny wiersz zastępuje this.draw = function(rayX, rayY){ this.draw = function(rayX, rayY, distance, i, rayAngle){ ... var adjustedDistance = Math.cos(rayAngle - player.angle) * distance; var wallHalfHeight = canvas.height / adjustedDistance / 2; var wallTop = Math.max(0, canvas.halfHeight - wallHalfHeight); var wallBottom = Math.min(canvas.height, canvas.halfHeight + wallHalfHeight); canvas.drawSliver(i, wallTop, wallBottom, "#000")

Receptura. Imitacja trójwymiarowości przy użyciu raycastingu  191

} } };

Ze względu na nasze potrzeby dotyczące renderowania musimy przekazywać więcej danych. W pierwszych czterech pogrubionych wierszach zaznaczamy, że będziemy potrzebować numeru promienia, i oraz odległości od kolizji i kąta promienia. Następnie w funkcji draw wywołujemy funkcję canvas.drawSliver. Wkrótce do tego dojdziemy, a na razie zwróć uwagę, że jako parametry przekazujemy pozycję na osi x, i, pozycje na osi y góry i dołu ściany oraz kolor ustawiony na czarny. W czterech poprzednich wierszach wartości te są obliczane. Zaczynamy od obliczenia wartości adjustedDistance poprzez pomnożenie odległości gracza od kolizji przez cosinus kąta gracza minus kierunek promienia. Pamiętaj, że rayAngle uwzględnia już kąt gracza. Odejmując go, kąt można traktować niezależnie, w istocie tak samo jak wtedy, gdy kąt gracza wynosi 0. Cosinus otrzymanego kąta jest stosunkiem odległości od miejsca, w którym środkowy promień kolidowałby ze ścianą, do odległości wzdłuż promienia do miejsca, w którym kolizja rzeczywiście ma miejsce. Mnożąc cosinus kąta przez odległość (distance), otrzymujemy hipotetyczną odległość od miejsca kolizji, która by nastąpiła, gdyby promień znajdował się na środku pola widzenia gracza. Robimy to po to, aby uniknąć efektu rybiego oka, który powstałby w wyniku tego, że boczne sekcje ścian byłyby oddalone. Efekt ten wygląda tak, że ściana wydaje się wyższa na środku i niższa po bokach. Potrzebujesz realnego przykładu? Jeśli spojrzysz wprost na ścianę oddaloną o sześć metrów, czy masz wrażenie, że boki są trochę bardziej oddalone? Raczej nie, prawda? To właśnie osiągniemy dzięki tym dodatkowym obliczeniom trygonometrycznym. Po określeniu poprawionej odległości używamy jej do znalezienia połowy wysokości ściany. Mając tę wartość, możemy obliczyć pozycję góry i dołu ściany. Funkcje min i max uniemożliwiają nam wyjście poza kanwę. Naszą ostatnią czynnością w raycasterze jest wywołanie funkcji drawSliver kanwy. WSKAZÓWKA Jeśli po dodaniu obiektu kanwy chcesz zobaczyć, jak wygląda efekt rybiego oka, to wystarczy, że poniższy wiersz kodu: var wallHalfHeight = canvas.height / adjustedDistance / 2;

zamienisz na ten: var wallHalfHeight = canvas.height / distance / 2;

Już trzy razy odwołujemy się do obiektu canvas, którego jeszcze nie utworzyliśmy. Czas to nadrobić przy użyciu kodu przedstawionego na listingu 8.15. Kod ten należy wkleić nad główną funkcją draw, pod zakończeniem obiektu minimap. Listing 8.15. Definicja obiektu canvas var canvas = { init: function(){ this.element = document.getElementById('canvas'); this.context = this.element.getContext("2d");

192  Rozdział 8. GRY FPS this.width = this.element.width; this.height = this.element.height; this.halfHeight = this.height/2; this.ground = '#DFD3C3'; this.sky = '#418DFB'; this.blank = function(){ this.context.clearRect(0, 0, this.width, this.height); this.context.fillStyle = this.sky; this.context.fillRect(0, 0, this.width, this.halfHeight); this.context.fillStyle = this.ground; this.context.fillRect(0, this.halfHeight, this.width, this.height); } this.drawSliver = function(sliver, wallTop, wallBottom, color){ this.context.beginPath(); this.context.strokeStyle = color; this.context.moveTo(sliver + .5, wallTop); this.context.lineTo(sliver + .5, wallBottom); this.context.closePath(); this.context.stroke(); } } };

Podobnie jak w poprzednich obiektach, cały kod umieściliśmy w funkcji init. Pobieramy element canvas, zwracamy kontekst, ustawiamy kilka zmiennych dotyczących wymiarów oraz ustawiamy wartości kolorów na niebieski reprezentujący niebo i piaskowy reprezentujący ziemię. Funkcja blank rozpoczyna działanie od wyczyszczenia kanwy oraz narysowania nieba i ziemi. Wywołujemy ją w pętli draw za każdym razem przed wywołaniem funkcji castRays, która z kolei wywołuje funkcję drawSliver dla każdego promienia. W funkcji drawSliver używamy standardowych funkcji kanwy. Jedyny fragment w tym kodzie, który może się wydawać nie na miejscu, to przesunięcie o 0,5 dodane do pozycji x rysowanej linii. Jeśli zastanawiasz się, czemu to służy, to usuń fragment + .5, aby zobaczyć, że kolory nieba i ziemi wychodzą poza ściany. Po dokonaniu wszystkich opisanych zmian powinniśmy już móc patrzeć oczami gracza, jak widać na rysunku 8.4.

Receptura. Dodawanie kamery Mając gotowy raycaster, możemy utworzyć grę jednego z kilku rodzajów, z których większość to różne typy strzelanek. W tej recepturze nie będziemy implementować pocisków, tylko poznamy pewne cechy kanwy, o których możesz jeszcze nie wiedzieć. W tym celu gracza wyposażymy w aparat fotograficzny zamiast w karabin. Nie staniemy się jednak dzięki temu pionierami, ponieważ podobny pomysł robienia zdjęć został już wykorzystany w grach Pokemon Snap i Pilot Wings na Nintendo 64. Zaczniemy od wprowadzenia kilku zmian w pliku index.html przedstawionych na listingu 8.16. Listing 8.16. Dodanie aparatu fotograficznego w pliku HTML ...


Receptura. Dodawanie kamery  193

Rysunek 8.4. Raycasting ...

src="jquery.js"> src="filtrr2.js"> src="jaws.js"> src="game.js">

Najpierw dodajemy obraz aparatu. Następnie dodajemy nowy element canvas (w kontenerze), w którym będą wyświetlane robione przez nas zdjęcia. Później ładujemy bibliotekę JavaScript o nazwie filtrr oraz potrzebną do jej działania bibliotekę jQuery. Kolejnym zadaniem będzie zdefiniowanie arkuszy stylów dla nowo dodanych elementów. Kod przedstawiony na listingu 8.17 należy dodać do pliku game.css. Listing 8.17. Style elementów aparatu fotograficznego #screenshot-wrapper{ position:absolute; left:700px; border:1px solid black;

194  Rozdział 8. GRY FPS } #camera{ width:100px; position:absolute; left:505px; top:180px; }

Nie ma tu nic zaskakującego i dlatego od razu przechodzimy do modyfikowania pliku game.js. Zaczniemy od zmian w funkcjach update i setup przedstawionych na listingu 8.18. Listing 8.18. Uruchamianie aparatu w pliku game.js this.setup = function() { camera.init(); ... }; this.update = function(){ ... if(jaws.on_keyup(["up", "down"], function(){ player.speed = 0; })); if(jaws.pressed("space")) { camera.takePicture(); }; player.move(); };

Podobnie jak w przypadku wszystkich pozostałych obiektów, funkcję init tego obiektu wywołujemy w funkcji setup. W funkcji update, za wiązaniami klawiszy, dodaliśmy wywołanie funkcji camera.takePicture wyzwalane naciśnięciem spacji. Pozostało nam już tylko zdefiniowanie obiektu camera, którego kod jest przedstawiony na listingu 8.19. Definicję tę można wkleić nad główną funkcją rysującą gry. Listing 8.19. Obiekt camera var camera = { init: function(){ this.context = document.getElementById('screenshot').getContext('2d'); var filtered = false; var f; $("#screenshot").on("click", function() { if(filtered){ filtered = false; f.reset().render(); } else{ filtered = true; f = Filtrr2("#screenshot", function() { this.expose(50) .render(); }, {store: false}); }; }); this.takePicture = function(){ var image = new Image();

Receptura. Dodawanie kamery  195

image.src = canvas.element.toDataURL('image/png'); image.onload = function() { camera.context.drawImage(image,0,0); } filtered = false; } } }; this.draw = function(){

Na początku funkcji init ustawiamy kontekst kanwy elementu screenshot. Następnie ustawiamy zmienną f na obiekt, którego będziemy używać w kodzie filtrr, oraz zmienną filtered na false, ponieważ jeszcze nie zastosowaliśmy żadnego filtra. Później wiążemy funkcję click z elementem screenshot, aby jego kliknięcie powodowało zastosowanie lub usunięcie filtra. W obu gałęziach instrukcji if – else jest wywoływana funkcja render na obiekcie. Znajdująca się przed nią funkcja reset usuwa filtr. Druga gałąź stosuje filtr expose na rzecz obiektu, co powoduje, że staje się on jaśniejszy. WSKAZÓWKA Początkowa konfiguracja i praca z biblioteką filtrr może być trochę trudna, ale gdy już się pozna podstawy, ma się do dyspozycji wiele filtrów, które można stosować do obrazów. Poniżej znajduje się lista kilku przykładowych filtrów, których można użyć zamiast this.expose(50). this.adjust(10, 25, 50) this.brighten(50) this.alpha(50) this.saturate(50) this.invert() this.posterize(10) this.gamma(50) this.contrast(50) this.sepia() this.subtract(10, 25, 50) this.fill(100, 25, 50) this.blur('simple') this.blur('gaussian') this.sharpen()

Na końcu znajduje się funkcja takePicture wywoływana w reakcji na naciśnięcie spacji. W tej funkcji tworzymy nowy obraz, ustawiamy źródło obrazu na zakodowany w URI główny element canvas oraz rysujemy obraz w polu screenshot po jego załadowaniu. Opis kodowania obrazów i elementów canvas w adresach URI nie mieści się w zakresie tej książki, ale warto znać tę technikę. Jeśli chcesz dowiedzieć się więcej, poszukaj w wyszukiwarce internetowej informacji na temat funkcji toDataURL. Jeśli lubisz korzystać z różnych nietypowych narzędzi, to może spodoba Ci się wyszukiwarka https://duckduckgo.com/. Została stworzona z myślą o hakerach i szanuje prywatność użytkowników. W tej recepturze dodaliśmy ekran HUD oraz możliwość robienia zdjęć kanwy i zapisywania ich w postaci danych zakodowanych w URI oraz odtwarzania ich na kanwie i manipulowania obrazem. Efekt tych działań jest przedstawiony na rysunku 8.5.

196  Rozdział 8. GRY FPS

Rysunek 8.5. Obraz z zastosowanym filtrem

Zanim przejdziemy do następnej receptury, powinniśmy zwrócić uwagę na kilka kwestii. Po pierwsze, jeśli naciśniemy spację i ją przytrzymamy, to zamiast aparatu fotograficznego mamy kamerę wideo. Po drugie, funkcji toDataUrl można użyć w inny sposób, niż my to zrobiliśmy. Zdjęcia można by było zapisywać w magazynie lokalnym albo na serwerze, aby utworzyć album. Można także wywołać funkcję window.open(canvas.element.toDataURL('image/png'));, by otworzyć obraz w nowym oknie. Jeśli to zrobimy, kod obrazu będzie widoczny w pasku adresu i będzie można go skopiować, żeby pokazać komuś innemu wybraną część kanwy. Ostatnia ważna kwestia to fakt, że aparatu nie widać na zdjęciach. Jest to spowodowane tym, że aparat jest elementem DOM znajdującym się nad kanwą, tzn. nie należy do kanwy, która jest kopiowana.

Receptura. Uatrakcyjnianie świata pod względem wizualnym Nasz raycaster nabiera powoli kształtu i można już nawet robić zdjęcia, gdy napotkamy w naszym wirtualnym świecie coś ciekawego. Jednak niewiele jest do fotografowania. Różne rodzaje bloków dodaliśmy z myślą, że w jakiś sposób je od siebie odróżnimy. Na razie wszystkie są po prostu czarne. Aby to zmienić, wprowadzimy nowy obiekt o nazwie palette. Zaczniemy od jego zainicjowania w funkcji setup, jak pokazano na listingu 8.20. Listing 8.20. Funkcja setup z obiektem palette this.setup = function() { ... palette.init(); };

Receptura. Uatrakcyjnianie świata pod względem wizualnym  197

Następnie utworzymy sam obiekt palette, jak pokazano na listingu 8.21. Kod ten można umieścić pod obiektem camera, a nad funkcją draw. Listing 8.21. Obiekt palette var palette = { init: function(){ this.ground = '#DFD3C3'; this.sky = '#418DFB'; this.shades = 300; var initialWallColors = [[85, 68, 102], [255, 53, 91], [255, 201, 52], [118, 204, 159]]; this.walls = []; for(var i = 0; i < initialWallColors.length; i++){ this.walls[i] = []; for(var j = 0; j < this.shades; j++){ var red = Math.round(initialWallColors[i][0] * j / this.shades); var green = Math.round(initialWallColors[i][1] * j / this.shades); var blue = Math.round(initialWallColors[i][2] * j / this.shades); var color = "rgb("+red+","+green+","+blue+")"; this.walls[i].push(color); }; }; } }

Najpierw zadeklarowaliśmy kolory nieba i ziemi, które wcześniej znajdowały się w obiekcie canvas. Skoro utworzyliśmy obiekt specjalnie przeznaczony do pracy z kolorami, to wydaje się, że te dwa kolory również powinny się w nim znaleźć. Na listingu 8.22 pokazano, jakich zmian należy dokonać w obiekcie canvas. Ale na razie pozostaniemy jeszcze przy listingu 8.21. Własność shades określa, ile odcieni ma mieć każda ze ścian. Zmienna initialWallColors definiuje najjaśniejszą wersję koloru. Będzie on użyty, gdy gracz znajdzie się w najmniejszej odległości od ściany. Następnie wstawiamy do tablicy walls tablicę odcieni koloru dla każdego rodzaju ścian. Pierwszy kolor w tej tablicy to czarny, a ostatni będzie kolorem określonym przez initialWallColors. UWAGA W językach JavaScript i CSS istnieje kilka sposobów na określanie kolorów. W tym przypadku użyliśmy formatu RGB, ponieważ dzięki temu łatwo nam jest manipulować poszczególnymi składowymi koloru. Pomaga nam także fakt, że wartości są zakodowane w systemie dziesiętnym, a nie szesnastkowym, jak np. wartości typu #ff23e9. Oczywiście można dokonywać konwersji między tymi dwoma systemami, ale używając formatu RGB, znacznie skróciliśmy kod. Listing 8.22. Modyfikacje obiektu canvas var canvas = { init: function(){ ...

// this.ground = '#DFD3C3'; // Ten wiersz usuwamy //this.sky = '#418DFB'; // Ten też

198  Rozdział 8. GRY FPS this.blank = function(){ this.context.clearRect(0, 0, this.width, this.height); this.context.fillStyle = palette.sky; this.context.fillRect(0, 0, this.width, this.halfHeight); this.context.fillStyle = palette.ground; this.context.fillRect(0, this.halfHeight, this.width, this.height); } ... } };

Potrzebne są też zmiany w funkcji castRay obiektu raycaster. Należy dodać do niej wiersze kodu oznaczone pogrubieniem na listingu 8.23. Listing 8.23. Zmiany w funkcji castRay obiektu raycaster związane z kolorowaniem ścian this.castRay = function(rayAngle, i){ ... var wallType; while (x >= 0 && x < minimap.cellsAcross && y >= 0 && y < minimap.cellsDown) { ... if (map[wallY][wallX] > -1) { ... wallType = map[wallY][wallX]; break; } ... } ... while (x >= 0 && x < minimap.cellsAcross && y >= 0 && y < minimap.cellsDown){ ... if (map[wallY][wallX] > -1) { ... if (!distance || blockDistance < distance) { ... wallType = map[wallY][wallX]; } break; } ... } this.draw(xHit, yHit, distance, i, rayAngle, wallType); // także przekazujemy wallType };

Wprowadzone zmiany dotyczą przekazania funkcji raycaster.draw zmiennej wallType określającej, z jakiego rodzaju ścianą zderzył się promień. Na zakończenie tej receptury zobaczymy, jak użyć tego w funkcji draw — listing 8.24. Listing 8.24. Rysowanie kolorów i odcieni this.draw = function(rayX, rayY, distance, i, rayAngle, wallType){ minimap.context.beginPath(); minimap.context.moveTo(minimap.cellWidth*player.x, minimap.cellHeight*player.y);

Receptura. Uatrakcyjnianie świata pod względem wizualnym  199

minimap.context.lineTo(rayX * minimap.cellWidth, rayY * minimap.cellHeight); minimap.context.closePath(); minimap.context.stroke(); var adjustedDistance = Math.cos(rayAngle - player.angle) * distance; var wallHalfHeight = canvas.height / adjustedDistance / 2; var wallTop = Math.max(0, canvas.halfHeight - wallHalfHeight); var wallBottom = Math.min(canvas.height, canvas.halfHeight + wallHalfHeight); var percentageDistance = adjustedDistance / Math.sqrt(minimap.cellsAcross * minimap.cellsAcross + minimap.cellsDown * minimap.cellsDown); var brightness = 1 - percentageDistance; var shade = Math.floor(palette.shades * brightness); var color = palette.walls[wallType][shade]; canvas.drawSliver(i, wallTop, wallBottom, color) }

Zwróć uwagę, że w pierwszym wierszu do funkcji jest przekazywany dodatkowy parametr wallType. Jest używany jako pierwszy indeks w wielowymiarowej tablicy palette.walls. Pobranie odcienia jest jednak nieco bardziej skomplikowane. Za pomocą twierdzenia Pitagorasa określamy maksymalną odległość, na jaką może podróżować promień (z jednego rogu ekranu do przeciwnego). Przez otrzymaną wartość dzielimy wartość adjustedDistance, aby określić stosunek odległości, jaką musi przemierzyć promień, zanim uderzy w ścianę, do dystansu, jaki mógłby przebyć. UWAGA Twierdzenie Pitagorasa głosi, że w trójkącie prostokątnym (mającym jeden kąt o mierze 90 stopni) długość przeciwprostokątnej jest równa pierwiastkowi kwadratowemu z sumy kwadratów długości przyprostokątnych. Można to przedstawić za pomocą następującego równania: h = Math.sqrt(pierwszyBok * pierwszyBok + drugiBok * drugiBok);

Wartość ta ma odwrotną zależność z wartością brightness, tzn. im krótszy dystans promienia, tym wartość brightness bliższa 1 (100%). Odcień (shade) obliczamy, mnożąc brightness przez liczbę możliwych odcieni i zaokrąglając wynik w dół. Później znajdujemy kolor w utworzonej wcześniej tabeli palette.walls. Na koniec przekazujemy znaleziony kolor do obiektu canvas, który działa dokładnie tak, jak to zaplanowaliśmy. Jeśli wszystko poszło zgodnie z planem, gra powinna wyglądać jak na rysunku 8.6. WSKAZÓWKA Utworzenie zawczasu tabeli wyszukiwania to technika optymalizacji, którą warto rozważyć, gdy ma się obawy, że zbyt duża ilość obliczeń spowolni działanie pętli rysującej. Jeśli da się coś obliczyć wcześniej, to dobrze jest zapisać wynik, aby nie musieć go obliczać ponownie, gdy będzie już potrzebny. Pamiętaj także, że to, co w jednej przeglądarce powoduje przyspieszenie działania programu, w innej może powodować spowolnienie. Po prostu testuj swoją aplikację we wszystkich przeglądarkach.

200  Rozdział 8. GRY FPS

Rysunek 8.6. Cieniowanie i kolorowanie

Receptura. Dodawanie przyjaciół i wrogów Aktualnie nasza postać jest całkiem samotna w grze i może tylko zwiedzać duże prostokątne pomieszczenie, robiąc zdjęcia kolorowym blokom. Żeby trochę urozmaicić jej życie, dodamy do gry dinozaura z rozdziału 3. Pracę rozpoczniemy od zmodyfikowania skryptu znajdującego się w pliku index.html, aby ładował dinozaura — listing 8.25. Listing 8.25. Ładowanie dinozaura ... ...

Jak się pewnie spodziewasz, teraz zainicjujemy obiekt dino w funkcji setup, jak pokazano na listingu 8.26. Listing 8.26. Inicjowanie dinozaura this.setup = function() { ... dino.init(); };

Receptura. Dodawanie przyjaciół i wrogów  201

Zanim zdefiniujemy obiekt dino, musimy jeszcze nanieść poprawki w kilku miejscach. Po pierwsze, przygotujemy obiekt raycaster do obsługi dinozaura — listing 8.27. Listing 8.27. Modyfikacje obiektu raycaster var twoPi = Math.PI * 2; var raycaster = { init: function(){ this.maxDistance = Math.sqrt(minimap.cellsAcross * minimap.cellsAcross + minimap.cellsDown * minimap.cellsDown); var numberOfRays = 300; var angleBetweenRays = .2 * Math.PI /180; this.castRays = function() { foregroundSlivers = []; backgroundSlivers = []; minimap.rays = []; dino.show = false; for (var i=0;i twoPi * 0.75 || rayAngle < twoPi * 0.25); var up = rayAngle > Math.PI; var slope = Math.tan(rayAngle); var distance = 0; var xHit = 0; var yHit = 0; var wallX; var wallY; var dX = right ? 1 : -1; var dY = dX * slope; var x = right ? Math.ceil(player.x) : Math.floor(player.x); var y = player.y + (x - player.x) * slope; var wallType; while (x >= 0 && x < minimap.cellsAcross && y >= 0 && y < minimap.cellsDown) { wallX = Math.floor(x + (right ? 0 : -1)); wallY = Math.floor(y); if (map[wallY][wallX] > -1) { var distanceX = x - player.x; var distanceY = y - player.y; distance = Math.sqrt(distanceX*distanceX + distanceY*distanceY); xHit = x; yHit = y; wallType = map[wallY][wallX]; break; } else{ if(dino.x === wallX && dino.y === wallY){ dino.show = true; }; } x += dX;

202  Rozdział 8. GRY FPS y += dY; } slope = 1/slope; dY = up ? -1 : 1; dX = dY * slope; y = up ? Math.floor(player.y) : Math.ceil(player.y); x = player.x + (y - player.y) * slope; while (x >= 0 && x < minimap.cellsAcross && y >= 0 && y < minimap.cellsDown) { wallY = Math.floor(y + (up ? -1 : 0)); wallX = Math.floor(x); if (map[wallY][wallX] > -1) { var distanceX = x - player.x; var distanceY = y - player.y; var blockDistance = Math.sqrt(distanceX*distanceX + distanceY*distanceY); if (!distance || blockDistance < distance) { distance = blockDistance; xHit = x; yHit = y; wallType = map[wallY][wallX]; } break; }else{ if(dino.x === wallX && dino.y === wallY){ dino.show = true; }; } x += dX; y += dY; } if(dino.show === true){ var dinoDistanceX = dino.x + .5 - player.x; var dinoDistanceY = dino.y + .5 - player.y; dino.angle = Math.atan(dinoDistanceY/dinoDistanceX) - player.angle; dino.distance = Math.sqrt(dinoDistanceX*dinoDistanceX + dinoDistanceY * dinoDistanceY); }; minimap.rays.push([xHit, yHit]); var adjustedDistance = Math.cos(rayAngle - player.angle) * distance; var wallHalfHeight = canvas.height / adjustedDistance / 2; var wallTop = Math.max(0, canvas.halfHeight - wallHalfHeight); var wallBottom = Math.min(canvas.height, canvas.halfHeight + wallHalfHeight); var percentageDistance = adjustedDistance / this.maxDistance; var brightness = 1 - percentageDistance; var shade = Math.floor(palette.shades * brightness); var color = palette.walls[wallType][shade]; if(adjustedDistance < dino.distance){ foregroundSlivers.push([i, wallTop, wallBottom, color]); }else{ backgroundSlivers.push([i, wallTop, wallBottom, color]); }; } } }

Zmienna twoPi będzie nam potrzebna dopiero później i dlatego umieściliśmy ją poza obiektem raycaster.

Receptura. Dodawanie przyjaciół i wrogów  203

Jeśli chodzi o sam obiekt raycaster, zmiany w nim są na tyle znaczące, że postanowiłem pokazać jego kod w całości. Przejrzymy teraz po kolei wszystkie oznaczone pogrubieniem dodatki. Najpierw zdefiniowaliśmy zmienną maxDistance określającą maksymalną odległość, na jaką gracz może widzieć na mapie (od jednego rogu do przeciwnego). W każdym wywołaniu funkcji castRays inicjujemy trzy nowe tablice do przechowywania informacji o tym, co chcemy wyrenderować. Na razie obiekt raycaster służy nam tylko do opisywania tego, co zostanie wyrenderowane, ale nie jest używany do bezpośredniego wywoływania funkcji rysujących. Ustawiliśmy też własność dino.show na false, aby dinozaur nie był renderowany, dopóki my tego nie zechcemy. Dodaliśmy też test obecności dinozaura do wszystkich rzutów na osiach x i y. Jeśli dinozaur zostanie znaleziony, ustawiamy jego własność show na true. W kolejnej pogrubionej części dla każdego rzucanego promienia sprawdzamy, czy chcemy wyświetlić dinozaura. Jeśli tak, obliczamy odległości x i y poprzez dodanie do każdej z nich wartości .5 (aby ustawić dinozaura na środku bloku) i odejmując od nich wartości x i y obiektu gracza. Następnie za pomocą funkcji Math.atan(arctan) obliczamy kąt ze stosunku przeciwprostokątnej przeciwstawnej do przyległej trójkąta. Funkcja arctan jest odwrotnością funkcji tan służącej do obliczania stosunku przyprostokątnych trójkąta na podstawie kąta. Podobnie jak w przypadku odległości gracza, odejmujemy kąt gracza, aby otrzymać kontekst relacji między dinozaurem a graczem, zamiast kąta bezwzględnego. By obliczyć odległość dinozaura od gracza w linii prostej, ponownie używamy twierdzenia Pitagorasa (suma kwadratów przyprostokątnych równa się kwadratowi przeciwprostokątnej). Następnie wstawiamy tablicę współrzędnych x i y opisujących punkty kolizji promieni do tablicy rays obiektu minimap. W tym miejscu wcześniej wywołalibyśmy funkcję draw. Jednak wywołanie to wraz z definicją funkcji zostało usunięte. Jako że teraz zamiast rysować, wstawiamy dane do tablic, wydaje się rozsądne usunąć stąd ten kod. Jako że usunęliśmy wywołania renderowania promieni z obiektu minimap, renderowanie obsłużymy przy użyciu tablicy, w której przechowujemy promienie. Ponadto w obliczaniu wartości zmiennej percentageDistance od tej pory wykorzystujemy ustawioną wcześniej własność maxDistance. Ostatnia zmiana w tej funkcji polega na usunięciu bezpośredniego wywołania funkcji canvas. drawSliver i zastąpieniu go testem sprawdzającym, czy ściana do wyrenderowania jest bliżej, czy dalej od gracza niż dinozaur. Jeśli jest blisko, dodajemy ją do tablicy foregroundSlivers. Jeśli jest dalej, dodajemy ją do tablicy backgroundSlivers. Pamiętaj, że tablice te są czyszczone w każdym wywołaniu funkcji castRays. Teraz obiekt raycaster służy jedynie do generowania danych do wyrenderowania. Dzięki temu raycastera nie musimy uruchamiać w pętli rysującej. Możemy przenieść wywołanie funkcji castRays do funkcji update, jak pokazano na listingu 8.28. Listing 8.28. Wywołanie funkcji 8.28 w funkcji update this.update = function(){ raycaster.castRays(); ... };

204  Rozdział 8. GRY FPS Teraz funkcja draw nie musi już wywoływać funkcji castRays, ale musi za to przeglądać utworzone przez nas tablice, aby wyrenderować bloki i dinozaura. Dodaj do niej pogrubiony kod pokazany na listingu 8.29. Listing 8.29. Modyfikacje głównej funkcji rysowania this.draw = function(){ minimap.draw(); player.draw(); canvas.blank(); for(var i = 0; i < backgroundSlivers.length; i++){ canvas.drawSliver.apply(canvas, backgroundSlivers[i]); }; if (dino.show){ dino.draw(); }; for(var i = 0; i < foregroundSlivers.length; i++){ canvas.drawSliver.apply(canvas, foregroundSlivers[i]); }; };

W tym kodzie wykonujemy trzy czynności i kolejność ich wykonywania jest ważna. Najpierw renderujemy wszystkie części tła. Następnie renderujemy dinozaura na tle. Na koniec renderujemy bloki przed dinozaurem. Zwróć uwagę, że została użyta funkcja apply wywołująca funkcję drawSliver z kontekstem this (w tym przypadku jest to kanwa) jako pierwszym parametrem i tablicą argumentów jako drugim parametrem. Do obiektu dino wkrótce przejdziemy, ale najpierw dokonamy w funkcji draw obiektu minimap zmian przedstawionych na listingu 8.30. Listing 8.30. Modyfikacje funkcji draw obiektu minimap var minimap = { init: function(){ ... this.draw = function(){ ... for(var i = 0; i < this.rays.length; i++){ this.drawRay(this.rays[i][0], this.rays[i][1]) } }; this.drawRay = function(xHit, yHit){ this.context.beginPath(); this.context.moveTo(this.cellWidth*player.x, this.cellHeight*player.y); this.context.lineTo(xHit * this.cellWidth, yHit * this.cellHeight); this.context.closePath(); this.context.stroke(); }; } };

Większość kodu obiektu minimap pozostała niezmieniona. Zmiany dotyczą tego, że teraz przeglądamy tablicę rays, a funkcji drawRay, która działa tak samo jak wcześniej, przekazujemy tylko zmienne xHit i yHit. Cała zawartość funkcji drawRay jest kopią zawartości funkcji draw obiektu raycaster z kilkoma nowymi nazwami zmiennych.

Receptura. Dodawanie przyjaciół i wrogów  205

Na koniec zdefiniujemy jeszcze obiekt dino. Kod przedstawiony na listingu 8.31 wklej nad funkcją draw i pod obiektem palette. Listing 8.31. Obiekt dino var dino = { init: function(){ this.sprite = new jaws.Sprite({image: "dino.png", x: 0, y: canvas.height/2, anchor: "center"}); this.x = 12; this.y = 4; this.show = false; this.distance = 10000; this.draw = function(){ this.scale = raycaster.maxDistance / dino.distance / 2; this.sprite.scaleTo(this.scale); this.angle %= twoPi; if (this.angle < 0) this.angle += twoPi; this.angleInDegrees = this.angle * 180 / Math.PI; var potentialWidth = 300*.2; var halfAngularWidth = potentialWidth/2; this.adjustedAngle = this.angleInDegrees + halfAngularWidth; if(this.adjustedAngle > 180 || this.adjustedAngle < -180){ this.adjustedAngle %= 180; }; this.sprite.x = this.adjustedAngle/potentialWidth*canvas.width; this.sprite.draw(); }; } };

Najpierw utworzyliśmy sprite’a jaws jako własność obiektu dino. Przy wyświetlaniu dinozaura najważniejsza jest dla nas własność x sprite’a. Zakotwiczenie (anchor) sprite’a na środku i ustawienie własności y na połowę wysokości kanwy sprawia, że dinozaur będzie wyśrodkowany na osi y. Następnie we własnościach x i y określamy położenie dinozaura na poziomie bloków. W przypadku naszej mapy są dostępne wartości z przedziału od 0 do 15. Nie pomyl tych własności z własnościami x i y sprite’a, które określają położenie na kanwie właśnie sprite’a. Następnie ustawiamy własność show na false, aby dinozaur był początkowo niewidoczny. Własność distance została pierwotnie ustawiona na dużą wartość, aby ściany z foregroundSlivers były renderowane, dopóki nie pojawi się dinozaur. Funkcja dino.draw rozpoczyna się od ustawienia skali sprite’a na maksymalną możliwą odległość (przekątna mapy) podzieloną przez odległość między graczem a dinozaurem. Dzielenie przez dino.distance jest ważne, ponieważ dzięki temu w miarę zbliżania się gracza do dinozaura zwierz będzie się stawał coraz większy. Liczba 2 została wybrana arbitralnie, ale dzięki niej dinozaur całkiem dobrze wpasowuje się w komórkę, w której się znajduje. Następnie wykorzystujemy funkcję Jaws scaleTo do zmiany rozmiaru dinozaura. Później upewniamy się, że kąt mieści się w dodatnim przedziale i nie jest zbyt duży. Następną czynnością jest konwersja kąta na stopnie i przypisanie go do zmiennej angleInDegrees. Robimy to, ponieważ szerokość używanej przez nas cząstki wynosi .2 stopnia. Mnożymy tę wartość przez 300, czyli liczbę promieni, aby otrzymać 60, czyli pole widzenia gracza. Wartość tę przypisujemy do zmiennej potentialWidth, którą następnie dzielimy przez dwa, aby obliczyć

206  Rozdział 8. GRY FPS miarę kąta połowy kanwy (halfAngularWidth). Otrzymaną wartość dodajemy do zmiennej angle InDegrees i wynik przypisujemy do zmiennej adjustedAngle. Określa ona, gdzie w przedziale od 0 do 60 stopni ma się pojawić sprite. Upewniamy się, że wartość zmiennej adjustedAngle mieści się w odpowiednim przedziale, a następnie dzielimy ją przez potentialWidth (60) i mnożymy przez canvas.width (300). Wynik tego działania przypisujemy własności x sprite’a. Na koniec renderujemy sprite’a przy użyciu funkcji sprite.draw biblioteki Jaws. Teraz powinno już dać się robić zdjęcia dinozaura oraz kolorowych ścian. Dla zabawy zmienimy filtr aparatu na sepię, jak pokazano na listingu 8.32. Listing 8.32. Włączenie sepii var camera = { ...

// Zastąp //this.expose(50) // tym this.sepia() .render(); ... }

Teraz możesz robić staroświeckie zdjęcia, takie jak pokazane na rysunku 8.7.

Rysunek 8.7. Staroświeckie zdjęcie dinozaura

Podsumowanie  207

Podsumowanie Zaczynając czytać ten rozdział, może nie wiedziałeś nic o raycasterach i niewiele na temat geometrii oraz mogłeś nigdy do tej pory nie słyszeć o funkcji dataUri do kodowania danych kanwy w adresach. Jeśli znałeś te wszystkie techniki już wcześniej, to przynajmniej teraz masz do dyspozycji prosty silnik, na bazie którego możesz budować różne typy gier, takie jak różnego rodzaju strzelanki, gry o przetrwanie, wyścigi i przygodowe podobne do Myst. Do najważniejszych naszych zadań należało posługiwanie się pętlą gry, obsługa danych wejściowych oraz korzystanie z funkcji zarządzania sprite’ami biblioteki Jaws. Jeśli zainteresowała Cię ta biblioteka, to ma ona jeszcze wiele innych ciekawych funkcji, np. obsługę niesynchronicznie przewijanych teł, map kafelkowych oraz obszarów widoku (możliwości wyświetlania części kanwy wokół gracza). Jeśli chodzi o możliwości rozbudowy gry, to jest ich wiele. Można zaimplementować możliwość kucania, skakania i chodzenia w bok. Kolory na ścianach można zastąpić teksturami. Jeśli lubisz fotografować prehistoryczne obiekty, możesz dodać kilka włochatych mamutów albo utworzyć zoo. Gdyby mapa była dostatecznie duża, można by było utworzyć na niej grę w chowanego z różnymi stworami. Można dodać zegar oraz losowe generowanie map i rozmieszczanie na nich stworów. Można też zbudować własne konkretne poziomy. Możesz też w grze z rozdziału 2. zrobić przekierowanie do tej gry, gdy gracz kliknie maszynę czasu. Oczywiście można też zbudować klasyczną strzelankę z paskiem zdrowia, przedmiotami zwiększającymi moc, różnymi rodzajami broni oraz ganiającymi gracza i strzelającymi do niego wrogami. Gracz może odpłacać się wrogom tym samym. W istocie zarówno Minecraft, jak i Myst mogłyby stać się klonami gry Doom. Nie wahaj się też szukać jeszcze innych możliwości.

208  Rozdział 8. GRY FPS

9 RPG

Gra może być wciągająca z dwóch powodów: dzięki możliwości eksploracji ciekawego świata albo wymagającym zadaniom o odpowiednim poziomie trudności. Weźmy np. takie gry jak World of Warcraft i Pong. W WoW cały czas w niewidoczny dla gracza sposób jest tworzona nowa treść. Natomiast w grze Pong gracza wciąga rywalizacja z komputerem lub z innym graczem. Także wiele nowych gier zawiera elementy eksploracji świata, a kategoria ta jest domeną gier RPG. W grze Dragon Warrior na konsolę NES jest tak dużo rozmaitych rodzajów treści, że konieczne było zastosowanie interfejsów do zarządzania treścią, dialogów, robienia zakupów, przechowywania przedmiotów oraz walczenia. Czas potrzebny na eksplorację tych światów jest na tyle długi, że wygodniejsze stało się zapisywanie postępu gry przy użyciu baterii niż stosowanie niezgrabnego systemu hasłowego. Każda wypełniona treścią gra czerpie z tradycji gier RPG.

210  Rozdział 9. RPG

Receptura. Wprowadzenie do biblioteki enchant.js Kiedyś ściśle rozróżniało się gry RPG akcji i strategie turowe, ale z czasem te różnice zaczęły się zacierać. W strategicznych grach akcji walka odbywa się na tym samym ekranie co eksploracja świata. To odróżnia takie gry jak The Legend of Zelda od turowych gier RPG, takich jak Final Fantasy czy Dragon Warrior. Składniki budowy gry akcji (wykrywanie kolizji) zbadaliśmy już we wcześniejszych rozdziałach. W tym rozdziale zatem pozostaje nam budowa symulatora bitwy z menu, magazynem, sklepem i systemem poziomów, a także zaimplementowanie funkcji zapisywania postępu w grze. Zaczniemy jak zwykle od utworzenia pliku index.html o zawartości przedstawionej na listingu 9.1. Listing 9.1. Plik index.html RPG

Kod ten powinien wyglądać znajomo. Wczytujemy w nim pliki JavaScript i arkusz stylów oraz ustawiamy tytuł. W czwartym wierszu znajduje się jednak coś nowego. Jest to znacznik ustawiający kodowanie strony. W wielu przypadkach można go opuścić, ale może to spowodować problemy z wyświetlaniem polskich znaków. Jeśli takie wystąpią, należy dodać ten niewielki fragment kodu i wszystko powinno wrócić do normy. Kolejną czynnością jest utworzenie pliku game.css o treści przedstawionej na listingu 9.2. Listing 9.2. Zawartość pliku game.css body { margin: 0; }

Ostatnie zadanie w tej recepturze to utworzenie pliku game.js z kodem pokazanym na listingu 9.3. Listing 9.3. Wstępna wersja pliku game.js enchant(); window.onload = function(){ var game = new Game(300, 300); game.fps = 15; game.onload = function(){ alert("Cześć");

Receptura. Tworzenie mapy  211

}; game.start(); };

Pierwszy wiersz tego kodu sprawia, że mamy dostęp do głównych klas biblioteki enchant.js, takich jak np. Game. Blok window.onload jest wykonywany po załadowaniu okna. Robimy to w ten sposób po to, aby mieć czas na załadowanie elementu body, co jest ważne, ponieważ utworzenie nowego obiektu Game wymaga utworzenia dwóch elementów div w body. Parametry przekazywane do konstruktora Game to wysokość i szerokość planszy gry. Następnie ustawiamy liczbę klatek na sekundę (fps). Funkcja game.onload jest wykonywana po uruchomieniu funkcji game.start. Jeśli teraz otworzysz plik index.html w przeglądarce internetowej, zostanie wyświetlone okienko alertu z napisem „Cześć”. UWAGA Biblioteka enchant.js ma kilka właściwości, o których warto wiedzieć. Po pierwsze, jeśli zapomnisz umieścić kod tworzący grę w bloku window.onload, biblioteka zgłosi w konsoli błąd, informując, co zostało zrobione źle. To świadczy o tym, jak solidnie została napisana, czego nie można powiedzieć o wielu innych bibliotekach. Druga ważna właściwość, której nie widać od razu, to tworzenie wielu obiektów canvas i innych elementów w strukturze DOM. Nie widzieliśmy jeszcze tylko mieszanego rozwiązania tego typu.

W następnej recepturze utworzymy mapę.

Receptura. Tworzenie mapy Mapa (duża dwuwymiarowa tablica) wymaga dużej ilości kodu, więc umieścimy ją w osobnym pliku JavaScript. W związku z tym dodaj do pliku index.html wiersz kodu pogrubiony na listingu 9.4. Listing 9.4. Ładowanie pliku mapy

Następnie utworzymy plik map.js do przechowywania dwuwymiarowych tablic reprezentujących mapy tła i pierwszego planu. Ich kod jest przedstawiony na listingu 9.5. Listing 9.5. Plik map.js var mapData = 1, 1, 1], [1, 1, 1, [1, 1, 1, [1, 1, 1, [1, 1, 1, [1, 1, 1,

[[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1, 1, 1, 1, 1,

1], 1], 1], 1], 1],

212  Rozdział 9. RPG [1, [1, [1, [1, [1, [1, [1, [1, [1, [1, [1, [1, [0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,

1], 1], 1], 1], 1], 1], 1], 1], 1], 1], 1], 1], 0]];

var foregroundData = [[-1,-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 3, 3, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1], [3, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 3, 3, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1], [-1, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 3, 3, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1], [3, -1, -1, 3, 3, 3, -1, -1, -1, -1, -1, -1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 13], [-1,-1, -1, 3, -1, 3, -1, -1, -1, 3, -1, -1, 3, 3, 3, 3, -1, -1, -1, -1, -1, 1, -1, -1, 3, 13], [-1,-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 3, 3, -1, -1, -1, -1, -1, -1, -1, -1, 3, 13], [-1,-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 3, 3, -1, -1, -1, -1, -1, -1, -1, -1, 3, 13], [-1,-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 3, 3, -1, -1, -1, -1, -1, -1, -1, -1, 3, 13], [-1,-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 3, 3, -1, -1, -1, -1, -1, -1, -1, -1, 3, 13], [-1,-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 3, 3, -1, -1, -1, -1, -1, -1, -1, -1, 3, 13], [-1,-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 3, 3, -1, -1, -1, -1, -1, -1, -1, -1, 3, 13], [-1,-1, -1, 3, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 3, 3, -1, -1, -1, -1, -1, -1, -1, -1, 3, 13], [-1,-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 3, 3, -1, -1, -1, -1, -1, -1, -1, -1, 3, 13], [-1,-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 3, 3, -1, -1, -1, -1, -1, -1, -1, -1, 3, 13], [-1,-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 3, 3, -1, -1, -1, -1, -1, -1, -1, -1, 3, 13], [-1,-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 3, 3, -1, -1, -1, -1, -1, -1, -1, -1, 3, 13], [-1,-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 3, 3, -1, -1, -1, -1, -1, -1, -1, -1, 3, 13], [-1,-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 3, 3, -1, -1, -1, -1, -1, -1, -1, -1, 3, 13], [14,14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14]];

Utworzyliśmy zmienne mapData i foregroundData. Aby załadować mapy z tymi danymi, przejdziemy z powrotem do pliku game.js i dodamy kod wyróżniony pogrubieniem na listingu 9.6.

Receptura. Tworzenie mapy  213

Listing 9.6. Ładowanie mapy enchant(); window.onload = function(){ var game = new Game(300, 300); game.fps = 15; game.spriteWidth = 16; game.spriteHeight = 16; game.preload('sprites.png'); var map = new Map(game.spriteWidth, game.spriteHeight); var foregroundMap = new Map(game.spriteWidth, game.spriteHeight); var setMaps = function(){ map.image = game.assets['sprites.png']; map.loadData(mapData); foregroundMap.image = game.assets['sprites.png']; foregroundMap.loadData(foregroundData); }; var setStage = function(){ var stage = new Group(); stage.addChild(map); stage.addChild(foregroundMap); game.rootScene.addChild(stage); }; game.onload = function(){ setMaps(); setStage(); }; game.start(); };

Najpierw zdefiniowaliśmy nowe atrybuty dla obiektu game dotyczące wymiarów sprite’a. Następnie ładujemy plik sprite’a. Później tworzymy obiekty mapy (map) i mapy pierwszego planu (foregroundMap) przy użyciu wcześniej zdefiniowanych szerokości i wysokości sprite’a. Deklarujemy dwie funkcje, które są wywoływane w funkcji game.onload. Pierwsza z nich to setMaps służąca do ustawiania pliku obrazu dla każdej z map na sprites.png i ładująca odpowiednie dane z pliku map.js. Druga funkcja nazywa się setStage i służy do tworzenia nowego obiektu biblioteki enchant.js typu Group o nazwie stage (plansza). Obiekt ten zawiera wszystko, co jest potrzebne na scenie. Za pomocą funkcji addChild dodajemy mapy do tej planszy. Na koniec dodajemy planszę do sceny głównej (rootScene) obiektu game, która w naszym przypadku będzie widokiem mapy z lotu ptaka. Gdybyśmy mieli ekran tytułowy, to on mógłby być sceną główną. Później utworzymy też sceny do prowadzenia bitw i robienia zakupów. Pewnie zastanawiasz się, co te obiekty reprezentują w strukturze DOM i jaki mają związek z elementem canvas. Jeśli teraz sprawdzisz zawartość drzewa DOM, znajdziesz dwa nowe elementy div, z których każdy zawiera po jednym elemencie canvas. Odpowiadają one obiektom map. Natomiast obiekt stage reprezentuje jedynie kolekcję elementów w pamięci, które nie mają reprezentacji w DOM. Po dodaniu do pliku kodu z listingu 9.6 plik index.html w przeglądarce internetowej przedstawia się tak jak na rysunku 9.1. Zwróć uwagę, że przez przezroczyste części drzew widać trawę.

214  Rozdział 9. RPG

Rysunek 9.1. Załadowane mapy tła i pierwszego planu

Receptura. Dodawanie gracza Mając gotową mapę, możemy dodać obiekt gracza, którym będziemy mogli po tej mapie się poruszać. Efektem tego będzie pojawienie się na stronie nowego bezwzględnie pozycjonowanego elementu div. Na listingu 9.7 jest przedstawiona nowa wersja zawartości pliku game.js. Listing 9.7. Plik game.js z obsługą ruchu gracza enchant(); window.onload = function(){ var game = new Game(300, 300); game.spriteSheetWidth = 256; game.spriteSheetHeight = 16; ... var setStage = function(){ var stage = new Group(); stage.addChild(map);

Receptura. Dodawanie gracza  215

stage.addChild(player); stage.addChild(foregroundMap); game.rootScene.addChild(stage); }; var player = new Sprite(game.spriteWidth, game.spriteHeight); var setPlayer = function(){ player.spriteOffset = 5; player.startingX = 6; player.startingY = 14; player.x = player.startingX * game.spriteWidth; player.y = player.startingY * game.spriteHeight; player.direction = 0; player.walk = 0; player.frame = player.spriteOffset + player.direction; player.image = new Surface(game.spriteSheetWidth, game.spriteSheetHeight); player.image.draw(game.assets['sprites.png']); }; player.move = function(){ this.frame = this.spriteOffset + this.direction * 2 + this.walk; if (this.isMoving) { this.moveBy(this.xMovement, this.yMovement); if (!(game.frame % 2)) { this.walk++; this.walk %= 2; } if ((this.xMovement && this.x % 16 === 0) || (this.yMovement && this.y % 16 === 0)) { this.isMoving = false; this.walk = 1; } } else { this.xMovement = 0; this.yMovement = 0; if (game.input.up) { this.direction = 1; this.yMovement = -4; } else if (game.input.right) { this.direction = 2; this.xMovement = 4; } else if (game.input.left) { this.direction = 3; this.xMovement = -4; } else if (game.input.down) { this.direction = 0; this.yMovement = 4; } if (this.xMovement || this.yMovement) { var x = this.x + (this.xMovement ? this.xMovement / Math.abs(this.xMovement) * 16 : 0); var y = this.y + (this.yMovement ? this.yMovement / Math.abs(this.yMovement) * 16 : 0); if (0 <= x && x < map.width && 0 <= y && y < map.height) { this.isMoving = true; this.move(); } } } };

216  Rozdział 9. RPG game.focusViewport = function(){ var x = Math.min((game.width - 16) / 2 - player.x, 0); var y = Math.min((game.height - 16) / 2 - player.y, 0); x = Math.max(game.width, x + map.width) - map.width; y = Math.max(game.height, y + map.height) - map.height; game.rootScene.firstChild.x = x; game.rootScene.firstChild.y = y; }; game.onload = function(){ setMaps(); setPlayer(); setStage(); player.on('enterframe', function() { player.move(); }); game.rootScene.on('enterframe', function(e) { game.focusViewport(); }); }; game.start(); };

Patrząc od góry na pogrubione wiersze, najpierw ustawiliśmy wymiary arkusza sprite’ów, ponieważ będą nam później potrzebne przy deklaracji powierzchni (surface) i gracza (player). Później w funkcji setStage dodaliśmy gracza (przypomnę, że ostatecznie ma on postać elementu div) do stage. Obiekt player zadeklarowaliśmy jako nowego sprite’a o wymiarach 16×16 pikseli. Dalej mamy trzy funkcje związane z graczem. Przejdźmy na koniec listingu (do funkcji game. onload), aby zobaczyć, jak są wywoływane. Najpierw funkcja setPlayer definiuje własności obiektu player. Własność spriteOffset przechowuje dane miejsca rozpoczęcia pojawiania się sprite’ów gracza na arkuszu sprite’ów (jeśli otworzysz plik PNG, to zauważysz, że sprite’y nie są ustawione przy lewej krawędzi). Własności startingX i startingY określają, w której komórce mapy powinien początkowo znaleźć się gracz. Są mnożone przez 16 (szerokość sprite’a). Własność direction jest liczbową reprezentacją kierunku, w którym patrzy gracz. Własność walk może mieć wartość 0 lub 1 w zależności od tego, na jakim etapie w procesie ruchu między prostokątami znajduje się gracz. Własność frame to pojedynczy sprite, którego chcemy wyrenderować. Początkowo jej wartość to spriteOffset plus direction. Ostatnia czynność w funkcji setPlayer to utworzenie nowej powierzchni (Surface), przypisanie jej do własności player.image oraz narysowanie jej w kontekście sprite’ów opisanych w funkcji draw. O tym, który sprite zostanie narysowany, decyduje ustawienie własności frame. WSKAZÓWKA Jako że zdefiniowaliśmy trochę nowych własności gracza, może być trudno zorientować się, które własności i funkcje należą do biblioteki enchant.js (frame i image), a które są nasze własne (pozostałe) oraz które są rodzimymi elementami języka JavaScript (np. length). Aby się nie pogubić, najlepiej cały czas mieć pod ręką dokumentację biblioteki enchant.js, którą można znaleźć pod adresem (http://wise9.github.com/enchant.js/doc/core/en/index.html). Jeśli chodzi o język JavaScript, to doskonałym źródłem informacji jest dokumentacja w serwisie Mozilla Developer Network. Natomiast własne własności można zapamiętać dzięki komentarzom w kodzie i tworzeniu dla nich nowych obiektów.

Receptura. Dodawanie warstwy kolizji  217

Dalej mamy funkcję player.move, która jest wywoływana w funkcji player.on("enterframe"). Funkcja ta jest wywoływana przy przetwarzaniu każdej kolejnej klatki gracza. Jest czymś w rodzaju pętli aktualizującej tylko dla tego obiektu. Funkcja player.move wykorzystuje kilka atrybutów, ale ogólnie rzecz biorąc, jej najważniejszym zadaniem jest określenie klatki i pozycji gracza. Spójrz na początek deklaracji funkcji player.move. W części określającej klatkę wartość direction jest mnożona przez dwa, ponieważ na każdy kierunek są po dwie klatki. Własność walk może mieć wartość 0 lub 1 oznaczającą zmianę sprite’a. Później sprawdzamy, czy gracz się porusza (isMoving). Od razu przejdziemy do klauzuli else, bo ta zostanie wykonana jako pierwsza (własność isMoving jest niezdefiniowana). Najpierw inicjujemy zerem własności xMovement i yMovement. Następnie sprawdzamy, czy są jakieś dane wejściowe, oraz odpowiednio określamy kierunek ruchu i jego odległość w pionie lub poziomie. Te ustawienia działają jak określanie prędkości i dlatego ich zmiana spowoduje, że gracz będzie poruszał się szybciej lub wolniej. Później, jeśli jest zamierzony jakiś ruch, następuje ustawienie zmiennej x lub y na bieżącą pozycję gracza plus szerokość sprite’a (16 dla dodatnich wartości, 0 dla 0 oraz -16 dla ujemnych wartości). Ostatnia instrukcja warunkowa w tym miejscu określa, czy ruch, jaki gracz chce wykonać, nie spowoduje wyjścia poza mapę. Jeśli tak, własność isMoving zostaje ustawiona na true i ponownie rekurencyjnie wywołujemy tę samą funkcję move. Teraz ponownie aktualizujemy klatkę sprite’a. Własność isMoving ma wartość true, więc wywołujemy funkcję biblioteki enchant.js moveBy, aby przesunąć sprite’a o xMovement i yMovement. Jeśli klatka jest parzysta, to walk ustawiamy na 0. Następna instrukcja warunkowa sprawdza, czy znajdujemy się w samym środku kwadratu i czy jest planowany ruch. Jeśli tak, ustawiamy walk na 1 oraz, co ważniejsze, ustawiamy isMoving na false. W następnym wykonaniu tej funkcji, które nastąpi w wyniku odświeżenia sprite’a, zostanie wykonana ścieżka else w celu odnalezienia danych wejściowych od użytkownika i ponownego rozpoczęcia cyklu. W innych rozdziałach tej książki widzieliśmy prostsze metody aktualizacji pozycji sprite’ów w pionie i poziomie, ale w ten sposób otrzymujemy typowy dla gier RPG sposób poruszania się po jednym kwadracie. Funkcja focusViewport, która jest uruchamiana przy każdej aktualizacji obszaru widoku, ustawia pozycję x i y ekranu tak, aby gracz w miarę możliwości zawsze znajdował się na jego środku. Po wprowadzeniu opisanych zmian na planszy gry powinna już być widoczna postać gracza, jak widać na rysunku 9.2. Za pomocą klawiszy strzałek można nią poruszać.

Receptura. Dodawanie warstwy kolizji Aktualnie nasza postać może przechodzić przez drzewa. Koniecznie trzeba to zmienić, wprowadzając kilka drobnych poprawek w pliku game.js. Na listingu 9.8 umieściłem zmodyfikowany kod funkcji setMaps, zawierający warstwę kolizji opartą na typach sprite’ów, które znajdują się na mapie foregroundMap. Listing 9.8. Dodanie danych dotyczących kolizji do mapy var setMaps = function(){ map.image = game.assets['sprites.png']; map.loadData(mapData); foregroundMap.image = game.assets['sprites.png']; foregroundMap.loadData(foregroundData);

218  Rozdział 9. RPG

Rysunek 9.2. Postać gracza na ekranie var collisionData = []; for(var i = 0; i< foregroundData.length; i++){ collisionData.push([]); for(var j = 0; j< foregroundData[0].length; j++){ var collision = foregroundData[i][j] %13 > 1 ? 1 : 0; collisionData[i][j] = collision; } } map.collisionData = collisionData; };

Utworzyliśmy dwuwymiarową tablicę, ustawiającą każdy rodzaj sprite’a znajdującego się w mapie pierwszego planu oprócz -1 (pusty), 0 (trawa), 1 (woda) oraz 13 (drzewa, przez które można przechodzić, są dobre do tworzenia tajemnych przejść) na 0. Wszystko pozostałe zostaje ustawione na 1, co oznacza, że nie da się przez to przejść. Mając dane kolizji ustawione jako atrybut obiektu map, możemy przejść do implementacji algorytmu wykrywania. Umieścimy go w funkcji player.move — pogrubiony kod na listingu 9.9.

Receptura. Dodawanie warstwy kolizji  219

UWAGA W tym rozdziale od czasu do czasu używam pojęcia obszaru widoku, powinniśmy zatem uporządkować pewne kwestie związane z tym pojęciem. W tym tekście obszar widoku (ang. viewport) odnosi się do kamery podążającej za graczem. Niektóre silniki gier udostępniają funkcje wyższego poziomu umożliwiające powiązanie obszaru widoku (kamery) z wybranym obiektem, tak jak to zrobiliśmy tutaj. Istnieje też pokrewne pojęcie w postaci znacznika meta o nazwie viewport, który jest przydatny do kontrolowania renderowania na urządzeniach przenośnych. W przypadku gier wyłączenie funkcji zoomu może być uzasadnione, ale pamiętaj też o wszystkich użytkownikach niemających sokolego wzroku i nie uniemożliwiaj powiększania tekstu, odnośników ani grafik. Tego rozszerzenia używa się w następujący sposób:

Mobilna wersja przeglądarki Safari (dostępna w iPhonie) ma jeszcze dwa dodatkowe znaczniki meta: apple-mobile-web-app-status-bar-style i apple-mobile-web-appcapable. Pierwszy służy do kontrolowania stylu paska stanu, a drugi do włączania trybu pełnoekranowego w przeglądarce. W komputerach stacjonarnych tryb pełnoekranowy przeglądarki można ustawić przy użyciu API HTML5. W tym trybie znikają wszystkie elementy dostarczone przez system operacyjny, np. paski narzędzi, jak również elementy przeglądarki, takie jak np. pasek adresu. API to jest obsługiwane przez wszystkie przeglądarki na komputery stacjonarne (z wyjątkiem Internet Explorera) i przez Firefoksa na urządzenia przenośne. Problem z tą funkcją polega na tym, że do jej włączenia jest potrzebna zgoda użytkownika. W typowej aplikacji sieciowej takie pytanie nękające użytkownika jest źle widziane, ale gracze zapewne lepiej zareagują na pytanie typu: „Czy chcesz włączyć tryb pełnoekranowy?”.

OSTRZEŻENIE PROBLEMY Z PRZEGLĄDARKĄ CHROME. Przeglądarka Chrome może mieć problemy z napisanym przez nas kodem. Firefox i Safari będą działać bez zarzutu, ale Chrome nie podoba się sposób, w jaki są zmieniane sprite’y postaci. Przeglądarka ta nie aktualizuje poprawnie animacji chodzenia, a co gorsza, postać wyświetla tylko wtedy, gdy znajduje się ona w lewym górnym kwadracie ekranu. Przeglądarka Chrome rzadko sprawia problemy, ale w tym przypadku wydaje się, że napisane przez nas funkcje nie działają przez jakieś zabiegi mające na celu optymalizację wydajności przeglądarki. Listing 9.9. Wykrywanie kolizji player.move = function(){ this.frame = this.spriteOffset + this.direction * 2 + this.walk; if (this.isMoving) { ... } else { ... if (this.xMovement || this.yMovement) { var x = this.x + (this.xMovement ? this.xMovement / Math.abs(this.xMovement) * 16 : 0); var y = this.y + (this.yMovement ? this.yMovement / Math.abs(this.yMovement) * 16 : 0);

220  Rozdział 9. RPG if (0 <= x && x < map.width && 0 <= y && y < map.height && !map.hitTest(x, y)) { this.isMoving = true; this.move(); } } } };

Funkcja hitTest i własność collisionData pochodzą z biblioteki enchant.js. Teraz można przechodzić tylko przez specjalny rodzaj drzew, które zajmują cały pas przy prawej krawędzi ekranu. Można też chodzić po wodzie. Zwróć uwagę, że woda ma dwie warstwy. Warstwa tła jest pełnym sprite’em, a warstwa pierwszego planu w górnej części jest przezroczysta. Dzięki temu powstaje wrażenie, jakby gracz brnął przez wodę.

Receptura. Ekran stanu Czas wzbogacić rozgrywkę o dodatkowe możliwości, bo na razie nasz ludzik może tylko chodzić między drzewami, co może mu się szybko znudzić. Nadamy mu kilka atrybutów, którymi będzie mógł się pochwalić. Na początek włączymy dodatkowy klawisz powodujący wyświetlenie bieżącego stanu gracza. W tym celu zdefiniujemy nowe wiązanie klawiszy i dodamy do niego procedurę nasłuchową zdarzeń. Kod wiązania możemy umieścić na początku pliku game.js — listing 9.10. Listing 9.10. Powiązanie spacji z klawiszem „a” enchant(); window.onload = function(){ var game = new Game(300, 300); game.keybind(32, 'a');

Z początku może się wydawać, że wiążemy klawisz „a”. Ale w rzeczywistości wiązana jest spacja, która ma numer 32. W bibliotece enchant.js istnieje specjalny system wiązania klawiszy z tzw. przyciskami (ang. button). Klawisze strzałek są domyślnie powiązane z odpowiednimi przyciskami, a przyciski „a” i „b” pozostają nieprzypisane. Może się wydawać dziwne, że przyciski nie mają zdefiniowanych stałych, jak to jest w innych bibliotekach, takich jak np. Jaws czy Atom. Prawdopodobnie ma to jednak związek z tym, że biblioteka enchant.js jest przystosowana do pracy na urządzeniach przenośnych, w których z reguły nie ma zwykłej klawiatury. W rozszerzeniu biblioteki znajdują się implementacje zarówno wirtualnego krzyżaka do sterowania, jak i analogowego pada. Jeśli więc zdecydujesz się napisać grę dla urządzeń przenośnych, to umieszczenie na ekranie wirtualnego krzyżaka do sterowania i klikalnych lub dotykalnych przycisków może być lepszym rozwiązaniem niż obsługa klawiatury. UWAGA Jeśli opisane możliwości odbierania danych od gracza Cię nie zadowalają, to może zainteresuje Cię API obsługi padów do gier, które umożliwia wiązanie zdarzeń z przyciskami podłączonego do komputera kontrolera. Dla fanów gier może to być dobry odpoczynek od myszy, interfejsu dotykowego i klawiatury.

Receptura. Ekran stanu  221

Teraz utworzymy procedurę nasłuchową dla naszego „przycisku a”, aby jego naciśnięcie powodowało wyświetlenie informacji o stanie gracza. Pogrubiony kod przedstawiony na listingu 9.11 należy dodać pod koniec pliku game.js w funkcji game.onload. Listing 9.11. Procedura nasłuchowa dla „przycisku a” game.onload = function(){ setMaps(); setPlayer(); setStage(); player.on('enterframe', function() { player.move(); if (game.input.a) { player.displayStatus(); }; }); ...

Nie ma wątpliwości, że kolejną naszą czynnością powinno być napisanie implementacji funkcji displayStatus. Ale funkcja ta powinna wyświetlać atrybuty gracza. Ponadto jest jeszcze do rozwiązania problem samego rysowania obiektu. Wszystkie potrzebne zmiany i dodatki zostały pokazane na listingu 9.12. Listing 9.12. Przygotowywanie danych do wyświetlania i ich wyświetlanie var setStage = function(){ ... stage.addChild(foregroundMap); stage.addChild(player.statusLabel); game.rootScene.addChild(stage); }; var player = new Sprite(game.spriteWidth, game.spriteHeight); var setPlayer = function(){ ... player.name = "Ryszard"; player.characterClass = "Klanu"; player.exp = 0; player.level = 1; player.gp = 100; player.hp = 10; player.maxHp = 10; player.mp = 0; player.maxMp = 0; player.statusLabel = new Label(""); player.statusLabel.width = game.width; player.statusLabel.y = undefined; player.statusLabel.x = undefined; player.statusLabel.color = '#fff'; player.statusLabel.backgroundColor = '#000'; }; player.displayStatus = function(){ player.statusLabel.text = "--" + player.name + " z " + player.characterClass + "
--Zdrowie: "+player.hp + "/" + player.maxHp + "
--Magia: "+player.mp + "/" + player.maxMp + "
--Dośw.: "+player.exp +

222  Rozdział 9. RPG "
--Poziom: " + player.level + "
--Złoto: " + player.gp; };

W funkcji setStage dodaliśmy etykietę, którą wypełniamy tekstem w wywołaniu funkcji player. displayStatus. Jeśli nie ma żadnego tekstu, etykieta ukrywa się. Będziemy wykorzystywać ten fakt zamiast bezpośrednio wyświetlać i ukrywać etykietę. Następnie w funkcji setPlayer inicjujemy wartości typowych atrybutów gier RPG. Skrót gp oznacza „gold pieces”, czyli liczbę sztuk złota, hp to zdrowie (ang. hit points), mp to punkty magii (mana, ang. magic points), a exp to punkty doświadczenia (ang. experience). Jeszcze słowo o własności characterClass. Można by było napisać po prostu class, gdyby nie to, że w języku JavaScript słowo to jest zarezerwowane. Następne są atrybuty etykiety stanu (statusLabel). Najpierw inicjujemy samą etykietę pustym łańcuchem, aby pozostawała ukryta, dopóki nie zostanie do niej dodany jakiś tekst. Szerokość etykiety ustawiliśmy na szerokość gry. Przypisanie własnościom x i y wartości undefined może wydawać się dziwne, ale chodzi o to, aby domyślna wartość tych atrybutów wynosiła 0, dzięki czemu etykieta zostanie umieszczona w lewym górnym rogu mapy niezależnie od miejsca, w którym aktualnie przebywa postać w grze. Użycie wartości undefined powoduje dokładnie to, czego chcemy, czyli umieszcza etykietę zawsze na górze ekranu. Kod jest prostszy i przynajmniej w tej grze takie rozwiązanie jest niezawodne. Po ustawieniu kolorów przechodzimy do funkcji displayStatus. Jej zadaniem jest ustawienie tekstu etykiety na łańcuch powstały z połączenia kilku łańcuchów z atrybutów gracza. Kod HTML w etykietach jest interpretowany, więc dodaliśmy znacznik
w celu poprawnego rozmieszczenia poszczególnych linijek tekstu. Prawie skończyliśmy pracę w tej recepturze. Pozostało już tylko dodać jakiś sposób na ukrywanie etykiety (ustawianie tekstu na pusty łańcuch). Zrealizujemy to tak, że etykieta będzie znikać po naciśnięciu przez gracza któregokolwiek z klawiszy służących do chodzenia. W tym celu musimy wprowadzić tylko kilka dodatków w kodzie obsługi danych wejściowych w funkcji player.move, jak pokazano na listingu 9.13. Listing 9.13. Funkcja obsługi danych wejściowych wzbogacona o usuwanie informacji o użytkowniku if (game.input.up) { this.direction = 1; this.yMovement = -4; player.statusLabel.text = ""; } else if (game.input.right) { this.direction = 2; this.xMovement = 4; player.statusLabel.text = ""; } else if (game.input.left) { this.direction = 3; this.xMovement = -4; player.statusLabel.text = ""; } else if (game.input.down) { this.direction = 0; this.yMovement = 4; player.statusLabel.text = ""; }

Receptura. Interakcja z postaciami w grze  223

Teraz można wyświetlić informacje o stanie gracza, jak na rysunku 9.3. Gdy zaczniesz iść w którymkolwiek kierunku, to pole zniknie.

Rysunek 9.3. Informacje o stanie gracza

Receptura. Interakcja z postaciami w grze W grach RPG występują postaci, którymi nie można sterować, a z którymi można wchodzić w różne interakcje. Mogą to być np. właściciele sklepów albo mędrcy udzielający wskazówek itp. Czasami zadaniem takiej postaci może być tylko poinformowanie gracza, jak nazywa się pobliskie miasto. Ogólnie rzecz biorąc, te postaci dzięki swoim podpowiedziom, humorowi, zgłaszanym wątpliwościom albo bezmyślności stwarzają wrażenie realnego świata. W tej recepturze utworzymy osobę mówiącą „Cześć”.

224  Rozdział 9. RPG Jeśli nasz gracz ma mieć możliwość prowadzenia rozmów, to przede wszystkim musimy wiedzieć, co się przed nim znajduje. Do tego będą nam potrzebne trzy nowe funkcje w obiekcie player, które dodamy za funkcją player.move, jak pokazano na listingu 9.14. Listing 9.14. Funkcje do określania, jaki sprite znajduje się przed graczem player.square = function(){ return {x: Math.floor(this.x /game.spriteWidth), y: Math.floor(this.y/game.spriteHeight)} } player.facingSquare = function(){ var playerSquare = player.square(); var facingSquare; if(player.direction === 0){ facingSquare = {x: playerSquare.x, y: playerSquare.y + 1} }else if (player.direction === 1) { facingSquare = {x: playerSquare.x, y: playerSquare.y - 1} }else if (player.direction === 2) { facingSquare = {x: playerSquare.x + 1, y: playerSquare.y} }else if (player.direction === 3) { facingSquare = {x: playerSquare.x - 1, y: playerSquare.y} } if ((facingSquare.x < 0 || facingSquare.x >= map.width/16) || (facingSquare.y < 0 || facingSquare.y >= map.height/16)) { return null; } else { return facingSquare; } } player.facing = function(){ var facingSquare = player.facingSquare(); if (!facingSquare){ return null; }else{ return foregroundData[facingSquare.y][facingSquare.x]; } }

Pierwsza z tych funkcji zwraca obiekt informujący, w którym kwadracie aktualnie znajduje się gracz. Funkcja facing.Square zwraca podobny obiekt, tylko dotyczący kwadratu znajdującego się przed graczem, chyba że gracz stoi przy krawędzi mapy, wówczas funkcja zwraca wartość null. Trzecia funkcja o nazwie facing również zwraca null, jeśli gracz spogląda poza mapę. A jeśli gracz patrzy w inne miejsce, funkcja ta zwraca numer sprite’a z obiektu foregroundData. Kod źródłowy, dzięki któremu będzie można rozmawiać z postaciami w grze, znajduje się na listingu 9.15 i należy go wkleić za kodem z listingu 9.14. Listing 9.15. Obiekty umożliwiające rozmawianie z postaciami z gry var npc = { say: function(message){ player.statusLabel.height = 12; player.statusLabel.text = message; } } var greeter = {

Receptura. Interakcja z postaciami w grze  225

action: function(){ npc.say("Cześć"); } }; var spriteRoles = [,,greeter,,,,,,,,,,,,,]

Obiekt npc zawiera tylko jedną metodę o nazwie say, która aktualizuje stan przekazanym do niej tekstem w parametrze message. Następny jest obiekt greeter odpowiedzialny za mówienie „Cześć” poprzez metodę say obiektu npc. Na końcu definiujemy tablicę spriteRoles, przechowującą typy obiektów dla sprite’ów znajdujących się w pliku sprites.png. Jak widać, greeter znajduje się pod indeksem 2, a więc instrukcja spriteRoles[2] powoduje zwrócenie obiektu greeter. Między przecinkami moglibyśmy wpisać 0, undefined albo null, ale do naszych celów wystarczy niejawna wartość undefined zwracana w wyniku pobrania wartości spod indeksu, pod którym nic nie ma. Ostatnia większa zmiana, jaką musimy wprowadzić, znajduje się w funkcji game.onload. Zamiast tylko wywoływać funkcję displayStatus, gdy zostanie naciśnięty przycisk „a”, teraz najpierw sprawdzamy, czy przed graczem nie ma jakiejś postaci, z którą można porozmawiać. Zmodyfikowana funkcja game.onload znajduje się na listingu 9.16. Zmiany jak zwykle zostały oznaczone pogrubieniem. Listing 9.16. Sprawdzenie, czy wyświetlić informacje o stanie gracza, czy wykonać akcję sprite’a game.onload = function(){ setMaps(); setPlayer(); setStage(); player.on('enterframe', function() { player.move(); if (game.input.a) { var playerFacing = player.facing(); if(!playerFacing || !spriteRoles[playerFacing]){ player.displayStatus(); }else{ spriteRoles[playerFacing].action(); }; }; }); game.rootScene.on('enterframe', function(e) { game.focusViewport(); }); };

Czekamy na naciśnięcie spacji (przycisk „a”). Jeśli gracz ma przed sobą kwadrat ze sprite’em, to sprawdzamy numer tego sprite’a w indeksach tablicy spriteRoles. Jeśli go znajdziemy (np. greeter pod indeksem 2), to wykonujemy akcję danego obiektu sprite’a. Wówczas następuje wysłanie tekstu „Cześć” do statusLabel. Na koniec musimy jeszcze tylko jakiemuś sprite’owi w tablicy foregroundData w pliku map.js nadać wartość 2. Później będzie można odezwać się do sprite’a i odpowie nam „Cześć”. Technikę tę można też zaadaptować do drzew. Trzeba by tylko było dodać obiekt drzewa w taki sam sposób jak greeter oraz wstawić go pod indeksem 3 do tablicy spriteRoles.

226  Rozdział 9. RPG

Receptura. Tworzenie schowka Dostarczymy naszemu ludzikowi kilka pomocnych w grze przedmiotów. W tym celu dodamy nowy zestaw sprite’ów przy użyciu kodu pogrubionego na listingu 9.17. Listing 9.17. Nowy zestaw sprite’ów z przedmiotami dla gracza window.onload = function(){ ... game.spriteSheetHeight = 16; game.itemSpriteSheetWidth = 64; game.preload(['sprites.png', 'items.png']);

Arkusz ten zawiera tylko cztery sprite’y, więc jego szerokość wynosi tylko 64 piksele (4·16). W następnym wierszu ładujemy ten arkusz wraz z głównym zestawem sprite’ów. Teraz zmienimy sposób wyświetlania i ukrywania etykiety stanu. Do tej pory można ją było wyświetlić tylko poprzez dodanie do niej tekstu, a ukryć poprzez usunięcie tekstu. Teraz potrzebujemy solidniejszego rozwiązania. W tym celu zmienimy funkcję displayStatus i dodamy funkcję clearStatus, jak pokazano na listingu 9.18. Listing 9.18. Przełączanie widoczności informacji o stanie gracza player.displayStatus = function(){ player.statusLabel.text = "--" + player.name + " z " + player.characterClass + "
--Zdrowie: "+player.hp + "/" + player.maxHp + "
--Magia: "+player.mp + "/" + player.maxMp + "
--Dośw.: "+player.exp + "
--Poziom: " + player.level + "
--Złoto: " + player.gp + "

--Schowek:"; player.statusLabel.height = 170; player.showInventory(); }; player.clearStatus = function(){ player.statusLabel.text = ""; player.statusLabel.height = 0; player.hideInventory(); };

Większość funkcji displayStatus pozostała niezmieniona. Od poprzedniej wersji różni się dodatkiem wywołania funkcji showInventory oraz ustawieniem wysokości obiektu statusLabel. W funkcji clearStatus oprócz tekstu na pusty łańcuch ustawiliśmy dodatkowo wysokość na 0 i wywołaliśmy funkcję hideInventory. Do funkcji hideInventory i showInventory za moment dojdziemy, ale wpierw zastąpimy stary sposób kasowania etykiety nową funkcją clearStatus. Może pamiętasz, że dzieje się to w momencie naciśnięcia jednego z klawiszy strzałek. Na listingu 9.19 pokazano zmiany w części kodu odpowiedzialnej za odbieranie danych wejściowych od użytkownika. Listing 9.19. Wywoływanie funkcji clearStatus, gdy zostanie naciśnięta strzałka if (game.input.up) {

Receptura. Tworzenie schowka  227

this.direction = 1; this.yMovement = -4; player.clearStatus(); } else if (game.input.right) { this.direction = 2; this.xMovement = 4; player.clearStatus(); } else if (game.input.left) { this.direction = 3; this.xMovement = -4; player.clearStatus(); } else if (game.input.down) { this.direction = 0; this.yMovement = 4; player.clearStatus(); }

Po dokonaniu tej prostej zmiany pozostaje nam już tylko napisanie jeszcze jednego bloku kodu, aby móc wyświetlić schowek na ekranie. Pod funkcją player.facing dodaj kod zaznaczony pogrubieniem na listingu 9.20. Listing 9.20. Wyświetlanie i ukrywanie schowka player.facing = function(){ ... } player.visibleItems = []; player.itemSurface = new Surface(game.itemSpriteSheetWidth, game.spriteSheetHeight); player.inventory = [0, 1, 2, 3]; player.hideInventory = function(){ for(var i = 0; i < player.visibleItems.length; i++){ player.visibleItems[i].remove(); } player.visibleItems = []; }; player.showInventory = function(){ if(player.visibleItems.length === 0){ player.itemSurface.draw(game.assets['items.png']); for (var i = 0; i < player.inventory.length; i++){ var item = new Sprite(game.spriteWidth, game.spriteHeight); item.y = 130; item.x = 30 + 70*i; item.frame = player.inventory[i]; item.scaleX = 2; item.scaleY = 2; item.image = player.itemSurface; player.visibleItems.push(item); game.rootScene.addChild(item); } } };

Najpierw inicjujemy nową tablicę dla widocznych przedmiotów. Referencję tę przechowujemy tylko po to, aby później usunąć te przedmioty. Następnie inicjujemy nową powierzchnię (Surface),

228  Rozdział 9. RPG którą sprite’y będą wykorzystywać do dowiadywania się, jaki zasób jest używany jako zestaw sprite’ów. Zwróć uwagę, że dla tego zestawu sprite’ów używamy naszej mniejszej szerokości zamiast większej, którą wykorzystujemy dla pliku sprites.png. UWAGA Wszystkie silniki gier mają własne pojęcia i terminy. To, co nazywa się powierzchnią (ang. surface) w jednym silniku, w innym może być nazywane arkuszem sprite’ów (ang. spritesheet). Jeśli zaczynasz się uczyć tworzenia gier przy użyciu nowego silnika, to najłatwiej jest dowiedzieć się, co jest czym, sprawdzając, co dany element robi. Zajrzyj do dokumentacji silnika albo napisz nazwę obiektu (np. itemSurface) i dodaj kropkę. W Firefoksie i Chrome zostaną wyświetlone funkcje i inne dostępne własności.

Następnie tworzymy tablicę obiektów posiadanych przez gracza. Domyślnie dajemy mu wszystko. Następnie implementujemy funkcję hideInventory. W tej wywoływanej w funkcji clearStatus funkcji przeglądamy za pomocą pętli wszystkie widoczne elementy i je usuwamy (z drzewa DOM). Później opróżniamy tablicę visibleItems. Funkcja showInventory zaczyna się od sprawdzenia, czy tablica visibleItems jest pusta. Test ten wykonujemy po to, aby nie można było wyświetlić więcej niż czterech przedmiotów, nawet jeśli spacja będzie wciśnięta. Rysujemy powierzchnię, robiąc to nie tyle tak, jak się rysuje grafikę, ile raczej wybierając obraz używany przez zestaw sprite’ów. Następnie dla każdego przedmiotu w schowku gracza tworzymy nowego sprite’a i ustawiamy pozycję, ramkę oraz skalę. Ustawiamy też odpowiedni zestaw sprite’ów. Jako że musimy mieć możliwość późniejszego usunięcia przedmiotu, dodajemy go do tablicy visibleItems. Na koniec dodajemy go do rootScene, umożliwiając jego wyświetlenie na ekranie. Na rysunku 9.4 pokazano efekt naszej dotychczasowej pracy — nie zapomnij nacisnąć spacji.

Receptura. Tworzenie sklepu Czy to miecz? A czy to pazur smoka? Stop. Myślę, że to trochę za dużo dobrego dla naszego bohatera, jeśli sobie nie zapracuje. W tej recepturze utworzymy sklep, aby nasz bohater musiał zarobić na zakup tego smoczego pazura. Pierwszą czynnością będzie zamiana wartości -1 na 4 gdzieś w tablicy foregroundData w pliku map.js. W ten sposób umieścimy na ekranie sprite’a kota przynoszącego szczęście. Jeśli teraz spróbujesz porozmawiać z tym kotem, to zostanie wyświetlony schowek. Aby to zmienić, musimy zmodyfikować tablicę spriteRoles i dodać nowy obiekt. Na razie sprawimy, że nasz kot będzie mówił „Hej”. Wydaje się, że gadający kot sprzedawca mógłby używać takich słów. Odpowiedni kod do realizacji opisanych zamiarów jest pokazany na listingu 9.21. Listing 9.21. Sprawienie, aby kot zaczął mówić var greeter = { action: function(){ npc.say("hello"); } };

Receptura. Tworzenie sklepu  229

Rysunek 9.4. Wyświetlenie schowka gracza var cat = { action: function(){ npc.say("ahoy"); } }; var spriteRoles = [,,greeter,,cat,,,,,,,,,,,]

Zanim zamienimy naszego kota w prawdziwego kupca, wyczyścimy schowek gracza — listing 9.22. Listing 9.22. Usunięcie zawartości schowka // Znajdź wiersz: // player.inventory = [0, 1, 2, 3]; // i zamień go na: player.inventory = [];

Na listingu 9.23 definiujemy dodatkowe informacje na temat dostępnych w grze przedmiotów.

230  Rozdział 9. RPG Listing 9.23. Dodatkowe szczegóły w tablicy game.items enchant(); window.onload = function(){ ... game.preload(['sprites.png', 'items.png']); game.items = [{price: 1000, description: "Osłona", id: 0}, {price: 5000, description: "Pazur smoka ", id: 1}, {price: 5000, description: "Magiczny lód", id: 2}, {price: 60, description: "Szachy", id: 3}]

Następnie w sklepie wykorzystamy funkcję showInventory, a więc musimy umożliwić dostosowywanie pozycji w pionie. Najpierw zmienimy funkcję displayStatus, jak pokazano na listingu 9.24. Listing 9.24. Modyfikacja funkcji displayStatus player.displayStatus = function(){ ... player.showInventory(0); };

Przekazujemy teraz funkcji wartość, aby móc ją ustawić na niższą. W tym konkretnym wywołaniu może pozostać wartość 0. Kolejną czynnością jest dokonanie analogicznej zmiany w definicji funkcji showInventory, jak pokazano na listingu 9.25. Listing 9.25. Modyfikacja funkcji showInventory player.showInventory = function(yOffset){ if(player.visibleItems.length === 0){ player.itemSurface.draw(game.assets['items.png']); for (var i = 0; i < player.inventory.length; i++){ var item = new Sprite(game.spriteWidth, game.spriteHeight); item.y = 130 + yOffset; item.x = 30 + 70*i; ... game.currentScene.addChild(item); } } };

Musieliśmy też zmienić rootScene na currentScene, aby móc stosować wywołania w więcej niż jednej scenie. W tym celu użyjemy funkcji pushScene obiektu game. Zmiany w kodzie przedstawione na listingu 9.26 spowodują, że kot zamiast tylko się witać, będzie otwierał sklep. Listing 9.26. Użycie funkcji pushScene do otwierania sklepu var greeter = { action: function(){ npc.say("hello"); } }; var shopScene = new Scene(); var cat = { action: function(){

Receptura. Tworzenie sklepu  231

game.pushScene(shopScene); } };

Jeśli jednak teraz rozpoczniesz pogawędkę z kotem, to gra będzie wyglądała, jakby się zawiesiła, bo aktualnie scena sklepu jest pusta. Przyciski sterowania przestałyby działać, ponieważ wejście jest związane ze zdarzeniem enterframe sprite’a player. Może się wydawać, że ten sprite jest na tej scenie, ale w rzeczywistości jest związany ze sceną rootScene, w której kontekście w tym momencie nie działamy. Sprite gracza nie będzie aktualizowany, przez co nie zostanie wykonany kod z bloku player.on('enterframe', function() {}, który zawiera także obsługę wejścia. Aby zdefiniować, czego potrzebujemy w sklepie, wprowadzimy niewielką zmianę w funkcji game.onload, jak pokazano na listingu 9.27. Listing 9.27. Uruchamianie sklepu game.onload = function(){ setMaps(); setPlayer(); setStage(); setShopping(); player.on('enterframe', function() { ... }); game.rootScene.on('enterframe', function(e) { game.focusViewport(); }); };

Teraz zdefiniujemy tę funkcję. Jej definicja jest długa, więc podzieliłem ją na kilka części, które są przedstawione na listingach 9.28 – 9.33. Na listingu 9.28 znajduje się pierwsza część funkcji setShopping, którą należy umieścić za nową tożsamością naszego kota (pokazana dla kontekstu). Listing 9.28. Początek funkcji setShopping var shopScene = new Scene(); var cat = { action: function(){ game.pushScene(shopScene); } }; var spriteRoles = [,,greeter,,cat,,,,,,,,,,,] var setShopping = function(){ var shop = new Group(); shop.itemSelected = 0; shop.shoppingFunds = function(){ return "Złoto: " + player.gp; }; ...

Zaczęliśmy od zdefiniowania nowej grupy (Group) o nazwie shop do przechowywania tekstu i sprite’ów do wyrenderowania oraz od razu dodaliśmy do niej dwie własności i funkcję. Własność

232  Rozdział 9. RPG itemSelected

określa indeks przedmiotu wybranego do zakupu spośród dostępnych przedmiotów, których wyświetleniem zajmiemy się później. Funkcja shoppingFunds zwraca tekstową reprezentację ilości posiadanego przez gracza złota. Na listingu 9.29 rysujemy kota. Listing 9.29. Rysowanie kota ...

// listing 9.28

shop.drawManeki = function(){ var image = new Surface(game.spriteSheetWidth, game.spriteSheetHeight); var maneki = new Sprite(game.spriteWidth, game.spriteHeight); maneki.image = image; image.draw(game.assets['sprites.png']); maneki.frame = 4; maneki.y = 10; maneki.x = 10; maneki.scaleX = 2; maneki.scaleY = 2; this.addChild(maneki); this.message.x = 40; this.message.y = 10; this.message.color = '#fff'; this.addChild(this.message); }; ... // listing 9.30

Biorąc pod uwagę, że wcześniej definiowaliśmy już sprite’y i etykiety, cały ten kod powinien wyglądać znajomo. W tym przypadku do obiektu shop dodajemy jedne i drugie. Chociaż pewnie zastanawiasz się, do czego służy instrukcja this.message. Dotyczy własności shop.message, której jeszcze nie zdefiniowaliśmy, a która będzie zawierała to, co będzie mówił do nas kot. Kod przedstawiony na listingu 9.30 służy do rysowania asortymentu sklepu. Listing 9.30. Rysowanie produktów na sprzedaż ...

// listing 9.29

shop.drawItemsForSale = function(){ for(var i = 0; i < game.items.length; i++){ var image = new Surface(game.itemSpriteSheetWidth, game.spriteSheetHeight); var item = new Sprite(game.spriteWidth, game.spriteHeight); image.draw(game.assets['items.png']); itemLocationX = 30 + 70*i; itemLocationY = 70; item.y = itemLocationY; item.x = itemLocationX; item.frame = i; item.scaleX = 2; item.scaleY = 2; item.image = image; this.addChild(item); var itemDescription = new Label(game.items[i].price + "
" + game.items[i].description); itemDescription.x = itemLocationX - 8; itemDescription.y = itemLocationY + 40; itemDescription.color = '#fff';

Receptura. Tworzenie sklepu  233

this.addChild(itemDescription); if(i === this.itemSelected){ var image = new Surface(game.spriteSheetWidth, game.spriteSheetHeight); this.itemSelector = new Sprite(game.spriteWidth, game.spriteHeight); image.draw(game.assets['sprites.png']); itemLocationX = 30 + 70*i; itemLocationY = 160; this.itemSelector.scaleX = 2; this.itemSelector.scaleY = 2; this.itemSelector.y = itemLocationY; this.itemSelector.x = itemLocationX; this.itemSelector.frame = 7; this.itemSelector.image = image; this.addChild(this.itemSelector); }; }; }; ... // listing 9.31

Cała ta funkcja to pętla for wykonywana po razie dla każdego produktu znajdującego się w tablicy game.items. Ma trzy zadania do wykonania na każdym z tych elementów. Po pierwsze, rysuje sprite’a odpowiadającego elementowi. Po drugie, drukuje cenę i opis produktu (dlatego właśnie wcześniej napisaliśmy opisy produktów). A po trzecie, rysuje sprite’a gracza przodem do produktu. Zauważ, że nie nadpisaliśmy zmiennej gracza używanej w nadświecie, tylko użyliśmy sprite’a gracza jako shop.itemSelector. W kodzie na listingu 9.31 tworzymy kilka nowych procedur obsługi zdarzeń. Listing 9.31. Procedury obsługi zdarzeń sklepu ...

// listing 9.30

shop.on('enter', function(){ shoppingFunds.text = shop.shoppingFunds(); }); shop.on('enterframe', function() { setTimeout(function(){ if (game.input.a){ shop.attemptToBuy(); } else if (game.input.down) { shop.message.text = shop.farewell; setTimeout(function(){ game.popScene(); shop.message.text = shop.greeting; }, 1000); } else if (game.input.left) { shop.itemSelected = shop.itemSelected + game.items.length - 1; shop.itemSelected = shop.itemSelected % game.items.length; shop.itemSelector.x = 30 + 70*shop.itemSelected; shop.message.text = shop.greeting; } else if (game.input.right) { shop.itemSelected = (shop.itemSelected + 1) % game.items.length; shop.itemSelector.x = 30 + 70*shop.itemSelected; shop.message.text = shop.greeting; } }, 500); player.showInventory(100);

234  Rozdział 9. RPG shoppingFunds.text = shop.shoppingFunds(); }); ... // listing 9.32

W procedurze obsługi zdarzenia enter (wykonywanej raz, przy pierwszym pojawieniu się sprite’a) ustawiamy tekst dotyczący środków, jakimi dysponuje gracz. Po dokonaniu zakupu zostanie on zmieniony, jeśli więc gracz gdzieś zdobędzie albo straci pieniądze, ta wartość musi być poprawnie ustawiona przy wchodzeniu do sklepu. Następna jest procedura enterframe (wykonywana w pętli). Na jej początku wywołujemy funkcję setTimeout opóźniającą przyjmowanie danych od użytkownika o pół sekundy, aby uniemożliwić przypadkowe kupienie czegoś od razu po wejściu do sklepu. Wiążemy przycisk „a” (spację) z funkcją attemptToBuy. Niżej ustawiamy wiadomość pożegnalną od kota i po odczekaniu jednej sekundy wracamy do nadrzędnego świata. Resetujemy przy okazji wiadomość, aby kot nas od razu nie żegnał przy następnym wejściu do sklepu. Przyciski left i right służą do wybierania produktu do kupienia oraz aktualizują sprite’a wybranego przedmiotu i ustawiają wiadomość od kota na greeting. W funkcji showInventory wykorzystujemy wcześniej dokonaną modyfikację dotyczącą umożliwienia zmiany wartości przesunięcia w pionie i ustawiamy ją na 100, aby umieścić schowek gracza niżej niż na mapie nadrzędnego świata. Ponadto zmieniliśmy tekst shoppingFunds. Powodem wywołania obu tych funkcji w tym miejscu jest to, że coś może się zmienić, gdy gracz dokona zakupu. Na listingu 9.32 obsługujemy skutki dokonania przez gracza zakupu. Listing 9.32. Obsługa zakupu produktu ...

// listing 9.31

shop.attemptToBuy = function(){ var itemPrice = game.items[this.itemSelected].price; if (player.gp < itemPrice){ this.message.text = this.apology; }else{ player.visibleItems = []; player.gp = player.gp - itemPrice; player.inventory.push(game.items[this.itemSelected].id); this.message.text = this.sale; } }; ... // listing 9.33

Najpierw ustawiliśmy zmienną visibleItems na pustą tablicę, aby schowek był poprawnie wyświetlany dla wielu przedmiotów. Jeśli gracz ma za mało złota, kot wyraża współczucie. Jeśli gracz ma wystarczające środki, aby dokonać zakupu, następuje zmniejszenie ilości posiadanego przez niego złota o cenę zakupionego produktu, dodanie numerycznej reprezentacji obiektu do tablicy reprezentującej schowek oraz wyświetlenie gratulacji kota z okazji dokonania udanego zakupu. Kod obsługujący wyświetlanie zaktualizowanej ilości złota i zawartości schowka znajduje się na listingu 9.31. Na zakończenie receptury mamy jeszcze kilka przypisań własności i wywołań funkcji, które są pokazane na listingu 9.33.

Receptura. Tworzenie interfejsu bitwy  235

Listing 9.33. Wywołania funkcji i przypisania własności w sklepie ...

// listing 9.32

shop.greeting = "Cześć! Jestem Maneki. Miau. Sprzedaję różne rzeczy."; shop.apology = "Miau... przykro mi, ale masz za mało pieniędzy."; shop.sale = "Proszę bardzo!"; shop.farewell = "Zapraszam ponownie! Miau!"; shop.message = new Label(shop.greeting); shop.drawManeki(); var shoppingFunds = new Label(shop.shoppingFunds()); shoppingFunds.color = '#fff'; shoppingFunds.y = 200; shoppingFunds.x = 10; shop.addChild(shoppingFunds); shop.drawItemsForSale(); shopScene.backgroundColor = '#000'; shopScene.addChild(shop); };

Cztery pierwsze wiersze zawierają definicje czterech wypowiedzi, jakimi może posłużyć się kot. Pod nimi znajduje się etykieta message. Po niej wywołujemy funkcję rysującą kota wraz z wypowiadanym przez niego zdaniem. Później dodajemy do grupy shop etykietę shoppingFunds, którą można modyfikować, gdy zostanie dokonany zakup. Następne jest wywołanie funkcji drawItems ForSale rysującej sprite’y produktów, ich ceny i opisy oraz sprite’a gracza pełniącego rolę wybieraka produktu. Tło całej sceny ustawiamy na kolor czarny i na koniec dodajemy shop do sceny shopScene. Na rysunku 9.5 widać efekt zakupu przez gracza szachów. Po powrocie do nadrzędnego świata w schowku gracza pojawi się nowy przedmiot.

Receptura. Tworzenie interfejsu bitwy W typowej grze RPG jednym z warunków postępu jest wygrywanie bitw. Czasami wygranie bitwy oznacza uratowanie jakiejś części świata i wszystkie postaci z gry dziękują nam za uratowanie im życia. Ale czasami bitwy toczy się o pieniądze i punkty doświadczenia. Bitwy mogą rozpoczynać się losowo podczas zwiedzania świata albo być uruchamiane przez określone zdarzenia, takie jak znalezienie się gracza w danym miejscu lub wejście w drogę jakiejś osobie lub jakiemuś potworowi. Na nasze potrzeby utworzymy postać, która będzie z nami walczyć, gdy spróbujemy z nią porozmawiać. Podobnie jak w przypadku kota, musimy dodać do mapy sprite’a nowej postaci. Tym razem wykorzystamy obrazek numer 15. Gdzieś na mapie foregroundData w pliku map.js zmień wartość -1 na 15. Następnie dodamy naszego włóczęgę w pobliżu miejsca, w którym umieściliśmy kota — listing 9.34. Listing 9.34. Dodawanie włóczęgi var shopScene = new Scene(); var cat = { action: function(){ game.pushScene(shopScene); } }; var battleScene = new Scene();

236  Rozdział 9. RPG

Rysunek 9.5. Zakup szachów var brawler = { maxHp: 20, hp: 20, sprite: 15, attack: 3, exp: 3, gp: 5, action: function(){ player.currentEnemy = this; game.pushScene(battleScene); } }; var spriteRoles = [,,greeter,,cat,,,,,,,,,,,brawler]

Tak jak w przypadku sceny shopScene, najpierw dodajemy scenę, w której występuje włóczęga, o nazwie battleScene. Następnie dodajemy włóczędze kilka atrybutów przydatnych w walce, które wykorzystamy także w przypadku wygrania przez gracza bitwy, oraz ustawiamy akcję na początek nowej sceny. Dostarczamy też graczowi informacji na temat tego, z kim walczy. Na końcu na 16. pozycji w tablicy spriteRoles ustawiamy zmienną włóczęgi brawler. Pamiętaj, że numery indeksów w tablicach zaczynają się od zera i dlatego szesnasty element ma numer 15. Ponadto przypomnę, że ten numer musi odpowiadać numerowi sprite’a.

Receptura. Tworzenie interfejsu bitwy  237

Kolejną czynnością jest wprowadzenie zmian w obiekcie gracza, jak pokazano na listingu 9.35. Listing 9.35. Zmiany w obiekcie gracza mające umożliwić przechodzenie poziomów i atakowanie var player = new Sprite(game.spriteWidth, game.spriteHeight); var setPlayer = function(){ ...

// player.hp = 10; // usuń to // player.maxHp = 10; // usuń to // player.mp = 0; // usuń to // player.maxMp = 0; // usuń to player.gp = 100; player.levelStats = [{},{attack: 4, maxHp: 10, maxMp: 0, expMax: 10}, {attack: 6, maxHp: 14, maxMp: 0, expMax: 30}, {attack: 7, maxHp: 20, maxMp: 5, expMax: 50} ]; player.attack = function(){ return player.levelStats[player.level].attack; }; player.hp = player.levelStats[player.level].maxHp; player.mp = player.levelStats[player.level].maxMp;

Teraz w naszym obiekcie proste przypisania wartości do zmiennych maxHp, mp oraz maxMp już nie wystarczą. Dlatego usuwamy stare atrybuty, które w kodzie zostały oznaczone pogrubieniem i umieszczone w komentarzach. Teraz będziemy się do nich odwoływać poprzez poziom gracza wg tablicy levelStats. Podczas bitwy będzie wykorzystywana funkcja attack. W niej można dodawać moc oręża, gdyby gracz jakieś zdobył. Zmienna expMax przechowuje maksymalną liczbę punktów doświadczenia potrzebną do przejścia do następnego poziomu. Jeśli zastanawiasz się, dlaczego tablica rozpoczyna się od pustego obiektu, pozwala to utrzymać spójność między poziomami gracza, które zazwyczaj nie są numerowane od 0, a strukturą tablicy, w której indeksowanie rozpoczyna się od 0. Teraz w funkcji displayStatus zmodyfikujemy części dotyczące zdrowia i magii, jak pokazano na listingu 9.36. Listing 9.36. Zmodyfikowany sposób wyświetlania informacji o stanie gracza player.displayStatus = function(){ player.statusLabel.text = "--" + player.name + " z " + player.characterClass + "
--Zdrowie: "+player.hp + "/" + player.levelStats[player.level].maxHp + "
--Magia: "+player.mp + "/" + player.levelStats[player.level].maxMp + ... };

W tym kodzie odwołujemy się do atrybutów gracza poprzez kontekst bieżącego poziomu. W funkcji game.onload jest potrzebna drobna zmiana dotycząca inicjowania kodu bitwy. Dodaj kod oznaczony pogrubieniem na listingu 9.37. Definicją funkcji setBattle zajmiemy się już za moment, ale wpierw wprowadzimy jeszcze jedną zmianę w pliku game.css. Dodamy kod przedstawiony na listingu 9.38.

238  Rozdział 9. RPG Listing 9.37. Przygotowanie bitwy przy uruchamianiu gry game.onload = function(){ setMaps(); setPlayer(); setStage(); setShopping(); setBattle(); ...

Listing 9.38. Wyróżnienie wybranej opcji w bitwie .active-option{ color:red; }

Teraz wybrana opcja (Walcz, Magia albo Uciekaj) zostanie zaznaczona na czerwono, dzięki czemu gracz będzie widzieć, co wybrał. Możemy przejść do definicji funkcji setBattle. Podzieliłem ją na kilka części, które, podobnie jak w przypadku kodu funkcji setShopping, przedstawiłem na kilku listingach (9.39 – 9.51). Listing 9.39. Kilka obiektów na początek var spriteRoles = [,,greeter,,cat,,,,,,,,,,,brawler] var setBattle = function(){ battleScene.backgroundColor = '#000'; var battle = new Group(); battle.menu = new Label(); battle.menu.x = 20; battle.menu.y = 170; battle.menu.color = '#fff'; battle.activeAction = 0; ... // listing 9.40

Najpierw pod tablicą spriteRoles otworzyliśmy definicję funkcji setBattle i ustawiliśmy czarne tło w scenie. Następnie dodaliśmy grupę battle, która posłuży nam jako kontener sprite’ów, etykiet i własności. Obiekt battle.menu reprezentuje biały tekst opcji bitwy. Na listingu 9.40 tworzymy kolejną etykietę o nazwie playerStatus do opisu bieżących wartości zdrowia (hp) i magii (mp) gracza (player). Listing 9.40. Etykieta na status gracza ...

// listing 9.39

battle.getPlayerStatus = function(){ return "Zdrowie: " + player.hp + "
Magia: " + player.mp; }; battle.playerStatus = new Label(battle.getPlayerStatus()); battle.playerStatus.color = '#fff'; battle.playerStatus.x = 200; battle.playerStatus.y = 120; ... // listing 9.41

Receptura. Tworzenie interfejsu bitwy  239

Ten kod powinien być zrozumiały, więc od razu przechodzę do funkcji hitStrength przedstawionej na listingu 9.41. Listing 9.41. Określanie, ile punktów zdrowia odejmie uderzenie ...

// listing 9.40

battle.hitStrength = function(hit){ return Math.round((Math.random() + .5) * hit); }; ... // listing 9.42

Przy określaniu liczby punktów do odjęcia graczowi lub jego przeciwnikowi można by było polegać wyłącznie na wartościach ataku (i ewentualnie obrony). Ta funkcja wprowadza jednak element losowości, dzięki czemu bitwy są bardziej nieprzewidywalne. W kodzie na listingu 9.42 zajmujemy się obsługą sytuacji, gdy gracz zwycięży. Listing 9.42. Wygrana w bitwie ...

// listing 9.41

battle.won = function(){ battle.over = true; player.exp += player.currentEnemy.exp; player.gp += player.currentEnemy.gp; player.currentEnemy.hp = player.currentEnemy.maxHp; player.statusLabel.text = "Wygrałeś!
" + "Otrzymujesz "+ player.currentEnemy.exp + " punktów doświadczenia
"+ "i " + player.currentEnemy.gp + " złotych monet!"; player.statusLabel.height = 45; if(player.exp > player.levelStats[player.level].expMax){ player.level += 1; player.statusLabel.text = player.statusLabel.text + "
Przechodzisz na wyższy poziom!"+ "
Teraz jesteś na poziomie " + player.level +"!"; player.statusLabel.height = 75; } }; ... // listing 9.43

Gdy gracz wygrywa bitwę, ustawiamy zmienną battle.over na true, aby zakończyć działanie na scenie. Następnie graczowi są przypisywane punkty doświadczenia i złoto przeciwnika. Jeśli liczba doświadczenia gracza przekroczy określony pułap, przechodzi on do następnego poziomu. Zdrowie przeciwnika zostaje zresetowane od następnej bitwy. Następnie ustawiamy etykietę statusLabel (ze świata nadrzędnego) na opis zwycięstwa. Jeśli nie podoba Ci się takie rozwiązanie, to możesz też użyć jednej z etykiet sceny battleScene. Na listingu 9.43 znajduje się kod obsługujący przypadek przegranej. Listing 9.43. Przegranie bitwy ...

// listing 9.42

battle.lost = function(){ battle.over = true; player.hp = player.levelStats[player.level].maxHp;

240  Rozdział 9. RPG player.mp = player.levelStats[player.level].maxMp; player.gp = Math.round(player.gp/2); player.statusLabel.text = "Przegrałeś!"; player.statusLabel.height = 12; }; ...

// listing 9.44

Teraz zajmujemy się sytuacją w przypadku przegranej gracza. Podobnie jak przy wygranej, ustawiamy zmienną battle.over na true, aby przygotować się do opuszczenia sceny. Resetujemy zdrowie gracza i magię, aby mógł stoczyć kolejną bitwę, oraz odejmujemy mu połowę posiadanego przez niego złota. Opis, co się stało tym razem, także wyświetlamy przy użyciu etykiety ze świata nadrzędnego. Na listingu 9.44 znajduje się kod umożliwiający graczowi atakowanie i bycie atakowanym. Listing 9.44. Gdy dojdzie do ataku ...

// listing 9.43

battle.playerAttack = function(){ var currentEnemy = player.currentEnemy; var playerHit = battle.hitStrength(player.attack()); currentEnemy.hp = currentEnemy.hp - playerHit; battle.menu.text = "Wyrządziłeś " + playerHit + " punktów szkody!"; if(currentEnemy.hp <= 0){ battle.won(); }; }; battle.enemyAttack = function(){ var currentEnemy = player.currentEnemy; var enemyHit = battle.hitStrength(currentEnemy.attack); player.hp = player.hp - enemyHit; battle.menu.text = "Straciłeś " + enemyHit + " punktów zdrowia!"; if(player.hp <= 0){ battle.lost(); }; }; ... // listing 9.45

Na tym listingu znajdują się funkcje obsługi ataku przez gracza i jego przeciwnika. W obu przypadkach obliczamy siłę uderzenia na podstawie siły atakującego i elementu losowego dostarczanego przez funkcję hitStrength. Następnie odejmujemy wartość uderzenia od wartości zdrowia, ustawiamy powiadomienie i jeśli został wyłoniony zwycięzca, wywołujemy odpowiednio do sytuacji funkcję won (wygrana) lub lost (przegrana). Na listingu 9.45 znajduje się kod obsługujący wszystkie czynności, jakie gracz może wykonać w bitwie. Listing 9.45. Możliwe działania wojenne ...

// listing 9.44

battle.actions = [{name: "Walcz", action: function(){ battle.wait = true; battle.playerAttack(); setTimeout(function(){

Receptura. Tworzenie interfejsu bitwy  241

if(!battle.over){ battle.enemyAttack(); }; if(!battle.over){ setTimeout(function(){ battle.menu.text = battle.listActions(); battle.wait = false; }, 1000) } else { setTimeout(function(){ battle.menu.text = ""; game.popScene(); }, 1000) }; }, 1000); }}, {name: "Magia", action: function(){ battle.menu.text = "Nie umiesz jeszcze czarować!"; battle.wait = true; battle.activeAction = 0; setTimeout(function(){ battle.menu.text = battle.listActions(); battle.wait = false; }, 1000); }}, {name: "Uciekaj", action: function(){ game.pause(); player.statusLabel.text = "Uciekłeś!"; player.statusLabel.height = 12; battle.menu.text = ""; game.popScene(); }} ]; ... // listing 9.46

Kod przedstawiony na listingu 9.45 służy do obsługi opcji dostępnych do wyboru przed bitwą. Zaczniemy go omawiać od końca. Funkcja Run wstrzymuje grę przy użyciu funkcji pause biblioteki enchant.js i ustawia etykietę statusLabel na opis naszego tchórzostwa, gdy opuścimy scenę. Następnie przygotowuje menu bitwy, aby gdy zdecydujemy się walczyć jeszcze raz, nie zostało na moment wyświetlone poprzednie menu. Na koniec zamyka sceną i przenosi nas z powrotem do nadrzędnego świata. Opcja Magia na razie nie jest zbyt interesująca. Ustawia tylko opóźnienie, jednocześnie informując nas, że nie potrafimy jeszcze czarować. Pewnie zastanawiasz się, dlaczego została użyta funkcja setTimeout zamiast game.pause. Powodem tego oraz ustawienia wartości battle.wait jest to, że poza tym kodem mogło zostać zgromadzonych wiele danych wejściowych, co spowoduje próbę wielokrotnego wykonania tego kodu. Nawet jeśli wstrzymamy grę na samym początku funkcji, będzie już za późno. Akcja Fight jest najbardziej skomplikowana ze wszystkich. Blokujemy przyjmowanie danych wejściowych w taki sam sposób jak poprzednio. Następnie wywołujemy funkcję playerAttack, którą zdefiniowaliśmy poprzednio, i ustawiamy limit czasu, aby mieć pewność, że komunikaty dotyczące ataku będą miały szansę zostać wyświetlone. Jeśli wróg nadal żyje, to dajemy mu możliwość zaatakowania. Jeśli gracz przeżyje, to znowu czekamy. Po odczekaniu odpowiedniej ilości

242  Rozdział 9. RPG czasu wyświetlamy menu i przyjmujemy decyzję dotyczącą następnej rundy bitwy. Jeżeli bitwa się zakończy, resetujemy menu do następnej bitwy i zamykamy scenę. Na listingu 9.46 znajduje się kod obsługi działań bitewnych. Listing 9.46. Wyświetlanie opcji walki ...

// listing 9.45

battle.listActions = function(){ battle.optionText = []; for(var i = 0; i < battle.actions.length; i++){ if(i === battle.activeAction){ battle.optionText[i] = ""+ battle. actions[i].name + ""; } else { battle.optionText[i] = battle.actions[i].name; } } return battle.optionText.join("
"); }; ... // listing 9.47

Na listingu 9.46 pokazano, czego używamy do wyświetlania naszych opcji. Za pomocą pętli przeglądamy tablicę akcji, które zdefiniowaliśmy, i budujemy tablicę. Zapisujemy nazwę i jeśli dana opcja została aktualnie wybrana poprzez naciśnięcie przycisku „a” (spacji), dodajemy klasę CSS, aby miała kolor czerwony zamiast białego. Na końcu pętli łączymy elementy tablicy w łańcuch, oddzielając je znacznikiem
. Na listingu 9.47 definiujemy atrybuty dla sprite’ów gracza i wroga. Listing 9.47. Dodawanie gracza i wroga ...

// listing 9.46

battle.addCombatants = function(){ var image = new Surface(game.spriteSheetWidth, game. spriteSheetHeight); image.draw(game.assets['sprites.png']); battle.player = new Sprite(game.spriteWidth, game.spriteHeight); battle.player.image = image; battle.player.frame = 7; battle.player.x = 150; battle.player.y = 120; battle.player.scaleX = 2; battle.player.scaleY = 2; battle.enemy = new Sprite(game.spriteWidth, game.spriteHeight); battle.enemy.image = image; battle.enemy.x = 150; battle.enemy.y = 70; battle.enemy.scaleX = 2; battle.enemy.scaleY = 2; battle.addChild(battle.enemy); }; battle.addCombatants(); ... // listing 9.48

Receptura. Tworzenie interfejsu bitwy  243

Na listingu 9.47 znajduje się definicja funkcji addCombatants, którą po zdefiniowaniu od razu wywołujemy. Powodem, dla którego definiujemy funkcję, zamiast ją po prostu wywołać w szeregu z innymi, jest chęć zwiększenia czytelności kodu; sam w sobie powinien on wyglądać znajomo. Na listingu 9.48 znajduje się procedura obsługi początku bitwy. Listing 9.48. Procedura obsługi początku bitwy ...

// listing 9.47

battleScene.on('enter', function() { battle.over = false; battle.wait = true; battle.menu.text = ""; battle.enemy.frame = player.currentEnemy.sprite; setTimeout(function(){ battle.menu.text = battle.listActions(); battle.wait = false; }, 500); }); ... // listing 9.49

Powyższa procedura jest wywoływana raz na początku bitwy. Jako że bitwa właśnie się rozpoczęła, zmienna over zostaje ustawiona na false. Następnie ustawiliśmy zmienną wait na true, aby wstrzymać odbieranie danych wejściowych. Tekst menu ustawiamy na pusty łańcuch, by wśród opcji nie została wyświetlona możliwość undefined. Klatkę wroga ustawiliśmy tutaj po to, żeby można było walczyć z więcej niż jednym typem przeciwnika i by mógł zostać wyświetlony jego sprite. Po odczekaniu pół sekundy pokazujemy menu zawierające listę możliwych do wykonania czynności oraz ustawiamy wait na false, aby umożliwić graczowi wybranie opcji. Na listingu 9.49 znajduje się definicja pętli obsługującej przebieg bitwy. Listing 9.49. Pętla bitwy ...

// listing 9.48

battleScene.on('enterframe', function() { if(!battle.wait){ if (game.input.a){ battle.actions[battle.activeAction].action(); } else if (game.input.down){ battle.activeAction = (battle.activeAction + 1) % battle.actions.length; battle.menu.text = battle.listActions(); } else if (game.input.up){ battle.activeAction = (battle.activeAction - 1 + battle.actions.length) % battle.actions.length; battle.menu.text = battle.listActions(); } battle.playerStatus.text = battle.getPlayerStatus(); }; }) ... // listing 9.50

Na powyższym listingu jest przedstawiona pętla bitwy. Teraz już wiadomo, po co całe to czekanie. Gdyby zmienna battle.wait była ustawiona na true, program nie przyjmowałby żadnych danych wejściowych. Natomiast teraz za pomocą klawiszy strzałek w górę i w dół gracz może

244  Rozdział 9. RPG wybrać opcję, co spowoduje aktualizację tekstu potwierdzającą wizualnie jego wybór. Przycisk „a” (spacja) powoduje wykonanie wybranej akcji. Ponadto w tej pętli aktualizujemy licznik zdrowia gracza przy użyciu funkcji getPlayerStatus. Na listingu 9.50 dodajemy procedurę zamykania trybu bitwy. Listing 9.50. Co się dzieje, gdy opuszczamy scenę bitwy ...

// listing 9.49

battleScene.on('exit', function() { setTimeout(function(){ battle.menu.text = ""; battle.activeAction = 0; battle.playerStatus.text = battle.getPlayerStatus(); game.resume(); }, 1000); }); ... // listing 9.51

W tym kodzie znajduje się nowy rodzaj procedury obsługi zdarzeń. Jest ona wywoływana przy zamykaniu sceny i powrocie do nadrzędnego świata. Resetujemy menu i wybór opcji bitwy. Następnie aktualizujemy licznik poziomu zdrowia gracza, aby w razie nowej bitwy nie została wyświetlona nieaktualna już wartość. Jeżeli gra zostanie wstrzymana, wznawiamy ją tu z jednosekundowym opóźnieniem. Opóźnienie to stosujemy po to, aby gracz przez przypadek nie powrócił do bitwy, jeśli zbyt długo przytrzyma wciśniętą spację. Na listingu 9.51 dodajemy utworzone funkcje do sceny bitwy. WSKAZÓWKA Jeśli zrozumienie zasad zarządzania scenami, wewnętrzną logiką gry oraz pozostałe działania sprawiają Ci problemy, zwłaszcza jeżeli budujesz większą grę, w zapanowaniu nad tym wszystkim może pomóc implementacja tzw. maszyny stanów. W takiej maszynie definiuje się, co wywołuje określone stany i jak można z nich wyjść oraz z których stanów można przejść do innych, a także za pomocą których funkcji można to zrobić. Częściowo technikę tę stosujemy w naszych procedurach obsługi zdarzeń, ale gdy przechodzimy z poziomu sceny do stanu w tej scenie, nasz interfejs do przechodzenia i pracy ze zmiennymi może być niespójny. Dobrze przetestowaną i popularną maszynę stanów można znaleźć na stronie https://github.com/jakesgordon/javascript-state-machine. Listing 9.51. Budowa sceny ...

// listing 9.50

battle.addChild(battle.playerStatus); battle.addChild(battle.menu); battle.addChild(battle.player); battleScene.addChild(battle); }; // Koniec funkcji setBattle

Receptura. Zapisywanie gry przy użyciu API Local Storage HTML5  245

Przypomnę, że kod przedstawiony na listingach od numeru 9.39 działa w funkcji setBattle. Jest wykonywany raz przy zdarzeniu game.onload w celach ustawienia wszystkiego, co jest potrzebne. Potem wszystkie funkcje i własności obiektu battle są używane w procedurach obsługi zdarzeń sceny, z których jedna pełni funkcję pętli aktualizującej i rysującej. Pamiętając o tym kontekście, możemy teraz dokończyć scenę battleScene. W przedstawionym kodzie dodaliśmy pozostałe obiekty potomne obiektu battle, będącego egzemplarzem Group, dodaliśmy battle do battleScene oraz dopisaliśmy klamrę zamykającą funkcję i zarazem stanowiącą zakończenie tej receptury. Jeśli podejmiesz walkę z facetem w czapeczce, na ekranie zobaczysz widok podobny do przedstawionego na rysunku 9.6.

Rysunek 9.6. Walka z facetem w czapeczce

Receptura. Zapisywanie gry przy użyciu API Local Storage HTML5 Biorąc pod uwagę, że w grze można przechodzić różne poziomy i toczyć zwycięskie bądź przegrane bitwy, przydałaby się możliwość zapisywania postępu. Do tego celu wykorzystamy API localStorage języka HTML5. Moglibyśmy oprzeć naszą pracę na obsłudze zdarzeń, ale dla uproszczenia będziemy zapisywać dane w zmiennych magazynu lokalnego (ang. local storage) w określonych odstępach czasu. Dodaj do kodu gry wiersze oznaczone pogrubieniem na listingu 9.52.

246  Rozdział 9. RPG Listing 9.52. Zapisywanie zmiennych w magazynie lokalnym co pięć sekund game.onload = function(){ game.storable = ['exp', 'level', 'gp', 'inventory']; game.saveToLocalStorage = function(){ for(var i = 0; i < game.storable.length; i++){ if(game.storable[i] === 'inventory'){ window.localStorage.setItem(game.storable[i], JSON.stringify(player[game.storable[i]])); } else { window.localStorage.setItem(game.storable[i], player[game.storable[i]]); } } }; setInterval(game.saveToLocalStorage, 5000); ...

Najpierw zdefiniowaliśmy tablicę z nazwami własności obiektu player, które chcemy zapisywać. Następnie przeglądamy tę tablicę i zapisujemy wartości za pomocą wywołania window.local Storage.setItem(nazwaKlucza, wartość);. W magazynie lokalnym można zapisywać tylko łańcuchy, co oznacza, że inventory jest specjalnym przypadkiem, ponieważ jest to tablica. Dlatego przed jej zapisaniem musimy ją przepuścić przez funkcję JSON.stringify. Problem mogą sprawiać także liczby całkowite, ale tylko przy odczytywaniu. Na listingu 9.53 znajduje się kod pobierający dane z magazynu lokalnego — pogrubiony druk. Listing 9.53. Odczytywanie danych z magazynu lokalnego player = new Sprite(game.spriteWidth, game.spriteHeight); var setPlayer = function(){ player.spriteOffset = 5; ... player.characterClass = "Klanu"; if (window.localStorage.getItem('exp')) { player.exp = parseInt(window.localStorage.getItem('exp')); } else { player.exp = 0; } if (window.localStorage.getItem('level')) { player.level = parseInt(window.localStorage.getItem('level')); } else { player.level = 1; } if (window.localStorage.getItem('gp')) { player.gp = parseInt(window.localStorage.getItem('gp')); } else { player.gp = 100; } if (window.localStorage.getItem('inventory')) { player.inventory = JSON.parse(window.localStorage. getItem('inventory')); } else { player.inventory = []; // To początkowo znajdowało się dalej }

Podsumowanie  247

player.levelStats = [{},{attack: 4, maxHp: 10, maxMp: 0, expMax: 10}, {attack: 6, maxHp: 14, maxMp: 0, expMax: 30}, {attack: 7, maxHp: 20, maxMp: 5, expMax: 50} ];

W tym kodzie próbujemy odczytać wszystkie wartości z magazynu lokalnego. Jeśli określona wartość jest zapisana, wywołujemy funkcję parseInt, aby przekonwertować łańcuchy na liczby całkowite. Ponownie specjalnym przypadkiem jest tablica inventory, którą konwertujemy na normalną tablicę języka JavaScript za pomocą funkcji JSON.parse. Jeśli jakiegoś obiektu nie ma w magazynie, to ustawiamy go na wartość domyślną, której używaliśmy wcześniej. Nie zapomnij usunąć kodu ustawiającego wartości, który napisaliśmy w poprzedniej recepturze. Większość tych instrukcji znajduje się w pobliżu tego obszaru, ale player.inventory = []; znajduje się niżej, nad definicją funkcji player.hideInventory. Teraz liczba punktów doświadczenia, poziom, schowek i liczba posiadanych monet będą zapisywane w magazynie lokalnym i przywracane z niego nawet po zamknięciu okna przeglądarki. UWAGA Nie ma miejsca w tej książce na szczegółowy opis języka HTML, ale warto wiedzieć, że istnieją jeszcze dwie inne możliwości zapisywania danych po stronie klienta. Pierwsza nosi nazwę sessionStorage. Dane zapisane przy jej użyciu są usuwane podczas zamykania okna przeglądarki. Jeśli trzeba zapisywać bardziej skomplikowane dane (relacyjne), to można skorzystać z relacyjnych magazynów lokalnych, takich jak IndexedDB (Chrome, Firefox, IE) oraz Web SQL Database (Chrome, Safari, Opera, iOS, Android oraz Opera Mobile).

Podsumowanie W tym rozdziale utworzyliśmy grę RPG od podstaw przy użyciu biblioteki enchant.js. Zaimplementowaliśmy mapę, system dialogów, sklep, interfejs bitwy, system poziomów oraz mechanizm zapisywania postępu w grze oparty na magazynie lokalnym HTML5. Jeśli chcesz rozbudować tę grę, to oczywistym kierunkiem rozwoju jest dodanie treści, np. postaci, broni, zbroi, magii, dialogów, wrogów itd. Także rozbudowanie fabuły z pewnością zwiększyłoby atrakcyjność gry. Można udoskonalić zdolności bitewne gracza. Aktualnie nawet gdyby miał zdolność posługiwania się magicznym lodem, to i tak nie mógłby jej wykorzystać w bitwie. Także inne przedmioty dostępne w schowku są bezużyteczne. A dodanie czegoś do siły ataku nie byłoby trudne. Ponadto gracz zawsze zaczyna, ataki nigdy nie są chybione, a gracz zawsze może uciec. Wprowadzając takie atrybuty jak szybkość, precyzja i zwinność, sprawilibyśmy, że gra byłaby bardziej dynamiczna. Także efekty wizualne podczas bitwy pozostawiają wiele do życzenia. Przydałyby się jakieś grafiki ilustrujące ciosy itp. Może uznasz, że walka na zmianę jak teraz jest nudna i wolisz bitwy bezpośrednie, jak w RPG akcji. Po lekturze tego i wcześniejszych rozdziałów dodanie mechanizmów wykrywania kolizji potrzebnych do implementacji tego pomysłu nie sprawi Ci problemu. Jeżeli chcesz dowiedzieć się więcej o bibliotece enchant.js, to masz szczęście. Biblioteka ta ma chyba najlepsze API ze wszystkich opisanych do tej pory. Ponadto ma wiele wyróżniających ją cech, o których nie wspomniałem w tym rozdziale, np. obiekt CanvasGroup, którego można

248  Rozdział 9. RPG używać zamiast Group, API Sound, obiekt ParallelAction oraz klasy Timeline i Tween do tworzenia animacji. Ponadto jest wiele wtyczek, np. umożliwiających połączenie się z Twitterem i platformą wise9 (są twórcami biblioteki enchant.js). Jest nawet serwer animacji awatarów, na którym można zmieniać cechy postaci, takie jak włosy czy ubrania. Jeśli planujesz napisać własną bibliotekę, to przejrzyj kod enchant.js, bo znajdziesz w nim wiele sprytnych i niezwykłych rozwiązań.

10 Gry RTS

Co jest lepszego od gry w grę RPG i zostania bohaterem? Prowadzenie w zwycięskiej bitwie całej armii bohaterów. Do klasyków tego gatunku należą takie produkcje jak Starcraft i Warcraft (nie mylić z World of Warcraft, który należy do gatunku MMORPG), ale jego wpływy widać także w innych grach symulacyjnych, choćby Sim City czy Roler Coaster Tycoon. Ponadto można spotkać turowe strategie taktyczne z podobnymi interfejsami. Wśród przykładów można wymienić Tactics Ogre i Final Fantasy Tactics, jak również klasyczne gry planszowe takie jak szachy. Także wiele sportów drużynowych zawiera elementy RTS, ale rozgrywka odbywa się w nich w realnym świecie i cele są ściślej określone, trzeba np. umieścić piłkę w konkretnym miejscu.

250  Rozdział 10. GRY RTS

Potrzebujemy serwera W tym rozdziale zbudujemy grę RTS na bazie biblioteki crafty.js oraz za pomocą bibliotek Node i Socket.IO. Utworzymy grę podobną do gry planszowej Stratego, tylko rozgrywaną na bieżąco, a nie turowo. Stratego jest grą dla dwóch osób, w której stoją naprzeciw siebie dwie armie: czerwona i niebieska. Każdy z graczy ma do dyspozycji jednostki o różnej sile rażenia, a celem jest zdobycie flagi przeciwnika. Jest to gra polegająca na jak najlepszym wykorzystaniu szczątkowych informacji o przeciwniku. Dla porównania w szachach gra się w „otwarte karty”, tzn. każdy z graczy wie wszystko o przeciwniku; gdzie znajdują się jego wieże, pionki itd. W tej grze rodzaj jednostki wroga można poznać tylko w wyniku potyczki. Z czasem można dowiedzieć się coraz więcej i na podstawie tych informacji opracowuje się strategię działania. W rozdziale 6. utworzyliśmy bijatykę dla dwóch graczy, ale nie musieliśmy się w niej przejmować ukrywaniem informacji. Aby osiągnąć równie dobry efekt w grze, nad którą pracujemy teraz, i nie musieć uciekać się do absurdalnych rozwiązań polegających na przegrodzeniu ekranu monitora kartonem, musimy każdemu graczowi zdefiniować własny ekran. Zanim przejdziemy do budowy gry, zastanowimy się, jak rozwiązać ten problem. We wszystkich poprzednich rozdziałach używaliśmy tylko kodu działającego po stronie klienta, czyli wykonywanego w przeglądarce internetowej. Otwieraliśmy plik HTML, a przeglądarka sama wczytywała dołączone do niego pliki JavaScript i przekazywała je interpreterowi. Programy te nie przestaną działać nawet po odłączeniu od komputera internetu. W tym rozdziale jednak zaczniemy eksplorować nowe terytorium, jakim jest pisanie programów działających po stronie serwera. Programy takie działają na specjalnych komputerach, które są dostępne dla innych komputerów podłączonych do sieci za pośrednictwem jakiegoś protokołu. Gdy wchodzisz na stronę internetową, to w istocie pobierasz dane z fizycznie istniejącego komputera, który znajduje się gdzieś na świecie. Na tym komputerze działa oprogramowanie interpretujące Twoje żądania i zwracające odpowiedzi (zwykle poprzez protokół HTTP). Czasami występują nieporozumienia terminologiczne, bo mianem serwera określa się zarówno metalowe pudełka zawierające plastikowe i krzemowe elementy (potocznie zwane komputerami), jak również programy działające na tych urządzeniach. Można je odróżnić od siebie, używając rozmaitych określeń w rodzaju serwer fizyczny, pudło, to coś, co stoi w centrum danych, albo posługując się marketingowym pojęciem chmura, które oznacza coś bardziej złożonego, abstrakcyjnego i wirtualnego, czego nie da się jednoznacznie wskazać, aby powiedzieć: „Tu są przechowywane moje dane”. Jeśli chodzi o te działające na serwerze specjalne programy, to jedną z najważniejszych informacji o nich jest to, jakiego protokołu komunikacyjnego używają. Można wyróżnić serwery HTTP, serwery sieciowe, serwery baz danych oraz serwery aplikacji, które są wyspecjalizowane do obsługi konkretnego języka programowania lub szkieletu programistycznego. Tak na marginesie, w internecie duże znaczenie ma pokrewna technologia o nazwie usługi sieciowe albo API sieciowe, która umożliwia pobieranie i wysyłanie danych. Pojęcia te są sobie tak bliskie, że jeśli ktoś nazwie programistyczny interfejs (API) Twittera serwerem Tweetera, to niewiele się pomyli. Skoro mamy już wyjaśnione zawiłości terminologii, możemy wrócić do zastanawiania się nad sposobem rozwiązania problemu wyświetlania powiązanych ze sobą, ale różnych informacji dwóm graczom. Potrzebny nam będzie serwer (zarówno fizyczny, jak i programowy). Rozwiązanie to ma wiele zalet. Po pierwsze, oprócz ukrywania informacji można wykonywać kod logiki programu niezależnie od renderowania. Ponadto jeśli kod działa powoli, to zamiast trudzić się nad

Receptura. Instalacja i uruchamianie Node  251

UWAGA W tym rozdziale będziesz używać swojego komputera jako serwera z uruchomionym serwerem Node i działającym Socket.IO. Taki komputer da się udostępnić w lokalnej, a nawet ogólnoświatowej sieci, ale łatwiej jest skorzystać z usługi hostingowej, aby uruchomić swoje programy na zdalnym komputerze. Wynajęcie całego fizycznego dedykowanego (niedzielonego z nikim innym) serwera jest dość drogie. Istnieje wiele firm, w których można zamówić serwer wirtualny, ale warto poszukać takiej oferty, która udostępnia gotowe do użytku potrzebne Ci oprogramowanie (node.js, Ruby on Rails itd.).

jego optymalizacją, możemy po prostu dopłacić usługodawcy za zwiększenie parametrów serwera. Co więcej, jeśli część kodu działa na serwerze, to przeglądarka jest odciążona i nie ma w programie jednego dużego wąskiego gardła. Po drugie, gracz nie musi pobierać na swój komputer wszystkich plików programu, ponieważ część z nich działa na serwerze. Do użytkownika wysyła się tylko to, co jest niezbędne, a więc kod HTML i CSS, obrazy i działający po stronie klienta JavaScript. Kodu serwerowego użytkownikom można w ogóle nie udostępniać, co stwarza możliwość korzystania z różnych form sprzedaży gry, z których w innym przypadku nie można by było skorzystać. I po trzecie, masz kontrolę nad wszystkim, co pokazujesz graczowi i co od niego przyjmujesz. Jeśli gra zdobędzie dużą popularność i będzie w niej można zdobywać różne dobra, takie jak np. banery, to niektórzy gracze na pewno spróbują oszukiwać, aby je zdobyć. W ramach ochrony przed takimi praktykami można skompresować kod JavaScript, żeby był kompletnie nieczytelny. To odstraszy najbardziej leniwych i najgłupszych hakerów. Kolejną warstwą ochrony może być przyjęcie zasady, że w kodzie klienckim mogą być udostępniane tylko funkcje dotyczące aspektów wizualnych gry, takie jak narysujCoś(), albo nieciekawe, takie jak obsługaWejścia(). Natomiast funkcje typu wygrana() czy dodajRzadkiPrzedmiotZaKtóryTrzebaZapłacić() powinny być skrzętnie ukryte. Ogólna zasada jest taka, że funkcje dotyczące wszystkiego, co może być cenne dla graczy, należy dobrze ukrywać. Nawet funkcja obsługi danych wejściowych może być niebezpieczna, bo ktoś może ją wykorzystać do zdobycia większej ilości złota albo przejścia na wyższy poziom. W przypadku bardzo popularnych gier hakerzy posuwają się nawet do klonowania całej gry i tworzenia różnych dziwnie działających programów. Negatywne skutki takich działań mogą być bardzo różne, od zachwiania ekonomicznej równowagi gry po uprzykrzenie na rozmaite sposoby grania uczciwym użytkownikom. Na ten problem można też spojrzeć z innej perspektywy. Czasami klon gry utworzony przez użytkownika może być sposobem na pokazanie, że naszej grze brakuje jakichś funkcji, obnażenie jej luk w zabezpieczeniach, dodanie nowych poziomów albo utworzenie dodatkowej treści (nowych poziomów, nowych postaci, miksów muzyki albo tłumaczeń na różne języki). Pamiętaj, że jeśli udostępniasz publicznie kod źródłowy swojej gry, to zapewne najważniejszymi jego odbiorcami będą właśnie różnej maści hakerzy.

Receptura. Instalacja i uruchamianie Node Skoro wiemy, do czego służy serwer, możemy postarać się o jeden dla siebie. Przypomnę, że naszym fizycznym serwerem będzie nasz własny komputer, więc nie pogramy przy jego użyciu ze znajomymi poza lokalną siecią, chyba że podłączymy nasz komputer do sieci ogólnej albo skorzystamy z usługi

252  Rozdział 10. GRY RTS hostingowej. Node, jak wiele bibliotek programistycznych, można zdobyć na wiele sposobów. Dostępne są specjalne menedżery pakietów, takie jak np. klejnoty Ruby czy jajka Pythona. Można też pobrać pliki źródłowe (binarne) zawierające kod (w dużej części w języku C), który można samodzielnie skompilować i zainstalować na swoim sprzęcie. Dostępne są też pakiety przeznaczone dla wybranych systemów operacyjnych, np. dla Mac OS-a poprzez homebrew i Debiana poprzez polecenie apt-get. Szkielet node.js ma nawet własny menedżer pakietów o nazwie npm (ang. node package manager). Jeśli jednak chcesz zainstalować node.js na komputerze z systemem Mac OS albo Windows, to są na to łatwiejsze sposoby. Wystarczy wejść na stronę http://nodejs.org/download/, która aktualnie wygląda tak jak na rysunku 10.1. Klikając logo Apple albo Windows, pobierzesz instalatora umożliwiającego instalację biblioteki przy użyciu typowego kreatora instalacji (wraz z nim jest dostępny pakiet npm, ale o tym za chwilę). Kreator jest najłatwiejszą opcją dla użytkowników systemów Mac i Windows. Użytkownicy Linuksa są jednak raczej przyzwyczajeni do korzystania z wiersza poleceń. Aby zainstalować program przy użyciu polecenia, w systemach Mac i Linux trzeba mieć Terminal, a w Windowsie narzędzie putty lub cygwin. Jeśli masz któryś z wymienionych programów, to instrukcje dotyczące instalacji node.js znajdziesz na stronie Joyenta (właściciela biblioteki) w serwisie GitHub pod adresem https://github.com/joyent/node/wiki/Installing -Node.js-via-package-manager.

Rysunek 10.1. Strona pobierania biblioteki node.js

Każdy, kto aktywnie uczestniczy w wymianie kodu źródłowego, doskonale zna serwis GitHub. Git to system kontroli wersji, czyli rozwiązanie pozwalające zapanować nad wersjami plików w trochę lepszy sposób niż poprzez tworzenie nazw mójNajnowszyPlik.roz, mójNajlepszyPlik.roz,

Receptura. Instalacja i uruchamianie Node  253

mójNaprawdęNajnowszyPlik.roz, mójNajnowszyPlikLipiec.roz. GitHub to serwis do przechowywania plików z kodem źródłowym w celu robienia kopii zapasowej oraz udostępniania ich innym programistom. Nie jest to system zarządzania pakietami, jak opisane wcześniej, ale jest bardzo często wykorzystywany także przez programistów JavaScriptu. Może się zdarzyć, że będzie to najczęściej odwiedzana przez Ciebie strona internetowa. Po zainstalowaniu node.js wybraną metodą będziesz gotowy do dalszego działania. Uruchom konsolę i wpisz polecenie node. Spowoduje to wyświetlenie znaku > oznaczającego, że można wpisywać polecenia Node. Jest to narzędzie podobne do konsoli JavaScript w przeglądarce internetowej. Możesz wpisać 1+3 i otrzymasz wynik 4. Możesz też używać funkcji JavaScript. Jedną z różnic w porównaniu z konsolą JavaScriptu jest brak dostępu do takich obiektów jak window czy document. Są to obiekty przeglądarkowe, a przecież ta konsola nie jest przeglądarką! Gratulacje. Poradziłeś sobie z instalacją odpowiedniej wersji biblioteki w swoim systemie. Teraz w tym interpreterze możesz używać prawdziwego języka programowania JavaScript, bez żadnych udziwnień wprowadzanych przez przeglądarki internetowe. Aby wyjść z interpretera, naciśnij klawisze Ctrl+C. Masz już do dyspozycji interpreter JavaScriptu, który można uruchomić w konsoli za pomocą polecenia node. Teraz dodatkowo napiszemy serwer. Utwórz plik o nazwie httpserver.js i zapisz w nim kod przedstawiony na listingu 10.1. Następnie w terminalu wpisz następujące polecenie, aby uruchomić ten serwer: node httpserver.js. Listing 10.1. Serwer HTTP Node console.log("Cześć, jestem pod adresem http://localhost:1234"); var http = require('http'); http.createServer(function (request, response) { console.log("Żądanie odebrano."); response.write('

Hej tam!

'); response.end(); }).listen(1234);

Przyzwyczaiłeś się, że funkcja console.log drukuje dane w przeglądarce, ale tutaj użyliśmy jej w celu wysłania danych do konsoli. W drugim wierszu pobieramy pakiet http Node, aby użyć go jako serwera. Następnie wywołujemy funkcję createServer, której przekazujemy funkcję określającą sposób obsługi przychodzących żądań. Obsługa ta polega na wydrukowaniu w terminalu informacji oraz nagłówka i zakończeniu połączenia. Jeśli nie zamknęlibyśmy połączenia za pomocą funkcji response.end, to na stronie wyświetliłaby się ikona oczekiwania i wydawałoby się, że nic się nie dzieje. Funkcja listen(1234) określa numer portu, na którym można zobaczyć serwer w akcji. A skoro już mowa o oglądaniu działania serwera, otwórz w przeglądarce internetowej adres http://localhost:1234, który wskazuje program w konsoli. Zobaczysz taką samą stronę jak pokazana na rysunku 10.2 (jest to przeglądarka Firefox z dodatkowo otwartą kartą Skrypt Firebuga). Także na karcie Sources narzędzi dla programistów przeglądarki Chrome nie znajdziesz skryptu httpserver.js. Przeglądarka „nie wie”, że użyliśmy języka JavaScript. Uwolniliśmy ją od niego. Jedyne, co „wie” przeglądarka, to że wysłała żądanie do serwera HTTP i otrzymała w odpowiedzi nagłówek, ale nie ma dla niej znaczenia, czy do jego wysłania użyto języka JavaScript, Ruby, Python, Perl, Java czy PHP.

254  Rozdział 10. GRY RTS

Rysunek 10.2. Plik httpserver.js jest przeznaczony do oglądania tylko dla nas

UWAGA JavaScript w pewnym sensie jest językiem przeglądarkowym i trzeba się z tym pogodzić. Mimo że do wyboru jest kilka języków serwerowych, użycie tego samego języka zarówno po stronie serwera, jak i klienta ma pewne zalety. Podczas pracy nad aplikacją zazwyczaj większość czasu spędza się nad stroną serwerową. Kod na serwerze jest zwykle dopieszczony pod każdym względem, natomiast część frontowa aplikacji jest zaniedbywana i robiona byle jak, aby tylko była. Przez to cierpią użytkownicy. Jeśli po obu stronach używa się tego samego języka programowania, to programista przynajmniej będzie wiedział, jak napisać kod frontowy. Po drugie, łatwiej jest przenosić kod serwerowy do klienta i odwrotnie. Niektóre biblioteki mają nawet kod pozwalający połączyć oba te konteksty, o czym przekonasz się wkrótce w opisie biblioteki Socket.IO.

Receptura. Synchronizacja przy użyciu biblioteki Socket.IO Zanim rozpoczniemy właściwą pracę nad grą, musimy poznać jeszcze jedną technologię: Socket.IO. Utworzyliśmy już serwer HTTP nasłuchujący na lokalnym porcie 1234. Tego serwera można by było użyć dla niesynchronicznych operacji do pobierania ukrytych informacji i wykonywania obliczeń. Następnie można by było udostępniać te informacje pod różnymi adresami typu localhost:1234/graczPierwszy. Do skomplikowanych działań związanych z wyznaczaniem tras itp. można by było użyć biblioteki express.js, która jest zbudowana na bazie Node. Aby w tym systemie wykonywać zmiany na bieżąco, można użyć Ajaksa, jak w poprzednich rozdziałach. Wówczas w celu pobrania nowych informacji za każdym razem trzeba by było połączyć się z serwerem i wykonać nowe żądanie HTTP, by otrzymać odpowiedź. Żeby to działało, potrzebna jest ścieżka (i ewentualnie parametry) dla każdego typu informacji albo można przyjmować duże ilości danych w odpowiedziach, które następnie trzeba przefiltrować.

Receptura. Synchronizacja przy użyciu biblioteki Socket.IO  255

Są różne rozwiązania oparte na Ajaksie, a jedno z najnowszych to protokół o nazwie gniazda sieciowe (WebSocket). Ta technika jest szybsza i pochłania mniej transferu niż Ajax, ale nie każda przeglądarka ją obsługuje. Dlatego powstała biblioteka Socket.IO, która udostępnia jednolity interfejs łączący wszystkie wymienione technologie. Jeśli przeglądarka nie obsługuje jednej, automatycznie zostaje użyta inna. Bibliotekę Socket.IO można pobrać przy użyciu menedżera pakietów Node npm za pomocą polecenia konsoli npm install [email protected] -g. Polecenie to spowoduje zainstalowanie biblioteki Socket.IO w systemie. Podczas procesu instalacji w konsoli zostanie wyświetlona informacja, gdzie dokładnie umieszczono pliki. Po zakończeniu instalacji w folderze, w którym będą znajdować się wszystkie pliki projektu (index.html, game.js i server.js), wykonaj polecenie npm link socket.io. Dodaje ono lokalne łącze do globalnego pakietu npm. Tą czynnością zakończyliśmy konfigurację serwera. UWAGA Czasami można spotkać instrukcje nakazujące pobranie modułu przy użyciu npm i za pomocą polecenia –g flag, a więc żeby zainstalować Socket.IO, należałoby napisać npm install socket.io -g. Dzięki temu pakiet npm jest dostępny globalnie w całym systemie. Wadą tego rozwiązania jest to, że jeśli pracuje się nad wieloma projektami, to aktualizacja pakietów może sprawić, że starsze projekty przestaną działać. W takim przypadku pakiety powinno się instalować lokalnie. Z drugiej strony jeśli potrzebujesz tylko środowiska testowego, to lepiej jest wybrać opcję globalną. Dla uproszczenia zastosowałem opcję globalną, ale aby mieć pewność, że mój projekt będzie działał także w przyszłości, określiłem, że zainstalowana ma zostać wersja 0.9.11 biblioteki. Bez tego zastrzeżenia kod przedstawiony w tej książce mógłby nie działać z nowszą wersją biblioteki Socket.IO.

Moduł Socket.IO również zawiera plik JavaScript i możesz do niego zajrzeć, jeśli chcesz. Ścieżka powinna zostać wyświetlona w konsoli podczas instalacji biblioteki. Możesz też go znaleźć w folderze 10_rts/początek w paczce kodu pobranej z serwera FTP. Kolejną czynnością jest utworzenie trzech plików. Zaczniemy od pliku index.html, którego treść jest przedstawiona na listingu 10.2. Listing 10.2. Plik index.html RTS

256  Rozdział 10. GRY RTS Ustawiliśmy tytuł strony, załadowaliśmy pobrany z modułu (kod serwera wciąż znajduje się w menedżerze pakietów) kod kliencki biblioteki Socket.IO oraz dołączyliśmy nowo utworzony plik game.js. Teraz wpiszemy do pliku game.js kod źródłowy przedstawiony na listingu 10.3. Listing 10.3. Treść pliku game.js zawierająca kod kliencki Socket.IO window.onload = function() { var socket = io.connect('http://localhost:1234'); socket.on('started', function(data){ console.log(data); }); };

Czekamy na załadowanie okna, po czym próbujemy połączyć się z gniazdem na porcie 1234. Funkcja on oczekuje na informację started od serwera i po jej otrzymaniu wykonuje anonimową funkcję zapisującą dane w dzienniku. Jeśli teraz uruchomisz ten kod (otwierając adres http://localhost:1234), w konsoli zobaczysz informację o błędzie. Gdyby jednak serwer httpserver.js działał na porcie 1234, to zobaczyłbyś napis „Żądanie odebrane” oznaczający, że żądanie zostało obsłużone. Przed rozpoczęciem tworzenia serwera Socket.IO zamknij dotychczasowy serwer HTTP, zamykając okno (albo kartę) w zawierającym je terminalu, lub naciśnij klawisze Ctrl+C, aby zamknąć proces. Teraz jest nam potrzebny serwer Socket.IO do komunikacji z plikiem game.js (tzn. chcemy wykorzystać wcześniej dodaną bibliotekę kodu). Utwórz plik server.js i wpisz w nim kod przedstawiony na listingu 10.4. Listing 10.4. Plik serwera Socket.IO var io = require('socket.io').listen(1234); io.sockets.on('connection', function (socket) { socket.emit('started', {ahoy: "Cześć!"}); });

Kod ten dodaje serwer Socket.IO przy użyciu słowa kluczowego Node require i ustawia jego nasłuch na port 1234. Następnie użyliśmy funkcji on (podobnie jak w kodzie klienckim), która oczekuje na zdarzenie połączenia i odpowiada na nie wysłaniem wiadomości do klienta za pomocą funkcji emit (tej funkcji można także używać do przesyłania danych z klienta na serwer). Wiadomość ta ma nazwę started, oznaczającą zdarzenie, na które oczekuje kod pokazany na listingu 10.3. Kod klienta odbiera obiekt wiadomości i drukuje ją w konsoli. Aby zobaczyć, jak to działa, wpisz polecenie node server.js w terminalu, by uruchomić serwer Socket.IO. Następnie otwórz plik index.html w przeglądarce. W konsoli przeglądarki zobaczysz, że wiadomość została przekazana. Ponadto w oknie terminala zostaną wyświetlone informacje serwera, podobne do pokazanych na rysunku 10.3. Nie wszystko, co tu widać, musisz rozumieć, ale jeśli podczas pracy nad grą wystąpią jakieś problemy, to oprócz konsoli przeglądarki jest to drugie miejsce, w które należy zajrzeć. Na przykład programiści często zapominają uruchomić serwer, o czym można się dowiedzieć właśnie w tym miejscu. Także jeśli serwer będzie uruchomiony, ale otworzysz niewłaściwy adres, dowiesz się o tym, widząc, że nie ma połączenia. Mając gotową platformę, możemy rozpocząć budowę samej gry.

Receptura. Tworzenie mapy izometrycznej przy użyciu silnika crafty.js  257

Rysunek 10.3. Przykładowe dane drukowane przez serwer

Receptura. Tworzenie mapy izometrycznej przy użyciu silnika crafty.js Ze wszystkich opisanych do tej pory silników crafty.js jest niewątpliwie jednym z najbardziej dojrzałych. Zawiera solidny system komponentowo-jednostkowy (ang. component-entity system), skupia aktywną społeczność, ma znakomitą dokumentację oraz dobrze obsługuje moduły (tworzone i obsługiwane przez wspomnianą społeczność). Pomimo niewątpliwych potencjalnych korzyści w silnikach gier HTML5 wciąż dość rzadko spotyka się rozwiązania podobne do modułów Node, jak Socket.IO, polegające na tworzeniu uzupełniających się interfejsów klient-serwer. Z tego powodu, jeśli nie zdecydujesz się dokonać poważnych przeróbek, biblioteki crafty.js będziesz używać tylko po stronie klienta. Zaczniemy od narysowania tablicy izometrycznej przy użyciu crafty.js. W tym celu dodaj do pliku game.js kod pogrubiony na listingu 10.5. Listing 10.5. Rysowanie tablicy izometrycznej przy użyciu biblioteki Crafty window.onload = function() { Crafty.init(); Crafty.viewport.scale(3.5); var iso = Crafty.isometric.size(16); var mapWidth = 20; var mapHeight = 40; Crafty.sprite(16, "sprites.png", { grass: [0,0,1,1], selected_grass: [1,0,1,1], blue_box: [2,0,1,1], blue_one: [3,0,1,1], blue_two: [4,0,1,1], blue_three: [5,0,1,1], blue_bomb: [6,0,1,1],

258  Rozdział 10. GRY RTS blue_flag: [7,0,1,1], red_box: [8,0,1,1], red_one: [9,0,1,1], red_two: [10,0,1,1], red_three: [11,0,1,1], red_bomb: [12,0,1,1], red_flag: [13,0,1,1], selected_box: [14,0,1,1] }); var setMap = function(){ for(var x = 0; x < mapWidth; x++) { for(var y = 0; y < mapHeight; y++) { var bias = ((y % 2) ? 1 : 0); var z = x+y + bias; var tile = Crafty.e("2D, DOM, grass, Mouse") .attr('z',z) .areaMap([7,0],[8,0],[15,5],[15,6],[8,9],[7,9],[0,6],[0,5]) .bind("MouseOver", function() { this.addComponent("selected_grass"); this.removeComponent("grass"); }).bind("MouseOut", function() { this.addComponent("grass"); this.removeComponent("selected_grass"); }); iso.place(x,y,0, tile); } } }; setMap(); var socket = io.connect('http://localhost:1234'); socket.on('started', function(data){ console.log(data); }); };

Po załadowaniu okna inicjujemy Crafty. Następnie jako że sprite’y mają wymiary tylko 16×16 pikseli, skalujemy rozmiar obszaru widoku. Później inicjujemy izometryczny obiekt Crafty, którego będziemy używać do rozmieszczania sprite’ów. Kolejną czynnością jest ustawienie wymiarów mapy. Po jej wykonaniu opisujemy, jakie sprite’y będą pojawiać się w grze, a następnie deklarujemy ich położenie i wymiary oraz nadajemy etykiety. W funkcji setMap za pomocą pętli przeglądamy wszystkie pozycje x i y, bazując na wartościach mapWidth i mapHeight. Ustawienie kafelków na osi z (określenie, na której warstwie znajduje się każdy kafelek) jest w tym przypadku niełatwe. W górnym wierszu są dostępne wartości na osi z od 0 do 19. W następnych wierszach wartość ta zależy od konkretnego wiersza. Dla kafelków w wierszach parzystych wartość na osi z dla kafelka leżącego bezpośrednio niżej i na prawo oraz bezpośrednio niżej i na lewo musi być zwiększona. Jeśli tego nie zrobimy, kafelki nie będą się poprawnie na siebie nakładać. Temu właśnie zapobiega wartość bias. Kafelki są deklarowane jako nowe jednostki przy użyciu funkcji Crafty.e, która zawiera przypominający tablicę łańcuch z listą komponentów. Patrząc na tę listę, można docenić elastyczność systemu. Komponent 2D nie tylko umożliwia rozmieszczanie izometrycznych kafelków, ale również zawiera funkcję areaMap, która określa pikselowe granice kafelków, wykorzystując przekazane jej jako parametry pary x-y. Komponent DOM umożliwia rysowanie jednostki i umieszczenie

Receptura. Rysowanie jednostek  259

jej w drzewie DOM. Komponent grass ustawia sprite’a dla każdego kafelka. Ponadto w tym bloku można wykorzystać komponent Mouse do implementacji funkcji „podświetlania” kafelka, nad którym znajduje się kursor. UWAGA Zamiast komponentu DOM można by było użyć canvas, ale nie obsługuje on skalowania, które jest niezbędne przy tak niewielkim rozmiarze sprite’ów. Dlatego do budowy tej gry użyjemy komponentu DOM.

Utworzyliśmy mapę izometryczną z funkcją podświetlenia kafelka, nad którym znajduje się kursor. Otwórz plik index.html w przeglądarce, aby zobaczyć widok podobny do pokazanego na rysunku 10.4.

Rysunek 10.4. Izometryczna mapa z wyróżnionym kafelkiem

Receptura. Rysowanie jednostek Wrócimy do serwera, który jeśli przez cały ten czas działał, nawrzucał na stronę mnóstwo do niczego nieprzydatnych danych. W porządku, ale teraz musimy to zmienić. Jeśli zmodyfikujesz serwer, podczas gdy działa, zmiany nie zostaną automatycznie zastosowane. Aby je zastosować, musisz wyłączyć serwer, klikając w konsoli klawisze Ctrl+C. Czynność tę należy wykonywać zawsze, gdy zmienia się coś w serwerze. W pliku server.js zmienimy wywołanie wykonywane przy nawiązaniu połączenia na coś bardziej przydatnego, co pokazano na listingu 10.6. Pogrubione wiersze kodu zastępują stare wywołanie wysyłające zdarzenie started. Listing 10.6. Wysyłanie jednostek miejsc do klienta var io = require('socket.io').listen(1234); io.sockets.on('connection', function (socket) { var units = placeUnits();

260  Rozdział 10. GRY RTS

WSKAZÓWKA Jeśli chcesz, aby serwer automatycznie przyjmował zmiany w pliku, to musisz poznać techniki (zapewne w postaci pakietów npm), w których nazwie można znaleźć słowa „hot reload”, „live reload”, „file watcher”, „demon”, „daemon” itp. W świecie Node wszystko szybko się zmienia i może kiedyś pojawi się opcja typu server.js -reload albo jakaś możliwość automatycznego resetowania serwera za pomocą polecenia w pliku konfiguracyjnym. Na razie jednak taki sam efekt uzyskuje się przy użyciu pakietów npm nodemon i supervisor. socket.emit('place units', units);

// socket.emit('started', {ahoy: "Cześć!"}); // Ten wiersz musi odejść });

W tym kodzie ustawiamy zmienną units na wynik działania jeszcze niezdefiniowanej funkcji placeUnits. Następnie przesyłamy ten wynik do klienta, z którym mamy połączenie. Teraz zajmiemy się definicją funkcji placeUnits, której kod źródłowy znajduje się na listingu 10.7. Jest on dość długi, ale od dziesiątego wiersza zawiera dużo powtórzeń. Funkcję tę można wpisać na początku pliku server.js. Listing 10.7. Określenie położenia jednostek w pliku server.js var placeUnits = function(){ var yLocations = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38]; var pickY = function(alignment){ var y = Math.floor(Math.random(yLocations.length)*yLocations.length); return yLocations.splice(y, 1) - alignment; }; var xPositionRed = 18; var xPositionBlue = 1; return [ {color: "red", type: "one", xPosition: xPositionRed, yPosition: pickY(1)}, {color: "red", type: "one", xPosition: xPositionRed, yPosition: pickY(1)}, {color: "red", type: "one", xPosition: xPositionRed, yPosition: pickY(1)}, {color: "red", type: "two", xPosition: xPositionRed, yPosition: pickY(1)}, {color: "red", type: "two", xPosition: xPositionRed,

Receptura. Rysowanie jednostek  261

yPosition: pickY(1)}, {color: "red", type: "three", xPosition: xPositionRed, yPosition: pickY(1)}, {color: "red", type: "three", xPosition: xPositionRed, yPosition: pickY(1)}, {color: "red", type: "bomb", xPosition: xPositionRed, yPosition: pickY(1)}, {color: "red", type: "flag", xPosition: xPositionRed, yPosition: pickY(1)}, {color: "blue", type: "one", xPosition: xPositionBlue, yPosition: pickY(0)}, {color: "blue", type: "one", xPosition: xPositionBlue, yPosition: pickY(0)}, {color: "blue", type: "one", xPosition: xPositionBlue, yPosition: pickY(0)}, {color: "blue", type: "two", xPosition: xPositionBlue, yPosition: pickY(0)}, {color: "blue", type: "two", xPosition: xPositionBlue, yPosition: pickY(0)}, {color: "blue", type: "three", xPosition: xPositionBlue, yPosition: pickY(0)}, {color: "blue", type: "three", xPosition: xPositionBlue, yPosition: pickY(0)}, {color: "blue", type: "bomb", xPosition: xPositionBlue, yPosition: pickY(0)}, {color: "blue", type: "flag", xPosition: xPositionBlue, yPosition: pickY(0)} ] };

262  Rozdział 10. GRY RTS W tablicy yLocations zapisaliśmy wszystkie możliwe pionowe pozycje, w których mogą pojawić się jednostki. Następnie zdefiniowaliśmy funkcję pobierającą losowy indeks z tej tablicy, usuwającą element znajdujący się pod tym indeksem i zwracającą jego wartość. Parametr alignment umożliwia wyświetlenie jednostek z obu stron w tej samej odległości od odpowiednich krawędzi mapy. Bez tego zabiegu najlepszym sposobem na osiągnięcie takiego samego efektu byłoby utworzenie tablicy yLocations zarówno dla czerwonych, jak i niebieskich jednostek i zapisanie wartości parzystych w jednej, a nieparzystych w drugiej. Następnie ustawiamy pozycje x dla czerwonych i niebieskich jednostek. Funkcja ta zwraca długą tablicę jednostek. Przypisujemy kolor i typ jako wartości łańcuchowe, przy użyciu których później można określić sprite’a. Własność xPosition jest oparta na zmiennej xPosition odpowiedniego koloru, a yPosition jest wybierana losowo przez funkcję pickY. Przypisanie tych własności jest wykonywane dla każdego rodzaju jednostki, w przypadku niektórych jednostek nawet kilka razy. Nie zapomnij zresetować serwera, aby zmiany zostały wprowadzone. Obowiązek narysowania jednostek na stronie spoczywa już na kliencie. Nieważne, ile zrobisz na samym serwerze, kiedyś w końcu i tak musisz sobie przypomnieć, że kolory, obrazy i tekst są jednak renderowane w przeglądarce. Aby do tego doprowadzić, musimy zmodyfikować procedurę nasłuchującą Socket.IO, tak by przyjmowała wiadomość place units — pogrubiony kod na listingu 10.8. Można nim zastąpić stary kod nasłuchujący znajdujący się w pliku game.js. Listing 10.8. Nasłuchiwanie wiadomości place units var socket = io.connect('http://localhost:1234');

// Zamień ten wiersz: socket.on('started', function(data){ socket.on('place units', function(units){

// Zamień ten wiersz: console.log(data); placeUnits(units); });

W tym kodzie oczekujemy na informację od serwera o nowych jednostkach. Następnie wywołujemy funkcję placeUnits w celu przejrzenia tablicy units i umieszczamy jednostki na mapie. Trzeba jeszcze tę funkcję zdefiniować. Jej kod jest przedstawiony na listingu 10.9 i można go wpisać nad kodem z listingu 10.8 w pliku game.js. Listing 10.9. Funkcja placeUnits w pliku game.js var placeUnits = function(units){ for(var i = 0; i < units.length; i++){ var componentList = "2D, DOM, Mouse, " + units[i].color + " _ " + units[i].type; var unit = Crafty.e(componentList) .attr('z',100) .areaMap([7,0],[8,0],[14,3],[14,8],[8,12],[7,12],[2,8],[2,3]); iso.place(units[i].xPosition,units[i].yPosition,0, unit); }; }

Przeglądamy tablicę units za pomocą pętli, dodając nowe wystąpienie dla każdej jednostki. Sprite jednostki jest dodawany do listy komponentów pod etykietą utworzoną z połączenia koloru i typu. Indeks na osi z jest ustawiony na 100, ponieważ nie obchodzi nas, czy jednostki nachodzą

Receptura. Poruszanie jednostkami  263

na siebie, ale chcemy, aby znajdowały się na kafelkach. Funkcja areaMap ponownie zostaje wykorzystana do określenia wymiarów sprite’a poprzez połączenie punktów wskazanych przez przekazane jako parametry współrzędne x i y. Na koniec jednostka zostaje umieszczona na mapie zgodnie z zasadami izometrii, tak aby mieściła się w granicach kafelka trawy, na którym jest ułożona. Po wykonaniu tych czynności i zrestartowaniu serwera zauważysz, że rozmieszczenie jednostek będzie się zmieniać po każdym odświeżeniu strony — rysunek 10.5.

Rysunek 10.5. Losowo rozmieszczone na stronie jednostki

Receptura. Poruszanie jednostkami Mając jednostki na planszy, możemy zająć się kodem, który pozwoli nam nimi poruszać. Potrzebne nam będą dwie procedury obsługi zdarzeń kliknięcia: po jednej dla jednostek i kafelków. Pierwsza zmiana, jakiej dokonamy (oznaczona pogrubieniem na listingu 10.10), to dodanie procedury obsługi kliknięć kafelków wewnątrz funkcji setMap w pliku game.js. Listing 10.10. Procedura obsługi kliknięć kafelków var unitClicked = null; var setMap = function(){ ... .bind("MouseOver", function() { this.addComponent("selected _ grass"); this.removeComponent("grass"); }).bind("MouseOut", function() { this.addComponent("grass"); this.removeComponent("selected _ grass"); }).bind("Click", function() { if(unitClicked){ moveUnit(this); } }); iso.place(x,y,0, tile);

264  Rozdział 10. GRY RTS Przed funkcją setMap zdefiniowaliśmy nową zmienną do przechowywania klikniętej jednostki. Następnie już w funkcji setMap (pogrubiona część kodu) wiążemy z każdym kafelkiem procedurę obsługi zdarzenia kliknięcia, która, jeśli jednostka zostanie kliknięta, wywołuje funkcję moveUnit. Na listingu 10.11 znajduje się definicja funkcji moveUnit. Można ją wpisać między deklaracją zmiennej unitClicked a funkcją setMap. Listing 10.11. Deklaracja funkcji moveUnit var unitClicked = null; var moveUnit = function(place){ var xDistance = Math.abs(unitClicked.x - place.x); var yDistance = Math.abs(unitClicked.y - place.y); var distance = Math.sqrt(Math.pow(xDistance, 2) + Math.pow(yDistance, 2)*4); unitClicked.tween({x: place.x, y: place.y}, Math.round(distance*3)) unitClicked.addComponent(unitClicked.trueSprite); unitClicked = null; }; var setMap = function(){

W tej funkcji obliczamy różnicę współrzędnych x i y klikniętej jednostki (unitClicked) i miejsca (place), przy czym place jest kępką trawy, do której wysyłamy jednostkę unitClicked. Po dostosowaniu kodu jednostki place będzie odnosić się również do lokalizacji drugiej klikniętej jednostki. Mając obliczone odległości między sekcjami w poziomie i pionie, używamy znanego nam już bardzo dobrze twierdzenia Pitagorasa do obliczenia odległości w linii prostej między punktami. Zwróć uwagę, że składnik y jest mnożony przez cztery, ponieważ mapa, mimo że zawiera dwa razy więcej pozycji w pionie niż w poziomie, jest o połowę niższa niż szersza. Potem w funkcji tween przekazujemy współrzędne dotyczące kierunku jednostki oraz wynik obliczeń odległości, który ma nam pozwolić obliczyć liczbę milisekund, jaką ma trwać ruch. Wartość tę mnożymy przez trzy, aby trochę ten ruch spowolnić. Następnie przywracamy unitClicked do oryginalnego sprite’a i ustawiamy unitClicked na null. UWAGA Celowo zmniejszyliśmy szybkość ruchu jednostek poprzez pomnożenie odległości przez trzy. W grze, w której steruje się tylko jedną jednostką, np. w bijatyce, zapewne lepiej byłoby przyspieszyć. Jednak w grze strategicznej zwolnienie daje graczowi chwilę na pomyślenie, a ponadto gracz musi zarządzać dużą liczbą jednostek, co też jest nie bez znaczenia. Możesz dostosować szybkość przemieszczania się jednostek wg własnych upodobań. Możesz nawet dla każdego typu jednostek zdefiniować inną prędkość.

Następną czynnością będzie poprawienie funkcji oznaczony pogrubieniem na listingu 10.12.

placeUnits.

Odpowiedni fragment został

Listing 10.12. Funkcja placeUnits z obsługą klikania i ruchu var placeUnits = function(units){ for(var i = 0; i < units.length; i++){ var unitSprite = units[i].color + " _ " + units[i].type; var componentList = "2D, DOM, Mouse, Tween, " + unitSprite; var unit = Crafty.e(componentList) .attr('z',100)

Receptura. Sterowanie gracza i widoczność  265

};

.areaMap([7,0],[8,0],[14,3],[14,8],[8,12],[7,12],[2,8],[2,3]); unit.trueSprite = unitSprite; unit.bind("Click", function() { if(unitClicked){ if(unitClicked !== this){ moveUnit(this); }; }else{ this.removeComponent(this.trueSprite); this.addComponent('selected _ box'); unitClicked = this; }; }); iso.place(units[i].xPosition,units[i].yPosition,0, unit);

}

Jako że zmienna opisująca sprite’a będzie nam potrzebna więcej niż raz, obliczamy jej wartość zawczasu, zanim użyjemy jej w liście komponentów. W trzecim pogrubionym wierszu wiążemy ją jako prawdziwego sprite’a (trueSprite) jednostki (unit). Dalej znajduje się procedura obsługi kliknięć, która, jeśli jakaś jednostka zostanie kliknięta i nie jest to aktualnie klikana jednostka, przesuwa jednostkę na miejsce aktualnie klikanej jednostki. Jeżeli jednostka nie była jeszcze kliknięta, zamieniamy ją w skrzynkę poprzez zamianę jej sprite’a trueSprite na sprite’a selected_box, a potem ustawiamy klikaną jednostkę na unitClicked. Teraz możemy już przesuwać sprite’y po planszy. Na rysunku 10.6 widać planszę gry z jednostkami poprzestawianymi w różne miejsca. Nie zapomnij włączyć serwera.

Rysunek 10.6. Plansza po przestawieniu jednostek w różne miejsca

Receptura. Sterowanie gracza i widoczność W naszej grze można już przemieszczać różne kolorowe kafelki po mapie. Jesteśmy na etapie wstępnego projektu i mamy teraz do wyboru udoskonalenie go albo zmienienie sprite’ów na coś bardziej aktualnego i udawanie, że to tylko wersja eksperymentalna, aby nikt się do niczego nie

266  Rozdział 10. GRY RTS przyczepił. Wybierzemy pierwszą możliwość i każdemu z graczy damy kontrolę nad jednym z kolorów oraz ukryjemy wartości (I, II, III, bomba i flaga) pozostałych kolorowych pionków. Plik server.js jest najprostszy, więc od niego zaczniemy. Zwróć uwagę, że aż do tej pory serwera używaliśmy wyłącznie do konfigurowania planszy. W tej recepturze to zmienimy. Pogrubiony kod na listingu 10.13 wpisz pod funkcją placeUnits i nad procedurą obsługi połączenia. Listing 10.13. Atrybuty room i player var playerId = 0; var playerColor = function(){ return (!(playerId % 2)) ? "red" : "blue"; } var roomId = function(){ return "room" + Math.floor(playerId / 2); } var units; var io = require('socket.io').listen(1234);

Najpierw zmiennej playerId nadaliśmy początkową wartość 0. W każdym połączeniu będziemy ją zwiększać. Następnie deklarujemy funkcję ustawiającą graczy o parzystych numerach na czerwony kolor, a graczy o nieparzystych numerach na kolor niebieski. Później deklarujemy drugą funkcję, która przydziela każdej kolejnej parze graczy własny pokój. Funkcja Math.floor sprawia w tym przypadku, że dwaj gracze mogą znajdować się w tym samym pokoju. Na końcu deklarujemy zmienną units, aby móc zachować jej pierwszą wartość dla drugiego gracza, który ma dołączyć do pokoju. Teraz przejdziemy do kodu obsługi połączenia w pliku server.js, który jest pokazany na listingu 10.14. Listing 10.14. Modyfikacje procedury obsługi połączenia var io = require('socket.io').listen(1234); io.sockets.on('connection', function (socket) { socket.playerColor = playerColor(); socket.roomId = roomId(); var player = {id: playerId, color: socket.playerColor, room: socket.roomId } socket.emit('initialize player', player); socket.join(socket.roomId); if(!(playerId % 2)){ units = placeUnits(); }; socket.emit('place units', units); socket.on('update positions', function (data) { socket.broadcast.to(socket.roomId).emit('update enemy positions', data); }); playerId = playerId + 1; socket.on('disconnect', function () { socket.broadcast.to(socket.roomId).emit('user disconnected'); }); });

Receptura. Sterowanie gracza i widoczność  267

Najpierw określamy kolor i identyfikator pokoju dla bieżącego gniazda. Następnie dodajemy te atrybuty do obiektu gracza i wysyłamy te dane w wiadomości initialize player do gniazda podłączonego klienta. W następnym wierszu łączymy pokój z bieżącym identyfikatorem pokoju. Pokoje są jednym ze sposobów w bibliotece Socket.IO na ograniczanie zakresu dostępności wiadomości do podzbioru połączonych klientów. Następnie jeżeli połączony klient ma parzysty numer, inicjujemy rozmieszczenie jednostek na planszy. Nie robimy tego dla nieparzystych numerów, aby uniknąć odtwarzania ułożenia jednostek za każdym razem. Gdybyśmy to robili, to gracze widzieliby różne plansze. Następnie tworzymy procedurę obsługi wiadomości update positions. Gdy otrzymamy tę wiadomość, wysyłamy drugiemu graczowi (poprzez mechanizm broadcast) wiadomość update enemy positions i przekazujemy nowe pozycje wrogów. Następnie zwiększamy wartość playerId, aby w każdym połączeniu była używana inna wartość oraz aby można było prawidłowo określić pokoje i kolory dla graczy. Jeżeli gracz opuści grę, ostatnia procedura obsługowa przekazuje wiadomość drugiemu graczowi (przy użyciu mechanizmu broadcast). Teraz zajmiemy się plikiem game.js. Najpierw wprowadzimy zmiany w funkcji placeUnits, jak pokazano na listingu 10.15. Listing 10.15. Modyfikacje funkcji placeUnits var placeUnits = function(units){ player.units = []; player.enemyUnits = []; for(var i = 0; i < units.length; i++){ var unitInfo = units[i]; var controllable = unitInfo.color === player.color; if(controllable){ unitSprite = unitInfo.color + " _ " + unitInfo.type; }else{ unitSprite = "selected _ box"; } var componentList = "2D, DOM, Mouse, Tween, " + unitSprite; var unit = Crafty.e(componentList) .attr('z',100) .areaMap([7,0],[8,0],[14,3],[14,8],[8,12],[7,12],[2,8],[2,3]); unit.controllable = controllable; unit.trueSprite = unitSprite; unit.xPosition = function(){ return this.x; }; unit.yPosition = function(){ return this.y; }; unit.bind("Click", function() { if(unitClicked){ if(unitClicked !== this){ moveUnit(this); }; }else{ if(this.controllable){ this.removeComponent(this.trueSprite); this.addComponent('selected _ box'); unitClicked = this; };

268  Rozdział 10. GRY RTS }; }); iso.place(unitInfo.xPosition, unitInfo.yPosition, 0, unit); if(unit.controllable){ player.units.push(unit); }else{ player.enemyUnits.push(unit); } }; }

Najpierw utworzyliśmy tablice units i enemyUnits. Pierwsza zostanie wysłana do serwera z informacją, w które miejsce ruszył się gracz. Druga służy do przetwarzania pozycji drugiego gracza podczas odbierania danych od serwera. Następnie ustawiamy zmienną unitInfo jako skrót do elementu tablicy, którego będziemy często używać. Zmienna controllable jest ustawiana w zależności od tego, czy kolor gracza pasuje do koloru jednostki. Jeśli kolory pasują, to następna instrukcja warunkowa umożliwia graczowi obejrzenie wartości jednostki. W przeciwnym razie nie jest to możliwe. Zmienną controllable ustawiamy jako atrybut jednostki, aby móc mieć do niego dostęp także w innych zakresach. Analogicznie jako atrybuty jednostki przypisujemy funkcje xPosition i yPosition. Jako że te wartości mogą się zmieniać, użycie zmiennej nie wchodzi w grę. W procedurze obsługi kliknięcia mamy nowy warunek dotyczący pierwszego kliknięcia (w celu „chwycenia” elementu, a nie jego przeniesienia w inne miejsce). Jeśli gracz nie ma kontroli nad jednostką, to nie może jej chwycić. Jest to druga połowa controllable. Pierwsza dotyczyła tego, czy gracz może zobaczyć wartość jednostki. Następnie, gdy gracz umieści kafelek na miejscu, przestajemy używać indeksu tablicowego współrzędnych x i y i w zamian używamy zmiennej unitInfo. Na koniec sprawdzamy, czy gracz ma kontrolę nad jednostką, i w zależności od wyniku testu dodajemy ją do tablicy units albo enemyUnits. Teraz zajmiemy się kodem komunikacyjnym na serwerze (listing 10.16). Nowe wiersze zostały pogrubione, chociaż jak widać, prawie wszystko jest tu nowe. Żeby nie ryzykować pomyłki, najlepiej jest skopiować wszystko z procedury obsługi wiadomości place units. Listing 10.16. Komunikacja z serwerem w celu przekazania informacji o zmianach pozycji var socket = io.connect('http://localhost:1234'); socket.on('place units', function(units){ placeUnits(units); var updateInterval = 1000/Crafty.timer.getFPS(); setInterval(function(){ socket.emit('update positions', player.unitsWithLimitedData()); }, updateInterval); }); socket.on('update enemy positions', function(enemies){ player.updateEnemyPositions(enemies); }); socket.on('initialize player', function(playerData){ player = playerData; player.unitsWithLimitedData = function(){ unitsToReturn = []; for(var i = 0; i < this.units.length; i++){

Receptura. Sterowanie gracza i widoczność  269

unitsToReturn.push({x: this.units[i].x, y: this.units[i].y}); } return unitsToReturn; }; player.updateEnemyPositions = function(enemies){ for(var i = 0; i < this.enemyUnits.length; i++){ this.enemyUnits[i].x = enemies[i].x; this.enemyUnits[i].y = enemies[i].y; } }; }); socket.on('user disconnected', function(){ alert("Twój przeciwnik zrezygnował albo odświeżył okno. Odśwież swoje okno, aby przejść do innego pokoju."); }); };

// Koniec pliku

Zaczynamy od wywołania funkcji setInterval, która ma działać z prędkością 50 klatek na sekundę, czyli taką samą jak główna szybkość zmiany klatek. Wynikiem działania tego pętlowego kodu są informacje o pozycjach w poziomie i pionie jednostek kontrolowanych przez gracza, które zostają przekazane serwerowi. Dalej znajduje się procedura obsługi aktualizacji pozycji wroga. Kolejna część kodu to procedura obsługi wiadomości initialize player. Ustawiamy w niej zmienne używane w dwóch pierwszych procedurach. Zmiennej player jest nadawana wartość pochodząca z serwera. Następnie mamy funkcję unitsWithLimitedData, która wysyła do serwera współrzędne x i y wszystkich znajdujących się pod kontrolą gracza jednostek. Potem deklarujemy funkcję updateEnemyPositions, która działa odwrotnie do funkcji unitsWithLimitedData, tzn. ustawia pozycje wroga zgodnie z informacjami otrzymanymi od serwera. Ostatnia procedura obsługi wiadomości dotyczy sytuacji, gdy jeden z graczy się rozłączy, i wyświetla informację u drugiego gracza, informując go o tym fakcie. Teraz możesz już zrestartować serwer i otworzyć dwa okna gry, aby zagrać sam ze sobą. Użyj dwóch różnych okien, nie kart. Jeśli użyjesz kart, przeglądarka nie będzie aktualizować danych. WSKAZÓWKA To dotyczy także zmieniania kodu podczas pracy. Jeśli masz uruchomione dwa klienty i serwer, to dzięki temu się nie pogubisz. Wyświetlane powiadomienia sprawią, że będziesz wiedział, które z okien trzeba odświeżyć, ale jeśli mimo to się pogubisz, wykonaj następujące czynności: 1. Zamknij serwer. 2. Odśwież albo zmień strony w oknach przeglądarki, która jest połączona z gniazdem. Jeśli tego nie zrobisz, otwarte klienty automatycznie połączą się z serwerem i może nastąpić nieoczekiwany przepływ danych. 3. Ponownie uruchom serwer. 4. Odśwież strony, aby ponownie połączyć klienty z serwerem.

Na rysunku 10.7 są przedstawione dwa okna gry, a na rysunku 10.8 pokazano okno terminala z informacjami o działającym serwerze.

270  Rozdział 10. GRY RTS

Rysunek 10.7. Dwa okna gry

Rysunek 10.8. Dane w terminalu wysłane przez serwer

Receptura. Kolizje dla destrukcji i sprawdzenia przeciwnika Osiągnęliśmy już prawie wszystkie zamierzone cele. Pozostało nam jeszcze tylko jedno zadanie: umożliwić jednostkom wzajemny atak. Potrzebne zmiany nie będą dotyczyły serwera. Możemy wykorzystać typy wiadomości, które już teraz przekazujemy między serwerem a klientem. Na początku pliku game.js musimy dodać dwa komponenty, jak pokazano na listingu 10.17.

Receptura. Kolizje dla destrukcji i sprawdzenia przeciwnika  271

Listing 10.17. Nowe komponenty window.onload = function() { Crafty.init(); Crafty.c("blue"); Crafty.c("red");

Komponenty te są potrzebne dlatego, że biblioteka crafty.js używa ich do wykrywania kolizji. Moglibyśmy wykorzystać bardziej konkretne komponenty, ale otrzymalibyśmy dłuższy i wolniej działający kod. Teraz wprowadzimy kilka zmian w funkcji placeUnits, jak pokazano na listingu 10.18. Listing 10.18. Zmiany w funkcji placeUnits w pliku game.js var placeUnits = function(units){ player.units = []; player.enemyUnits = []; for(var i = 0; i < units.length; i++){ var unitInfo = units[i]; var controllable = unitInfo.color === player.color; unitInfo.trueSprite = unitInfo.color + "_" + unitInfo.type; if(controllable){ unitSprite = unitInfo.color + "_" + unitInfo.type; }else{ unitSprite = unitInfo.color + "_box"; } var componentList = "2D, DOM, Mouse, Tween, Collision, " + unitInfo.color + ", " + unitSprite; var unit = Crafty.e(componentList) .attr('z',100) .areaMap([7,0],[8,0],[14,3],[14,8],[8,12],[7,12],[2,8],[2,3]) .collision([7,0],[8,0],[14,3],[14,8],[8,12],[7,12],[2,8],[2,3]) unit.controllable = controllable; unit.trueSprite = unitInfo.trueSprite; unit.xPosition = function(){ return this.x; }; unit.yPosition = function(){ return this.y; }; unit.hiding = true; unit.alive = true; unit.color = unitInfo.color; unit.type = unitInfo.type; unit.bind("Click", function() { if(unitClicked){ if(unitClicked !== this){ moveUnit(this); }; }else{ if(this.controllable){ this.removeComponent(this.trueSprite); this.addComponent(this.color + '_box'); unitClicked = this; }; };

272  Rozdział 10. GRY RTS }); var collidesWithColor = (unit.color === "blue" ? "red" : "blue"); unit.onHit(collidesWithColor, function(e){ this.hiding = false; e[0].obj.hiding = false; if(this.type === "one"){ if(e[0].obj.type === "one" || e[0].obj.type === "two" || e[0].obj.type === "three"){ this.alive = false; }; }else if(this.type === "two"){ if(e[0].obj.type === "two" || e[0].obj.type === "three" || e[0].obj.type === "bomb"){ this.alive = false; }; }else if(this.type === "three"){ if(e[0].obj.type === "three" || e[0].obj.type === "bomb"){ this.alive = false; }; }else if(this.type === "bomb"){ if(e[0].obj.type === "one"){ this.alive = false; }; }else if(this.type === "flag"){ if(e[0].obj.type === "one" || e[0].obj.type === "two" || e[0].obj.type === "three"){ this.alive = false; }; } }) iso.place(unitInfo.xPosition, unitInfo.yPosition, 0, unit); if(unit.controllable){ player.units.push(unit); }else{ player.enemyUnits.push(unit); } }; }

Zamiast ufać, że unitSprite i trueSprite są tym samym, teraz je rozróżniamy i przechowujemy referencję do trueSprite w jednostce, aby móc później odkryć wrogów. Ponadto obecnie do określenia stylu ukrytego pola używamy koloru sprite’a (zamiast selected_box). Do listy komponentów dodaliśmy kolor (do prostego wykrywania kolizji) i komponent Collision. W funkcji collision przekazujemy listę punktów określających obwód pola zderzenia, podobnie jak używamy areamap do określania klikalnego obszaru jednostek. Własności hiding i alive zostały dodane do jednostki, aby można było ją aktualizować i przekazywać do drugiego gracza informacje o jej stanie. Dzięki tym własnościom możemy odsłonić każdą jednostkę, która zderzy się z jednostką gracza, i zniszczyć tę, która przegra potyczkę. Własności color i type służą jako trwałe odwołania do tych wartości. W procedurze obsługi kliknięcia używamy własności color do określenia koloru pola po kliknięciu jednostki. Następnie implementujemy obsługę kolizji, zaczynając od sprawdzenia, jaki kolor ma wróg. Kolor ten przekazujemy jako pierwszy parametr funkcji onHit. Drugi parametr tej funkcji to funkcja odkrywająca wartość obu jednostek i obsługująca obu wrogów. W mechanizmach wykrywania kolizji biblioteki Crafty this odnosi się do jednostki, z którą jest związane wykrywanie kolizji,

Receptura. Kolizje dla destrukcji i sprawdzenia przeciwnika  273

a e jest tablicą kolizji. Typ jednostki określamy poprzez pobranie własności obj pierwszego elementu tej tablicy. Jeśli jest taki sam jak jakikolwiek typ jednostki, który może pobić tę jednostkę, przeznaczamy ją do usunięcia. Funkcja placeUnits jest już gotowa, możemy więc przejść do dostosowania wiadomości przesyłanych między serwerem a klientem. W procedurze obsługi wiadomości place units jest potrzebna jedna drobna zmiana, którą zaznaczono pogrubieniem na listingu 10.19. Jest to wywołanie funkcji updateCasualities. Listing 10.19. Modyfikacja procedury obsługi wiadomości place units socket.on('place units', function(units){ placeUnits(units); var updateInterval = 1000/Crafty.timer.getFPS(); setInterval(function(){ socket.emit('update positions', player.unitsWithLimitedData()); player.updateCasualities(); }, updateInterval); });

Ostatnie zmiany, jakich musimy dokonać w tym pliku, dotyczą procedury obsługi wiadomości initialize player i są pokazane na listingu 10.20. Listing 10.20. Modyfikacja procedury obsługi wiadomości initialize player socket.on('initialize player', function(playerData){ player = playerData; player.unitsWithLimitedData = function(){ unitsToReturn = []; for(var i = 0; i < this.units.length; i++){ unitsToReturn.push({x: this.units[i].x, y: this.units[i].y, hiding: this.units[i].hiding, alive: this.units[i].alive}); } return unitsToReturn; }; player.updateCasualities = function(enemies){ for(var i = 0; i < this.units.length; i++){ if(this.units[i].alive === false){ this.units[i].destroy(); } }; } player.updateEnemyPositions = function(enemies){ for(var i = 0; i < this.enemyUnits.length; i++){ this.enemyUnits[i].x = enemies[i].x; this.enemyUnits[i].y = enemies[i].y; this.enemyUnits[i].hiding = enemies[i].hiding; this.enemyUnits[i].alive = enemies[i].alive; if(this.enemyUnits[i].hiding === false){ this.enemyUnits[i].addComponent(this.enemyUnits[i].trueSprite); if(this.enemyUnits[i].alive === false){ player.markToDestroy(this.enemyUnits[i]); } if(this.enemyUnits[i].reallySuperDead === true){ this.enemyUnits[i].destroy(); };

274  Rozdział 10. GRY RTS }; } }; player.markToDestroy = function(enemy){ setTimeout(function(){ enemy.reallySuperDead = true; }, 1000); } });

W pierwszym pogrubionym wierszu zaznaczamy, że chcemy wysłać własności hiding i alive na serwer razem z każdą z jednostek gracza. Następnie deklarujemy funkcję updateCasualities, którą wywołaliśmy na listingu 10.19. Funkcja ta usuwa jednostki gracza, które zostały pobite przez jednostki przeciwnika. W funkcji updateEnemyPositions używamy przekazanych własności alive i hiding. Jeśli jednostka wroga nie jest ukryta, pokazujemy jej prawdziwego sprite’a i sprawdzamy, czy jest żywa. Jeśli nie, oznaczamy ją do zniszczenia. Funkcja markToDestroy czeka sekundę i ustawia nowy atrybut o nazwie reallySuperDead. Jeśli jednostka wroga jest oznaczona tym atrybutem, zostaje usunięta. Powodem takiego działania jest to, że chcemy pokazać pobitą jednostkę przeciwnika, aby gracz mógł ją wziąć pod uwagę w przyszłych atakach. Dzięki temu dostępnych jest więcej informacji i gra jest bardziej interesująca. Nowy atrybut wprowadziliśmy dlatego, że jeśli tylko odczekamy sekundę, a potem oznaczymy jednostkę do usunięcia, w tym czasie mogą wystąpić inne kolizje i powstanie błąd. Atrybut reallySuperDead chroni nas przed taką sytuacją. Gra jest ukończona! Jeśli teraz zrestartujesz serwer i otworzysz dwa okna przeglądarki, będziesz mógł zagrać w grę dla dwóch osób polegającą na wzajemnym niszczeniu jednostek. Po kilku minutach rozgrywki ekran czerwonego gracza może wyglądać tak jak na rysunku 10.9.

Rysunek 10.9. Gra po kilku minutach rozgrywki. Zwróć uwagę, że jednostka III niebieskiego gracza została odkryta w górnej części

Podsumowanie  275

Podsumowanie W tym rozdziale utworzyliśmy sieciową grę RTS dla dwóch osób na bazie bibliotek crafty.js i socket.io. Przy okazji poznałeś system komponentów i jednostek biblioteki crafty.js oraz podstawy używania serwera. Ponadto dowiedziałeś się, jak działa menedżer pakietów Node o nazwie npm. Jeśli chcesz lepiej poznać bibliotekę Crafty, to ma ona jeszcze trochę cech, o których nie było mowy w tym rozdziale. Biblioteka ta obsługuje funkcje zapisywania danych i mierzenia czasu, matematyczne narzędzia 2D oraz dodatkowe, nieopisane tutaj funkcje obsługi systemu komponentów i zarządzania scenami. Sporo czasu zajmie też poznawanie jej bogatego API. Ponadto na stronie craftycomponents.com znajduje się zbiór wtyczek usprawniających działanie biblioteki. Jedną z godnych uwagi dla kogoś, kto tworzy grę RTS, jest wtyczka AStar, której użycie pozwala na poruszanie jednostkami nie tylko w linii prostej. Gdyby na mapie znajdowały się dziury i inne przeszkody, to przy użyciu tej wtyczki można byłoby zbudować prosty interfejs pozwalający przemieszczać się jednostkom za pomocą kliknięć. Jednostki obierałyby najkrótszą drogę wokół miejsc, w które nie można wchodzić. Naszej grze sporo jeszcze brakuje do tego, aby stać się pełną grą RTS. Na szczęście nie jest to nic bardziej skomplikowanego niż to, co robiliśmy we wcześniejszych rozdziałach. Wszystkie nasze jednostki mogą się poruszać, ale w grach RTS często występują różne nieruchome budowle. Jednostki i budowle zazwyczaj mają określony poziom „zdrowia”, który w wyniku ataków maleje. Ponadto walkę komplikują różne rodzaje ataków, jakie mogą wykonywać jednostki, np. strzały z daleka i wybuchy o szerokim polu rażenia. Często można kupować i udoskonalać jednostki. Czasami można nawet zbierać jakieś pieniądze, aby móc dokonywać zakupów. Można byłoby także rozszerzyć warstwę sieciową gry. Zamiast automatycznie łączyć graczy, można utworzyć poczekalnię, w której gracze sami dobieraliby się w pary. Można też utworzyć czat, aby gracze mogli ze sobą rozmawiać. Ciekawą możliwością jest też wyeliminowanie drugiego gracza i umożliwienie gry przeciwko sztucznej inteligencji.

276  Rozdział 10. GRY RTS

11 Dalszy rozwój

Gratuluję ukończenia lektury tej książki. Bardzo Ci dziękuję za jej przeczytanie. Jeśli przeszedłeś od razu na tę stronę i właśnie czytasz te słowa, to w porządku. Też tak czasami czytam książki. Na razie jednak dalsza część tego rozdziału jest przeznaczona dla tych, którzy przeczytali poprzednie. Ciebie też zapraszam tutaj ponownie, gdy również przeczytasz pozostałe rozdziały. W tym rozdziale nie opisuję żadnej gry, dzięki czemu jest on znacznie krótszy od poprzednich. Oczywiście mam nadzieję, że to nie oznacza, iż właśnie skończyłeś tworzyć gry. Chcę tylko podsumować poprzednie 10 rozdziałów i przekazać Ci kilka pomysłów, czym mógłbyś zająć się w następnej kolejności, aby dalej rozwijać swoje kwalifikacje.

278  Rozdział 11. DALSZY ROZWÓJ

Co się wydarzyło? Podsumujmy, co się wydarzyło. Przestudiowałeś 10 rozdziałów — każdy zawierał opis jednego gatunku gier i przynajmniej jednej biblioteki JavaScript, przy użyciu której tworzenie takich gier jest możliwe. Powinieneś już wiedzieć, jak podzielić grę na elementy stanowiące problemy do rozwiązania. Powinieneś już być obeznany z podstawową terminologią, jak sprite, kolizja czy parallax. Możesz już mieć swój ulubiony zestaw narzędzi, czyli przeglądarkę internetową, edytor tekstu, terminal, konsolę JavaScript, gatunek gier oraz silnik gier. Może nawet masz już ulubioną funkcję trygonometryczną. Oczywiście nie musisz kurczowo trzymać się raz wybranych opcji, ale dla wielu osób lubienie czegoś oznacza jakąś konkretną umiejętność. To znaczy, że jeśli lubisz jakieś narzędzie do tworzenia programów, to prawdopodobnie ma to związek z tym, że umiesz coś zrobić przy jego użyciu. Oprócz wyrobienia sobie zdania na temat podstawowych narzędzi i znalezienia tych, z którymi pracuje Ci się najlepiej, poznałeś na poziomie podstawowym i średnim technologie HTML, CSS oraz JavaScript. Ponadto nauczyłeś się używać niektórych technik związanych z językiem HTML5, które nie są związane z kanwą, np. CSS3 w grze z gatunku fikcji interaktywnej oraz API magazynu lokalnego. Trzeba jednak podkreślić, że wymienione technologie nie są częścią języka HTML5, a jedynie częścią „HTML5” pojmowanego jako pojęcie marketingowe.

Co dalej? Na początek poznałeś 10 gatunków gier, ale na nich świat się nie kończy. Przestudiowane w książce gry traktuj jako szablony i wskazówki, jak podzielić grę na sprite’y, klatki, warstwy, zdarzenia, sceny, schowki, kolizje, mapy, jednostki itd. Jeśli przyjmiesz taką perspektywę, to praktycznie każda nowa gra, nad jaką będziesz pracować, będzie wyglądała znajomo w sensie technicznym. Istnieją gatunki bardzo zbliżone do typów gier, które już masz. Jeśli na przykład strzelankę obrócisz o 90 stopni, to otrzymasz inny rodzaj strzelanki. Jeśli zmienisz sprite’y w grze RTS i zdefiniujesz bardziej skomplikowane zasady, to możesz utworzyć grę w piłkę nożną podobną do gier na konsolę NES. Nie ma gatunków, które bardzo różniłyby się od pozostałych. Są natomiast konkretne cechy. Aktualnie minusem technologii HTML jest obsługa dźwięków, a programowanie grafiki trójwymiarowej jest bardzo skomplikowane. Jeśli chcesz utworzyć grę nowego gatunku, to nie wahaj się ani chwili. A jeśli chcesz udowodnić, że mylę się w kwestii dźwięków i programowania trójwymiarowego, to wspaniale. Oprócz szukania nowych gatunków możesz też spróbować rozbudować te gry, które już masz. W podsumowaniu każdego rozdziału znajdują się propozycje dalszego rozwijania opisanej gry. Możesz z nich skorzystać albo wymyślić jeszcze coś innego. Możesz nawet umieścić gry jedną w drugiej albo połączyć je ze sobą. Oprócz wielu funkcji opisanym grom brakuje także pewnych cech. Można powiedzieć, że nie są zbyt wyszukane pod względem artystycznym. Może czasami takie dziwne gry bywają czarujące, ale nie są zbyt wciągające. Gry mogą być dziełami sztuki dostarczającymi graczom niezapomnianych wrażeń. Kolejnym problemem naszych gier jest praktycznie brak jakiejkolwiek fabuły i wyrazistej postaci. Najprostszym sposobem na ich spersonalizowanie jest zaczerpnięcie z własnych doświadczeń. Nie ma jeszcze gier wyrażających Twoją osobowość, chyba że już jakąś zrobiłeś. Pamiętaj, że tworzenie gier nie jest trudne, jeśli ma się odpowiednie do tego narzędzia (do których można zali-

Co dalej?  279

czyć opisane w tej książce szablony). Jeśli przeczytałeś i zrozumiałeś całą tę książkę, to możesz teraz popracować trochę nad przystosowaniem jednej z gier w taki sposób, aby spodobała się jakiejś osobie, na której Ci zależy. Z drugiej strony zapewne są jakieś duże studia, które chętnie wykorzystałyby Twoje umiejętności, aby zarobić trochę pieniędzy. Firmy takie chcą jak najdłużej zatrzymać odwiedzających na swoich stronach, aby jeszcze bardziej je lubili. Najlepszym sposobem na to jest dostarczenie im rozrywki. „Opublikujemy w naszym portalu jakieś ciekawe gry! To spowoduje wzrost przychodów o 14% w następnym kwartale. Świetnie!”. To też jest jakaś perspektywa. Zamiast skupiać się na grupie docelowej, możesz jako motywację wykorzystać perspektywy nauczenia się czegoś nowego. Jeśli chcesz szybko tworzyć gry, poćwicz tworzenie jednej niewielkiej gry, aż nauczysz się ją robić bardzo szybko. Ludzie doskonale radzą sobie w czynnościach, w których wykonywaniu mają duże doświadczenie. Utwórz i usuń wybraną grę wiele razy. Potem wybierz inną i zrób to samo. Jeśli w międzyczasie nauczysz się czegoś, to nie będzie to marnowanie energii. Niektórzy ludzie poświęcają znaczną część swojego życia na doprowadzenie jakiegoś dzieła do perfekcji. Może to być dobrym pomysłem, ale jest też niebezpieczne. Trzeba mocno wierzyć w to, co się robi, mieć wsparcie innych osób, nie dać się zdekoncentrować oraz pamiętać, że upatrzony cel jest ciągle możliwy do osiągnięcia. A jeśli nie, to trzeba umieć się z tym pogodzić. Czasami taka postawa życiowa przynosi efekty i tak powstają niektóre gry, jak choćby Braid. Jeśli chodzi o kwestie techniczne, to oprócz wielokrotnego tworzenia jednej małej gry i wieloletniego dopieszczania jednej produkcji warto też skupić się na doskonaleniu i pogłębianiu swojej wiedzy. Zawsze można nauczyć się czegoś nowego o JavaScripcie, przeglądarkach itp., co może się przydać w pracy nad kolejną grą. Jednak w branży gier ważne są także umiejętności innego rodzaju, takie jak wiedza na temat sztuki, talent muzyczny, wiedza na temat ekonomii, biznesu, psychologii itd. Ponadto projektowanie gier coraz większą popularność zdobywa jako kierunek badań. Jest to szeroka dziedzina, dzięki której studenci mogą przekazywać swoje pomysły wielu osobom, a przy okazji mają szansę zdobyć wiedzę techniczną. Doskonałą książką o projektowaniu gier jest The Art of Game Design Jessego Schella. Można się z niej nauczyć, jak testować gry, jak je doskonalić oraz jak sprawić, aby były zabawne. Więcej wartych uwagi źródeł informacji o tworzeniu gier można znaleźć w dodatku C. Niezależnie od tego, na co się zdecydujesz, nawet jeśli zamkniesz się w jaskini na pięć lat, aby pracować nad ściśle osobistym projektem, to i tak potrzebujesz pomocy innych osób. Musisz znaleźć osoby do wykonania zadań, z którymi sam sobie nie poradzisz, a więc możesz potrzebować projektantów, artystów, programistów, pisarzy, producentów, kompozytorów i testerów. A gdy uporasz się z tym wszystkim, to dodatkowo potrzebujesz jeszcze osób, które będą Cię wspierać, które będą Ci przeszkadzać i pomagać się skupić, które będą Ci mówić, że gra jest dobra, nawet jeśli nie będzie, oraz osób, które podpowiedzą Ci, że potrafisz to zrobić lepiej. A jeśli te osoby będą wymagać od Ciebie tego samego, to powinieneś im to dać. Krótko mówiąc, staraj się mieć w swoim otoczeniu jak najwięcej ludzi. Są o wiele ciekawsi od gier.

280  Rozdział 11. DALSZY ROZWÓJ

A Podstawy JavaScriptu

Jeśli słabo znasz język JavaScript, to w tym rozdziale znajdziesz absolutne minimum informacji, których potrzebujesz do zrozumienia treści tej książki. Jeśli czytanie kodu JavaScript nie sprawia Ci problemu dzięki znajomości innego języka programowania, to w niniejszym podręczniku możesz znaleźć cenne informacje na temat szczegółów składni. W tym rozdziale nie zamieściłem pełnego opisu wszystkich aspektów języka JavaScript ani nawet opisu wszystkich jego elementów użytych w poprzednich rozdziałach, ale dzięki zrozumieniu omówionych tu podstaw będziesz w stanie pojąć wszystkie opisy gier. W różnych częściach książki można znaleźć alternatywne sposoby używania składni i adaptacje podstawowych technik. Jeśli czegoś nie zrozumiesz podczas lektury rozdziałów, zajrzyj tutaj, aby odświeżyć sobie pamięć na temat podstaw.

282  Rozdział A PODSTAWY JAVASCRIPTU

Główne typy API w JavaScripcie Akronim API oznacza Application Programming Interface (interfejs programistyczny) i ogólnie rzecz biorąc, odnosi się do zestawu narzędzi, jaki mamy do dyspozycji, programując w określonym języku programowania. Jeżeli zastanawiasz się, czy masz dostęp do jakiejś funkcji albo zmiennej, to jest to właśnie kwestia tego, co jest dostępne w API, którym dysponujesz. Aby dowiedzieć się, co udostępnia wybrane API, należy zajrzeć do jego dokumentacji. Czasami dokumentacja jest dołączona do używanych plików, czasami jest dostępna w internecie, a niekiedy dostęp do niej można uzyskać przy użyciu jakiegoś dodatkowego narzędzia, np. w edytorze albo terminalu. Te źródła nie wykluczają się wzajemnie i czasami jeden sposób dostępu do informacji jest tylko pośrednią metodą korzystania z innego źródła (przykładowo dokumentacji internetowej). W języku JavaScript występują cztery rodzaje API.

API rdzenne Jest to podstawowy zestaw narzędzi, który decyduje, że JavaScript jest tym, czym jest. Używanie tych narzędzi sprawia, że program lub grę można określić jako napisane w tym języku. W tym dodatku znajduje się opis podstaw JavaScriptu, takich jak liczby, funkcje, tablice oraz zmienne. Bardziej wyczerpujące informacje można znaleźć w portalu Mozilla Developer Network pod adresem developer.mozilla.org.

Implementacja API Różne przeglądarkowe i nieprzeglądarkowe implementacje API języka JavaScript mogą zawierać różne narzędzia. Jeśli używasz Node w wierszu poleceń, nie oczekuj, że znajdziesz przeglądarkowe obiekty takie jak window. Gdy używasz biblioteki jQuery, kanwy, magazynu lokalnego albo jakiejkolwiek innej typowej dla przeglądarek własności JavaScriptu, to opierasz się na implementacji tego języka obecnej w konkretnej przeglądarce. Analogicznie jeśli w wierszu poleceń używasz jakiejś typowej dla Node funkcji, to korzystasz z implementacji Node języka JavaScript. Dobrym źródłem informacji o różnych implementacjach w przeglądarkach i o tym, jak sobie z nimi radzić, jest portal Mozilla Developer Network.

API bibliotek Niezależnie od tego, czy nazywa się je bibliotekami, pakietami, czy zasobami, w języku JavaScript można używać zewnętrznych programów pomocniczych. Interakcja z nimi odbywa się poprzez ich API. Zazwyczaj takie dodatki występują w postaci jednego pliku, ale może też być ich więcej, jeśli pakiet zawiera dodatkowo testy, skrypty kompilacyjne, obrazy, arkusze stylów i inne. Nowe narzędzia open source czasami mają doskonałe funkcje i znakomite API, ale brakuje im dobrej dokumentacji. Ta powstaje dopiero po jakimś czasie, gdy zawiąże się gotowa pomagać w rozwoju społeczność. Przeglądając kod źródłowy projektu, można dowiedzieć się, co zawiera i czy może się przydać. A jeśli zdecydujesz się na napisanie dokumentacji, może to być pierwszym krokiem do zaangażowania się w prace nad projektem. Przy okazji, wszystkie opisane w tej książce silniki gier są typu open source i można je znaleźć w serwisie GitHub. Jeśli chcesz lepiej je poznać, możesz pomóc w pisaniu dokumentacji, zgłosić znalezione błędy albo opracować nowe funkcje. Dla wielu z opisanych w dodatku C zasobów podałem ich adresy internetowe w serwisie GitHub (albo Google Code w przypadku Box2D) lub adresy ich oficjalnych stron, na których znajdują się odnośniki do kodu źródłowego.

Instrukcje  283

Własne API Tworząc program, budujesz interfejs, którego potem sam używasz. Każda funkcja i zmienna, jaką zdefiniujesz, automatycznie staje się częścią Twojego zestawu narzędzi, czyli API. Gdy wywołujesz jedną ze swoich funkcji albo używasz którejś z własnych zmiennych, to używasz zdefiniowanego przez siebie API. W istocie jest to dokładnie takie samo API jak w bibliotekach, tylko że napisane przez Ciebie. Jeśli chodzi o ogólną budowę API, to w języku JavaScript najłatwiej jest zdefiniować nazwę dla czegoś, co będzie szczególnie przydatne (najczęściej jest to funkcja albo zmienna), i odłożyć szczegółową implementację tego czegoś na później. Z drugiej strony podział programu na mniejsze części jest również ważną, choć sprzeczną z poprzednią zasadą programowania. Najlepiej jest umieć pogodzić te dwa cele i sprawnie je łączyć. Tworzenie API jest równoznaczne z pisaniem programu, ale oznacza patrzenie z innej perspektywy. To dotyczy każdego programu, nie tylko tych, które Ty napiszesz.

Instrukcje Instrukcje, nazywane też wierszami kodu, to podstawowa jednostka kodu, którą posługujemy się w języku JavaScript. Najprostsze instrukcje zajmują jeden wiersz i są zakończone średnikiem. Jeśli chcesz umieścić w jednym wierszu kilka instrukcji, to możesz to zrobić, rozdzielając je średnikami, np.: x = "Jedna instrukcja w wierszu"; y = "wiele"; z = "instrukcji"; zz = "w"; zzz = "jednym"; zzzz = "wierszu";

Zmienne Zmienne w języku JavaScript służą do przechowywania informacji. Zwykle definiuje się je przy użyciu słowa kluczowego var, które sprawia, że zakres ich dostępności jest ograniczony do funkcji zawierającej ich definicję. Zmienne zdefiniowane bez tego słowa są dostępne w zakresie globalnym, co oznacza, że można ich używać w każdym miejscu programu. Zmienne globalne czasami powodują problemy, bo można przez przypadek dwa razy użyć tej samej nazwy zmiennej. A w większych systemach identycznej nazwy mogą użyć dwie różne osoby. Efekt tego jest trudny do przewidzenia i zawsze powoduje to problemy, dlatego definiuj zmienne przy użyciu słowa kluczowego var. Poniżej znajduje się deklaracja zmiennej: var myVariableName;

Jeśli deklarowanej zmiennej chcesz od razu przypisać wartość który jest operatorem przypisania:

34212,

to użyj znaku równości,

var myVariableName = 34212;

Jeśli w dalszej części programu trzeba zmienić wartość zmiennej, to należy to zrobić, już nie używając słowa kluczowego var. Służy ono jedynie do inicjacji. W tej książce zostało użytych wiele zmiennych o różnych nazwach. W większości przypadków do tworzenia nazw najlepiej jest używać liter i cyfr (tylko nie na początku nazwy). W języku JavaScript często stosuje się tzw. notacjęWielbłądzią, tzn. jeśli nazwa zmiennej składa się z kilku

284  Rozdział A PODSTAWY JAVASCRIPTU wyrazów, to każdy poza pierwszym rozpoczyna się wielką literą. W innych językach do oddzielania od siebie poszczególnych członów nazw używa_się_znaków_podkreślenia. Jest to jednak tylko przyjęta konwencja, a nie obowiązek.

Łańcuchy W programie nie można tak po prostu pisać wszystkiego, co się zechce. Interpreter odczytuje nasz tekst i próbuje odgadnąć jego znaczenie w kontekście dostępnego API. Jeśli chcesz poinformować interpreter, że to, co piszesz, ma być traktowane nie jako kod programu, ale jako zwykły tekst do przekazania albo wydrukowania, należy to ująć w cudzysłów prosty. Czasami używa się do tego cudzysłowów podwójnych ("), a czasami pojedynczych (apostrofów). Aby wstawić jeden z tych znaków do łańcucha, należy wstawić przed nim znak \.

Liczby W programowaniu rozróżnia się dwa rodzaje liczb: całkowite i zmiennoprzecinkowe. Liczby całkowite to oczywiście takie, które nie mają części ułamkowej. Jeśli chcesz użyć interpretera jako kalkulatora, to za pomocą operatorów +, -, * oraz / możesz wykonywać podstawowe działania arytmetyczne, a przy użyciu biblioteki Math możesz wykonać wiele bardziej zaawansowanych działań. Przykładowo jeśli napiszesz poniższą instrukcję, interpreter zwróci wynik 15. 5 * 2 + 8 – 3;

Należy tylko uważać na operator +. Jeśli umieści się go między dwiema liczbami, to zostaną one zsumowane. Ale jeśli użyjesz go między łańcuchami, to zostaną one połączone, np. "Witaj," + "świecie" da wynik Witajświecie. Jeśli doda się liczbę do łańcucha (nie celowo), to można stracić sporo czasu na poszukiwanie przyczyny niedziałania programu.

Tablice Tablice służą do przechowywania list danych. Danymi tymi mogą być liczby, łańcuchy, inne tablice, obiekty, funkcje itd. Do deklaracji tablicy można użyć kwadratowych nawiasów, w których wypisuje się elementy rozdzielane przecinkami: var mojaTablica = ["to jest pierwsza wartość", 3, 1, 64, "to jest ostatnia wartość"];

Do ustawiania i pobierania wartości wybranych elementów tablicy używa się indeksów (numerowanych od 0). Aby na przykład pobrać pierwszą wartość tablicy, należy napisać następującą instrukcję: mojaTablica[0];

By ustawić trzeci element tablicy, można napisać taką instrukcję: mojaTablica[2] = "Cześć, ustaw to tutaj";

Do sprawdzania długości tablicy służy następujące polecenie: mojaTablica.length;

Funkcje  285

Funkcje W języku JavaScript funkcje można definiować na kilka sposobów, ale najczęściej spotyka się definicje podobne do poniższej: function(parametr1, parametr2){

// Kod funkcji };

Jest to definicja funkcji z dwoma parametrami (zwanymi również argumentami), które są do niej przekazywane. Nazwy parametrów podlegają takim samym zasadom jak nazwy zmiennych. Wiersz zawierający dwa ukośniki to komentarz. Interpreter nie czyta komentarzy. Poniżej znajduje się definicja bardzo prostej działającej funkcji: var addition = function(parameter1, parameter2){ return parameter1 + parameter2; };

Dzięki powiązaniu funkcji ze zmienną o nazwie addition funkcję tę można wywoływać. Gdyby w funkcji nie było słowa kluczowego return, to nie zwracałaby ona żadnej użytecznej wartości, a jedynie undefined. Aby zrozumieć, co to znaczy, wywołaj funkcję w następujący sposób: addition(5, 7);

To wywołanie zwróci wartość 12, którą można wydrukować, przypisać do zmiennej albo wykorzystać w innych funkcjach.

Obiekty Obiekty są podobne do tablic, ponieważ tak jak one mogą być wykorzystywane do przechowywania list danych. Jednak obiekty są bardziej elastyczne, ponieważ zamiast liczbowych indeksów używa się w nich par klucz-wartość. Obiekty otacza się klamrami. Klucz od wartości oddziela się dwukropkiem, a pary klucz-wartość rozdziela się przecinkami: var mojObiekt = {wartość1: "cześć", wartość2: 4343, kolejnyKlucz: 2, ostatniKlucz: 34};

Do ustawiania i sprawdzania tych wartości można używać kwadratowych nawiasów, podobnie jak w przypadku tablic, ale zamiast indeksów wykorzystuje się klucze: mojObiekt[wartość2];

// To jest równe 4343

Można także korzystać z notacji z kropką: mojObiekt.wartość2;

// To jest równe 4343

Warto też pamiętać, że wartości obiektów mogą być funkcjami. Gdy funkcja jest własnością obiektu, można ją także nazywać metodą. Wygląda to tak: var mojObiekt = {pierwszaWartość: function(){

// Kod funkcji }, zwykłaWłasność: 4};

Aby wywołać tę metodę (funkcję), można użyć instrukcji mojObiekt.pierwszaWartość().

286  Rozdział A PODSTAWY JAVASCRIPTU

Instrukcje warunkowe Czasami trzeba sprawdzić, czy dwie wartości są takie same, czy się różnią albo czy jedna jest większa od drugiej lub odwrotnie. Do wykonywania takich operacji służą operatory porównywania (<, >, ===, !==) z instrukcjami warunkowymi. Poniżej jest przedstawiona składnia takiej najprostszej instrukcji warunkowej: if(x < y){

// Kod wykonywany, gdy x ma wartość mniejszą od y };

Istnieją też inne formy instrukcji warunkowych: if(x === y){

// Kod wykonywany, gdy x ma wartość równą y }else{

// Kod wykonywany, gdy x ma wartość inną niż y }

Można też łączyć instrukcje if i else w dłuższe struktury, aby tworzyć bardziej skomplikowane warunki: if(x === y){

// Kod wykonywany, gdy x ma wartość równą y }else if(x > y){

// Kod wykonywany, gdy x ma wartość większą niż y }else{

// Kod wykonywany, gdy powyższe warunki nie zostaną spełnione }

Operatory porównywania zwracają wartości logiczne true i false. W języku JavaScript wszystko ma jakąś wartość logiczną. Ogólnie można przyjąć, że wartości true odpowiada wszystko, co nie jest wartością false, null, undefined, 0, NaN oraz pustym łańcuchem (""). W miejsce warunku można wpisać, cokolwiek się chce, i interpreter bez problemu określi prawdziwość tego czegoś, np.: if(5){

// 5 odpowiada wartości true, a więc znajdujące się tutaj instrukcje zostałyby wykonane };

Pętle W programach często trzeba wielokrotnie wykonywać różne czynności, czasami za każdym razem tak samo, a czasami coś zmieniając. Do tego celu używa się pętli. Poniżej jest przedstawiona najprostsza możliwa pętla: for(var i=0;i < 7; i++){

// Znajdujący się w tym miejscu kod zostanie wykonany 7 razy };

Zmienna i jest tzw. licznikiem pętli o początkowej wartości 0. Za jej definicją znajduje się warunek zakończenia wykonywania pętli. W tym przypadku pętla będzie działać, dopóki wartość licznika będzie mniejsza od 7. Trzecia instrukcja w nawiasie zwiększa licznik o 1 w każdej iteracji (powtórzeniu). Istnieją jeszcze inne rodzaje pętli, ale ten jest najczęściej używany.

Komentarze  287

Komentarze Czasami w kodzie trzeba zostawić jakąś notatkę dla siebie albo innych osób, aby w przyszłości można było go łatwiej zrozumieć. Zapiski na kartce albo wysyłane e-mailem nie są tak wygodne jak tekst umieszczony w kodzie wprost przed oczami. Do robienia takich notatek służą tzw. komentarze, czyli zapiski, które pomagają ludziom, ale są ignorowane przez interpreter. Przeglądając przykłady kodu przedstawione w tym dodatku, bez trudu można odgadnąć, jak wygląda komentarz, ale żeby wszystko było absolutnie jasne, poniżej przedstawiam dodatkowe przykłady: // Cały ten wiersz jest komentarzem, ponieważ zaczyna się od dwóch ukośników 3 + 5; // Tylko ta część tego wiersza jest komentarzem, instrukcja 3 + 5 zostanie wykonana normalnie

Można także tworzyć komentarze wielowierszowe: /* Dzięki możliwości dzielenia komentarzy na kilka wierszy za pomocą ukośników i gwiazdek można pisać krótkie wiersze */

288  Rozdział A PODSTAWY JAVASCRIPTU

B Kontrola jakości

Jeśli kod nie działa tak, jak powinien, to przyczyn tego nieprzyjemnego stanu rzeczy może być wiele. Czasami ktoś pomyli się przy wpisywaniu nazwy funkcji, ktoś zapomni wczytać bibliotekę albo jedna część kodu niewiadomo czemu zostanie wykonana przed inną. Są to błędy programistyczne, potocznie zwane bugami, i trzeba pozbyć się ich wszystkich z kodu.

290  Rozdział B KONTROLA JAKOŚCI

Przeglądarkowe narzędzia do diagnostyki błędów W wielu językach programowania błędy programisty są szybko wychwytywane przez kompilator albo środowisko wykonawcze programu szczegółowo informuje, dlaczego program nie działa tak, jak powinien. Wielką zaletą języka JavaScript jest to, że do rozpoczęcia pisania w nim programów nie są potrzebne żadne specjalne narzędzia. Wystarczy edytor tekstu i przeglądarka internetowa. Niestety przez to środowisko programistyczne JavaScriptu (edytor tekstu i przeglądarka) „nie wie”, że jesteśmy programistami i właśnie piszemy jakiś program. Najprostszym sposobem na podejrzenie kodu HTML strony jest użycie narzędzia podglądu kodu źródłowego przeglądarki internetowej. Nie jest to wyszukane narzędzie pozwalające edytować kod, ale czasami się przydaje. Jest wiele narzędzi, dzięki którym praca programisty może stać się łatwiejsza, a czasami po prostu możliwa. Niektóre z nich są ukryte, a o niektóre trzeba się trochę postarać. Twórcy przeglądarek stoją na słusznym stanowisku, że większość użytkowników chce tylko pooglądać obrazki kotków, i dlatego nie eksponują narzędzi, które mogą być przydatne dla programistów. W Firefoksie należy zainstalować dodatek Firebug. Można go pobrać ze strony getfirebug.com, na której znajdują się też poradniki i instrukcja obsługi. W przeglądarce Chrome do dyspozycji mamy gotowe narzędzia, których instrukcja obsługi znajduje się na stronie developers.google.com/chrome-developer-tools/. Wymienione narzędzia programistyczne mają podobne funkcje. Można przy ich użyciu przeglądać kod źródłowy plików HTML, CSS i JavaScript. Zawierają inspektora drzewa DOM pozwalającego sprawdzić, jak łączą się ze sobą występujące na stronie elementy. Dostępny jest też inspektor informujący, spod jakich adresów zostały pobrane wszystkie pliki, z których została złożona strona. A co najważniejsze, każdy z tych zestawów narzędzi zawiera konsolę (interpreter) JavaScript. Za pomocą konsoli można tworzyć zmienne, wywoływać funkcje i robić wszystko, co zwykle robi się w typowych środowiskach programistycznych. Najlepiej wykonuje się pojedyncze instrukcje. Jeśli chcesz uruchomić program, to łatwiej jest wczytać zawierający go plik. Jeśli skrypt albo instrukcje w konsoli nie działają poprawnie, konsola zgłasza błąd. Osoby „nienawidzące JavaScriptu” często za rzadko używają konsoli, przez co mają wrażenie, że muszą zgadywać, o co chodzi, gdy wystąpi błąd. Niektórzy zamiast używać konsoli, wpisują do programu komunikaty diagnostyczne, jak poniższy: alert("Wydrukuj to w okienku alertu!");

Sztuczkę tę stosuje się, aby uzyskać natychmiastowe potwierdzenie, że określona część kodu jest wykonywana. Jest to działanie według następującego schematu: załaduj stronę, kod zadziałał/nie zadziałał, wróć do kodu. Jednak metoda ta nie jest dobra do drukowania wartości zmiennych. Spójrz na poniższy przykład: alert(mójObiekt);

Wynik wykonania tej instrukcji jest pokazany na rysunku B.1. Taki komunikat jest bezwartościowy. Wiemy, że obiekt jest ładowany, ale nie możemy z nim nic zrobić. Gdybyśmy znali tylko tę jedną technikę diagnozowania programów, to bylibyśmy zmuszeni zgadywać nazwy zmiennych, indeksy tablic itd., aż w końcu znaleźlibyśmy odpowiedź. Za każdym razem musielibyśmy zmieniać kod obiektu, aby zobaczyć jego inną część. Trochę lepszym rozwiązaniem jest użycie instrukcji console.log(mójObiekt) w celu wydrukowania obiektu (i dodatkowych dotyczących go informacji) w konsoli. Dzięki temu można

Testowanie  291

Rysunek B.1. Nieprzydatne okienko alertu

przejrzeć własności obiektu i jego podobiektów. Można też posłużyć się sztuczką polegającą na zamianie zmiennych (albo instrukcji), które chce się dokładnie obejrzeć, na globalne (wystarczy w tym celu usunąć słowo kluczowe var). Nie jest to jednak idealne rozwiązanie, ponieważ oprócz potencjalnych problemów z nazwami zmiennych globalnych opisanych w dodatku A można zwyczajnie zapomnieć o przywróceniu słów kluczowych var. Jest też jeszcze inna możliwość, którą udostępniają narzędzia dla webmasterów. Zatrzymać wszystko. Do tego właśnie służą punkty wstrzymania. Jeśli wiesz, w którym wierszu kodu zaczyna dziać się coś złego (np. nie masz pewności, do czego służy jakaś zmienna, albo zastanawiasz się, czy pętla wykonuje się 10, czy 11 razy), to możesz ustawić w tym miejscu punkt wstrzymania. Potem za pomocą konsoli można wykonać kod, który powinien zostać wykonany w skrypcie. Aby np. dowiedzieć się, czym jest toCoś, należy napisać console.log(toCoś);, a żeby dowiedzieć się, co robi funkcja zróbCoś, możesz napisać zróbCoś();. Poza przeglądarką, np. w środowisku Node, dostępne są bardziej typowe narzędzia programistyczne. Gdy np. wykonasz plik albo wiersz kodu w terminalu Node, to w tym oknie zostaną wyświetlone wszystkie pojawiające się błędy. W odróżnieniu od przeglądarki, Node nie jest przede wszystkim narzędziem do oglądania kotków, tylko właśnie środowiskiem programistycznym.

Testowanie W dodatku znajduje się zwięzły opis instrukcji warunkowych. Testowanie kodu polega na przyjmowaniu założeń, że jakaś wartość jest powiązana z inną wartością w określonym kontekście. Testy mogą być wysokopoziomowe, tzn. mogą zawierać stwierdzenia typu: „jeśli ktoś kliknie ten przycisk, na stronie powinien pojawić się pies”, albo niskopoziomowe, np. „po wykonaniu tej funkcji zmienna x powinna mieć wartość 5”. Przeprowadzając testy, zawieramy rodzaj umowy ze swoim kodem. Jeśli ktoś zmieni kod w taki sposób, że zostaną złamane warunki umowy, można to szybko wykryć. Do najpopularniejszych szkieletów testowych umożliwiających tworzenie testów typu „czy to działa tak, jak powinno” należy QUnit. System ten można znaleźć pod adresem github.com/jquery/qunit.

292  Rozdział B KONTROLA JAKOŚCI Innym rodzajem testowania jest testowanie wydajności. Przy budowie gry przeglądarkowej wydajność można rozumieć na wiele sposobów. Po pierwsze, można posługiwać się ogólnym pojęciem wydajności, tzn. jak szybko rozpocznie się wczytywanie strony po wpisaniu przez użytkownika jej adresu w przeglądarce oraz ile czasu zajmie pobranie całej strony wraz z jej zasobami. Tego rodzaju dane można zdobyć na stronie webpagetest.org. Jednak w grach najważniejszym parametrem wydajności jest liczba klatek na sekundę. Doskonałe artykuły na ten temat można znaleźć w serwisie html5rocks: html5rocks.com/en/features/performance. Jeśli rozszerzymy pojęcie testowania, aby obejmowało także ogólną kontrolę jakości, to warto znać kilka narzędzi. Jednym z nich jest utworzony przez Douglasa Crockforda (autora książki JavaScript — mocne strony) system JSLint. Narzędzie sprawdza, czy w kodzie znajdują się jakieś złe elementy JavaScriptu. Jest ono dostępne w wielu środowiskach, ale jeśli chcesz tylko zobaczyć, jak działa, to możesz skopiować fragment kodu do okienka na stronie jslint.com. Istnieją też automatyczne narzędzia do testowania kodu (działające zgodnie ze zdefiniowanymi założeniami) i sprawdzania jego jakości. Wszystkie te systemy opierają się na sobie wzajemnie i można z nich utworzyć naprawdę skomplikowane środowisko. W pewnym momencie testowanie przeradza się w używanie zwykłego zestawu narzędzi. Jest to ważne. Znajomość narzędzi, z którymi się pracuje, edytora, terminala, procesów i własnego kodu pozwala szybciej i skuteczniej znajdować oraz usuwać usterki. Większość programistów co jakiś czas robi przegląd swoich narzędzi, uzupełniając i udoskonalając wszystko, co się da, od terminala przez szkielety testowe po gry i animacje w edytorze. Z drugiej strony nadmierne oprzyrządowanie (nawet jeśli występuje w przebraniu testowania) może pochłaniać dużo czasu i stać się problematyczne, jeśli zaczniemy używać narzędzi, których działania dobrze nie rozumiemy, albo nie będziemy umieli radzić sobie bez nich. Prawdopodobnie lubisz robić różne rzeczy, pisząc programy. Odpowiednie narzędzia w dłuższej perspektywie pozwolą Ci pracować szybciej, ale początkowo Cię spowolnią. Traktuj zaopatrywanie się w narzędzia jako sposób na osobisty rozwój, ale nie pozwól, aby stało się to Twoją obsesją.

Współpraca Jeśli chcesz polepszyć jakość swojego kodu, wybierz kilka prominentnych postaci z interesującej Cię społeczności. Nie pcham się na Twoją listę, ale dzięki tej książce przekazuję Ci część mojej listy. Znajdują się na niej wszyscy twórcy opisanych silników gier i innych narzędzi, dzięki którym napisanie tej książki było możliwe. Masz wielkie szczęście pracować lub hobbystycznie zajmować się dziedziną, w której bardzo łatwo można uzyskać dostęp do największych gwiazd. Możesz ich posłuchać na konferencjach, a nawet porozmawiać w czasie różnych imprez. Możesz współpracować z nimi przy ich projektach. Porównaj tę sytuację z gwiazdami sceny muzycznej. To prawie tak, jakby każdy początkujący piosenkarz mógł zaśpiewać w chórkach na każdej płycie, na jakiej chce. Oczywiście nie twierdzę, że każdy ma prawo żądać od innych, aby poświęcali mu swój czas. Chodzi mi jedynie o to, że istnieją bardzo uzdolnione osoby, które w odpowiedniej sytuacji są gotowe podać pomocną dłoń innym. Dla niektórych oznacza to napisanie kodu do czyjegoś projektu albo poszukanie informacji o ich twórczości open source. Znakomitym miejscem na tego typu spotkania jest serwis GitHub. Projekty często mają listy mailingowe, do których można się zapisać, aby być na bieżąco z wydarzeniami.

Współpraca  293

Można też utworzyć własny projekt albo społeczność, budując coś, co jest potrzebne innym. Większość doświadczonych programistów lubi używać znakomitych narzędzi, więc jeśli napiszesz coś świetnego, to na pewno ludzie będą z tego korzystać. Dotyczy to zarówno narzędzi, jak i gier. Mimo trzymania ręki na pulsie bieżących wydarzeń w świecie narzędzi i API oraz wynurzania się co jakiś czas ze swojej jaskini, aby poznać parę osób (nie piszę tego w obraźliwym tonie, ja też siedzę w jaskini i jest mi z tym dobrze), czasami można po prostu utknąć nad czymś. Jeśli przeczytasz wszystko, co się da, na dany temat i nadal nie masz żadnego pomysłu, to znaczy, że czas poprosić kogoś innego o pomoc. Na niektórych forach panuje dość ponura atmosfera, podobnie jak na listach mailingowych i kanałach IRC. Ale ogólnie rzecz biorąc, jeśli precyzyjnie sformułujesz pytanie, to zwykle otrzymasz równie rzeczową odpowiedź. Jeśli napiszesz na forum: „To narzędzie nie działa”, to w najlepszym przypadku ktoś grzecznie poprosi, byś podał więcej szczegółów, ale równie dobrze możesz zostać zignorowany albo wyśmiany (publicznie i na stałe). Uważaj! Aby tego uniknąć, sprawdź najpierw, czy ktoś już o coś podobnego nie pytał, i zadając pytanie, podaj jak najwięcej szczegółów. Miałem kilka razy przypadki, kiedy myślałem sobie, że „internet uzna mnie za głupka już na zawsze”, bo zadawałem zbyt ogólne pytania. Nie przejmuj się za bardzo, jeśli też tak Ci się zdarzy. Jeżeli w odpowiedzi na swoje pytania otrzymujesz prośby o podanie dodatkowych szczegółów, to prawdopodobnie po prostu jeszcze nie wiesz, co jest w określonej sytuacji ważne. W najgorszym przypadku ktoś nieuprzejmie napisze, żebyś użył wyszukiwarki albo przeczytał dokumentację. Nie chcę się chwalić, ale do dokumentacji odesłano mnie jak na razie tylko raz, i to nie w internecie. Osoba, która to zrobiła, mimo wysokich kompetencji nie była zbyt szanowana ani ceniona. Niemniej większość ludzi jest miła, a jeszcze lepiej zachowuje się w czasie osobistych spotkań. Oprócz list mailingowych i kanałów IRC istnieją też ogólne fora dla programistów. Jednym z najlepszych takich miejsc w internecie jest forum stackoverflow.com. Natomiast w serwisie jsfiddle.net można pokazać innym działającą wersję swojego kodu. Jeśli będą mieli ochotę, to może Ci go nawet poprawią i odeślą. Dzięki rozmaitym społecznościom i narzędziom, jeśli lubisz się uczyć i nie boisz się wyzwań, możesz rozwiązać praktycznie każdy problem, jaki napotkasz. Tworzenie gier i dostarczanie ich ludziom nigdy nie było takie łatwe.

294  Rozdział B KONTROLA JAKOŚCI

C Zasoby

Istnieje masa znakomitych źródeł informacji na wszystkie tematy związane z tworzeniem gier. Dysponując ich listą przedstawioną w tym dodatku, bez trudu znajdziesz sposoby na podniesienie swoich kwalifikacji daleko poza materiał opisany w tej książce.

296  Rozdział C ZASOBY

Silniki gier Niektóre silniki gier są bardziej standardowe niż inne. Niektóre skupiają lepsze społeczności albo mają więcej funkcji. Mimo to wszystkie są doskonałe i w zależności od tego, jakiego rodzaju grę chcesz utworzyć, każdy z nich może się przydać. Wszystkie są zbudowane przy użyciu języka JavaScript, więc jeśli duża część gry nie korzysta z funkcji silnika, jak w przypadku raycastingu w grze FPS, to nie ma problemu. Można wziąć z silnika to, co jest potrzebne, a resztę dopisać samodzielnie. Ponadto, jak pokazałem w rozdziale 1., można też użyć więcej niż jednego silnika naraz i z każdego z nich wybrać to, co się chce.  Akihabara (kesiev.com/akihabara) To jest chyba pierwsza biblioteka, która pokazała, jakie są możliwości technologii HTML5. Niektóre inne opisane w tej książce biblioteki są łatwiejsze w użyciu, ale ten silnik był inspiracją dla autora tej książki, aby opisać silniki gier na podstawie różnych klasycznych gatunków gier.  Atom (github.com/nornagon/atom) Nie należy mylić tej biblioteki z innymi dostępnymi w serwisie GitHub projektami o takiej samej nazwie. Ten niewielki silnik jest dowodem na to, że duża liczba funkcji nie jest konieczna, aby biblioteka była przydatna do tworzenia gier. Tego silnika użyliśmy w rozdziale 3.  Crafty (craftyjs.com) Projekt wyróżniający się doskonałą dokumentacją, solidnym zestawem funkcji, dużą liczbą wtyczek oraz gromadzący aktywną społeczność. A poza tym w logo biblioteki znajduje się fajny lisi ogon, który wygląda jak ręcznie namalowany. Tego silnika użyliśmy do budowy mapy izometrycznej w grze RTS.  Easel (easeljs.com) Jest to część zestawu create.js udostępniająca API będące nadbudową na kanwie. Nie nazywa się tego silnikiem gier, ale skrypt ten doskonale sprawdził się przy budowie łamigłówki.  Enchant (enchantjs.com) Silnik gier prosto z Japonii zawierający funkcje umożliwiające integrację z Twitterem i własną platformą dystrybucyjną. Czy wiedziałeś, że potrzebujesz serwera do animacji awatarów? Twórcy silnika Enchant uważają, że jest Ci on potrzebny. Ze wszystkich opisanych bibliotek ta ma w sobie zdecydowanie najwięcej niespodzianek. Użyliśmy jej do budowy gry RPG.  Game.js (gamejs.org) Jest to biblioteka oparta na silniku pygame w języku Python. Wyróżnia się świetną stroną demonstracyjną. Użyliśmy jej do budowy bijatyki.  GameQuery (gamequeryjs.com) Ten silnik wyróżnia się na tle pozostałych z dwóch powodów. Po pierwsze, jego działanie nie tylko zależy od innego skryptu, ale wręcz jest to traktowane jako zaleta i ogłoszone w samej nazwie. Po drugie, co jest częściowo związane z integracją z biblioteką jQuery, bazuje na DOM, a nie na kanwie. Użyliśmy go do budowy strzelanki.  Jaws (jawsjs.com) Nie jest tak mały jak Atom, ale API tego silnika nie jest tak rozbudowane jak Enchant albo Crafty. Jest to świetny zawodnik wagi średniej, którego użyliśmy przy budowie gry FPS.

Edytory tekstu  297



Melon (melonjs.org) Silnik zawierający mnóstwo znakomitych funkcji i opatrzony doskonałą dokumentacją. Użyliśmy go do budowy platformówki.

Edytory tekstu Kod źródłowy trzeba mieć gdzie wpisać. Poniżej znajduje się lista kilku edytorów. Możesz wybrać jeden z nich, ale równie dobrze możesz poszukać jeszcze czegoś innego. Jest wiele możliwości do wyboru. Jeśli dopiero zaczynasz programować, wybierz coś z tej listy i używaj jednego programu przynajmniej przez jakiś czas.  Emacs (gnu.org/software/emacs/) Ten edytor jest podatny na przeróbki w języku LISP i dlatego ma wielu zwolenników, głównie w środowisku akademickim. Jest dostępny na systemy Linux, Mac oraz Windows.  Gedit (projects.gnome.org/gedit) Jest to „oficjalny edytor środowiska GNOME”, co znaczy, że jest popularny wśród użytkowników Linuksa, chociaż można go zainstalować także w systemach Mac i Windows.  Notepad++ (notepad-plus-plus.org) Jest to edytor dostępny tylko w systemie Windows, którego użytkownicy mają mniej dobrych edytorów do wyboru niż użytkownicy systemów Mac i Linux. Jest to prosty i bardzo dobry edytor.  Sublime Text (sublimetext.com) Względnie nowy produkt zawierający ciekawe funkcje, który z założenia ma być „wyrafinowany”. Załóż swój monokl, wyciągnij kawior i sprawdź, do czego się nadaje. Podoba się wielu ludziom.  Vim (vim.org) To jest mój ulubiony edytor. Trudno się do niego przyzwyczaić i może to trochę zająć… kilka lat. Jednak potem staje się trzecią ręką.

Przeglądarki Podczas pisania tej książki regularnie używałem tylko dwóch przeglądarek: Firefoksa i Chrome. Każda z nich ma zalety, pozwala na uruchamianie nowoczesnych aplikacji oraz jest dostępna w systemach Windows, Mac i Linux. Ponadto do listy przeglądarek, którymi warto się zajmować, należy jeszcze doliczyć Operę, Safari i mobilne wersje wszystkich wymienionych. Internet Explorer ma wiele dziwnych cech i często nie nadąża za rozwojem technologii. W zależności od tego, kto będzie grał w Twoje gry i jakich funkcji potrzebujesz, możesz próbować dostosować swój produkt do jak największej liczby przeglądarek albo tylko do jednej.  Chrome (google.com/chrome) Jest to druga przeglądarka, której używałem do budowy opisanych gier. Zwykle działa szybciej od Firefoksa.  Firefox (mozilla.org/firefox) Pierwsza przeglądarka używana do budowy opisanych gier. Nie zawsze dorównuje szybkością Chrome, ale jej twórcy ciągle eksperymentują i mają inne priorytety niż ci od Chrome.

298  Rozdział C ZASOBY

Inne narzędzia Poniżej przedstawiam listę narzędzi, które nie są silnikami gier, ale były przydatne podczas tworzenia gier opisanych w tej książce.  CoffeeScript (coffeescript.org) Jest to język programowania, który kompiluje się na JavaScript i dla niektórych osób jest łatwiejszy od JavaScriptu. Istnieje spore prawdopodobieństwo, że kiedyś natkniesz się na projekt napisany w tym języku, więc nie zaszkodzi go poznać. W tej książce biblioteka atom.js początkowo była biblioteką atom.coffee, dopóki jej nie przekonwertowaliśmy w rozdziale 3.  Filtrr (github.com/alexmic/filtrr/tree/master/filtrr2) Narzędzie, którego użyliśmy w grze FPS.  Firebug (getfirebug.com) Konsola do przeglądania kodu i konsola JavaScript dla przeglądarki Firefox.  Impress (github.com/bartaz/impress.js) Doskonałe narzędzie do robienia prezentacji. Ponadto dobrze nadaje się do tworzenia interaktywnych opowieści w grach, jak w opisanej w rozdziale 2.  jQuery (jquery.com) Biblioteka programistyczna JavaScript używana ze względu na dużą liczbę efektów i funkcje do manipulowania elementami drzewa DOM. W książce została użyta w kilku rozdziałach.  Modernizr (modernizr.com) Narzędzie pozwalające sprawdzić, czy przeglądarka internetowa obsługuje wybrane funkcje, aby w razie potrzeby można było zastosować rozwiązanie awaryjne.  Node (nodejs.org) Działający na serwerze szkielet programistyczny JavaScript, którego użyliśmy w rozdziale 10. do budowy gry RTS.  NPM (npmjs.org) Menedżer pakietów Node użyty w rozdziale 10. do pobrania pakietu socket.io.  Raptorize (zurb.com/playground/jquery-raptorize) Wtyczka do jQuery, przy użyciu której zostało utworzone dramatyczne zakończenie gry w rozdziale 2.  Socket.io (http://socket.io) Pakiet NPM udostępniający interfejs do komunikacji klienta z serwerem na bieżąco.  Tiled (http://mapeditor.org) Narzędzie użyte w rozdziale 5. do utworzenia warstw sprite’ów i kolizji oraz rozmieszczenia jednostek.  Yabble (http://github.com/jbrantly/yabble) Zależność silnika game.js użyta w rozdziale 6. przy budowie bijatyki.

Tworzenie i wyszukiwanie sztuki  299

Tworzenie i wyszukiwanie sztuki Możesz mieć najwspanialszy pomysł na grę i wymyślić najdoskonalszą historię wspartą nietuzinkowymi zadaniami, ale jeśli nie dodasz do tego równie dobrej oprawy wizualnej, to nie uda Ci się przekonać wielu ludzi do swojego projektu. W tej części przedstawiam kilka miejsc, w których można utworzyć lub kupić prace w sam raz do użycia w grach.  Etsy (etsy.com/search?q=pixel) Ilość grafiki pikselowej dostępna na tej stronie jest wprost porażająca.  Gimp (gimp.org) Nie jest to najlepsze narzędzie do tworzenia grafiki pikselowej, ale jest to najbardziej zbliżony do Photoshopa darmowy program do obróbki grafiki (przecież nie wszystkie grafiki w grach muszą pochodzić z około 1994 roku, jak chciałby autor).  Inkscape (inkscape.org) Dobre międzyplatformowe narzędzie do tworzenia obrazów w formacie SVG.  Open Game Art (opengameart.org) Większość dostępnych tu grafik można wykorzystywać i modyfikować, ale nie można ich sprzedawać razem z grą.  Pickle (pickleeditor.com) Najlepszy edytor sprite’ów, jaki udało mi się znaleźć. Przy jego użyciu utworzyłem wszystkie sprite’y wykorzystane w tej książce. Narzędzie jest darmowe, ale od czasu do czasu wyświetla prośbę o dotację w wysokości 9 dolarów. Warto zapłacić.  Pixel Joint (pixeljoint.com) W tym portalu można znaleźć wielu artystów zajmujących się grafiką pikselową. Pewnie już to wiesz, ale warto dać ostrzeżenie na wypadek, gdyby ktoś się jeszcze z tym nie spotkał: jeśli poprosisz artystę o utworzenie grafik do gry, nie oferując zapłaty, to może się obrazić i będzie miał rację.  Sprite Database (http://spritedatabase.net) Jeśli szukasz inspiracji, to jest to doskonałe miejsce do obejrzenia najlepszych prac z gier retro. Pamiętaj, że większość prezentowanych sprite’ów jest chroniona prawami autorskimi, a więc nie licz, że zarobisz na sprzedaży znanej postaci.

Dema i poradniki Wybierając silniki do tej książki, szukałem takich, które są łatwe w konfiguracji, mają dostępną dokumentację oraz udostępniają sensowne przykłady zastosowania. Grafiki, kod i koncepcje gier użyte w tej książce są oryginalne. W przypadku niektórych bibliotek przykłady kodu były tak przydatne, że warto o nich napisać na równi z samymi silnikami, do których zostały napisane.  Platformówka na stronie melonjs.org/tutorial/index.html  Strzelanka na stronie gamequeryjs.com/documentation/first-tutorial/  Gra FPS (raycaster oparty na DOM) na stronie dev.opera.com/articles/view/creating-pseudo3d-games-with-html-5-can-1/  Gra FPS (raycaster oparty na kanwie) na stronie developer.mozilla.org/enUS/docs/HTML/Canvas/A_Basic_RayCaster

300  Rozdział C ZASOBY 

Gra RPG na stronie https://github.com/wise9/enchant.js/tree/master/examples/expert/rpg

Książki Jest to lista książek, które miałem na myśli, pisząc tę książkę, ale zawarty w nich materiał nie mieści się w zakresie tej publikacji. Z każdej z nich nauczyłem się czegoś całkiem innego.  The Art of Game Design, Jesse Schell Ta książka zawiera wszystko, co trzeba wiedzieć o projektowaniu gier, i bardzo wyczerpująco opisuje temat. To bardzo rzadkie uczucie mieć po przeczytaniu książki wrażenie, że zna się odpowiedzi na wszystkie wcześniej nurtujące pytania, ale po lekturze tej pozycji tak właśnie się poczujesz.  JavaScript — mocne strony, Douglas Crockford Mimo niewielkiej objętości ta książka zawiera niezwykle dogłębny opis tematu. Jej treść przekonała wiele osób, że JavaScript to porządny język programowania, nawet mimo niewątpliwych wad. Jest to przełomowa publikacja, która wywarła wpływ na kształt dzisiejszego internetu.  Learning JavaScript, Tim Wright Doskonała pozycja dla wszystkich początkujących i średnio zaawansowanych użytkowników JavaScriptu. Zawiera opis nowoczesnych technik sieciowych i elegancko traktuje temat wad i zalet języka JavaScript związanych z programowaniem pod przeglądarki internetowe.  Rise of the Videogame Zinesters, Anna Anthropy Jeśli chcesz się przekonać, czy warto jest w projektowanie wplatać osobiste doświadczenia, oraz poznać punkt widzenia ze środka wydarzeń na scenie gier niezależnych, to ta książka jest dla Ciebie. Autorka przytacza w niej bardzo rzeczowe argumenty udowadniające, dlaczego tworzenie gier należy traktować jak swego rodzaju misję, aby przekazać światu jakąś wiadomość, nieważne, czy jest to coś prostego, skomplikowanego, niezwykłego, czy też osobistego. Jeśli nie zrazisz się do tej książki, to dasz się namówić, by w grach opowiadać własne historie, oraz zastanowisz się, czym tak naprawdę powinny być gry.

Portale internetowe W tej książce napisałem, że zajmowanie się dźwiękami w grach i programowaniem grafiki trójwymiarowej jest zbyt trudnym zajęciem, by zaprzątać sobie tym głowę. To nie do końca prawda. Są to skomplikowane dziedziny, w których ciągle dużo się zmienia, ale technologie HTML5 ciągle się rozwijają i są jeszcze bardzo dynamiczne. Poniżej znajduje się lista serwisów internetowych zawierających opisy zagadnień, które były zbyt skomplikowane lub dynamiczne, aby poświęcać im miejsce na papierze.  Box 2D Web (code.google.com/p/box2dweb/) box2d to popularne narzędzie do implementacji realistycznej dwuwymiarowej fizyki w grach.  Can I use (caniuse.com) Witryna zawierająca tabele z informacjami o obsłudze różnych technologii (np. API Web Audio) we wszystkich najważniejszych przeglądarkach internetowych w różnych wersjach.  Daily JS (dailyjs.com)

Portale internetowe  301











Jeśli chcesz wiedzieć, co ciekawego dzisiaj (i wczoraj, i przedwczoraj itd.) wydarzyło się w świecie JavaScriptu, to ta strona jest dla Ciebie. Częstotliwość publikacji i jakość wpisów robią wrażenie. HTML5 Audio (html5audio.org) Blog poświęcony nowinkom z dziedziny odtwarzania dźwięków na stronach internetowych. Można na nim znaleźć informacje o najciekawszych narzędziach oraz doniesienia o największych wydarzeniach. Jeśli np. Firefox w końcu zaimplementuje API Web Audio, ten blog poinformuje o tym jako pierwszy. HTML5 Game Development (html5gamedevelopment.org) Doskonałe źródło informacji dla tych, którzy chcą trzymać rękę na pulsie świata gier HTML5. Jeżeli chcesz wiedzieć, co się dzieje w społeczności, i znać wszystkie powstające narzędzia, to ta strona jest wprost wymarzonym miejscem dla Ciebie. HTML5 Rocks (html5rocks.com) Serwis zawierający poparte rzetelnymi badaniami artykuły na takie tematy jak buforowanie czy szczegóły działania przeglądarek internetowych w odniesieniu do technologii. three.js (http://mrdoob.github.com/three.js) Dobre miejsce na początek dla osób chcących rozpocząć programowanie grafiki trójwymiarowej. three.js działa na bazie WebGL i udostępnia uproszczone API. Dema przedstawione w serwisie stanowią dowód, że przy użyciu tego narzędzia można zdziałać cuda. Ponadto twórca three.js opublikował więcej niesamowitych publikacji w serwisie mrdoob.com. TIGSource (http://tigsource.com) Zajrzyj, jeśli chcesz wiedzieć, co ciekawego dzieje się w świecie gier niezależnych, albo jeśli szukasz inspiracji dla następnej gry.

302  Rozdział C ZASOBY

Skorowidz

A activeMole, 81 aktualizowanie graczy, 144 anchor, 205 API, 282 bibliotek, 282 implementacja, 282 localStorage, 245 rdzenne, 282 sieciowe, 250 własne, 283 argument, 285 atom.js, 29, 65 dostęp do elementu canvas, 70 podstawowy plik, 66 tworzenie przykładowej gry, 66 atrybut canChange, 144 class, 21 data-x, 42 fillStyle, 71 href, 21 id, 21 moleOffset, 78 name, 24 onclick, 33 onload, 89 player, 266 reallySuperDead, 274 rel, 21 room, 266 type, 21 type=, 24 value, 24 audiocontext.play(noteOrFrequency), 83

B backbone.js, 66 Bejeweled, 106 biblioteka Akihabara, 296 Atom, 296 atom.js, 30, 66 Crafty, 296 crafty.js, 250 cechy, 275 rysowanie tablicy izometrycznej, 257 wykrywanie kolizji, 271 dokumentacja dodatków, 92 easel.js, 88 buforowanie, 104 renderowanie, 88 enchant.js, 210 API, 247 cechy, 247 dokumentacja, 216 obiekt Group, 213 praca na urządzeniach przenośnych, 220 wiązanie klawiszy, 220 właściwości, 211 filtrr, 195 game.js, 134 blit, 135 Game.js, 296 gameQuery, 160 dokumentacja, 164 funkcje, 175 interfejs playground, 163 impress.js dodawanie złożonych interakcji, 51

304  Skorowidz biblioteka Jaws, 178 jQuery dodawanie do pliku, 27 pobieranie, 27 Raptorize, 61 selektory, 166 silnik gier, 29 melonJS, 114 kontekst renderowania kanwy, 124 Melon Engine, 118 narzędzia, 131 przestrzeń nazw, 134 warstwy kolizji, 121 zapisywanie mapy, 116 Node, 250 instalacja i uruchamianie, 251 pygame, 134 Socket.IO, 250 pobieranie, 255 pokoje, 267 synchronizacja, 254 zalety wykorzystania, 178 bijatyka, 133 blit, 135 definicje form, 155 definicje nazw, 139 definicje zmiennych pomocniczych, 148 dodanie tekstu do gry, 143 implementacja masek bitowych, 146 koniec gry, 155 maskowanie kolizji, 150 narzędzia mask, 151 niszczenie z wzajemnością, 152 obsługa naciśnięć klawiszy, 139 odbieranie danych od dwóch graczy, 137 początkowy plik, 134 poruszanie się, 141 powiększanie, 135 przesunięcie obiektów graczy, 155 przyjmowanie danych od obu graczy naraz, 144 rejestrowanie ciosów, 153 silnik gry, 29 sprite’y sprite'y, 135 tworzenie obiektów graczy, 143 wybieranie z zestawu, 136 zmiana rozmiaru, 150 zmiana sposobu obsługi klawiszy, 142 zmienianie formy, 141 block image transfer, 135 blokady, 25 blokowanie treści, 25 używania elementów formularza, 34 blokowe przesyłanie obrazu, 135

błąd składniowy, 49 Box 2D Web, 300 buforowanie, 104 wyłączanie, 105 bug, 289 button, 220

C caching, 104 Can I use, 300 Chrome, 297 Chrono Trigger, 43 class, 24 closure, 50 CoffeeScript, 66, 298 dziedziczenie, 70 konwersja na JavaScript, 66 przykład kodu, 67 utrudnione znajdowanie błędów, 66 component-entity system, 257 compositing, 135 crafty.js, 29 silnik gry, 257 Crockford Douglas, 38 cross-site scripting, 173 CSS, 161 definiowanie formatu stron, 39 formatowanie, 21 funkcja przeciągania przedmiotów, 45 nawigacja między stronami, 39 określanie kolorów, 197 reset, 41 ukrycie elementu canvas, 32 ukrywanie części strony, 25 wygląd w przeglądarkach, 41

D Daily JS, 300 dane w formacie JSON, 54 debugowanie buforowanego systemu, 104 definiowanie schowka, 46 tytułu pliku HTML, 67 deklaracja display block, 28 none, 26 DOCTYPE, 21 html, 67 margin-left 50px, 26 stylu, 26

Skorowidz  305

distance, 191 dodatek Firebug, 290 DOM, 161 dostępność dokumentu dla czytników, 39 Double Fine Adventure, 38 dragDrop.js, 46 modyfikacja, 52

E Easel, 29,88, 296 edytor map kafelkowych, 114 edytor tekstu, 297 wybór, 20 ekran PlayScreen, 117 element body, 21 gradient tła, 41 procedura obsługi kliknięcia, 33 canvas, 30, 32, 67, 70, 90, 179, 181 blokowanie myszy, 34 dwuwymiarowy kontekst rysunkowy, 71 game.js, 134 znajdowanie wpliku HTML, 70 dino, 54 div, 21, 38 dodawanie pytań quizu, 22 head, 21 HTML, 20 input, 24 inventory-box, 47 label, 24 link, 21 meta, 21 minimap arkusz stylów, 182 dodanie do pliku, 182 myAudio, 84 playerBody dodanie symbolu, 172 replay, 103 screenshot, 195 script, 61, 89, 134 title, 21 z identyfikatorem liczbowym, 51 Emacs, 297 Enchant, 29, 296 entities.js, 120 funkcja gameOver, 125 ładowanie pliku, 120 entity, 120 Etsy, 299 etykieta stanu, 222

F fikcja interaktywna, 37 dinozaura, 61 dodanie kontenerów przedmiotów, 44 schowka, 44 stron historii, 38 złożonych interakcji, 50 dramatyczne zakończenie, 61 formatowanie wnętrza slajdów, 43 kod stron, 39 nawigacja okruszkowa, 59 obsługa interakcji, 46 przechowywanie i pobieranie elementów, 48 rozpoczęcie gry od nowa, 43 slajdy, 38 strona decyzyjna, 41 strona zakończenia gry, 42 fikcja literacka style okruszków, 60 Filtrr, 298 finkcja update wywołanie dla graczy, 146 Firebug, 298 Firefox, 297 folder gotowe, 17 po_recepturze, 17 for, 94 fora dla programistów, 293 forEach, 46 porównanie z pętlą for, 46 foreground, 114 format .tmx, 114 Base64, 116 fps, 211 funkcja, 285 add, 49 addChild, 213 addChildAt, 108 addCombatants, 243 addItem, 56 apply, 204 arctan, 203 areaMap, 263 attack, 237 beginPath, 71 blank, 190, 192 budowanie tablicy, 95 call, 46 callDino, 62 camera.takePicture, 194 canPlayType, 84

306  Skorowidz funkcja canvas.drawSliver, 191 canvas.init, 190 castRay kolorowanie ścian, 198 castRays, 186 przeniesienie, 203 changeForm, 144 modyfikacja, 153 checkAnswers, 33, 34 checkIfCorrect, 34 clearInventory, 57 clearStatus, 226, 228 console.log, 253 containsBlock, 185 clearStatus wywołanie, 226 deleteItem, 56 dino.draw, 205 disable, 34 displayStatus, 222 modyfikacja, 230 modyfikacja wyświetlania informacji, 237 przełączanie widoczości informacji, 226 doJump, 123 doWalk, 123 draw, 76, 124, 188, 189 modyfikacja, 83, 190, 204 drawHoles, 74 drawItemsForSale, 235 drawSliver kanwy, 191 drawSquare, 93 modyfikacja, 91, 96 drawTextTile, 108 drawWhiskers, 76 dropItemInto, 62 end, 164, 165 eval, 173 zastąpienie, 173 facing, 224 facing.Square, 224 fillRect, 185 findTextNode, 56 floor, 185 focusViewport, 217 forEach, 47 function Eval, 173 game.onload modyfikacja, 225, 231 game.slide, 55 gameOver, 110, 124 modyfikacja, 129 gameOver(), 102, 103 gameTick, 140 modyfikacja, 152, 155 getElementById, 90

getInventory, 56 getPlayerStatus, 244 getRandomPlacement, 96, 109 graphics.beginFill, 91 handleDragOver, 48 handleDragStart, 47 handleDrop, 48 handleEvent, 142 handleOnPress, 98, 109 aktualizacja bufora, 105 modyfikacja, 100, 103 hideInventory, 226, 228 hitStrength, 239, 240 hitTest, 220 init, 89, 118 deklaracje zmiennych, 92 dodawanie kwadratów, 102 modyfikacja, 122 modyfikacja pętli, 96 obiektu minimap, 181 pętla for, 108 renderowanie kwadratów, 94 renderowanie par, 107 wiązanie z oknem, 89 jsApp.onload, 118 JSON.parse, 247 JSON.stringify, 246 keydown, 171 lineTo, 76, 189 listen, 253 load, 150 loaded, 118 lost, 240 main, 135, 139, 140 renderowanie sprite'ów, 137 makeHoles, 77 markToDestroy, 274 Math.atan, 203 Math.floor, 91, 266 Math.round, 173 mieszająca, 32 move, 184 moveBy, 217 moveTo, 76, 189 moveUnit, 264 onDestroyEvent, 130 onHit, 272 onload, 118 dodanie własności coins i totalCoins, 130 onResetEvent, 124 instrukcje dla gracza, 129 parseInt, 247 pause, 241 placeUnits, 260 modyfikacja, 267 obsługa klikania i ruchu, 264 obsługa kolizji, 271

Skorowidz  307

Player zapisywanie infomacji o graczu, 152 Player draw modyfikacja, 142 player.displayStatus, 222 player.draw, 183 player.move, 217 modyfikacja, 222 preload, 118, 150 przeciągania i upuszczania, 44 push, 49, 96 pushScene, 230 randomColor, 91, 93 registerCallbacks, 163 registerHit, 153, 155 remove, 49 render, 195 replay, 103, 110 odświeżenie strony, 106 reset, 195 response.end, 253 run, 67 Run, 241 samowykonująca, 52 scaleUp, 136 setBattle, 238 setInterval, 269 setMaps, 213 dodanie warstwy kolizji, 217 setPlacementArray, 95, 109 setPlayer, 216, 222 setShopping, 231 setStage, 213 setText, 56 setTimeout, 234 setup, 179 modyfikacja, 190 obiekt palette, 196 shoppingFunds, 232 show, 28 showInventory, 226, 228 modyfikacja, 230 splice, 49 sprite.draw, 206 standardowa, 90, 92 start, 179 startGame, 163 state.change, 118 takePicture, 195 text, 166 tick, 102, 110 toDataURL, 195 uncache, 105 unitsWithLimitedData, 269 update modyfikacja, 79, 122 obiektu Player, 154

obsługa animacji podczas ruchu, 120 poruszanie graczem, 183 updateEnemyPositions, 269 window.OnReady, 118 window.open, 196 with_key, 81, 82 won, 240 wywołanie, 33 zmiany rozmiaru ekranu, 66 zmienianie form, 141 funkcje trygonometryczne w grze, 185

G game jam, 15 game.css, 179, 210 game.js, 26, 179, 210 dodanie własności screen, 58 funkcja game.slide, 55 funkcja placeUnits, 262 kod kliencki Socket.IO, 256 kod wiązania klawiszy, 220 kolizje, 270 nasłuchiwanie wiadomości place units, 262 obsługa ruchu gracza, 214 procedura obsługi kliknięć kafelków, 263 silnik gry wykrywanie kolizji, 150 uruchamianie aparatu, 194 window.onload, 211 GameQuery, 296 Gedit, 297 Gimp, 299 magiczna różdżka, 150 Git, 29, 252 GitHub, 29, 253, 282 współpraca, 292 globalna przestrzeń nazw, 118 gniazda sieciowe, 255 gra FPS, 177 dodawanie kamery, 192 dodawanie postaci gracza, 182 dodawanie przyjaciół i wrogów, 200 imitacja trójwymiarowości, 190 kierunek patrzenia, 185 konfiguracja raycastera, 186 ładowanie dinozaura, 200 podstawowy plik HTML, 178 poruszanie postacią, 184 raycasting widoku z góry, 186 rejestrowanie danych wejściowych, 183 rysowanie kolorów i odcieni, 198 rzucanie promieni, 187

308  Skorowidz silnik gry, 29 style elementów aparatu fotograficznego, 193 tworzenie mapy dwuwymiarowej, 179 uatrakcyjnianie świata, 196 umieszczanie gracza na mapie, 183 włączenie sepii, 206 zasoby, 299 platformowa, 113 automatyczne resetowanie, 124 budowa mapy kolizji, 121 chodzenie i skakanie, 121 definicja wygranej, 130 dodanie kontenerów na wiadomości i instrukcje, 129 dodawanie postaci, 119 dodawanie przedmiotów do zbierania, 125 dodawanie ziemi, 121 edycja mapy, 115 ekran tytułowy, 123 gameOver, 129 informacje, 129 inicjowanie aplikacji, 118 jednostka EnemyEntity, 127 obsługa ruchu gracza, 122 obsługa stanu MENU, 124 przegrywanie i wygrywanie, 129 przycisk dodawania obiektu, 119 resetowanie monet, 130 silnik gry, 117 tworzenie mapy kafelkowej, 114 uruchamianie, 116 wiązanie klawiszy ruchu, 122 wrogowie, 126 youWin, 130 zakończenie gry, 125 załadowanie zasobów, 118 zapis danych mapy, 116 zasoby, 299 zwiększanie mocy postaci, 128 ROG rysowanie kota, 232 RPG, 209 atakowanie, 240 atakowanie i przechodzenie poziomów, 237 budowa sceny, 244 dodawanie gracza, 214 dodawanie gracza i wroga, 242 dodawanie warstwy kolizji, 217 dodawanie włóczęgi, 235 działania wojenne, 240 ekran stanu, 220 etykieta na status gracza, 238 funkcja obsługi danych wejściowych, 222 interakcja z postaciami, 223 magazyn lokalny, 246 mówiący kot, 228

obsługa początku bitwy, 243 odczytywanie danych z magazynu lokalnego, 246 odejmowanie punktów zdrowia, 239 określanie sprite’a przed graczem, 224 opuszczanie sceny bitwy, 244 otwieranie sklepu, 230 pętla bitwy, 243 plik index.html, 210 poruszanie gracza, 217 procedura obsługi zdarzeń sklepu, 233 przeglądarka Chrome, 219 przegranie bitwy, 239 przygotowanie bitwy, 238 przygotowywanie danych do wyświetlania, 221 rozmawianie z postaciami z gry, 224 rysowanie produktów w sklepie, 232 skróty atrybutów, 222 sprite’y przedmiotów, 226 stan gracza, 221 turowa, 210 tworzenie interfejsu bitwy, 235 tworzenie mapy, 211 tworzenie sklepu, 228 ukrywanie etykiety, 222 uruchamianie sklepu, 231 usunięcie zawartości schowka, 229 widoczność informacji o stanie gracza, 226 worzenie schowka, 226 wygrana w bitwie, 239 wykrywanie kolizji, 219 wyświetlanie danych, 221 wyświetlanie i ukrywanie schowka, 227 wyświetlanie opcji walki, 242 wywołania funkcji i przypisania własności w sklepie, 235 zakup produktu, 234 zapisywanie, 245 zasoby, 300 RTS, 249 dodawanie sprite’ów, 258 informacja o zmianach pozycji, 268 kolizje dla destrukcji i sprawdzenia przeciwnika, 270 obsługa kliknięć kafelków, 263 plik index.html, 255 poruszanie jednostkami, 263 procedura obsługi połączenia, 266 procedura obsługi wiadomości initialize player, 273 procedura obsługi wiadomości place units, 273 rysowanie jednostek, 259 sterowanie gracza, 265 tworzenie mapy izometrycznej, 257 ustawienie kafelków, 258

Skorowidz  309

warunek pierwszego kliknięcia, 268 widoczność, 265 wysyłanie jednostek miejsc do klienta, 259 typu, 38 grupa battle, 238 shop, 231 guard, 82

H halfAngularWidth, 206 Harvest Moon, 38 hipertekst, 20 hitbox, 150 HTML, 20 otwieranie pliku w przeglądarce, 21 struktura dokumentu, 20 HTML5 Audio, 301 HTML5 Game Development, 301 HTML5 Rocks, 301 httpserver.js, 253 Hypertext Markup Language, 20

I id, 24 identyfikator, 24 impress, 39 player_inventory, 45 if else, 54 importowanie plików na stronę, 28 zestawu kafelków, 114 Impress, 298 impress.js, 29, 38 okruszki, 59 impreza, 65 bicie kretów, 80 dynamiczne pokazywanie kreta, 79 rysowanie dziur, 72 rysowanie kreta, 74 rysowanie na kanwie, 70 rysowanie tła, 71 skrypt, 29 sprawdzenie trafienia, 82 umieszczanie kretów w dziurach, 77 ustawienie stanu aktywności dziur, 80 zapisywanie wyniku, 81 inicjowanie obiektu inwentarza, 49 initialize player, 269 Inkscape, 299 instrukcja bind.this, 118 console.log, 27

console.log(mójObiekt), 290 console.log(toCzegoNieRozumiem), 73 game.constructor, 70 this.message, 232 this.nazwaWłasności, 74 warunkowa, 48 instrukcje, 283 warunkowe, 286 interakcje z obiektami, 50 interfejs książka, 38 programistyczny, 282 interpreter, 284, 290 komentarze, 285 interpretery, 66

J JavaScript brenchmarking, 104 definiowanie własności obiektów, 98 dodawanie plików, 30 dołączanie plików do systemu, 28 funkcja, 285 główne typy API, 282 gra platformowa, 116 instrukcje, 283 instrukcje warunkowe, 286 interpreter, 253 język przeglądarkowy, 254 komentarze, 287 konwersja z CoffeeScript, 68 lista numerów klawiszy, 169 ładowanie kodu, 26 ładowanie skryptu, 89 łańcuchy, 284 metody API, 49 nawiasy, 49 notacjaWielbłądzia, 283 obiekt, 285 ogólna budowa API, 283 określanie dostępności zmiennych, 69 określanie kolorów, 197 operatory, 172 opisowe nazwy zmiennych i funkcji, 93 pętle, 286 przecinki, 117 przykładowa gra, 68 tablica, 284 unobtrusive, 89 wartość zwrotna, 49 wczytywanie plików, 39 wzorce, 89 zmienna, 283 Jaws, 29, 296 jednostka gracza, 120

310  Skorowidz jQuery, 298 jquery.gamequery.js, 29 jquery.js, 29 js2coffee.org, 66 jsfiddle.net, 293 JSLint, 292 jsperf.com, 104

K kafelek kolizji, 121 solid, 121 kanały IRC, 293 kanwa, 160 z dwuwymiarowym kontekstem, 160, 161 z trójwymiarowym kontekstem, 160, 161 katalog start, 17 klasa, 24 Bitmap, 111 BitmapAnimation, 111 correct, 32, 33 empty, 46 enemy, 166 event-text, 51 inventory-box, 46 itemable, 45 item-container, 45 playerMissiles, 171 question, 24 slide, 39, 41 slide-text, 42 SpriteSheets, 111 step, 39 kod błędy programistyczne, 289 elementy ułatwiające zrozumienie, 93 oznaczenie, 16 testowanie, 291 komentarz, 285, 287 komponent DOM, 258 grass, 259 komunikaty diagnostyczne, 290 konsola, 290 konstruktor, 138 Enemy, 164 obiektu, 69 Player, 141 height, 167 width, 167 z identyfikatorem formy, 156 kontekst renderowania kanwy, 124 kontekst trójwymiarowy, 71 kontrola jakości, 289 konwencje typograficzne, 16

konwersja kąta na stopnie, 205 kształt, 108

L Legend of Zelda, 88 licencjonowanie oprogramowania, 31 liczby, 146, 284 binarne, 147 dziesiętne, 147 listy mailingowe, 292 literał game.keys, 80 local storage, 245 losowanie kolorów, 91 Lufia 2, 88

Ł ładowanie zewnętrznego pliku JavaScript, 26 ładowanie obrazu ekranu jako zasobu, 124 łańcuchy, 284 wywołań, 49

M magazyn lokalny relacyjny, 247 Magic wand, 150 main.css, 39 przeciąganie przedmiotów, 45 ukrywanie treści strony, 26 main.js dodanie butów do puli jednostek, 128 dodanie monet do puli jednostek, 125 dodanie wroga do puli jednostek, 126 dodawanie modułu czcionek, 141 Maniac Mansion, 38 map.js, 211 mapa, 211 maper kodu, 66 maski bitowe, 146 obsługa zdarzeń, 149 maszyna stanów, 244 mechanizm broadcast, 267 Melon, 297 melon.js, 29 silnik gry, 117 menedżer pakietów nmp, 252 menedżery pakietów, 252 message, 54

Skorowidz  311

metoda add, 49 addChild(), 90 addEventListener, 46 addGroup, 165 addItem, 57 addSprite, 165 arc, 71 attachEvent, 46 beginFill(), 91 beginStroke(), 91 bitowa, 147 context.fillText, 74 context.font, 74 currentSlide, 56 deleteItem, 57 draw, 71 bez skalowania, 150 modyfikacja, 78 uproszczenie, 72 Draw w pętli, 67 drawHoles, 73 drawSquare(), 90 dropItemInto, 54 fill, 71 fillRect, 71 fillStyle, 71 game.bop.with_key, 83 game.drawBackground, 72 game.screen.draw, 55 game.slide.SetText, 54 game.update, 81 get, 49, 54 graphics.setStrokeStyle(), 90 item, 47 items, 54 łączenie wywołań w łańcuchy, 165 Object.create, 69, 70 prywatna, 56 publiczna, 49 querySelectorAll, 46 rect(), 91 remove, 49 rysowanie figur, 72 stage.update(), 90 stroke, 76 update bitowa, 148 w pętli, 67 Minecraft, 38 Modernizr, 298 modularyzacja, 50 module pattern, 50 Mozilla Developer Network, 282 Myst, 207

N nagłówek h1, 21 narzędzia przydatne podczas tworzenia gier, 298 Node, 298 node package manager, 252 notacjaWielbłądzia, 283 Notepad++, 297 NPM, 298

O obiekt, 285 bat, 54 battle.menu, 238 BootsEntity, 128 bop, 81 buforowanie, 104 camera, 194 canvas definiowanie, 191 modyfikowanie, 197 CoinEntity, 126 Crafty, 258 dino, 200, 205 sprite jaws, 205 eksperymentowanie w konsoli, 181 Enemy, 166 EnemyEntity, 126 definiowanie, 127 forms, 142 game, 52, 67 wymiary sprite'a, 213 Game, 66, 67, 211 game.hole, 74 Graphics, 92 greeter, 225 Group, 213 hole dodatkowy kod rysowania, 77 imgSize, 137 inventory, 49 inventoryObject, 48 kanwy, 191 map, 213 dodanie danych kolizji, 217 mapujacy metody publiczne na prywatne, 54 minima funkcja draw, 181 minimap, 181 funkcja draw, 204 mole, 75 NodeList, 46 npc, 225

312  Skorowidz obiekt opis przeglądarek, 70 palette, 196 player, 183, 216 atakowanie i przechodzenie poziomów, 237 Player, 137 atrybut mask, 148 funkcja update, 144 rejestr naciśnięć klawiszy, 144 rejestrowanie danych wejściowych, 145 PlayerEntity dodawanie, 121 playerInventory, 56 potomny tworzenie, 69 raycaster, 186 modyfikacja, 190 modyfikowanie, 201 rect, 137 reprezentujący kreta, 75 Stage, 90 surface, 137 Ticker, 102 tile, 98 tileClicked, 98 TitleScreen, 123 tworzenie konwencje, 138 window, 89 object, 54 obsługa padów do gier, 220 raycastingu, 178 zdarzeń klawiatury i myszy, 66 obszar widoku, 219 odblokowanie pytań, 28 odtwarzanie dźwięków, 83 w przeglądarkach, 84 okruszki, 59 implementacja, 59 Open Game Art, 299 open source, 282 operacje na bitach, 147 operatory bitowe, 147 oznaczanie poprawnych odpowiedzi tworzenie stylu, 32

P pakiet npm, 252, 255 parallax scrolling, 131 parametr, 285 alignment, 262 context, 124 dt, 79 formIndex, 152

itemNode, 54 message, 56 slideId, 56 source-overlay, 105 target, 54 perspektywa izometryczna, 178 pętla, 286 for, 233 w stylu funkcyjnym, 46 w stylu proceduralnym, 46 pętle, 94 Pickle, 299 Piętnastka, 88 Pixel Joint, 299 playground, 162 pliki index.html, 17 źródłowe, 17 pobieranie danych od graczy, 144 pobieranie losowego elementu, 97 pola kolizyjne, 150 polecenie node, 253 poradniki, 299 procedura dragenter, 48 dragleave, 48 enterframe, 234 onPress, 108 procedura nasłuchowa dla przycisku, 221 programowanie niskopoziomowe, 88 wysokopoziomowe, 88 programowanie funkcyjne, 47 programy działające po stronie serwera, 250 projektowanie gier kierunek badań, 279 prototyp obiektu, 69 przechowywanie danych, 284 przeglądarki, 297 bufory, 104 przeniesienie fokusu, 24 przywracanie do widoku, 26 punkty wstrzymania, 291 puzzle, 87 aktualizacja bufora, 105 buforowanie i wydajność, 104 dopasowywanie i usuwanie par, 97 dopasowywanie par zamiast duplikatów, 106 inicjowanie bufora, 104 logika wygranej i przegranej, 103 Memory, 88, 99 obsługa kliknięć, 97 przechowywanie czasu gry, 100

Skorowidz  313

skrypt, 29 tworzenie kwadratów, 92 tworzenie par, 94 ukrycie koloru kwadratów, 99 ukrywanie i przekręcanie obrazków, 99 wstępny plik HTML, 88 wygrywanie i przegrywanie, 100 wyłączanie buforowania, 105

Q quiz, 19 dodawanie pytań, 22 lista zakupów, 28 oznaczanie poprawnych odpowiedzi, 32 plik index.html, 20 przywracanie pytań do widoku, 26 przywrócenie pytań do widoku, 31 reagowanie na kliknięcia, 32 sprawdzanie odpowiedzi, 24 sprawdzenie odpowiedzi, 33 styl poprawnych odpowiedzi, 32 ukrywanie i pokazywanie, 25 ukrywanie pytań, 27 wynik porównania odpowiedzi, 34 wyświetlenie pierwszego pytania, 28 zablokowane pytania, 25 zbiór pytań, 20 QUnit, 291

R Raptorize, 298 ray casting, 177 raycasting, 178 imitacja trójwymiarowości, 190 widoku z góry, 186 receptury, 17 refactoring, 73 refaktoryzacja, 73 kodu, 137 renderowanie, 160 grafiki, 88 kolorów śródliniowo, 106 kontrolowanie, 219 kwadratów, 94 na kanwie, 160 technologie, 161 przeglądarkowe, 160 większej liczby obiektów, 92 requestAnimationFrame normalizacja, 66 reset CSS, 41 resources.js, 117

dodanie sprite'a monet, 125 dodanie wroga, 126 dodanie zasobu boots, 128 dodawanie gracza, 119 dodawanie postaci gry, 120 RGB, 33 rootScene, 213 Ruby on Rails, 66 rysowanie dziur, 72 funkcje bibliotek, 76 kształtów, 90 na elemencie canvas, 70 na kanwie, 70, 88 na ścieżce, 71 sumy trafień, 83 tła, 71 wykorzystanie obiektów graficznych, 76 wyniku, 81 rzutowanie izometryczne, 178

S scena battleScene, 236, 245 schowek zapełnianie, 49 screen, 58 screen.js dodanie obiektu PlayScreen, 117 wiązanie klawiszy ruchu, 121 screens.js, 117 ekran tytułowy, 123 instrukcje dla gracza, 129 usunięcie starych wiadomości, 129 selektor body, 41 server.js obsługa połączenia, 266 określenie położenia jednostek, 260 serwer, 250 aktualizowanie zmian, 269 automatyczne przyjmowanie zmian, 260 kod serwerowy, 251, 254 komputer użytkownika, 251 protokół komunikacyjny, 250 Socket.IO, 256 zapisywanie zmian, 259 sessionStorage, 247 shade, 199 Shadowgate, 38 sikniki gier, 29 silnik wykrywanie kolizji, 150 silnik gry, 296 pojęcia i terminy, 228

314  Skorowidz silniki gier uruchamianie, 30 składanie, 135 skrypt yabble.js, 134 słowo kluczowe super, 67 this, 73 var, 118, 283 Socket.io, 298 Socket.IO procedura nasłuchująca, 262 sprawdzenie dopasowania kwadratów, 99 sprite, 76 Sprite Database, 299 spritesheet, 114 stage, 213 statusLabel, 222 Stratego, 250 strażnik, 82 struktura DOM, 160 obiekty game i stage, 213 strzelanka, 159 dodanie gracza do planszy, 167 nowej warstwy, 164 wrogów, 163 dynamiczne dodawanie wrogów, 165 formatowanie pocisków, 172 kod sterowania pojazdem, 168 kolizje z pociskami udoskonalenie obsługi, 173 wykrywanie, 170 kolizje z wrogami, 169 obsługa kolizji, 169 początkowy kod HTML, 160 podstawowe elementy gry, 162 prędkość pocisku, 170 przeglądanie funkcji, 174 silnik gry, 29 strzelanie, 170 style statku kosmicznego, 168 style wrogów, 166 tworzenie pocisków, 171 tworzenie pojazdu, 167 uzupełnianie mocy, 172 warstwa pocisku, 170 zasoby, 299 zmienne statku kosmicznego, 167 subject, 54 Sublime Text, 297 Surface, 216 SVG, 161 system kontroli wersji, 252

Ś ścieżka, 21

T tablica, 284 backgroundSlivers, 203 do przechowywania slajdów, 52 enemyUnits, 268 flashcards, 107 foregroundSlivers, 203 game.holes, 77 game.items szczegóły przedmiotów, 230 indeksy, 284 jednowymiarowa, 108 map, 181 maskCache, 151 budowa, 151 numberOfTiles, 96 placementArray, 94 spriteRoles, 225 squares dodawanie kwadratów, 102 stepsTaken, 56 surfaceCache, 137 textiles, 106 units, 268 visibleItems, 228 walls, 197 yLocations, 262 technika parallax scrolling, 178 ray tracing, 178 rzutowanie izometryczne, 178 test playground, 31 kodu, 291 niskopoziomowe, 291 wysokopoziomowe, 291 wydajności, 292 three.js, 301 TIGSource, 301 Tiled, 114, 298 dodawanie postaci, 119 tworzenie mapy kafelkowej, 114 nowego poziomu, 115 pozycji startowej, 119 warstwa boots, 128 coin, 125 enemy, 126 kaflekowa, 121 tileset, 114

Skorowidz  315

tłumaczenie strony na wybrany język, 39 trueSprite, 265 tryb pełnoekranowy przeglądarki ustawianie, 219 twierdzenie Pitagorasa, 199 tworzenie dokumentu HTML, 21 grafiki, 299 obiektów potomnych, 69 obiektu z szablonu, 69 stron internetowych, 89

U ukrycie pytań, 28 undefined, 49 unit, 265 unitClicked, 264 usługi sieciowe, 250 ustawienie tła pod tekstem, 108 ustawienie stanu aktywności, 80

V viewport, 120, 219 Vim, 297

effects, 54 frame, 216 game.things, 53 hiding, 272 isMoving, 217 itemSelected, 232 nadpisywanie, 98 name, 54 node, 164 opacity, 41 prototype, 69 screen, 58 shades, 197 spriteOffset, 216 startingX, 216 startingY, 216 type, 272 walk, 216 współpraca, 292 wydajność aplikacji na platformie, 104 wykrywanie klawiszy, 81 wypełnianie tła, 109 wyszukiwanie grafiki, 299 wyświetlanie błędów w konsoli, 39 wywołanie e.preventDefault(), 48

X

W warstwa enemies, 164 dodawanie sprite'ów, 165 player dodawanie sprite’ów, 167 pocisku, 170 warstwa collision, 121 wartość adjustedDistance, 191 bias, 258 brightness, 199 totalCoins, 130 WebSocket, 255 wiązanie klawiszy, 80 wiązanie przycisku z funkcją, 234 wiązanie zdarzeń myszy, 71 wiersze kodu, 283 własność active, 80 alive, 272 collisionData, 220 color, 272 constructor, 69 dino.show, 203 direction, 216

XSS, 173

Y Yabble, 298 yabble.js, 29 YAGNI, 89

Z zamknięcie, 50 zapisywanie danych po stronie klienta, 247 zasoby, 295 książki, 300 portale internetowe, 300 zdarzenie enter, 234 onload wiązanie, 89 zmiana łącza do slajdów, 51 sposobu odnoszenia do elementów, 51 zmiana kodu podczas pracy, 269 zmienianie stron, 41

316  Skorowidz zmienna, 283 activeGame, 155 adjustedAngle, 206 angle, 185 angleBetweenRays, 186 angleInDegrees, 205 battle.over, 239 color, 96 columns, 93 controllable, 268 counter, 58 currentMoleTime, 79 definiowanie, 93 definiowanie jako niezdefiniowana, 98 direction, 185 distance, 188 draggingObject, 47 dX, 188 dY, 188 enemyHeight, 164 enemySpawnRate, 164, 165 enemyWidth, 164 expMax, 237 filtered, 195 foregroundData, 212 game, 68 Game, 67, 68, 69 highlight usunięcie, 100 hit, 154 initialWallColors, 197 items, 57 mapData, 212 max_rgb_color_value, 92 maxDistance, 203 movementSpeed, 185 moveStep, 185

numberOfTiles, 95, 96 pairIndex, 108 parallax, 162 percentageDistance, 203 placement, 96 PlayerEntity, 120 potentialWidth, 205 rayNumber, 187 rows, 93 speed, 164, 185 squareOutline, 92 squarePadding, 93 squareSide, 92 textTiles, 107 tileClicked, 97 total, 81 turnSpeed, 185 twoPi, 188 unitInfo, 268 units, 266 ustawienie na obiekt, 98 value, 164 wallType, 198 wallX, 188 wallY, 188 xHit, 188 yHit, 188 zmienne globalne, 283 zmniejszenie szybkości ruchu jednostek, 264 znacznik, 20 , 21 HTML, 20 viewport, 219 znaczniki przeglądarki Safari, 219

Notatki

Recommend Documents