Talking about Space is Key game, Game development, HTML5, Javascript, Phaser and TypeScript.
While the world is waiting for clarification from Unity regarding its future pricing policies, I would like to remind you that Phaser is completely free and today I am updating a big classic like Space is Key to Phaser 3.60 using TypeScript.
All posts in this tutorial series:
Step 1: First TypeScript prototype using Arcade physics and tweens.
Step 2: Creation of some kind of proprietary engine to manage any kind of level
Step 3: Introducing pixel perfect collisions and text messages.
Step 4: Removing Arcade physics and tweens, only using delta time between frames.
Step 5: Using Tiled to draw levels.
The original game was written by Christopher Jeffrey in Flash, and it’s an action game where you have to jump all the obstacles by dying as few times as possible.
Look at the prototype:
Jump by clicking or tapping on the canvas, do not fall on obstacles.
The game, as you can see, is very simple, and the masterstroke, on Christopher part, was the level design, so highly addictive.
Here is the source code, uncommented because you can check the logic by looking at the Flash prototype or the Phaser 2 prototype. Anyway, it consists in one HTML file, one CSS file and five TypeScript files.
index.html
The web page which hosts the game, to be run inside thegame element.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="style.css">
<script src="main.js"></script>
</head>
<body>
<div id = "thegame"></div>
</body>
</html>
style.css
The cascading style sheets of the main web page.
* {
padding : 0;
margin : 0;
}
body {
background-color: #000000;
}
canvas {
touch-action : none;
-ms-touch-action : none;
}
gameOptions.ts
Configurable game options. It’s a good practice to place all configurable game options, if possible, in a single and separate file, for a quick tuning of the game. I also grouped the variables to keep them more organized.
// CONFIGURABLE GAME OPTIONS
// changing these values will affect gameplay
export const GameOptions : any = {
level : {
width : 800,
height : 600,
start : {
x : 0,
y : 0
},
colors : [0xff0000, 0x00ff00, 0x0000ff]
},
floor : {
tickness : 20,
amount : 6
},
square : {
size : 16,
speed : 170,
gravity : 450,
jump : {
force : 210,
time : 600
}
}
}
main.ts
This is where the game is created, with all Phaser related options.
// MAIN GAME FILE
// modules to import
import Phaser from 'phaser';
import { PreloadAssets } from './preloadAssets';
import { PlayGame } from './playGame';
// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
mode : Phaser.Scale.FIT,
autoCenter : Phaser.Scale.CENTER_BOTH,
parent : 'thegame',
width : 800,
height : 600
}
// game configuration object
const configObject : Phaser.Types.Core.GameConfig = {
type : Phaser.AUTO,
backgroundColor : 0x000000,
scale : scaleObject,
scene : [PreloadAssets, PlayGame],
physics : {
default : 'arcade',
arcade : {
gravity : {
y : 0
}
}
}
}
// the game itself
new Phaser.Game(configObject);
preloadAssets.ts
Here we preload all assets to be used in the game. Actually, just one tile I am resizing and tinting when needed.
// CLASS TO PRELOAD ASSETS
// this class extends Scene class
export class PreloadAssets extends Phaser.Scene {
// constructor
constructor() {
super({
key : 'PreloadAssets'
});
}
// method to be called during class preloading
preload() : void {
// this is how to load an image
this.load.image('tile', 'assets/sprites/tile.png');
}
// method to be called once the instance has been created
create() : void {
// call PlayGame class
this.scene.start('PlayGame');
}
}
playGame.ts
Main game file, all game logic is stored here.
// THE GAME ITSELF
import { GameOptions } from './gameOptions';
import { Levels } from './levels';
// this class extends Scene class
export class PlayGame extends Phaser.Scene {
constructor() {
super({
key : 'PlayGame'
});
}
floorLevel : number;
theSquare : Phaser.Physics.Arcade.Sprite;
canJump : boolean;
groundGroup : Phaser.Physics.Arcade.Group;
spikeGroup : Phaser.Physics.Arcade.Group;
jumpTween : Phaser.Tweens.Tween;
emitter : Phaser.GameObjects.Particles.ParticleEmitter;
// method to be called once the instance has been created
create() : void {
this.groundGroup = this.physics.add.group();
this.spikeGroup = this.physics.add.group();
this.floorLevel = 0;
this.canJump = true;
let floorHeight : number = GameOptions.level.height / GameOptions.floor.amount;
for (let i : number = 0; i < GameOptions.floor.amount; i ++) {
let background : Phaser.GameObjects.TileSprite = this.add.tileSprite(GameOptions.level.start.x, GameOptions.level.start.y + floorHeight * i, GameOptions.level.width, floorHeight, 'tile');
background.setTint(Phaser.Utils.Array.GetRandom(GameOptions.level.colors));
background.setAlpha(0.4);
background.setOrigin(0);
let floor : Phaser.GameObjects.TileSprite = this.add.tileSprite(GameOptions.level.start.x, GameOptions.level.start.y + floorHeight * i + floorHeight - GameOptions.floor.tickness, GameOptions.level.width, GameOptions.floor.tickness, 'tile');
floor.setTint(Phaser.Utils.Array.GetRandom(GameOptions.level.colors));
floor.setOrigin(0);
this.physics.add.existing(floor);
this.groundGroup.add(floor);
// @ts-ignore
floor.body.pushable = false;
for (let j : number = 0; j < Levels[i].length; j ++) {
let spike : Phaser.GameObjects.TileSprite = this.add.tileSprite(GameOptions.level.start.x + Levels[i][j].x, GameOptions.level.start.y + floorHeight * i + floorHeight - GameOptions.floor.tickness, Levels[i][j].width, Levels[i][j].height, 'tile');
spike.setTint(floor.tintTopLeft);
spike.setOrigin(0.5, 1);
this.physics.add.existing(spike);
// @ts-ignore
spike.body.pushable = false;
this.spikeGroup.add(spike);
}
}
this.emitter = this.add.particles(0, 0, 'tile', {
gravityY : 20,
speed : {
min : 20,
max : 50
},
scale : {
min : 0.05,
max : 0.1
},
lifespan : 800,
alpha : {
start : 1,
end: 0
},
emitting : false
});
this.theSquare = this.physics.add.sprite(0, 0, 'tile');
this.theSquare.displayWidth = GameOptions.square.size;
this.theSquare.displayHeight = GameOptions.square.size;
this.theSquare.setGravityY(GameOptions.square.gravity);
this.placeSquare();
this.input.on('pointerdown', this.squareJump, this);
}
placeSquare() : void {
this.theSquare.setTint(Phaser.Utils.Array.GetRandom(GameOptions.level.colors));
this.theSquare.setVelocity((this.floorLevel % 2 == 0) ? GameOptions.square.speed : - GameOptions.square.speed, 0);
this.canJump = true;
this.theSquare.setPosition((this.floorLevel % 2 == 0) ? GameOptions.level.start.x : GameOptions.level.start.x + GameOptions.level.width, GameOptions.level.start.y + GameOptions.level.height / GameOptions.floor.amount * (this.floorLevel + 1) - GameOptions.floor.tickness - GameOptions.square.size / 2);
if (this.jumpTween) {
this.jumpTween.stop();
this.theSquare.angle = 0;
}
}
squareJump() : void {
if (this.canJump) {
this.canJump = false;
this.theSquare.setVelocityY(GameOptions.square.jump.force * -1);
let jumpAngle : number = this.floorLevel % 2 == 0 ? 180 : -180;
this.jumpTween = this.tweens.add({
targets : this.theSquare,
angle : this.theSquare.angle + jumpAngle,
duration : GameOptions.square.jump.time
})
}
}
handleCollision() : void {
this.emitter.x = this.theSquare.x;
this.emitter.y = this.theSquare.y;
this.emitter.explode(32);
this.emitter.forEachAlive((particle : Phaser.GameObjects.Particles.Particle) => {
particle.tint = this.theSquare.tintTopLeft;
}, this);
this.placeSquare();
}
update() : void {
this.physics.collide(this.theSquare, this.groundGroup);
this.physics.overlap(this.theSquare, this.spikeGroup, this.handleCollision, undefined, this);
// @ts-ignore
if (this.theSquare.body.touching.down) {
this.canJump = true;
}
if ((this.theSquare.x > GameOptions.level.start.x + GameOptions.level.width && this.floorLevel % 2 == 0) || (this.theSquare.x < GameOptions.level.start.x && this.floorLevel % 2 == 1)) {
this.floorLevel = (this.floorLevel + 1) % GameOptions.floor.amount;
this.placeSquare();
}
}
}
levels.ts
I am storing levels information in a separate file.
export const Levels : any = [
// floor 0
[
{
width: 60,
height: 30,
x: 200
},
{
width: 60,
height: 30,
x: 400
}
],
// floor 1
[
{
width: 40,
height: 30,
x: 250
},
{
width: 70,
height: 25,
x: 450
},
{
width: 30,
height: 20,
x: 100
}
],
// floor 2
[
{
width: 10,
height: 35,
x: 150
},
{
width: 10,
height: 35,
x: 300
},
{
width: 10,
height: 35,
x: 550
}
],
// floor 3
[
{
width: 80,
height: 10,
x: 280
},
{
width: 80,
height: 10,
x: 480
}
],
// floor 4
[
{
width: 10,
height: 10,
x: 100
},
{
width: 10,
height: 10,
x: 200
},
{
width: 10,
height: 10,
x: 300
},
{
width: 10,
height: 10,
x: 400
},
{
width: 10,
height: 10,
x: 500
},
{
width: 10,
height: 10,
x: 600
}
],
// floor 5
[
{
width: 10,
height: 40,
x: 350
}
]
]
Doing this was easy, next time I’ll try to create some challenging and fun levels, just like Christopher did. Meanwhile, download the source code.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.