jsHopper jest już ‚skończony’. Dlaczego cudzysłów? Bo tak naprawdę w tej grze mógłbym jeszcze wiele ulepszyć, ale już nie będę kontynuował prac nad tym projektem 🙂 . Jego celem było przede wszystkim sprawdzenie nowego frameworka, czyli Phaser.js.
Już w ostatnim poście zdradziłem, że phaser bardzo mi się podoba. Dziś wstępnie opiszę mechanizmy, które tak mnie urzekły.
W aktualną wersję gry można pograć klikając w obrazek. Kod projektu do obejrzenia jest w źródle jego strony lub na moim githubie 🙂 .
Obiecałem, że tym razem przejdę prosto do kodu i tak zrobię. Przy okazji postaram się wyjaśnić jak działają mechanizmy frameworka, a jest ich tu sporo. Program w 90% składa się z metod phasera, które chciałbym w miarę dokładnie opisać. Aby nie wyszła z tego posta wielka kobyła postanowiłem podzielić go na dwie części.
Cały kod umieściłem w jednym pliku main.js. Jest on podpięty w znaczniku script do pliku index.html. Phaser dodaję tak samo jak każdy inny plik zewnętrzny czyli też używając znacznika script.
Trzeba pamiętać aby skrypt z autorskim kodem, dodany był po skrypcie z phaserem, inaczej metody frameworka nie będą działały. Mam nadzieję, że nie muszę nikomu o tym mówić 😉 . Ważne jest też to, że do działania phaser potrzebuje odpalonego serwera. Każdy kto chce testować pisane przez siebie gry lokalnie, musi o tym pamiętać. Ja korzystałem z prostego serwera w node i śmigało idealnie.
Kod pliku main.js można podzielić na cztery odrębne części. Pierwsze trzy to obiekty loadState, menuState oraz gameState. Jak łatwo się domyślić dzięki nazwom, są to obiekty stanów gry. W phaser obsługa stanów jest bardzo prosta, co zaraz zademonstruje. Czwarta część kodu, znajdująca się na końcu pliku, to inicjalizacja gry. Od tego fragmentu rozpocznę opis. Oto on:
1 2 3 4 5 6 |
var game = new Phaser.Game(384, 544, Phaser.AUTO, 'game'); game.state.add('load', loadState) game.state.add('menu', menuState) game.state.add('game', gameState); game.state.start('load'); |
Najpierw tworzę nowy obiekt gry, używając konstruktora Game obiektu Phaser. Nową instancję gry przypisuję go do zmiennej game. Konstruktor przyjmuje cztery argumenty. Pierwsze dwa to wymiary gry, szerokość i wysokość wyrażone w pikselach. Kolejny argument, pozwala na zdecydowanie w jaki sposób gra będzie renderowana, w kontekście 2d płótna, czy przez webGL. Ja wykorzystuje metodę AUTO, dzięki której phaser sam dobierze odpowiednie dla używanego urządzenia rozwiązania. Ostatni argument to ID elementu html, w którym umieszczona zostanie gra. W moim przypadku jest to div o id game.
Kolejne trzy linijki, to deklaracja stanów używanych w grze. Kto czytał opisy moich wcześniejszych projektów, powinien wiedzieć czym są stany (zwane też scenami) w grach. Może to być loading screen, menu główne, poziom gry, pojedynek z bossem itp. Można powiedzieć, że każdy stan to niezależny element programu.
Aby dodać stan do gry budowane w phaser, na obiekcie game wywołuje metodę add pola state. Metoda add przyjmuje dwa argumenty. Pierwszy to łańcuch znaków, po którym będę mógł odwoływac się do konkretnego stanu. Drugi argument to odnośnik do obiektu stanu.
Na koniec wywołuję pierwszy stan czyli load. Robię to za pomocą metody start pola state obiektu gry. Ta metoda, wykonuje wszystkie działania potrzebne do zamknięcia aktualnego (jeżeli jakiś działa) stanu, posprzątania po nim i odpalenia nowego stanu, do którego klucz przekazuję jako argument.
I to wszystko! W czterech linijkach kodu dodałem do mojej gry trzy stany i odpaliłem jeden z nich.
Pierwszy uruchamiany stan to loadState. Oto, krótki kod tego stanu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var loadState = { preload: function() { var loadingText = game.add.text(game.world.centerX, 150, 'loading...', { font: '30px Arial', fill: '#FFF' }); loadingText.anchor.setTo(0.5); game.load.image('splash', 'GFX/splashScreen.png'); game.load.image('background', 'GFX/BG.png'); game.load.image('bunny', 'GFX/bunny.png'); game.load.image('car1', 'GFX/car1.png'); game.load.image('car2', 'GFX/car2.png'); game.load.image('truck', 'GFX/truck.png'); game.load.image('car1F', 'GFX/car1F.png'); game.load.image('car2F', 'GFX/car2F.png'); game.load.image('truckF', 'GFX/truckF.png'); game.load.image('logx3', 'GFX/logx3.png'); game.load.image('logx5', 'GFX/logx5.png'); game.load.image('logFix', 'GFX/logFix.png'); }, create: function() { game.state.start('menu'); }, } |
W phaser, każdy stan ma z góry zdefiniowane parę bardzo przydatnych metod. Te najczęściej używane to preload, create oraz update. Oczywiście nie są one wymagane w każdym stanie, używam ich tylko jeśli są potrzebne. preload (jeżeli istnieje) jest pierwszą funkcją odpalaną przez stan. Powinna ona służyć to wczytania wszelkich danych, obrazków lub plików muzycznych potrzebnych w grze.
Gdy preload skończy swoje działanie, odpalana jest metoda create. Ta metoda służy do zainicjalizowania wszystkich obiektów, które wykorzystywane będą w danym stanie.
Gdy create się wykona, uruchamiana jest metoda update, którą stan odpala 60 razy na sekundę. To taki game loop.
W przypadku stanu load, nie korzystam z metody update, ponieważ, gdy tylko załadowane zostaną obrazki i wykona się funkcja create, przechodzę do następnego stanu. Ponieważ obrazki dodane do gry, są dostępne w każdym stanie, dodaje je wszystkie na początku. dzięki temu gracz nie musi czekać na wczytywanie danych pomiędzy stanami. Wolę uniknąć sytuacji, w której wszystkie obrazki ładowane są za każdym razem, gdy gracz przejdzie do menu.
Obrazki dodaje za pomocą metody image pola load głównego obiektu gry. Metoda przyjmuje dwa argumenty. Pierwszy to klucz, który będę używać do wywołania obrazka, drugi to ścieżka do odpowiedniego pliku graficznego.
W metodzie create, która odpali się, gdy wszystkie obrazki zostaną wczytane, startuje kolejny stan czyli menu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var menuState = { create: function() { game.add.image(0, 0, 'splash'); style = {font: '25px Arial', fill: '#ffffff'}; this.welcomeText = game.add.text(15, -50, 'Welcome to the game! \npress Space!',style) var welcomeTween = game.add.tween(this.welcomeText); welcomeTween.to({y: 160}, 700).easing(Phaser.Easing.Bounce.Out); welcomeTween.start(); var spaceKey = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR); spaceKey.onDown.addOnce(this.startGame, this); }, startGame: function() { game.state.start('game'); } } |
W tym stanie używam tylko metody create oraz zdefiniowanej przeze mnie startGame. W metodzie create najpierw dodaje do gry obrazek menu głównego. Robię to za pomocą metody image, pola add głównego obiektu gry. image przyjmuje trzy argumenty, współrzędne w których znajdować się ma lewy górny róg obrazka oraz klucz do odpowiedniego pliku graficznego.
Następnie metodą text pola add dodaję do gry tekst. Tworząca go metoda przyjmuje 4 argumenty. Pierwsze dwa do współrzędne na których ma się pokazać. Trzeci argument do zawartość tekstu a czwarty to obiekt ze stylami dla niego. Jak widać napis jest początkowo umieszczony nad planszą z grą (ujemna wartość wysokości). To dlatego, że pojawi się dopiero po chwili a to dzięki mechanizmowi tween, z którego korzystam w kolejnych linijkach.
Tween to coś jak animate w jQuery. Mechanizm ten przypisywany jest do jednego z elementów gry. przyjmuje dane o wartość końcowej jakiejś właściwości tego elementu oraz w jakim czasie obiekt ma ją osiągnąć. zmiana jest animowana. W kodzie powyżej widać jak to zrobiłem. Metoda start uruchamia tę animacje.
Ostatnie dwie linijki kodu metody create to kontrola, przycisku spacji. Gdy zostanie ona naciśnięta program wywołuje metodę startGame, która z kolei zmienia stan na game, czyli główny stan gry.
Stan game jest najdłuższy z trójki stanów składających się na grę i jemu poświęcę osobny post. Myślę jednak, że już teraz widać jak łatwo dzięki phaserowi, zarządzać zasobami gry i dodawać do niej całkiem zaawansowane efekty.
Na koniec, jak zwykle, zachęcam do polubienia mojej strony na facebooku. Nowe posty już wkrótce 🙂 .