Tiled map editor segítségével:
Hozzunk létre egy új fájlt a js/classes
mappában, spawner.js
néven, a következő tartalommal:
class Spawner { constructor(config, clock, spawnLocations, addObject, deleteObject) { this.id = config.id; this.spawnInterval = config.spawnInterval; this.limit = config.limit; this.objectType = config.objectType; this.clock = clock; this.spawnLocations = spawnLocations; this.addObject = addObject; this.deleteObject = deleteObject; this.objectsCreated = []; this.objectId = 1; } }
Ez az osztály fogja szabályozni a spawner-ek működését. Az osztály konstruktora a következő adatokat fogadja:
config
objektum:id
: a spawner azonosítója,spawnInterval
: objektum spawn-olás időköze,limit
: aktív objektumok maximális száma,objectType
: objektum típusa (MONSTER
vagy CHEST
)clock
: a megfelelő scene-hez tartozó Clock objektum (this.time
)spawnLocations
: az objektumok lehetséges pozíciói,addObject
, deleteObject
: callback függvények, melyeket valamely objektum hozzáadása, illetve törlése esetén fog meghívni a Spawner
osztály.
Az objectsCreated
tömb a spawner által létrehozott aktív objektumokat tartalmazza, az objectId
pedig az első létrehozandó objektum egyedi azonosítóját.
Az osztályon belül hozzunk létre egy start
metódust, mely az objektumok spawn-olását fogja időzíteni:
start() { this.interval = this.clock.addEvent({ delay: this.spawnInterval, loop: true, callback: () => { if (this.objectsCreated.length < this.limit) { this.spawnObject(); } } }); }
A konstruktorban, az adattagok inicializálását követően hívjuk is meg a létrehozott metódust: this.start();
A spawnObject
metódus az objectType
adattagnak megfelelő objektumot fog létrehozni (egyelőre a Spawner
osztály csak ládák létrehozását támogatja):
spawnObject() { if (this.objectType === 'CHEST') { this.spawnChest(); } }
A spawnChest
metódus kisorsolja, hogy a spawner melyik lokációra spawnolja a ládát, majd létrehozza a láda adatait. Minden láda egyedi azonosítót kap, mely a spawner id-ból, és egy generált objektum azonosítóból áll. Ezen kívül minden ládához tartozik egy coins
érték. A létrehozott ládát a Spawner osztály objectsCreated
tömbjében eltároljuk, majd meghívjuk a konstruktorban kapott addObject
callback függvényt (ez a GameScene
osztályban lesz később implementálva, és a láda tényleges megjelenítéséért fog felelni).
spawnChest() { const location = this.pickRandomLocation(); const newChest = { id: `${this.id}-${this.getNewObjectId()}`, spawnerId: this.id, x: location[0], y: location[1], coins: Phaser.Math.RND.between(10, 20) }; this.objectsCreated.push(newChest); this.addObject(newChest); }
A lehetséges lokációk közül a pickRandomLocation
metódus fog választani. Amennyiben a létrehozott objektumok között már található olyan, ami a kisorsolt lokáción található (Array.prototype.some() függvény működése), akkor újabb pozíció sorsolódik rekurzív módon, addig, amíg egy szabad helyet találunk:
pickRandomLocation() { const location = Phaser.Math.RND.pick(this.spawnLocations); const invalidLocation = this.objectsCreated.some((obj) => obj.x === location[0] && obj.y === location[1]); return invalidLocation ? this.pickRandomLocation() : location; }
A getNewObjectId
metódus eggyel növeli az objectId
-t, majd visszaadja az értéket, így egyedi azonosítót generál:
getNewObjectId() { return ++this.objectId; }
Ezen kívül szükségünk lesz a későbbiekben a spawnolt objektumok törlésére (pl. ha a játékos kinyitotta a ládát), ehhez is létrehozunk egy metódust, mely az objektum azonosítója alapján törli a megfelelő objektumot.
removeObject(id) { this.objectsCreated = this.objectsCreated.filter(obj => obj.id !== id); }
Ahhoz, hogy a Spawner
osztályt ténylegesen használni tudjuk, hivatkozzunk rá az index.html
fájlban, még a scene-k betöltése előtt:
<script src="js/classes/spawner.js"></script>
A ládák spawnolása a Spawner
osztályban most már megtörténik, viszont itt még csak a ládákhoz tartozó adatok (id, pozíció, tárolt coin-ok száma) kerülnek létrehozásra. A ládák tényleges megjelenítését a GameScene
fogja végezni.
A scenes/game.scene.js
fájlban szeretnénk tárolni a létrehozott spawnereket, ezért az init
metódusban hozzunk létre számukra egy új objektumot:
this.spawners = {};
Ezen kívül egy setupSpawners
metódust is létre fogunk hozni, mely a láda lokációk alapján hozza létre a spawner-eket (Object.keys() függvény működése):
gameScene.setupSpawners = function () { Object.keys(this.chestLocations).forEach(id => { const config = { id: `chest-${id}`, spawnInterval: 3000, limit: 3, objectType: 'CHEST' }; const spawner = new Spawner( config, this.time, this.chestLocations[id], (chest) => this.addChest(chest), (id) => this.deleteChest(id) ); this.spawners[spawner.id] = spawner; }); };
A setupSpawner
metódust hívjuk meg a create
metódusban, közvetlenül a player spawn-olása után! Emellett létre kell hoznunk egy Physics Group-ot is, melyben a láda sprite-okat fogjuk tárolni:
this.setupSpawners(); this.chests = this.physics.add.group();
A spawner-ek létrehozásakor hivatkoztunk az addChest
és deleteChest
metódusokra is, hozzuk létre ezeket is:
gameScene.addChest = function (chestData) { console.log('Spawning', chestData); this.chests[chestData.id] = chestData; const chest = this.add.sprite(chestData.x * this.scale, chestData.y * this.scale, 'items', 0); chest.config = { coins: chestData.coins, id: chestData.id }; chest.setScale(this.scale); this.chests.add(chest, true); }; gameScene.deleteChest = function (chestId) { delete this.chests[chestId]; };
A chest
sprite létrehozásakor egy config
objektumot rendeltünk a sprite-hoz, mely a láda azonosítóját és a benne tárolt coinok számát tárolja.
Akkor dolgoztunk jól, ha 3 másodperc elteltével a konzolon látjuk, hogy különböző id-kkal új ládák kerültek létrehozásra, és a pályán ezek meg is jelentek.
Következő lépésként, amikor a láda és a játékos átfedésbe kerül, akkor szeretnénk a ládában tárolt pontokat jóváírni, a ládát eltüntetni, majd helyette új ládát spawnolni. Ennek vizsgálatához adjuk hozzá a következő sort a create
metódus megfelelő részéhez:
this.physics.add.overlap(this.player, this.chests, (player, chest) => this.collectChest(player, chest));
A collectChest
metódust a következőképpen implementáljuk. Ha a hivatkozott láda ténylegesen létezik, akkor a hozzá tartozó adatokat (ezt a Spawner
osztály tárolja) és sprite-ot is töröljük!
gameScene.collectChest = function (player, chest) { this.score += chest.config.coins; uiScene.updateScore(this.score); if (this.chests[chest.config.id]) { this.spawners[this.chests[chest.config.id].spawnerId].removeObject(chest.config.id); chest.destroy(); } };
A láda begyűjtése után a coin-ok jóváíródnak, a láda eltűnik, és helyette új láda spawn-olódik egy véletlenszerűen kiválasztott pozíción (utóbbi a konzolon ellenőrizhető).
A szörnyek spawn-olása a ládákkal analóg módon fog történni. Először a Spawner
osztályt kell módosítanunk, a spawnObject
metódus egy else if
ággal egészül ki:
spawnObject() { if (this.objectType === 'CHEST') { this.spawnChest(); } else if (this.objectType === 'MONSTER') { this.spawnMonster(); } }
Ezen kívül az új spawnMonster
metódust is implementálnunk kell. A szörnyek a ládák tulajdonságai mellett frame
(melyik szörny legyen megjelenítve a spritesheet-ről), health
(életerő), attack
(sebzés) értékekkel is rendelkeznek.
spawnMonster() { const location = this.pickRandomLocation(); const newMonster = { id: `${this.id}-${this.getNewObjectId()}`, spawnerId: this.id, x: location[0], y: location[1], coins: Phaser.Math.RND.between(10, 20), frame: Phaser.Math.RND.between(0, 19), health: Phaser.Math.RND.between(3, 5), attack: 1 }; this.objectsCreated.push(newMonster); this.addObject(newMonster); }
A spawnerek példányosítását és a spawnolt szörnyek tényleges megjelenítését a GameScene
fogja végezni. Itt a szörnyeket egy új Physics Group-ban fogjuk tárolni, melyet a create
metódusban hozunk létre:
this.monsters = this.physics.add.group();
A szörnyek megjelenítéséhez ezentúl a BootScene
-en be kell töltenünk a hozzájuk tartozó spritesheet-et is:
this.load.spritesheet('monsters', 'assets/monsters.png', { frameWidth: 32, frameHeight: 32 });
Ezután a GameScene
-n létre kell hoznunk a spawnereket, a setupSpawners
metódushoz a következő sorokat kell hozzáadni (a láda spawnerek létrehozásával analóg módon):
Object.keys(this.monsterLocations).forEach(id => { const config = { id: `monster-${id}`, spawnInterval: 3000, limit: 3, objectType: 'MONSTER' }; const spawner = new Spawner( config, this.time, this.monsterLocations[id], (monster) => this.addMonster(monster), (id) => this.deleteMonster(id) ); this.spawners[spawner.id] = spawner; });
Szintén létre kell hoznunk az itt hivatkozott addMonster
és deleteMonster
metódusokat:
gameScene.addMonster = function (monsterData) { this.monsters[monsterData.id] = monsterData; const monster = this.add.sprite(monsterData.x * this.scale, monsterData.y * this.scale, 'monsters', monsterData.frame); monster.config = { id: monsterData.id, coins: monsterData.coins, health: monsterData.health, attack: monsterData.attack }; monster.setScale(this.scale); this.monsters.add(monster, true); }; gameScene.deleteMonster = function (monsterId) { delete this.monsters[monsterId]; };
Ha ezután a pályán mozogva a ládák mellett szörnyekkel is találkozunk, jól dolgoztunk:
Minden szörny rendelkezik életerővel (health
), sebzéssel (attack
), és coin-okkal (coins
). Ezeket az adatokat a játékos szörnyekkel történő harca során használjuk fel.
Kezdésként a játékos kezében szeretnénk megjeleníteni egy kardot, amit mindig a játékossal együtt mozgatunk. Ehhez a játékos spawn-olásán módosítani fogunk. A Player
sprite mellé egy Weapon
sprite-ot is létrehozunk, majd ezt a kettőt egy konténerben kapcsoljuk össze (Container osztály dokumentációja).
A spawnPlayer
metódust változtassuk meg:
gameScene.spawnPlayer = function () { const location = Phaser.Math.RND.pick(this.playerLocations); this.player = this.add.image(0, 0, 'characters', 0); this.physics.add.existing(this.player); this.player.setScale(this.scale); this.player.body.setCollideWorldBounds(true); this.weapon = this.add.image(this.player.x - 32, this.player.y, 'items', 4); this.weapon.flipX = true; this.physics.add.existing(this.weapon); this.weapon.body.setCollideWorldBounds(true); this.playerContainer = this.add.container(location[0] * this.scale, location[1] * this.scale, [this.player, this.weapon]); this.playerContainer.setSize(32 * this.scale, 32 * this.scale); this.physics.world.enable(this.playerContainer); this.playerContainer.body.setCollideWorldBounds(true); };
A kódban látható, hogy abszolút pozíciója csak a konténernek van, a játékos és a fegyver koordinátái a konténer pozíciójához képest vannak megadva.
A kamerának a továbbiakban nem a player
sprite-ot, hanem a playerContainer
-t kell követnie, ezt módosítsuk:
this.cameras.main.startFollow(this.playerContainer);
Módosítanunk kell továbbá az updatePlayer
metódust is, a megfelelő irányú erőket ezután magán a konténeren helyezzük el, a játékos helyett. Amikor jobbra vagy balra mozdulunk, akkor a fegyvert is ennek megfelelően fordítjuk el, illetve helyezzük át.
gameScene.updatePlayer = function () { this.playerContainer.body.setVelocity(0); if (this.cursors.left.isDown) { this.player.flipX = false; this.playerContainer.body.setVelocityX(-this.playerSpeed); this.weapon.flipX = true; this.weapon.setPosition(this.player.x - 32, this.player.y); } else if (this.cursors.right.isDown) { this.player.flipX = true; this.playerContainer.body.setVelocityX(this.playerSpeed); this.weapon.flipX = false; this.weapon.setPosition(this.player.x + 32, this.player.y); } if (this.cursors.up.isDown) { this.playerContainer.body.setVelocityY(-this.playerSpeed); } else if (this.cursors.down.isDown) { this.playerContainer.body.setVelocityY(this.playerSpeed); } };
Módosítsuk továbbá a játékos és a blocked layer közötti collider-t is. Itt szintén a konténer és a blocked layer közötti ütközést kell vizsgálni:
this.physics.add.collider(this.playerContainer, this.blockedLayer);
A harc implementációjának első lépéseként adattagokat fogunk felvenni. Egyrészt tárolnunk kell azt, hogy a játékos éppen harcol-e, ezt adjuk hozzá az init
metódushoz:
this.playerAttacking = false;
Másrészt tárolnunk kell a játékos életerejét (health
), illetve azt, hogy az aktuális támadás során már megütötte-e a támadott szörnyet (swordHit
). Ezt a spawnPlayer
metódusban vezetjük be, a player
sprite létrehozása után:
this.player.config = { swordHit: false, health: 7 };
Következő lépésként egy tween animációt fogunk létrehozni, mely a SPACE billentyű lenyomása esetén a kardot 360°-ban megforgatja. Ehhez az updatePlayer
metódust egészítsük ki a következő sorokkal:
if (Phaser.Input.Keyboard.JustDown(this.cursors.space) && !this.playerAttacking) { this.playerAttacking = true; this.tweens.add({ targets: this.weapon, duration: 150, angle: this.weapon.flipX ? -360 : 360, paused: false, onComplete: () => { this.playerAttacking = false; this.player.config.swordHit = false; } }); }
Ha jól dolgoztunk, a SPACE lenyomása után a játékos kardja 360°-ban körbefordul.
A Phaser.Input.Keyboard.JustDown()
metódus a SPACE billentyű lenyomása után egyszer fog true
értéket visszaadni, majd ezután mindig false
-t (ezzel azt jelzi, hogy éppen most lett-e lenyomva a billentyű, vagy már hosszabb ideje nyomva van). A billentyű újbóli lenyomásakor ugyanez fog történni, így a SPACE egyszeri lenyomásakor egyetlen alkalommal forgatja meg a kardot a játék. Az animáció végeztével a playerAttacking
és swordHit
tulajdonságok értéke false
-ra áll vissza, így újabb támadás indítható a SPACE újbóli lenyomásával.
A szörnyekkel történő harc tényleges megvalósításához helyezzünk el egy vizsgálatot a create
metódusban, ami a játékos fegyverének és egy szörnynek az átfedését vizsgálja.
this.physics.add.overlap(this.weapon, this.monsters, (weapon, monster) => this.enemyOverlap(weapon, monster));
Ezután adjunk egy kezdeti implementációt az enemyOverlap
metódusnak. Ez azt vizsgálja, hogy a játékos támadásban van-e, illetve még nem ütötte-e meg a szörnyet. Ha a feltétel teljesül, akkor rögzítjük, hogy az adott támadás során a játékos megütötte a szörnyet, és a későbbiekben változtatni fogjuk a szörny, valamint a játékos életerejét is (de egyelőre csak egy üzenetet íratunk ki a konzolra).
gameScene.enemyOverlap = function (weapon, monster) { if (this.playerAttacking && !this.player.config.swordHit) { this.player.config.swordHit = true; console.log('MONSTER HIT!'); } };
Akkor dolgoztunk jól, ha a szörny támadásakor SPACE billentyű egyszeri lenyomásakor egyszer jelenik meg a konzolon a MONSTER HIT!
üzenet (ennek vizsgálatakor a spawnerek logja zavaró lehet, így azt törölhetjük a kódunkból).
A metódusunkat tovább bővítjük, minden ütéskor 1-et levonunk a szörny életerejéből, majd megvizsgáljuk, hogy az 0-ra csökkent-e. Amennyiben igen, a szörnyhöz tartozó coin-okat jóváírjuk a játékosnál (és frissítjük a kijelzést), majd a szörny adatait eltávolítjuk a spawner-ből, és végül magát a sprite-ot is megszüntetjük.
gameScene.enemyOverlap = function (weapon, monster) { if (this.playerAttacking && !this.player.config.swordHit) { this.player.config.swordHit = true; if (this.monsters[monster.config.id]) { monster.config.health -= 1; if (monster.config.health <= 0) { this.score += monster.config.coins; uiScene.updateScore(this.score); this.spawners[this.monsters[monster.config.id].spawnerId].removeObject(monster.config.id); monster.destroy(); } } } };
Ha jól dolgoztunk, azt kell látnunk a konzolon, hogy minden ütéssel 1-et csökken az ellenség életereje, majd ha elérte a 0-t, a szörny eltűnik és a játékosnál a pontok jóváíródnak.
Most tovább bővítjük az implementációt, amennyiben a szörny nem halt meg, a játékos életerejét is csökkentjük a szörny attack
tulajdonságának értékével. Emellett azt is megvizsgáljuk, hogy a játékos életereje 0-ra csökkent-e. Amennyiben igen, a játékot újraindítjuk.
gameScene.enemyOverlap = function (weapon, monster) { if (this.playerAttacking && !this.player.config.swordHit) { this.player.config.swordHit = true; if (this.monsters[monster.config.id]) { monster.config.health -= 1; console.log('monster health', monster.config.health); if (monster.config.health <= 0) { this.score += monster.config.coins; uiScene.updateScore(this.score); this.spawners[this.monsters[monster.config.id].spawnerId].removeObject(monster.config.id); monster.destroy(); } else { this.player.config.health -= monster.config.attack; console.log('player health', this.player.config.health); if (this.player.config.health <= 0) { this.scene.restart(); } } } } };
A konzolon ekkor a szörny és a játékos életerejének csökkenését is látnunk kell.
A játékos életerejét a felhasználó számára is láthatóvá szeretnénk tenni, ennek érdekében a UiScene
-t is módosítjuk. A create
metódust új szöveg létrehozásával egészítjük ki:
this.healthText = this.add.text(690, 8, 'Health: 0', { fontSize: '16px', fill: '#fff' });
Ezen kívül biztosítunk egy metódust az életerő értékének megváltoztatására:
uiScene.updateHealth = function (newHealth) { this.healthText.setText(`Health: ${newHealth}`); };
A GameScene
-en az update
metódusban meg is hívjuk az új életerő kijelzését biztosító metódust:
uiScene.updateHealth(this.player.config.health);
A kódból minden console.log
utasítást kitörölhetünk, ezek csak a fejlesztés során voltak segítségünkre.