First Phaser 4 version released, let’s have a look at it by building a HTML5 Sokoban game
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.