W ostatniej grze, którą zbudowałem przy użyciu phasera, próbowałem podzielić kod na moduły. Mógłbym je dzięki temu z łatwością łączyć i uniknąć powtarzalności kodu. Niestety poległem okrutnie. Do teraz śni mi się po nocach ten straszny, przerośnięty i nieczytelny główny stan gry.
Obiecałem sobie, że to się więcej nie powtórzy. Dlatego kolejnym krokiem podczas tworzenia mojej nowej platformówki, było wprowadzenie modularyzacji kodu. I tym razem mi się udało 🙂 .
Opisywaną w tym poście wersję gry sprawdzić można klikając w obrazek powyżej. Aktualizowany na bieżąco kod dostępny jest na moim koncie github. W razie czego, zawsze możesz zajrzeć w źródło strony 🙂 .
Nowości w kodzie nie ma zbyt wiele. Główne zmiany polegają na rozdzieleniu go na osobne pliki. Zacznę może od tego jak są one podpięte do HTMLa. Nie korzystam tutaj z żadnego automatycznego czary mary, po prostu w nagłówku dokumentu dodaję (ręcznie) kolejne znaczniki script. Nie jest to może idealne podejście, tym bardziej, że tych znaczników będzie sporo, ale w tym projekcie musi wystarczyć. Na pewno jest to coś co zaadresuję w kolejnych grach. Oto jakie pliki podłączem w index.html (reszta pliku wygląda tak jak ostatnio):
1 2 3 4 5 6 7 8 9 |
<script type="text/javascript" src="http://greeboro.linuxpl.eu/apps/Phaser/phaser.min.js"></script> <script type="text/javascript" src="states/init.js"></script> <script type="text/javascript" src="config.js"></script> <script type="text/javascript" src="actors/plasma.js"></script> <script type="text/javascript" src="actors/robot.js"></script> <script type="text/javascript" src="actors/exit.js"></script> <script type="text/javascript" src="states/level.js"></script> <script type="text/javascript" src="states/level2.js"></script> <script type="text/javascript" src="game.js"></script> |
Oczywiście kolejność ustawienia plików ma znaczenie. Nie będę już tego zaznaczał dalej, ale patrząc od góry, żaden plik nie może występować wcześniej niż plik, który zawiera dane mu potrzebne.
Pierwszy plik, to oczywiście kod źródłowy frameworka. Jest on oczywiście potrzebny reszcie kodu 🙂 . Kolejna pozycja to init.js:
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 |
var Robot = {} Robot.init = { preload: function() { game.load.image('tiles', 'GFX/tiles.png'); game.load.image('plasmaShot', 'GFX/plasmaShot.png'); game.load.image('exit', 'GFX/exit.png'); game.load.audio('plasma', 'SFX/plasma.wav'); game.load.image('pParticle', 'GFX/plasmaShotParticle.png'); game.load.tilemap('level1', 'levels/level.json', null, Phaser.Tilemap.TILED_JSON); game.load.tilemap('level2', 'levels/level2.json', null, Phaser.Tilemap.TILED_JSON); 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.cursor = game.input.keyboard.createCursorKeys(); game.spaceKey = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR); game.currTime; game.levels = ['level1','level2']; game.currLevel = 0; game.state.start(game.levels[game.currLevel]); } } |
Na początek tworzę zmienną Robot, to będzie mój główny namespace. Następnie przypisuję do niego pierwszy stan czyli init. Tutaj nie dzieje się nic nadzwyczajnego, najpierw w metodzie preload ładuję wymagane assety a następnie, wewnątrz create ustawiam zmienne konfiguracyjne, które przypisuję do głównego obiektu game. Najważniejsze to levels, tablica zawierająca spis kluczy do stanów poziomów oraz zmienna currLevel, która zawiera liczbę równą indeksowi aktualnego poziomu w tablicy levels. Wrzuciłem tu parę linijek kodu, które wcześniej znajdowały się w stanie gry a nie musiały 🙂 teraz każdy poziom będzie miał swój stan, bez sensu byłoby w każdym z nich włączać fizykę czy ustawiać referencje do przycisków, skoro mogę zrobić to teraz.
Kolejny plik dodany w index.html to config.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
Robot.config = { setMap: function(game,mapData){ var map = game.add.tilemap(mapData.tileMap); map.addTilesetImage(mapData.tileImage); map.setCollision(1); return map }, setLayer: function(map,name){ var layer = map.createLayer(name); layer.resizeWorld(); return layer }, checkExit: function(robot,exit){ var boundsR = robot.getBounds(); var boundsE = exit.getBounds(); if (Phaser.Rectangle.intersects(boundsR, boundsE)){ game.currLevel++; if(game.currLevel === game.levels.length){ game.currLevel = 0; } game.state.start(game.levels[game.currLevel]); } }, } |
Nie jestem pewny, czy dobrze nazwałem ten moduł, ale to najwyżej się zmieni. Przypisuję go oczywiście do głównego namespace’a gry. Wewnątrz znajdują się funkcje pomocnicze, pierwsze dwie, na podstawie przekazanych w argumentach danych, włączają mapę oraz jej warstwę kolizyjną.
Trzecia funkcja, sprawdza kolizje pomiędzy robotem a wyjściem z poziomu. Referencje do tych dwóch obiektów, przekazywane są jako parametry. Jeżeli się zetkną zmieniana jest wartość game.currLevel i włączany jest stan z listy game.levels o indeksie równym nowemu currLevel.
Kolejne pliki dodane do index.html to obiekty gry: plasma, robot oraz exit. Pierwszy obsługuję strzały robota, drugi samego robota a trzeci wyjścia z poziomu. Wszystkie działają na podobnej zasadzie, jako przykład zaprezentuję plik robot.js:
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 |
Robot.createRobot = function(game,x,y,plasma){ var robot = game.add.sprite(x, y, 'robot'); game.physics.arcade.enable(robot); robot.attacks = { 'plasma': plasma, } robot.currAttack = 'plasma'; robot.body.setSize(55, 100, 25, 20); robot.body.gravity.y = 800; robot.body.gravity.y = 800; robot.facingLeft = false; robot.lastShot = 0; robot.animations.add('walkRight', [0,1,2,3,4,5], 8, true); robot.animations.add('walkLeft', [6,7,8,9,10,11], 8, true); robot.moveRobot = function(cursor,space){ if (cursor.left.isDown) { this.body.velocity.x = -200; if(this.body.onFloor()){ this.animations.play('walkLeft'); } else { this.frame = 6; } this.facingLeft = true; } else if (cursor.right.isDown) { this.body.velocity.x = 200; if(this.body.onFloor()){ this.animations.play('walkRight'); } else { this.frame = 0; } this.facingLeft = false; } else { this.body.velocity.x = 0; this.animations.stop(); if(this.facingLeft){ this.frame = 6; } else { this.frame = 0; } } if (cursor.up.isDown && this.body.onFloor()) { this.body.velocity.y = -550; } if (space.isDown){ if(game.currTime - this.lastShot > this.attacks[this.currAttack].coolDown){ this.lastShot = game.currTime; this.attacks[this.currAttack].shoot(this.x,this.y,this.width,this.height, this.facingLeft); } } }; return robot; } |
Wewnątrz pliku, do głównego namespacea przypisuję funkcję-fabrykę createRobot. Przyjmuje ona cztery argumenty. Pierwszy to obiekt gry phasera, jest mi potrzebny aby wywoływać metody frameworka. Kolejne dwa argumenty to po prostu początkowe współrzędne postaci gracza. Ostatni argument to referencja do obiektu plazmy. Przyda się aby robot mógł strzelać 🙂
Wewnątrz funkcji tworzę obiekt robota dokładnie tak samo jak robiłem to do tej pory, nic się tutaj nie zmienia. Gdy wszystko jest już gotowe, zwracam przyszykowany obiekt. Dzięki tej konstrukcji mogę szybko i bez duplikowania sporej ilości kodu, stworzyć nowy obiekt robota w każdym nowym stanie.
Kolejne dwa pliki dodane do index.html to level.js oraz level2.js. Są to oczywiście stany poziomów gry. W tej chwili są one do siebie bardzo podobne, więc pokażę tylko ten pierwszy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Robot.level1 = { create: function() { this.map = Robot.config.setMap(game,{'tileMap':'level1','tileImage':'tiles'}) this.layer = Robot.config.setLayer(this.map,'Tile Layer 1') this.plasmaShots = Robot.createPlasma(game); this.robot = Robot.createRobot(game,40,510,this.plasmaShots); this.exit = Robot.createExit(game,992,64); game.camera.follow(this.robot, Phaser.Camera.FOLLOW_LOCKON); }, update: function() { game.currTime = this.game.time.now; game.physics.arcade.collide(this.robot, this.layer); this.robot.moveRobot(game.cursor,game.spaceKey); this.plasmaShots.update(this.layer); Robot.config.checkExit(this.robot,this.exit) }, } |
Prawda, że jest bardzo zwięzły i zgrabny? 🙂 w metodzie create tworzę nową mapę i obiekty gry używając zdefiniowanych wcześniej funkcji pomocniczych i fabryk. Wewnątrz update również używam aktualizujących metod poszczególnych obiektów. Wystarczy porównać to z poprzednią wersją stanu, od razu widać różnicę.
I to wszystko, kiedy cały kod jest już przygotowany, mogę całość zainicjalizować wewnątrz pliku game.js:
1 2 3 4 5 6 |
var game = new Phaser.Game(952, 736, Phaser.AUTO, 'game'); game.state.add('init', Robot.init); game.state.add('level1', Robot.level1); game.state.add('level2', Robot.level2); game.state.start('init',true,true); |
Tutaj nie zmieniło się nic, nie licząc tego, że stany wywołuję z wnętrza mojego namespace.
Teraz kiedy gra jest dużo lepiej zmodularyzowana, dodawanie nowych obiektów do gry będzie znacznie prostsze. Moim kolejnym krokiem będzie dodanie właśnie takich elementów 🙂
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.