Talking about Space is Key game, Game development, HTML5, Javascript, Phaser and TypeScript.
Space is Key is still a fun game, and above all its game mechanics are very simple, so once you build the engine, it’s just a matter of level design.
So I am starting developing a Space is Key engine capable of reading an object with level information and build the game around it.
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.
At the moment it can support any number of floors per level, although the recommended value ranges from 2 to 4, any combination of background and foreground color, an unlimited amount of obstacles, and a custom square with its own size, speed, gravity and jump force.
Have a look at this three levels example:
Jump by clicking or tapping on the canvas (no space key at the moment, ironically), do not hit obstacles.
As you can see the first level has three floors, the second level has four floors, and the third level has just two floors, each one with its own palette and its own square with different properties.
Here is the source code, which 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. There aren’t that much game options here, because most of them are stored in level configuration file.
// CONFIGURABLE GAME OPTIONS
// changing these values will affect gameplay
export const GameOptions : any = {
level : {
width : 800,
height : 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'
});
}
level : number;
floorLevel : number;
theSquare : Phaser.Physics.Arcade.Sprite;
canJump : boolean;
backgroundGroup : Phaser.GameObjects.Group;
groundGroup : Phaser.Physics.Arcade.Group;
obstacleGroup : 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.backgroundGroup = this.add.group();
this.groundGroup = this.physics.add.group();
this.obstacleGroup = this.physics.add.group();
this.level = 0;
this.floorLevel = 0;
this.canJump = true;
this.drawLevel();
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.emitter.setDepth(2);
this.theSquare = this.physics.add.sprite(0, 0, 'tile');
this.theSquare.setDepth(1);
this.placeSquare();
this.input.on('pointerdown', this.squareJump, this);
}
drawLevel() : void {
this.backgroundGroup.clear(true, true);
this.groundGroup.clear(true, true);
this.obstacleGroup.clear(true, true);
let floorHeight : number = GameOptions.level.height / Levels[this.level].floors.length;
for (let i : number = 0; i < Levels[this.level].floors.length; i ++) {
let background : Phaser.GameObjects.TileSprite = this.add.tileSprite(0, floorHeight * i, GameOptions.level.width, floorHeight, 'tile');
background.setTint(Levels[this.level].floors[i].colors.background);
background.setOrigin(0);
this.backgroundGroup.add(background);
let floor : Phaser.GameObjects.TileSprite = this.add.tileSprite(- Levels[this.level].floors[this.floorLevel].square.size, floorHeight * i + floorHeight, GameOptions.level.width + 2 * Levels[this.level].floors[this.floorLevel].square.size, 16, 'tile');
floor.setOrigin(0);
this.physics.add.existing(floor);
this.groundGroup.add(floor);
// @ts-ignore
floor.body.pushable = false;
if (Levels[this.level].floors[i].obstacles) {
for (let j : number = 0; j < Levels[this.level].floors[i].obstacles.length; j ++) {
let obstacleX : number = (i % 2 == 0) ? Levels[this.level].floors[i].obstacles[j].start : this.game.config.width as number - Levels[this.level].floors[i].obstacles[j].start - Levels[this.level].floors[i].obstacles[j].width;
let obstacleY : number = floorHeight * i + floorHeight - Levels[this.level].floors[i].obstacles[j].ground - Levels[this.level].floors[i].obstacles[j].height;
let spike : Phaser.GameObjects.TileSprite = this.add.tileSprite(obstacleX, obstacleY, Levels[this.level].floors[i].obstacles[j].width, Levels[this.level].floors[i].obstacles[j].height, 'tile');
spike.setTint(Levels[this.level].floors[i].colors.foreground);
spike.setOrigin(0);
this.physics.add.existing(spike);
// @ts-ignore
spike.body.pushable = false;
this.obstacleGroup.add(spike);
}
}
}
}
placeSquare() : void {
if (this.jumpTween) {
this.jumpTween.stop();
this.theSquare.angle = 0;
}
this.theSquare.displayWidth = Levels[this.level].floors[this.floorLevel].square.size;
this.theSquare.displayHeight = Levels[this.level].floors[this.floorLevel].square.size;
this.theSquare.setGravityY(Levels[this.level].floors[this.floorLevel].square.gravity);
this.theSquare.setTint(Levels[this.level].floors[this.floorLevel].colors.foreground);
this.theSquare.setVelocityX((this.floorLevel % 2 == 0) ? Levels[this.level].floors[this.floorLevel].square.speed : - Levels[this.level].floors[this.floorLevel].square.speed);
this.theSquare.setVelocityY(0);
this.theSquare.setX((this.floorLevel % 2 == 0) ? 0 - Levels[this.level].floors[this.floorLevel].square.size : GameOptions.level.width + Levels[this.level].floors[this.floorLevel].square.size);
this.theSquare.setY(GameOptions.level.height / Levels[this.level].floors.length * (this.floorLevel + 1) - Levels[this.level].floors[this.floorLevel].square.size / 2);
this.theSquare.setData('maxY', this.theSquare.y);
this.canJump = true;
}
squareJump() : void {
if (this.canJump) {
this.canJump = false;
this.theSquare.setVelocityY(Levels[this.level].floors[this.floorLevel].square.jumpForce * -1);
let jumpAngle : number = this.floorLevel % 2 == 0 ? 180 : -180;
this.jumpTween = this.tweens.add({
targets : this.theSquare,
angle : this.theSquare.angle + jumpAngle,
duration : Levels[this.level].floors[this.floorLevel].square.jumpForce / Levels[this.level].floors[this.floorLevel].square.gravity * 2000
})
}
}
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 {
console.log(this.theSquare.getData('maxY'))
if (this.theSquare.y > this.theSquare.getData('maxY')) {
this.theSquare.y = this.theSquare.getData('maxY');
}
this.physics.collide(this.theSquare, this.groundGroup);
this.physics.overlap(this.theSquare, this.obstacleGroup, this.handleCollision, undefined, this);
// @ts-ignore
if (this.theSquare.body.touching.down) {
this.canJump = true;
}
if ((this.theSquare.x > GameOptions.level.width && this.floorLevel % 2 == 0) || (this.theSquare.x < 0 && this.floorLevel % 2 == 1)) {
this.floorLevel ++;
if (this.floorLevel == Levels[this.level].floors.length) {
this.floorLevel = 0;
this.level ++;
if (this.level == Levels.length) {
this.level = 0;
}
this.drawLevel();
}
this.placeSquare();
}
}
}
levels.ts
I am storing levels information in a separate file.
export const Levels : any = [
// level 1
{
floors : [
// floor 0
{
square : {
size : 24,
speed : 170,
gravity : 200,
jumpForce : 200
},
colors : {
foreground : 0x6598fd,
background : 0x003232
},
obstacles : [
{
ground : 0,
start : 375,
width : 30,
height : 60
},
{
ground : 0,
start : 275,
width : 30,
height : 40
},
{
ground : 0,
start : 475,
width : 30,
height : 40
},
]
},
// floor 1
{
square : {
size : 30,
speed : 310,
gravity : 450,
jumpForce : 210
},
colors : {
foreground : 0x003232,
background : 0x6598fd
},
obstacles : [
{
ground : 0,
start : 520,
width : 40,
height : 40
},
{
ground : 0,
start : 240,
width : 40,
height : 40
}
]
},
// floor 2
{
square : {
size : 24,
speed : 170,
gravity : 650,
jumpForce : 410
},
colors : {
foreground : 0x6598fd,
background : 0x003232
},
obstacles : [
{
ground : 0,
start : 380,
width : 40,
height : 60
},
{
ground : 100,
start : 350,
width : 10,
height : 10
},
{
ground : 100,
start : 440,
width : 10,
height : 10
}
]
}
]
},
// level 2
{
floors : [
// floor 0
{
square : {
size : 40,
speed : 100,
gravity : 400,
jumpForce : 200
},
colors : {
foreground : 0x323332,
background : 0x973263
},
obstacles : [
{
ground : 0,
start : 190,
width : 20,
height : 20
},
{
ground : 0,
start : 390,
width : 20,
height : 20
},
{
ground : 0,
start : 590,
width : 20,
height : 20
}
]
},
// floor 1
{
square : {
size : 24,
speed : 500,
gravity : 450,
jumpForce : 210
},
colors : {
foreground : 0x973263,
background : 0x323332
},
obstacles : [
{
ground : 0,
start : 380,
width : 40,
height : 40
}
]
},
// floor 2
{
square : {
size : 24,
speed : 170,
gravity : 650,
jumpForce : 300
},
colors : {
foreground : 0x323332,
background : 0x973263
},
obstacles : [
{
ground : 0,
start : 185,
width : 80,
height : 30
},
{
ground : 0,
start : 365,
width : 70,
height : 40
},
{
ground : 0,
start : 565,
width : 80,
height : 30
},
]
},
// floor 3
{
square : {
size : 24,
speed : 170,
gravity : 100,
jumpForce : 100
},
colors : {
foreground : 0x973263,
background : 0x323332
},
obstacles : [
{
ground : 0,
start : 310,
width : 180,
height : 30
},
{
ground : 80,
start : 365,
width : 70,
height : 70
}
]
}
]
},
// level 3
{
floors : [
// floor 0
{
square : {
size : 60,
speed : 120,
gravity : 100,
jumpForce : 200
},
colors : {
foreground : 0x3363fc,
background : 0xfb96c7
},
obstacles : [
{
ground : 0,
start : 250,
width : 300,
height : 60
},
{
ground : 0,
start : 370,
width : 60,
height : 120
}
]
},
// floor 1
{
square : {
size : 20,
speed : 160,
gravity : 1200,
jumpForce : 600
},
colors : {
foreground : 0xfb96c7,
background : 0x3363fc
},
obstacles : [
{
ground : 0,
start : 190,
width : 20,
height : 40
},
{
ground : 0,
start : 290,
width : 20,
height : 40
},
{
ground : 0,
start : 490,
width : 20,
height : 40
},
{
ground : 0,
start : 590,
width : 20,
height : 40
}
]
}
]
}
]
Basically you should be able to create your own version of Space is KEY right now, but there are a couple of improvements I want to make, such as a pixel perfect collision detection, which Arcade physics does not feature, and the capability of adding text messages like in the original game. But you can start creating your levels if you 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.