2018. október 04.

Phaser kedvcsináló

10 perc olvasási idő

Phaser kedvcsináló

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:

  1. Betöltjük az asseteket
  2. Menüpontokat jelenítünk meg
  3. 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!

Források

Pardi Gergő

Webfejlesztő - napi fantasy adagját játék és könyv formájában fogyasztja. Tömegközlekedés rajongó, közösségi ember és csupa nyitottság

Pardi Gergő

Hozzászólások