A kiválasztott változat és az aktuális verzió közötti különbségek a következők.
Előző változat mindkét oldalon Előző változat Következő változat | Előző változat | ||
tanszek:oktatas:jatek_prototipusok:jatek_prototipusok:rpg_tutorial [2022/10/08 14:52] kissa [Térkép betöltése, játékos mozgatása] |
tanszek:oktatas:jatek_prototipusok:jatek_prototipusok:rpg_tutorial [2022/10/10 08:38] (aktuális) kissa |
||
---|---|---|---|
Sor 2: | Sor 2: | ||
===== Térkép felépítése ===== | ===== Térkép felépítése ===== | ||
- | Tiled map editor segítségével. | + | Tiled map editor segítségével: |
+ | - térkép létrehozása | ||
+ | - tileset betöltése | ||
+ | - tile layer-ek létrehozása: Egyszerű képi információkat tartalmaznak. | ||
+ | - object layer-ek létrehozása: A felhasználó számára közvetlenül nem látható objektumokat tartalmaznak, melyeknek saját tulajdonságai lehetnek. Az objektumokat a játékprogram fogja feldolgozni (pl. spawner-ek, portálok, effektek, savepoint-ok létrehozása céljából). | ||
+ | - térkép exportálása | ||
+ | - az exportált .tmx fájlt később lehet szerkeszteni Tiled-ban | ||
+ | - az exportált .json fájlt a Phaser közvetlenül be tudja tölteni | ||
===== Térkép betöltése, játékos mozgatása ===== | ===== Térkép betöltése, játékos mozgatása ===== | ||
- | 1. asset-ek betöltése | + | - asset-ek betöltése |
- | 2. tilemap létrehozása | + | - tilemap létrehozása |
- | 3. tileset betöltése | + | - tileset betöltése |
- | 4. layer-ek létrehozása, ütközések beállítása | + | - layer-ek létrehozása, ütközések beállítása |
- | 5. egyéb metaadatok kinyerése (spawner-ek) | + | - térkép metaadatok kinyerése (spawner objektumok) |
- | 6. játékos követése a kamerával | + | - játékos követése a kamerával |
===== Spawner osztály ===== | ===== Spawner osztály ===== | ||
+ | Hozzunk létre egy új fájlt a ''js/classes'' mappában, ''spawner.js'' néven, a következő tartalommal: | ||
<sxh js> | <sxh js> | ||
- | constructor(config, clock, spawnLocations, addObject, deleteObject) { | + | class Spawner { |
+ | constructor(config, clock, spawnLocations, addObject, deleteObject) { | ||
this.id = config.id; | this.id = config.id; | ||
this.spawnInterval = config.spawnInterval; | this.spawnInterval = config.spawnInterval; | ||
Sor 25: | Sor 34: | ||
this.objectsCreated = []; | this.objectsCreated = []; | ||
- | this.objectId = 1000; | + | this.objectId = 1; |
+ | } | ||
+ | } | ||
+ | </sxh> | ||
- | this.start(); | + | 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: | ||
+ | |||
+ | <sxh js> | ||
+ | start() { | ||
+ | this.interval = this.clock.addEvent({ | ||
+ | delay: this.spawnInterval, | ||
+ | loop: true, | ||
+ | callback: () => { | ||
+ | if (this.objectsCreated.length < this.limit) { | ||
+ | this.spawnObject(); | ||
+ | } | ||
+ | } | ||
+ | }); | ||
} | } | ||
</sxh> | </sxh> | ||
+ | |||
+ | 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): | ||
+ | |||
+ | <sxh js> | ||
+ | spawnObject() { | ||
+ | if (this.objectType === 'CHEST') { | ||
+ | this.spawnChest(); | ||
+ | } | ||
+ | } | ||
+ | </sxh> | ||
+ | |||
+ | 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). | ||
+ | |||
+ | <sxh js> | ||
+ | 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); | ||
+ | } | ||
+ | </sxh> | ||
+ | |||
+ | 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ó ([[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some|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: | ||
+ | |||
+ | <sxh js> | ||
+ | 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; | ||
+ | } | ||
+ | </sxh> | ||
+ | |||
+ | A ''getNewObjectId'' metódus eggyel növeli az ''objectId''-t, majd visszaadja az értéket, így egyedi azonosítót generál: | ||
+ | |||
+ | <sxh js> | ||
+ | getNewObjectId() { | ||
+ | return ++this.objectId; | ||
+ | } | ||
+ | </sxh> | ||
+ | |||
+ | 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. | ||
+ | |||
+ | <sxh js> | ||
+ | removeObject(id) { | ||
+ | this.objectsCreated = this.objectsCreated.filter(obj => obj.id !== id); | ||
+ | } | ||
+ | </sxh> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <sxh html> | ||
+ | <script src="js/classes/spawner.js"></script> | ||
+ | </sxh> | ||
+ | |||
==== Ládák spawn-olása ==== | ==== Ládák spawn-olása ==== | ||
+ | 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. | ||
- | ==== Ellenségek spawn-olása ==== | + | 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: |
+ | |||
+ | <sxh js> | ||
+ | this.spawners = {}; | ||
+ | </sxh> | ||
+ | |||
+ | 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 ([[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys|Object.keys() függvény működése]]): | ||
+ | |||
+ | <sxh js> | ||
+ | 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; | ||
+ | }); | ||
+ | }; | ||
+ | </sxh> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <sxh js> | ||
+ | this.setupSpawners(); | ||
+ | |||
+ | this.chests = this.physics.add.group(); | ||
+ | </sxh> | ||
+ | |||
+ | A spawner-ek létrehozásakor hivatkoztunk az ''addChest'' és ''deleteChest'' metódusokra is, hozzuk létre ezeket is: | ||
+ | |||
+ | <sxh js> | ||
+ | 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]; | ||
+ | }; | ||
+ | </sxh> | ||
+ | |||
+ | 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. | ||
+ | |||
+ | {{:tanszek:oktatas:jatek_prototipusok:jatek_prototipusok:chest-spawn.png?400|}} | ||
+ | |||
+ | ==== Ládák begyűjtése ==== | ||
+ | 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: | ||
+ | |||
+ | <sxh js> | ||
+ | this.physics.add.overlap(this.player, this.chests, (player, chest) => this.collectChest(player, chest)); | ||
+ | </sxh> | ||
+ | |||
+ | 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! | ||
+ | |||
+ | <sxh js> | ||
+ | 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(); | ||
+ | } | ||
+ | }; | ||
+ | </sxh> | ||
+ | |||
+ | 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ő). | ||
+ | |||
+ | ==== Szörnyek spawn-olása ==== | ||
+ | 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: | ||
+ | |||
+ | <sxh js> | ||
+ | spawnObject() { | ||
+ | if (this.objectType === 'CHEST') { | ||
+ | this.spawnChest(); | ||
+ | } else if (this.objectType === 'MONSTER') { | ||
+ | this.spawnMonster(); | ||
+ | } | ||
+ | } | ||
+ | </sxh> | ||
+ | |||
+ | 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. | ||
+ | |||
+ | <sxh js> | ||
+ | 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); | ||
+ | } | ||
+ | </sxh> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <sxh js> | ||
+ | this.monsters = this.physics.add.group(); | ||
+ | </sxh> | ||
+ | |||
+ | A szörnyek megjelenítéséhez ezentúl a ''BootScene''-en be kell töltenünk a hozzájuk tartozó spritesheet-et is: | ||
+ | |||
+ | <sxh js> | ||
+ | this.load.spritesheet('monsters', 'assets/monsters.png', { | ||
+ | frameWidth: 32, | ||
+ | frameHeight: 32 | ||
+ | }); | ||
+ | </sxh> | ||
+ | |||
+ | 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): | ||
+ | |||
+ | <sxh js> | ||
+ | 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; | ||
+ | }); | ||
+ | </sxh> | ||
+ | |||
+ | Szintén létre kell hoznunk az itt hivatkozott ''addMonster'' és ''deleteMonster'' metódusokat: | ||
+ | |||
+ | <sxh js> | ||
+ | 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]; | ||
+ | }; | ||
+ | </sxh> | ||
+ | |||
+ | Ha ezután a pályán mozogva a ládák mellett szörnyekkel is találkozunk, jól dolgoztunk: | ||
+ | |||
+ | {{:tanszek:oktatas:jatek_prototipusok:jatek_prototipusok:monster-spawned.png?400|}} | ||
+ | |||
+ | ===== Harc a szörnyekkel ===== | ||
+ | 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. | ||
+ | |||
+ | ==== Fegyver létrehozása, animálása ==== | ||
+ | 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 ([[https://photonstorm.github.io/phaser3-docs/Phaser.GameObjects.Container.html|Container osztály dokumentációja]]). | ||
+ | |||
+ | A ''spawnPlayer'' metódust változtassuk meg: | ||
+ | |||
+ | <sxh js> | ||
+ | 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); | ||
+ | }; | ||
+ | </sxh> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <sxh js> | ||
+ | this.cameras.main.startFollow(this.playerContainer); | ||
+ | </sxh> | ||
+ | |||
+ | 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. | ||
+ | |||
+ | <sxh js> | ||
+ | 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); | ||
+ | } | ||
+ | }; | ||
+ | </sxh> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <sxh js> | ||
+ | this.physics.add.collider(this.playerContainer, this.blockedLayer); | ||
+ | </sxh> | ||
+ | ==== Harc megvalósítása ==== | ||
+ | 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: | ||
+ | |||
+ | <sxh js> | ||
+ | this.playerAttacking = false; | ||
+ | </sxh> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <sxh js> | ||
+ | this.player.config = { | ||
+ | swordHit: false, | ||
+ | health: 7 | ||
+ | }; | ||
+ | </sxh> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <sxh js> | ||
+ | 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; | ||
+ | } | ||
+ | }); | ||
+ | } | ||
+ | </sxh> | ||
+ | |||
+ | 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. | ||
+ | |||
+ | <sxh js> | ||
+ | this.physics.add.overlap(this.weapon, this.monsters, (weapon, monster) => this.enemyOverlap(weapon, monster)); | ||
+ | </sxh> | ||
+ | |||
+ | 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). | ||
+ | |||
+ | <sxh js> | ||
+ | gameScene.enemyOverlap = function (weapon, monster) { | ||
+ | if (this.playerAttacking && !this.player.config.swordHit) { | ||
+ | this.player.config.swordHit = true; | ||
+ | console.log('MONSTER HIT!'); | ||
+ | } | ||
+ | }; | ||
+ | </sxh> | ||
+ | |||
+ | 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). | ||
+ | |||
+ | {{:tanszek:oktatas:jatek_prototipusok:jatek_prototipusok:monster-hit.png?400|}} | ||
+ | |||
+ | 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. | ||
+ | |||
+ | <sxh js> | ||
+ | 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(); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | }; | ||
+ | </sxh> | ||
+ | |||
+ | 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. | ||
+ | |||
+ | {{:tanszek:oktatas:jatek_prototipusok:jatek_prototipusok:monster-health.png?400|}} | ||
+ | |||
+ | 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. | ||
+ | |||
+ | <sxh js> | ||
+ | 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(); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | }; | ||
+ | </sxh> | ||
+ | |||
+ | A konzolon ekkor a szörny és a játékos életerejének csökkenését is látnunk kell. | ||
+ | |||
+ | {{:tanszek:oktatas:jatek_prototipusok:jatek_prototipusok:attack-health.png?400|}} | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <sxh js> | ||
+ | this.healthText = this.add.text(690, 8, 'Health: 0', { fontSize: '16px', fill: '#fff' }); | ||
+ | </sxh> | ||
+ | |||
+ | Ezen kívül biztosítunk egy metódust az életerő értékének megváltoztatására: | ||
+ | |||
+ | <sxh js> | ||
+ | uiScene.updateHealth = function (newHealth) { | ||
+ | this.healthText.setText(`Health: ${newHealth}`); | ||
+ | }; | ||
+ | </sxh> | ||
+ | |||
+ | A ''GameScene''-en az ''update'' metódusban meg is hívjuk az új életerő kijelzését biztosító metódust: | ||
+ | |||
+ | <sxh js> | ||
+ | uiScene.updateHealth(this.player.config.health); | ||
+ | </sxh> | ||
- | ===== Támadás ===== | + | 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. |
- | ===== Ellenségek mozgatása ===== | + | ===== Továbbfejlesztési lehetőségek ===== |
+ | * életerő meghatározott időközönkénti töltése, | ||
+ | * az életerőt grafikusan kijelző health bar létrehozása a játékosnak és az ellenségeknek (lásd virtual pet game > progress bar a betöltési képernyőn), | ||
+ | * ellenségek mozgatása véletlenszerű irányba, | ||
+ | * object pooling alkalmazása a ládákra/szörnyekre (lásd platformer példa), | ||
+ | * achievement rendszer létrehozása, | ||
+ | * új itemek bevezetése (pl. pajzs, páncélzat, stb., ezek csökkenthetik a szörnyek sebzését, és növelhetik a játékosét), | ||
+ | * inventory rendszer létrehozása, | ||
+ | * kereskedés itemekkel, | ||
+ | * új itemek craftolása meglévő alapanyagokból, | ||
+ | * szintrendszer bevezetése (pl. a magasabb szintű játékos rendelkezhet nagyobb sebzéssel), | ||
+ | * más pályákra irányító portálok létrehozása. |