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.