Do you like my tutorials?

Then consider supporting me on Ko-fi

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.

Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.