Żeby uniknąć gonitwy na ostatnią chwilę, już dziś zabrałem się za grę kwietnia. Tym razem ponownie korzystam z Phasera, framework ma spory potencjał i chcę poznać go lepiej. Wspomogłem się też grafikami ze starego warcrafta, które znalazłem tu (jeżeli czyta to ktoś z Blizzarda, proszę nie pozywajcie mnie 🙁 ).
Docelowo kwietniowa gra ma być czymś na kształt klasycznego Commando, ale w klimacie fantasy 🙂 .
Obecnie gra jest w bardzo wczesnym stadium rozwoju. Można ją odpalić klikając w obrazek powyżej, jednak póki co w nie dzieje się w niej zbyt wiele. Na prowizorycznej planszy znajduje się tylko postać bohatera. Można nim sterować przy pomocy strzałek. Po naciśnięciu spacji postać zamachnie się mieczem, ale prócz animacji, atak nie ma żadnych efektów. Przygotowałem też paczkę z aktualnym kodem gry. Należy pamiętać o tym, żeby gre uruchomić na lokalnym serwerze, inaczej Phaser nie zadziała.
Tym razem skupiłem się na tym aby w miarę rozsądnie rozłożyć kod projektu. Nie chcę tak jak ostatnio upychać wszystkiego w jeden plik. Udało mi się podzielić kod ale nie jestem jeszcze do końca zadowolony z efektu. Brak dogłębnej wiedzy na temat działania Phasera wciąż trochę mnie ogranicza.
Projekt składa się obecnie z czterech plików zawierających kod: index.html, game.js, init.js oraz level1.js. Pierwszy plik to po prostu kod html wyświetlany w przeglądarce. Jest on tym razem dość ważny ze względu na to jak dodaje w nim skrypty JS. game.js jest punktem wyjściowym dla całej aplikacji, to w nim uruchamiam grę. Dwa ostatnie pliki zawierają logikę wczesnych wersji stanów gry.
Zacznę od treści pliku HTML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>www.jsdn.pl js.n00b - Fantasy Commando</title> <script type="text/javascript" src="http://greeboro.linuxpl.eu/apps/Phaser/phaser.min.js"></script> <script type="text/javascript" src="init.js"></script> <script type="text/javascript" src="level1.js"></script> <script type="text/javascript" src="game.js"></script> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, minimal-ui" /> <style type="text/css"> body{ padding:0px; margin:0px; } </style> </head> <body> <div id="game"> </div> </body> </html> |
Chcę tu przede wszystkim zwrócić uwagę na kolejność tagów script. Aby wszystko działało, muszę dodawać skrypty w odpowiedniej kolejności. Zasada jest taka, żeby dodawać je tak aby wymagane w nich elementy były dodane wcześniej. Każdy plik wymaga Phasera, dlatego kod frameworka dodaję najpierw. Następnie dołączam pliki zawierające stany gry, a na końcu game.js, które to wszystko wywołuje.
Oto jak wygląda treść game.js:
1 2 3 4 5 |
var game = new Phaser.Game(510, 510, Phaser.AUTO, 'game'); game.state.add('init', FC.init); game.state.add('level1', FC.level1); game.state.start('init'); |
Jak widać, nie dzieje się tu nic nadzwyczajnego. Najpierw tworzę nową instancję gry Phasera, następnie dodaję do niej dwa stan. Na koniec odpalam stan init. Warto zwrócić uwagę na przekazywane obiekty stanów. Znajdują się one wewnątrz obiektu FC, który pełni rolę paczki / przestrzeni nazw. W końcu nie chcę zaśmiecać przestrzeni globalnej. Powyższy kod ma dostęp do kodu stanów, ponieważ jest dodany na stronę po nich.
Pierwszy uruchamiany stan to init:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var FC = {}; FC.init = { preload: function() { game.load.spritesheet('hero', 'GFX/hero.png', 32, 32); game.load.image('tile', 'GFX/tile.png'); }, create: function() { game.scale.pageAlignHorizontally = true; game.scale.pageAlignVertically = true; game.scale.refresh(); game.physics.startSystem(Phaser.Physics.ARCADE); game.state.start('level1'); } } |
Jest to pierwszy plik dodany do HTMLa po kodzie frameworka. To właśnie w nim tworzę pusty obiekt FC. Kod we wszystkich plikach, które dodam później, będzie miał dostęp do tej przestrzeni nazw.
Po za tym jest to standardowy stan Phaser. Korzystam z metody preload żeby załadować grafikę oraz z metody create aby wycentrować płótno na stronie. Włączam też silnik fizyki ARCADE.
Nowością jest obiekt grafiki hero. Zamiast metody load, używam metody spriteSheet. W ten sposób daje Phaserowi znać, że dodany obrazek jest arkuszem klatek dla animacji obiektu a nie pojedyncza grafiką. Arkusz zawiera wszystkie klatki ruchu bohatera gry. Rozmiar jednej klatki podaję jako dwa ostatnie parametry metody.
Gdy wszystko jest gotowe, program przechodzi w stan level1:
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
FC.level1 = { create: function() { this.spacePressed = false; this.cursor = game.input.keyboard.createCursorKeys(); this.spaceKey = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR); this.background = game.add.tileSprite(0, 0, 510, 510, "tile"); this.hero = game.add.sprite(250, 170, 'hero'); this.hero.animations.add('left', [30,31,32,33,34], 6, true); this.hero.animations.add('right', [10,11,12,13,14], 6, true); this.hero.animations.add('up', [0,1,2,3,4], 6, true); this.hero.animations.add('down', [20,21,22,23,24], 6, true); this.hero.animations.add('leftUp', [25,26,27,28,29], 6, true); this.hero.animations.add('rightUp', [5,6,7,8,9], 6, true); this.hero.animations.add('leftDown', [35,36,37,38,39], 6, true); this.hero.animations.add('rightDown', [15,16,17,18,19], 6, true); this.hero.animations.add('Aleft', [65,66,67,68,69], 6, false); this.hero.animations.add('Aright', [45,46,47,48,49], 6, false); this.hero.animations.add('Aup', [40,41,42,43,44], 6, false); this.hero.animations.add('Adown', [60,61,62,63,64], 6, false); this.hero.animations.add('AleftUp', [25,26,27,28,29], 6, false); this.hero.animations.add('ArightUp', [75,76,77,78,79], 6, false); this.hero.animations.add('AleftDown', [70,71,72,73,74], 6, false); this.hero.animations.add('ArightDown', [50,51,52,53,54], 6, false); this.hero.frame = 20; this.hero.facing = 5; game.physics.arcade.enable(this.hero); }, update: function() { this.moveHero(); this.heroAttack(); }, moveHero: function() { if (this.cursor.left.isDown && !this.cursor.right.isDown && !this.cursor.up.isDown && !this.cursor.down.isDown) { this.hero.body.velocity.x = -50; this.hero.body.velocity.y = 0; this.hero.facing = 7; this.hero.animations.play('left'); } if (!this.cursor.left.isDown && this.cursor.right.isDown && !this.cursor.up.isDown && !this.cursor.down.isDown) { this.hero.body.velocity.x = 50; this.hero.body.velocity.y = 0; this.hero.facing = 3; this.hero.animations.play('right'); } if (!this.cursor.left.isDown && !this.cursor.right.isDown && this.cursor.up.isDown && !this.cursor.down.isDown) { this.hero.body.velocity.y = -50; this.hero.body.velocity.x = 0; this.hero.facing = 1; this.hero.animations.play('up'); } if (!this.cursor.left.isDown && !this.cursor.right.isDown && !this.cursor.up.isDown && this.cursor.down.isDown) { this.hero.body.velocity.y = 50; this.hero.body.velocity.x = 0; this.hero.facing = 5; this.hero.animations.play('down'); } if (this.cursor.left.isDown && !this.cursor.right.isDown && this.cursor.up.isDown && !this.cursor.down.isDown) { this.hero.body.velocity.y = -50; this.hero.body.velocity.x = -50; this.hero.facing = 8; this.hero.animations.play('leftUp'); } if (this.cursor.left.isDown && !this.cursor.right.isDown && !this.cursor.up.isDown && this.cursor.down.isDown) { this.hero.body.velocity.y = 50; this.hero.body.velocity.x = -50; this.hero.facing = 6; this.hero.animations.play('leftDown'); } if (!this.cursor.left.isDown && this.cursor.right.isDown && this.cursor.up.isDown && !this.cursor.down.isDown) { this.hero.body.velocity.y = -50; this.hero.body.velocity.x = 50; this.hero.facing = 2; this.hero.animations.play('rightUp'); } if (!this.cursor.left.isDown && this.cursor.right.isDown && !this.cursor.up.isDown && this.cursor.down.isDown) { this.hero.body.velocity.y = 50; this.hero.body.velocity.x = 50; this.hero.facing = 4; this.hero.animations.play('rightDown'); } if (!this.cursor.left.isDown && !this.cursor.right.isDown && !this.cursor.up.isDown && !this.cursor.down.isDown) { this.hero.body.velocity.x = 0; this.hero.body.velocity.y = 0; if(this.hero.frame < 40){ this.hero.frame = this.hero.frame-(this.hero.frame%5); } } }, heroAttack: function(){ if (this.spaceKey.isDown && !this.spacePressed){ this.spacePressed = true; switch (this.hero.facing) { case 1: this.hero.animations.play('Aup') break; case 2: this.hero.animations.play('ArightUp') break; case 3: this.hero.animations.play('Aright') break; case 4: this.hero.animations.play('ArightDown') break; case 5: this.hero.animations.play('Adown') break; case 6: this.hero.animations.play('AleftDown') break; case 7: this.hero.animations.play('Aleft') break; case 8: this.hero.animations.play('AleftUp') break; default: } } else { this.spacePressed = false; } }, } |
Jest to obecnie zdecydowanie najobszerniejszy plik. No i tu pojawia się pierwszy ból, bo jakieś 90% tego pliku to kod obsługujący obiekt bohatera. Prawda jest taka, że będzie on też potrzebny w stanach reprezentujących kolejne poziomy gry. Nie chcę przecież za każdym razem kopiować te same 40 – 50 linijek kodu… Póki co nie wiem jak sobie z tym poradzić, ale główkuję 😉 . Obiekt gracza musi wylądować w osobnym pliku, ale czy będzie dobrze funkcjonował poza stanem? Muszę to sprawdzić…
Ale póki co skupię się na tym co już mam. Większość kodu ostatniego stanu powinna być jasna. Nowością jest kod obsługujący animację obiektów. Obiekt bohatera korzysta z grafiki oznaczonej jako arkusz klatek. Phaser w tle, dzieli taki arkusz na pojedyncze obrazki. Każdy z tych obrazków ma wymiary podane podczas inicjalizacji arkusza. Każda klatka ma swój indeks, zupełnie jak w tablicy. Co najważniejsze, z jednego arkusza mogę stworzyć wiele animacji. Ten arkusz na przykład zawiera animacje ruchu w lewo, ruchu w prawo, ataku w dół, ataku w górę itp.
Animacje muszę jednak najpierw zadeklarować i odpowiednio oznaczyć. Robię to w metodzie create stanu. Oto przykład
1 |
this.hero.animations.add('leftUp', [25,26,27,28,29], 6, true); |
Po prostu na obiekcie bohatera wywołuję odpowiednie metody. Najważniejsze są tutaj argumenty. Pierwszy z nich to etykieta dla danej animacji. Drugi to tablica z numerami klatek, składającymi się na daną animacje. Na arkuszu klatka o indeksie zero znajduje się w górnym lewym rogu, następne liczone są na prawo i w dół. Kolejny argument to częstotliwość zmiany klatki na sekundę. Im mniejsza wartość tym wolniej będzie następować będzie zmiana klatki. Ostatni argument to wartość logiczna. true oznacza, że animacja ma się zapętlać, po ostatniej klatce przechodzi od razu do pierwszej i tak w kółko.
Animacje ruchu są zapętlone, ale animacje ataku już nie. Dzięki temu wykonuje jeden zamach mieczem i spokojnie czeka na dalsze ‚rozkazy’.
Co ciekawe mogę w każdej chwili ustawić na obiekcie bohatera dowolną klatkę. Wystarcz, że odwołam się do jego pola frame. Oto przykład:
1 |
this.hero.frame = 20; |
W taki sam sposób mogę też pobierać wartość aktualnej klatki. Wszystko to sprawia, że obsługa animacji jest bardzo wygodna.
Nie będę dokładnie tłumaczył reszty kodu. Większość powinna być zrozumiała dla każdego kto zapoznał się z opisami mojej poprzedniej gry stworzonej z pomocą phasera. Jeżeli jednak coś jest nie jasne, nie wahajcie się pytać w komentarzach. Na wszystkie pytania odpowiem.
Dobrym miejscem na kontakt ze mną jest też moja strona na facebooku. Warto ją też polubić aby być na bieżąco, zawsze zamieszczam tam informacje o wszystkich nowościach na blogu 🙂 .
Z którego Warcrafta to rycerzyk, bo chyba nie z 2? 😀 Pewnie z 1. Zauważyłem, że używasz StateManager… zapominam ile Phaser ma rzeczy usprawniających pracę…
W każdym razie jestem ciekaw, co wyjdzie Ci w tym miesiącu.
Tak, to z pierwszego warcrafta. Grafika należy do Lothara Anduina, była unikatowa, on występował chyba tylko w jednej misji 🙂
Co do Phasera, ma naprawdę duże możliwości. Zdecydowanie, bardzo potężny framework.