Get the full commented source code of

HTML5 Suika Watermelon Game

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.