Ledwo skończyłem jedną grę a już zaczynam kolejną. Życie javascriptowego blogera nie jest łatwe 🙂 Tradycyjnie pierwszy wpis o grze miesiąca pojawia się w drugiej połowie tego miesiąca. Oj, muszę popracować nad dotrzymywaniem deadline’ów 😉
A co takiego mam przygotowane na czerwiec? Gra, którą chcę przedstawić, to platformówka o roboczym tytule Robot. Jeśli chodzi zaś o narzędzia, to wracam do frameworka Phaser. Jednak tworzenie gier z jego pomocą jest bardzo wygodne i pozwala oszczędzić sporo czasu.
Aktualna wersja gry dostępna jest po kliknięciu w obrazek powyżej. Powstało też nowe repo na moim githubie, w którym znajdować będzie się najaktualniejsza wersja kodu projektowego.
Zachęcam do studiowania źródła gry na własną rękę ponieważ nie wszystko będę w stanie opisać w postach. Jeśli zrozumienie jakiegoś aspektu mechanizmu gry, będzie sprawiało problemy, daj znać w komentarzach, postaram się odpowiedzieć na wszystkie pytania.
Zaznaczę jeszcze, że pierwsza wersja platformówki, którą prezentuję w tym poście to zaledwie szkic, struktura gry i kodu na pewno się zmieni. Póki co, chciałem sprawdzić jak wymagające będzie stworzenie podstawowych funkcjonalności gry platformowej z pomocą Phasera. Okazuje się, że można to zrobić bardzo łatwo, szybko i przyjemnie 🙂
W zaledwie kilka godzin udało mi się stworzyć obecną wersję gry. Zawiera ona prototyp poziomu i postać, która może poruszać się po platformach, skakać i strzelać. Jako smaczek dodałem efekt cząsteczek pojawiający się po strzeleniu w ścianę. Phaser to jednak bardzo potężny framework 🙂 .
Nie będę tu opisywał podstaw Phasera, więc jeżeli nie spotkałeś się wcześniej z tym frameworkiem, zachęcam do zapoznania się z moimi dwoma wcześniejszymi projektami stworzonymi z pomocą tego narzędzia. W postach ich dotyczących opisuje wszystkie podstawowe zagadnienia takie jak stany czy sprite’y.
Kod gry
Czas przejść do mięcha, czyli do kodu. Póki co wszystko wrzucone jest do jednego pliku game.js, W kolejnych aktualizacjach planuje rozdzielić to na kilka plików tak jak w przypadku fantasy commando. Na początek oczywiście deklaruję obiekt gry, przypisuję do niego stany i uruchamiam pierwszy z nich:
1 2 3 4 5 6 7 |
var game = new Phaser.Game(952, 736, Phaser.AUTO, 'game'); /*deklaracja stanów*/ game.state.add('init', init); game.state.add('level', level); game.state.start('init',true,true); |
Jak widać, póki co gra ma tylko dwa stany. Stan inicjalizacji i stan obsługujący poziom gry. Chciałbym aby tym razem każdy poziom był osobnym stanem. Zobaczymy czy uda mi się wykombinować jak to dobrze zrobić bez powtarzania zbyt dużej ilości kodu 😛
Ustawiłem większą rozdzielczość gry niż zwykle, mam nadzieję, że nie przekroczy to rozmiaru waszych monitorów. W razie czego dajcie znać w komentarzach. Można też użyć klawisza f11 aby schować kontrolki przeglądarki 🙂 .
Między deklaracją obiektu gry a przypisaniem stanów znajdują się deklaracje tych stanów, Pierwszy z nich to stan inicjalizujący grę.
stan init
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
var init = { preload: function() { game.load.tilemap('level', 'levels/level.json', null, Phaser.Tilemap.TILED_JSON); game.load.image('tiles', 'GFX/tiles.png'); game.load.image('plasmaShot', 'GFX/plasmaShot.png'); game.load.image('pParticle', 'GFX/plasmaShotParticle.png'); game.load.spritesheet('robot', 'GFX/robot.png', 102, 120); }, create: function() { game.stage.backgroundColor = '#3498db'; game.scale.pageAlignHorizontally = true; game.scale.pageAlignVertically = true; game.scale.refresh(); game.physics.startSystem(Phaser.Physics.ARCADE); game.state.start('level'); } }; |
Nie dzieje się tu nic zaskakującego. Najpierw w metodzie preload dodaję do gry wygenerowany przez tiled (<3) plik JSON. Okazuje się, że to narzędzie też świetnie nadaje się do budowania szkieletu poziomu w platformówkach. Następnie Dodaje grafiki dla kafelków mapy, cząsteczek, wystrzału oraz robota. Kolejna metoda, create, zawiera kod konfigurujący podstawowe ustawienia gry. Najważniejsze są dwie ostatnie linijki. W pierwszej włączam system fizyki ARCADE a w drugiej uruchamiam kolejny stan czyli level.
stan level
Pierwsza i chyba najobszerniejsza metoda w level to create. Ustawiam w niej wszystko potrzebne w obecnym stanie:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
create: function() { this.cursor = game.input.keyboard.createCursorKeys(); this.spaceKey = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR); this.map = game.add.tilemap('level'); this.map.addTilesetImage('tiles'); this.layer = this.map.createLayer('Tile Layer 1'); this.layer.resizeWorld(); this.map.setCollision(1); this.robot = game.add.sprite(40, 510, 'robot'); game.physics.arcade.enable(this.robot); this.robot.body.gravity.y = 800; this.robot.facingLeft = false; this.lastShot = 0; this.robot.animations.add('walkRight', [0,1,2,3,4,5], 8, true); this.robot.animations.add('walkLeft', [6,7,8,9,10,11], 8, true); game.camera.follow(this.robot, Phaser.Camera.FOLLOW_LOCKON); this.plasmaShots = this.game.add.group(); this.plasmaShots.enableBody = true; this.plasmaShots.physicsBodyType = Phaser.Physics.ARCADE; this.plasmaShots.createMultiple(20,'plasmaShot'); this.plasmaShots.setAll('outOfBoundsKill', true); this.plasmaShots.setAll('checkWorldBounds', true); this.emitter = game.add.emitter(0, 0, 100); this.emitter.makeParticles('pParticle'); this.emitter.gravity = 500; }, |
Większość tego kodu to standardowe dołączanie obiektów do gry. Najpierw deklaruję wskaźniki na potrzebne mi strzałki oraz spację. Następnie, na podstawie załączonego wcześniej pliku JSON dodaję do gry mapę i deklaruję warstwę kolizyjną (to będą platformy).
Kolejne kilka linijek kodu to deklaracja obiektu robota. Włączam na nim fizykę oraz przypisuję do niego wartość gravity. Dzięki temu będzie zawsze (o ile to możliwe) poruszał się w dół. Jeżeli zmienię wartość jego wektora Y na ujemną, będzie ona się zwiększać aż dojdzie do wyznaczonej tutaj (a przynajmniej tak myślę, że to działa, musiałbym zajrzeć w bebechy Phasera aby się upewnić 😛 ). Dla robota deklaruję również dwie animacje, ruchu w lewo i w prawo. Na koniec ustawiam kamerę gry tak aby była zawsze wycentrowana na naszego bohatera.
Po kodzie związanym z robotem, deklaruję phaserową grupę obiektów odpowiedzialną za pociski. Tutaj nie pojawią się żadna nowość.
Nowością za to są trzy ostatnie linijki, w których tworzę obiekt emitujący cząsteczki. Robię to za pomocą metody emitter pola add głównego obiektu gry. Przyjmuje on trzy parametry. pierwsze dwa to współrzędne w których mają się pojawić cząsteczki. Współrzędne te liczone są względem położenia emitera. Wartości zero i zero oznaczają, że cząsteczki pojawiać się będą w tym samym miejscu co emiter (który sam póki co, nie ma ustawionych współrzędnych). W drugiej linijce przypisuje sprite, który będzie przedstawiał cząsteczki. Co ciekawe nie musi to być pojedyncza grafika, może to być spritesheet. W ostatniej linijce przypisuje grawitacje cząsteczkom. Im wyższa wartość tym szybciej będą spadać w dół (powstanie efekt fontanny w przeciwieństwie do na przykład efektu sztucznych ogni).
Po metodzie create pojawia się metoda update, czyli pętla tego stanu:
1 2 3 4 5 6 |
update: function() { this.currTime = this.game.time.now; game.physics.arcade.collide(this.robot, this.layer); this.moveRobot(); this.updateShots(); }, |
W update, póki co, nie dzieje się zbyt wiele. Na początek zapisuję aktualny czas gry, przyda się później do sprawdzania czy minęła już jakiś określona liczba milisekund. Następnie sprawdzam kolizję pomiędzy robotem a platformami. Kolejne dwie linijki to wywołanie metod moveRobot oraz updateShots. Tej pierwszej nie będę cytował ani opisywał. Jest to zwyczajna standardowa funkcja odpowiedzialna za poruszanie sprite’a gdy zostanie przyciśnięty odpowiedni klawisz. Zaznaczę tylko, że po wciśnięciu spacji, odpalana jest metoda shoot. Jej też nie będę opisywał. Po prostu bierze ona pierwszy nieaktywny obiekt pocisku z grupy ustawia jego x i y mniej więcej na środku robota. Następnie zmienia jego wektor ruchu w osi x na określoną wartość, ujemna lub dodatnia zależnie od tego, w którą stronę patrzy robot. Co do updateShots, wygląda tak:
1 2 3 4 5 6 |
updateShots: function(){ game.physics.arcade.collide(this.plasmaShots, this.layer, function(shot){ this.particleBurst(shot.x,shot.y) shot.kill(); }, null, this); }, |
I tutaj nie ma żadnych zaskoczeń 🙂 Po prostu gdy wykryta zostanie kolizja pomiędzy pociskiem a ścianą, wywoływana jest metoda particleBurst a pocisk eliminowany jest z planszy.
I na koniec została właśnie metoda particleBurst:
1 2 3 4 5 |
particleBurst: function(x,y){ this.emitter.x = x; this.emitter.y = y; this.emitter.start(true, 300, null, 7); } |
Metoda ta przyjmuje dwa argumenty. Są to współrzędne, w których chcę aby pojawił się wybuch cząsteczek. Oczywiście, będą to współrzędne pocisku, w momencie gdy spotkał się ze ścianą 🙂 Przypisuję te wartości do odpowiednich wartości emitra i uruchamiam jego metodę start.
Metodzie start emitra przekazuję 4 argumenty. True w pierwszym z nich oznacza, że wszystkie cząsteczki mają pojawić się naraz. Wartość 300, to długość istnienia cząsteczki w grze w milisekundach. Nie wiem czym jest to null, ale tak było w przykładach w necie więc zostawiam :). Ostatni parametr – 7, to liczba cząsteczek jaka ma się pojawić. Musicie przyznać, że nie jest to specjalnie skomplikowane.
I to wszystko. Tak wygląda solidny prototyp platformówki stworzony z pomocą frameworka PhaserJS. Wszystko to w trochę ponad 100 linijek kodu (nie licząc treści pliku JSON z danymi o mapie). Kolejny krok to uporządkowanie tego kodu i stworzenie jakiejś namiastki modułów, która pozwoli mi swobodnie zmieniać stany reprezentujące poziomy. Ale to już materiał na kolejny post 🙂 .
Na koniec, jak zawsze, zachęcam do polubienia mojej strony na facebooku. Zawsze na bieżąco zamieszczam tam informacje o nowościach, więc warto polubić aby nie przegapić żadnego nowego wpisu.