W ostatnim poście przedstawiłem podstawowy setup mojej październikowej gry. Tym razem gracz wcieli się w rolę brawurowego awanturnika, który w poszukiwaniu potęgi i chwały zapuścił się w odmęty przeklętych podziemi leżących na granicy świata żywych i umarłych. Niestety miejsce to okazało się niebezpieczną pułapką. Jeżeli nie uda mu się uciec na czas, zostanie w nim uwięziony na zawsze 🙂 .
Całkiem nieźle znam już Phasera, jednak ponieważ używam TypeScriptu zamiast zwykłego JSa, pewne konstrukcje w grze będą wyglądać trochę inaczej. Na szczęście różnic jest niewiele, i są one raczej na plus. W dzisiejszym poście przedstawię implementację podstawowych elementów gry w nowym środowisku, dzięki temu przejście na TSa powinno być bezbolesne.
Aktualną wersję gry przetestować można klikając w obrazek powyżej. Na moim githubie znajduje się repozytorium zawierające jak najaktualniejszy stan gry. Wersja przedstawiona w poście nie będzie dostępna w źródle strony. To czego używa przeglądarka to przetranspilowany na JS kod. Wersję TypeScriptową zobaczyć można ściągając tę paczkę.
Bez owijania w bawełnę, przejdę od razu do kodu. Na pierwszy ogień idzie zawartość pliku app.ts, w którym znajduje się kod, uruchamiający całą grę. Oto on:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
module Necropolis { export class NecropolisGame { game: Phaser.Game; constructor() { this.game = new Phaser.Game(1024, 768, Phaser.AUTO, 'game', { create: this.create, preload: this.preload}) } preload() { this.game.load.atlasJSONHash('gameSheet', '/Assets/necropolis.png', '/Assets/necropolis.json'); this.game.load.image("tiles", "/Assets/spr_wall_0.png"); this.game.load.tilemap("tileMap", "/Assets/tiles.json", null, Phaser.Tilemap.TILED_JSON); } create() { this.game.state.add('welcomeState', Necropolis.welcomeState, true); this.game.state.add('gameState', Necropolis.gameState); } } } window.onload = () => { var game = new Necropolis.NecropolisGame(); }; |
Jak widać, plik nie jest specjalnie duży. Pierwsza rzecz która rzuca się w oczy to moduł Necropolis. Nie pisałem jeszcze na blogu o tym mechanizmie TSa. Jest to coś na kształt przestrzeni nazw, która segreguje kod. Póki co nie będę się o tym rozpisywał. Ważne jest to, że tylko te elementy modułu są dostępne po za nim, które mają przed swoją nazwą operator export.
W tej instancji modułu, jest tylko jeden element, klasa NecropolisGame, która jest eksportowana na zewnątrz. Ta klasa to właściwie cała gra. Posiada ona jeden główny element – obiekt game, który jest typu Phaser.Game. W konstruktorze klasy do game przypisuję instancję phaserowego obiektu gry, ten element powinien wyglądać znajomo. Jedyny mechanizm, którego wcześniej nie stosowałem, to obiekt, podany na końcu argumentów. W tym obiekcie mogę przypisać funkcje do metod preload oraz create. Zostaną one wywołane gdy tworzona będzie nowa instancja gry. Właściwie nie są potrzebne obie, ale lepiej jest rozdzielić ładowanie elementów gry od uruchamiania jej logiki.
Wewnątrz metody preload najpierw dodaję do gry spritesheet z grafikami oraz opisujący go plik JSON. W tym celu używam metody load.atlasJSONHash. Następnie ładuję grafikę oraz dane związane z mapą wygenerowaną w tiled.
Metoda create inicjuje dwa istniejące póki co w grze stany. Używając state.add dodaję stany do gry. Pierwszy argument state.add to klucz jaki otrzyma stan. Drugi to referencja do klasy stanu. Jak widać oba moje stany także znajdują się w module Necropolis. Do tego stan welcomeState posiada trzecie argument o wartości true. Oznacza to, że będzie to pierwszy uruchomiony w grze stan.
Przejdę może do jego kodu który znajduje się w pliku welcomeState.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
module Necropolis { export class welcomeState extends Phaser.State { bg: Phaser.Sprite; welcomeText: Phaser.Text; SPACE: Phaser.Key; create() { this.bg = this.game.add.sprite(0, 0, 'gameSheet', 'bg_main.png'); this.welcomeText = this.game.add.text(370, 200, 'welcome to Necropolis', { 'color': 'black' }); this.SPACE = this.game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR); this.SPACE.onDown.add(this.startGame, this) } startGame() { this.game.state.start('gameState'); } } }; |
welcomeState to bardzo prosty stan ale warto zwrócić uwagę na to jak jest skonstruowany. Cały stan to klasa która rozszerza phaserową klasę State. Dzięki temu wewnątrz mojej klasy mam dostępne wszystkie ważne phaserowe metody stanu. Przede wszystkim create oraz update. Ważne też jest to, że wewnątrz tej klasy mam bezpośredni dostęp do instancji gry, która znajduje się pod this.game, nie muszę jej nigdzie deklarować.
Deklaruję za to trzy inne elementy, każdy z nich otrzymuję odpowiedni typ phaserowego obiektu. Nawet nie będę pisał jak dobrze mieć świadomość, który element do czego konkretnie ma służyć. Uwielbiam TypeScript 🙂 . Wewnątrz metody create przypisuję wartości do zadeklarowanych wcześniej pól, co zarazem dodaje je do gry. Pierwszy element to bg, czyli tło gry. Otrzymuje ona cztery parametry. Pierwsze dwa to współrzędne. Trzeci parametr to klucz do grafiki spritesheeta zadeklarowanego w preload. Ostatni parametr to nazwa obrazka, który ma być użyty. Nazwa ta zdefiniowana jest w pliku necropolis.json, wygenerowanym przez TexturePacker’a.
Kolejny element to welcomeText, czyli zwykły tekst wypisywany na obszarze gry. Nie ma tu nic nadzwyczajnego. Następnie do pola SPACE przypisuję phaserową referencję do spacji. Następnie do eventu onDown zadeklarowanej przed chwilą spacji przypisuję funkcję startGame, która rozpoczyna kolejny stan gameState.
Wystarczy na ekranie powitalnym wcisnąć spację aby przejść do gry. Uruchamiany stan znajduje się wewnątrz pliku gameState.ts a oto jego treść:
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 |
module Necropolis { export class gameState extends Phaser.State { bg: Phaser.Sprite; player: Phaser.Sprite; map: Phaser.Tilemap; walls: Phaser.TilemapLayer; create() { this.game.physics.startSystem(Phaser.Physics.ARCADE); this.bg = this.game.add.sprite(0, 0, 'gameSheet', 'bg_main.png'); this.map = this.game.add.tilemap('tileMap') this.map.addTilesetImage('tiles'); this.walls = this.map.createLayer('layer1'); this.map.setCollision(1); this.walls.resizeWorld(); this.player = new Player(this.game, 40, 40); this.game.physics.enable(this.player); this.game.add.existing(this.player); } update() { this.game.physics.arcade.collide(this.player, this.walls); } } }; |
Stan ten jest trochę obszerniejszy, ale znajduje się w nim znacznie więcej znajomych elementów. Ten stan również deklaruję jako klase rozszerzającą odpowiednią klasę phasera. Również jest on częścią modułu Necropolis.
Tym razem deklaruję cztery elementy: tło, obiekt gracza, mapę tiled, oraz jej główną warstwę przedstawiającą ściany podziemi. Klasa posiada dwie metody create oraz update. Są to klasyczne elementy stanów phasera, nie muszę chyba tłumaczyć jak działają.
Wewnątrz create do odpowiednich obiektów przypisuję wszystkie potrzebne wartości. Włączam również silnik fizyki, aby móc korzystać z phaserowych kolizji. Dodawanie mapy oraz warstw wygląda tak samo jak w zwykłej phaserowej aplikacji. Warto zwrócić uwagę, że obiekt gracza dodaję z osobnej klasy Player, którą definiuję w innym pliku. Samo wywołanie konstruktora nie doda obiektu do gry, muszę wywołać metodę add.existing aby postać była widoczna w grze.
W metodzie update sprawdzam kolizję pomiędzy graczem a warstwą ścian.
I to cały stan, ostatni element który dziś opiszę to klasa reprezentująca gracza. Jej kod znajduje się w pliku player.ts:
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 |
module Necropolis { export class Player extends Phaser.Sprite { RIGHT: Phaser.Key; LEFT: Phaser.Key; UP: Phaser.Key; DOWN: Phaser.Key; constructor(game: Phaser.Game, x: number, y: number) { super(game, x, y, 'gameSheet', 'spr_Player_0.png') this.RIGHT = this.game.input.keyboard.addKey(Phaser.Keyboard.RIGHT); this.RIGHT.onDown.add(this.movePlayer, this, 0, 'right'); this.RIGHT.onUp.add(function () { this.body.velocity.x = 0; }, this); this.LEFT = this.game.input.keyboard.addKey(Phaser.Keyboard.LEFT); this.LEFT.onDown.add(this.movePlayer, this, 0, 'left'); this.LEFT.onUp.add(function () { this.body.velocity.x = 0; }, this); this.UP = this.game.input.keyboard.addKey(Phaser.Keyboard.UP); this.UP.onDown.add(this.movePlayer, this, 0, 'up'); this.UP.onUp.add(function () { this.body.velocity.y = 0; }, this); this.DOWN = this.game.input.keyboard.addKey(Phaser.Keyboard.DOWN); this.DOWN.onDown.add(this.movePlayer, this, 0, 'down'); this.DOWN.onUp.add(function () { this.body.velocity.y = 0; }, this); } movePlayer() { switch (arguments[1]) { case 'right': this.body.velocity.y = 0; this.body.velocity.x = 75; this.loadTexture('gameSheet', 'spr_Player_2.png') break; case 'left': this.body.velocity.y = 0; this.body.velocity.x = -75; this.loadTexture('gameSheet', 'spr_Player_3.png') break; case 'up': this.body.velocity.x = 0; this.body.velocity.y = -75; this.loadTexture('gameSheet', 'spr_Player_1.png') break; case 'down': this.body.velocity.x = 0; this.body.velocity.y = 75; this.loadTexture('gameSheet', 'spr_Player_0.png') break; } }; } } |
Klasa Player również przypisana jest do modułu Necropolis. Rozszerza ona klasę Phaser.Sprite, czyli tą dzięki której można dodawać obiekty do gry. Aby wszystko działało jak należy wewnątrz konstruktora muszę wywołać metodę super czyli konstruktor klasy nadrzędnej, w ten sposób stworzony zostanie Sprite jak w grze JSowej, który mogę dalej modyfikować wewnątrz konstruktora. Wywołanie super otrzymuje argumenty przekazane do konstruktora Player. Są to: referencja do obiektu gry, współrzędna x oraz współrzędna y. Do tego na koniec dodaję klucz do obrazka ze spritesheeta.
Wewnątrz konstruktora przypisuję również funkcje do klawiszy strzałek. Są one bardzo proste. Gdy przyciśnięta zostanie któraś ze strzałek obiekt otrzymuje prędkość poruszającą go w odpowiednim kierunku. Ponieważ gracz będzie mógł poruszać się tylko poziomo i pionowo, prędkość przeciwna jest zerowana.
I to cały obiekt player a zarazem cały aktualny stan gry. Tak jak pisałem na początku, nie mam zbyt wiele, ale ponieważ korzystam z nowego języka, nie chciałem wprowadzać zbyt wiele rzeczy na raz. Prawda jednak jest taka, że jeśli już wiem jak dodawać stany oraz interaktywne spritey, to mogę w Phaser zrobić już całkiem sporo 🙂
W kolejnym poście pokażę trochę więcej mechaniki gry. Tymczasem jeżeli chcesz być na bieżąco z postami na blogu zachęcam do polubienia mojej strony na facebooku. Zawsze zamieszczam tam informacje o wszystkich nowościach. Jest to też dobre miejsce na kontakt ze mną. Na wszystkie pytania zawsze odpowiem :). Do przeczytania.