First Phaser 4 version released, let’s have a look at it by building a HTML5 Sokoban game

Talking about Sokoban game, Game development, HTML5, Javascript and Phaser.

Here I am with a good news and a bad news. The good news is Phaser 4 has been released in a sort of beta version. The bad news is Phaser 4 is not ready for production at the moment, but being the good news positively GOOD, I don’t care about the bad news and I tried to develop something playable anywway.

There are some considerations to do: Phaser 4 is developed in TypeScript, and although we can continue developing in JavaScript, it’s recommended to switch to TypeScript, so during next Phaser tutorials I will show you how to switch from JavaScript to TypeScript. There’s nothing diffucult: you can check this guide I wrote 13 years ago about learning new languages. And bear in mind if you already know JavaScript, we can’t even consider TypeScript as a new language, let’s say it’s some kind of JavaScript on steroids.

When I said “not ready for production”, I meant “ready to start learning”, to get your knowledge grow as new and more stable versions are released.

This first example, is a Sokoban prototype built using my JavaScript Sokoban Class to handle game logic, so you will only need to focus on new Phaser features.

It’s not a pure TypeScript example, but rather a mix of JavaScript and Typescript, but it’s my “Hello World” so I will improve.

I used Parcel 2 to pack and bundle the game, if you are new to this kind of tool, don’t worry because next tutorial will focus on installing all necessary software on your machine.

Have a look at the game:

You know the rules, use arrow keys to move.

Have a look at the completely commented source code:

import { BackgroundColor, Parent, Scenes, WebGL, Size, GlobalVar } from '@phaserjs/phaser/config';
import { AddChild } from '@phaserjs/phaser/display/';
import { AddTween } from '@phaserjs/phaser/motion/tween/nano/AddTween';
import { Game } from '@phaserjs/phaser/Game';
import { Sprite } from '@phaserjs/phaser/gameobjects';
import { Scene } from '@phaserjs/phaser/scenes/Scene';
import { StaticWorld } from '@phaserjs/phaser/world/StaticWorld';
import { Keyboard } from '@phaserjs/phaser/input/keyboard';
import { LeftKey, RightKey, UpKey, DownKey } from '@phaserjs/phaser/input/keyboard/keys';
import { On } from '@phaserjs/phaser/events';
import { Sokoban } from './sokoban';
import * as tiles from './textures';

class SokobanGame extends Scene {

    // tile size, in pixels
    tileSize = 60;

    constructor () {
        super ();

        // initialize a new Phaser world
        const world = new StaticWorld (this);

        // array of static tiles to be placed.
        // these tiles never move
        const staticTiles = [
            tiles.floorTexture,     // when we have a floor tile
            tiles.wallTexture,      // when we have a wall tile
            tiles.goalTexture,      // when we have a goal tile
            tiles.floorTexture,     // when we have a crate tile, we draw the floor tile
            tiles.floorTexture,     // when we have the player tile, we draw the floor tile
            tiles.goalTexture,      // when we have a crate over goal tile, we draw the goal tile
            tiles.goalTexture       // when we have the player over goal tile, we draw the goal tile
        ]

        // add a keyboard input
        const keyboard = new Keyboard ();

        // define keys to be used in game
        this.leftKey = new LeftKey ();
        this.rightKey = new RightKey ();
        this.upKey = new UpKey ();
        this.downKey = new DownKey ();

        // add keyboard keys to check
        keyboard.addKeys (this.leftKey, this.rightKey, this.upKey, this.downKey);

        // player can move
        this.canMove = true;

        // array to store crates
        this.crates = [];

        // Sokoban level in standard text notation
        const levelString = '########\n#####@.#\n####.$$#\n#### $ #\n### .# #\n###    #\n###  ###\n########';

        // create a new Sokoban instance
        this.sokoban = new Sokoban ();

        // build the Sokoban level
        this.sokoban.buildLevelFromString (levelString);

        // iterate through Sokoban level
        this.sokoban.level.map (function (row, rowNumber) {

            // iterate through Sokoban level row
            row.map (function (element, columnNumber) {

                // create a new sprite using the proper tile according to element
                const tile = new Sprite(this.tileSize * (columnNumber + 0.5), this.tileSize * (rowNumber + 0.5), staticTiles[element]);

                // add the tile to the world
                AddChild (world, tile);

                // handle player and crates
                switch (element) {

                    // player
                    case 4:
                    case 6:

                        // create player sprite
                        this.player =  new Sprite (this.tileSize * (columnNumber + 0.5), this.tileSize * (rowNumber + 0.5), tiles.playerTexture);
                        break;

                    // crate
                    case 3:
                    case 5:

                        // create crate sprite and add to crates array
                        this.crates.push (new Sprite (this.tileSize * (columnNumber + 0.5), this.tileSize * (rowNumber + 0.5), tiles.crateTexture));
                }
            }.bind (this));
        }.bind (this));

        // add player sprite to the world
        AddChild (world, this.player);

        // add all crate sprites to the world
        this.crates.map (function (crate) {
            AddChild (world, crate);
        })

        // method to call at each frame
        On(this, 'update', () => this.update ());
    }

    // method called at each frame
    update () {

        // can the player move?
        if (this.canMove) {

            // check if the move is legal
            let move = this.leftKey.isDown ? this.sokoban.moveLeft () : (this.rightKey.isDown ? this.sokoban.moveRight () : (this.upKey.isDown ? this.sokoban.moveUp () : (this.downKey.isDown ? this.sokoban.moveDown () : false)))

            // is the move legal?
            if (move) {

                // now we can't move
                this.canMove = false;

                // retrieve plater information
                let player = this.sokoban.getPlayer();

                // retrieve crates information
                let crates = this.sokoban.getCrates();

                // move the player
                this.player.x += (player.getColumn () - player.getPrevColumn ()) * this.tileSize;
                this.player.y += (player.getRow () - player.getPrevRow ()) * this.tileSize;

                // iterate through all crates
                crates.map (function (crate, index) {

                    // did this crate move?
                    if (crate.hasMoved ()) {

                        // move crate sprite accordingly
                        this.crates[index].x += (crates[index].getColumn () - crates[index].getPrevColumn ()) * this.tileSize;
                        this.crates[index].y += (crates[index].getRow () - crates[index].getPrevRow ()) * this.tileSize;
                    }
                }.bind (this))
            }
        }

        // can't the player move?
        else {

            // if we aren't pressing any arrow key...
            if (!this.downKey.isDown && !this.upKey.isDown && !this.leftKey.isDown && !this.rightKey.isDown) {

                // now we can move again
                this.canMove = true;
            }
        }
    }
}

new Game(
    WebGL(),                // renderer
    GlobalVar('Phaser4'),   // global variables
    Parent('thegame'),      // <div> element where to render
    Size(480, 480, 1),      // game size
    Scenes(SokobanGame)     // game scenes
);

And this is the content of textures.js:

import { PixelTexture } from '@phaserjs/phaser/textures/types';
import { PICO8 } from '@phaserjs/phaser/textures/palettes';

export const floorTexture = PixelTexture({
    data: [
        '66666',
        '66666',
        '66666',
        '66666',
        '66666'
    ],
    pixelWidth: 12,
    pixelHeight: 12,
    palette: PICO8
});

export const wallTexture = PixelTexture({
    data: [
        '11111',
        '11111',
        '11111',
        '11111',
        '11111'
    ],
    pixelWidth: 12,
    pixelHeight: 12,
    palette: PICO8
});

export const goalTexture = PixelTexture({
    data: [
        '66666',
        '66866',
        '68886',
        '66866',
        '66666'
    ],
    pixelWidth: 12,
    pixelHeight: 12,
    palette: PICO8
});

export const playerTexture = PixelTexture({
    data: [
        '.CCC.',
        '.FFF.',
        'CC.CC',
        '.CCC.',
        '.C.C.'
    ],
    pixelWidth: 12,
    pixelHeight: 12,
    palette: PICO8
});

export const crateTexture = PixelTexture({
    data: [
        '55555',
        '54.45',
        '5...5',
        '54.45',
        '55555'
    ],
    pixelWidth: 12,
    pixelHeight: 12,
    palette: PICO8
});

It’s not that much, but it’s the first Phaser 4 working game you can find in the web. I will add swipe control and animations as soon as I’ll figure out how to handle some callbacks, meanwhile download the source code, Sokoban class included.