A napi szintű internetmágiába hosszú távon kissé bele lehet fásulni, ilyenkor szoktam a nosztalgiamániámat kihasználva HTML5 játékfejlesztésről olvasgatni vagy próbálkozni vele, hátha valami feleleveníti azt a kellemes hangulatot, amit annak idején egy Super Mario, vagy egy Donkey Kong végigjátszása okozott. A 2 dimenziós platformer kutatásom vezetett el a Phaser-hez, ami gyakorlatilag a szekrényen porosodó nintendot a böngészőre cseréli. Február környékén megesett egy 3.0-ás release, szóval még ráfogható, hogy aktuális vele a demózás.
WTF is Phaser?
Egy WebGL és Canvas renderelést támogató, 2D-s, HTML5 játékfejlesztéshez használatos JavaScript alapú keretrendszer. Ez tömören annyit jelent, hogy nem kell a fizikával, hitboxokkal, ütközésekkel foglalkoznod, a böngészős játékodhoz csak egy ötlet és egy működési elv kell, a nehezét ez majd megoldja helyetted. Ennek alátámasztására összedobtam egy kis „játékot”, aminek összesen annyi célja lenne, hogy a Phaser felépítése mellett szemléltesse kb. mennyi effortot is igényel egy ilyen alapszintű dolog.
See the Pen Infinite hopper by feo (@feoMango) on CodePen.
Az elv borzasztóan egyszerű: a kis nyúlszerű képződményt egy megállás nélkül lefelé mozgó platformszéria egyikére dobjuk, majd elvárjuk tőle, hogy a blokkok résein keresztül felugrálva elkerülje a képernyő legalját. Ha ez nem sikerül, a játék újraindul.
Játékmenet gyakorlatban
Először is össze kell rakni a projektet a phaser oldalán, vagy githubján felvázolt módszerek egyikével. Amint ez megvan, inicializálhatjuk a Phasert, hogy ellenőrizni tudjuk minden megfelelően működik-e, valahogy így:
let config = {
type: Phaser.AUTO,
width: window.innerWidth,
height: window.innerHeight,
physics: {
default: 'arcade',
arcade: {
gravity: {
y: 1200
},
debug: false
}
},
scene: [
gameLoader,
gameLauncher
],
input: {
activePointers: 2
},
backgroundColor: '#c34'
};
let game = new Phaser.Game(config);
Az elképzelésben ugyan még semmi extra nincsen, de a config paraméter rendelkezik néhány olyan értékkel, amik nem mondanak túl sokat, szóval fussunk rajta végig gyorsan:
- type: a Phaser renderelési módszere, amit ha automatikusra állítunk, ahol lehet WebGL-t használ, ahol nem támogatott, Canvas-ra fallback-kel.
- physics: a játékban használt fizika típusa, ezek közül az arcade a legkisebb és legegyszerűbb (az alternatívák egyébként az „impact” és a „matter”). Beállítunk egy globális gravitációt, illetve ki/be kapcsolhatjuk a debug funkciót (ami egyébként hitboxoktól kezdve a mozgó objektumok pályájáig minden apróságot megjelenít szóval fölöttébb hasznos tud lenni).
- scene: beállítunk 2 scene-t a játékhoz, az első az assetek betöltésére szolgál, a második a játék elindításáért és működéséért felel
- input: alaphangon minden játék 1 pointerrel indul, ez az egérkurzort vagy a tappolást reprezentálja, a minimális mobil használhatóság végett felvettem egy extra pointert, hogy a karakter draggelése mellett egyidőben tappolhassunk is
Ezek mindegyikéről borzasztó sokat lehet olvasgatni különféle Phaser-es dokumentációkban, így innen csak egy dolgot emelek ki:
Scene
Mint ahogy az elnevezésből is rá lehet jönni, ezeket legegyszerűbben egy felvonásként vagy jelenetként lehet elképzelni pl. így:
- Betöltjük az asseteket
- Menüpontokat jelenítünk meg
- Elindítjuk a játékot és megvalósítjuk a funkcionalitást
Innentől a játékunk kellemesen elkülöníthető blokkokból áll össze, amik között fölöttébb egyszerűen váltogathatunk:
scene.start('startGame');
Ezzel elkerülhetjük azt, hogy a rendszer újraindítása oldalfrissítést, assetek újrafeldolgozását vagy bármi hasonlóan tré dolgot igényeljen.
Szóval nézzük a kódot relatíve röviden csak a Phaser-specifikus részekre koncentrálva, az egyszerű JavaScript logikát max. futólag érintem.
Assetek betöltése
class gameLoader extends Phaser.Scene {
constructor() {
super('initializeGame');
}
preload() {
this.load.image(
'clouds',
'sky.png'
);
this.load.image(
'ground',
'ground.png'
);
this.load.spritesheet(
'player',
'playersprite.png',
{
frameWidth: 47.5,
frameHeight: 75
}
);
}
create() {
this.anims.create({
key: 'left',
frames: this.anims.generateFrameNumbers(
'player',
{
start: 0,
end: 1
}
),
frameRate: 10,
repeat: -1
});
this.scene.launch('startGame');
}
}
A preload() metódusban létrehozzuk statikus képeinket, a spritesheeteket és minden egyebet, majd ellátjuk őket egy kulccsal, amivel hivatkozhatunk rájuk. Ha ezek betöltődtek, a create()-ben összerakjuk az animációinkat, amik közül a karakter balra tartó mozgását emeltem ki: a „player”-ként generált spritesheetet 47.5*75-ös frame-ekre bontjuk, majd a „left”-ként definiált animáció meghívására 10 frame/sec sebességgel, ismétlődve meghívjuk ennek 0. és 1. elemét.
A játék indítása
class gameLauncher extends Phaser.Scene {
constructor() {
super('startGame');
}
create() {
this.initBaseElements();
this.initTimedEvents();
this.initTileParameters();
this.spawnInitialTiles();
}
update() {
if (this.cursors.left.isDown) {
this.moveLeft();
} else if(this.cursors.right.isDown) {
this.moveRight();
} else {
this.stayIdle();
}
if (this.cursors.up.isDown) {
this.jump();
}
if(this.player.body.onFloor()) {
this.gameOver();
}
}
}
Egyelőre maradjunk a Phaser meglévő metódusainál, a sajátokat az átláthatóság kedvéért kiszedtem. Itt már elhagyhatjuk a preload lépést, mivel minden fontosabb dolgot előre betöltöttünk az előző jelenetben. A create()-ünk létrehozza a definiált assetekészletünkből a játékhoz szükséges objektumokat, elindítja az időzített eseményeket illetve létrehozza a kezdéshez elengedhetetlen, egyszer használatos platformjainkat.
Az akció minden egyéb része az update() lépésben történik. Ez az, ami a játék minden egyes belső tickje alatt lefut, szóval minden valós idejű interakciót itt kezelünk:
- Az arrow gombok használatára a karakter jobbra-balra mozoghat, ugorhat vagy ácsoroghat magának (az animáció miatt szükséges)
- Ha a nyúl a meghatározott játékterület aljához ér, a játék véget ér (esetünkben újrakezdődik)
Olvass bővebben a Phaser belső lépéseiről, „tickjeiről”
A mozgatás megvalósítása
moveLeft() {
this.player.setVelocityX(-500);
this.player.anims.play('left', true);
}
A játékos objektumának egyszerűen beállítunk egy sebességet az x tengelyen, majd elindítjuk a korábban „left” kulccsal létrehozott animációt.
Ugyan ezen az elven működik az ugrásunk is, annyi különbséggel, hogy az y tengelyen állítunk sebességet az objektumnak:
this.player.setVelocityY(-750);
Még a földetéréssel sem kell foglalkoznunk, ugyanis ezt a korábban beállított gravitáció elintézi helyettünk.
A játékfolyamat
Most, hogy a jeleneteket letudtuk, megpróbálom kiemelni a játékfolyamat fontosabb részeit:
Az initBaseElements() metódusban definiáltuk paramétereink alapértelmezett értékét, beállítottuk a kurzort és pointer objektumainkat, illetve csináltunk egy hátteret a játékhoz.
Közvetlen utána létrehoztuk az időzített illetve a játék lépéseitől független, fix időközönként ismétlődő eseményeinket:
initTimedEvents() {
this.timedEvent = this.time.addEvent({
delay: 2500,
callback: this.addTile,
callbackScope: this,
loop: true
});
this.time.delayedCall(
2500,
this.startGame,
[],
this
);
}
Első eseményünk 2,5 másodpercenként ismétlődik, ez hivatott a fentről érkező platformokat legenerálni.
Közvetlen ezután beállítunk egy időzített hívást ami valami ilyesmit jelent: 2,5 másodperccel a felvonás kezdése után, indítsd el a játékfolyamatot.
Furcsán kinéző nyulunknak azonban szüksége van még némi támaszra, kéne neki egy alap, amin meg tud állni. Először is meghatározzuk a karakter és a platformot kezelő csoport ütközésének kezelését:
this.physics.add.collider(this.player, this.tiles);
majd a spawnInitialTiles() metódus segítségével létrehozzuk a statikus platformokat. Ezek minden egyes eleme ugyanazzal a módszerrel jön létre: fogjuk az 50x50-es képünket, kiszámítjuk hányszor fér ki a képernyőn, hagyunk benne némi helyet, amin a karakterünk átfér, majd legeneráljuk a négyzeteinket:
spawnTileSprite(index, alignY)
{
let tile = this.tiles.create(
(index * this.tileWidth), alignY,
'ground'
);
if(index % 2 == 0) {
tile.setFlipX(true);
}
tile.body.setAllowGravity(false);
tile.setImmovable(true);
tile.setVelocityY(this.tileVelocity);
}
- a tiles nevű csoportunk tagjaként létrehozzuk a sprite-ot a „ground” kulccsal ellátott képből
- minden másodikat vízszintesen tükrözzük, hogy megtartsa a hullám mintát
- kikapcsoljuk a rá ható gravitációt, hogy a lefelé irányuló sebességét ne befolyásolja
- „mozdíthatatlanná” tesszük, hogy ha a karakterünk nekimegy, ne tolja el őket
- elindítjuk negatív Y irányba egy előredefiniált sebességgel
Ezzel nagyjából véget is ér a játékfolyamat, remélem érezhető, hogy a munka nehezét a Phaser végzi, nekem csak az alap ötletemet kell köré építenem.
Nyilván rászorulna némi optimalizálásra, de ettől eltekintve kb. 280 sorból a funkcionalitását bőven ellátó játékot össze lehet tolni, szóval hajrá, set your phasers to stun.
Játssz a karácsonyi játékunkkal, ami szintén Phaserben készült!
Hozzászólások