Do you like my tutorials?

Then consider supporting me on Ko-fi

Talking about Platformer game, Game development, HTML5, Javascript, Phaser and TypeScript.

Although Phaser is shipped with Arcade, a great physics engine suitable for most platformer games, sometimes is good to reinvent the wheel basically for two reasons:

1 – Understanding how things work under the hood. I am not talking about complex tasks which would require a lot of time, but everybody should be able to code a basic platformer using no physics engines.

Do you really think there were physics engines in the days of 8bit platformers like Jet Set Willy? Absolutely not, it was all scripted.

2 – Arcade physics does not support slopes, and I want my platformer engine to feature slopes. So rather than tweak the engine like I did some years ago, I’ll write my simple script which is absolutely simpler than Arcade but it fits my need.

I already built custom physics engine for my games, like the one used in Space to Jump, so here’s my simple platformer engine with slopes:

Move with A and D, jump with W. Try to walk up and down along the slopes and jump on them.

Level has been built with Tiled editor and at the moment has three tiles: slope from left to right, normal tile and slope from right to left.

Slopes have 45 degrees of inclination so they can lead from a row to another.

Player behavior changes according to tile being walked on.

Look at the commented 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.

HTML
<!DOCTYPE html>

<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, 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.

CSS
/* remove margin and padding from all elements */
* {
    padding : 0;
    margin : 0;
}

/* set body background color */
body {
    background-color : #000000;    
}

/* Disable browser handling of all panning and zooming gestures. */
canvas {
    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.

TypeScript
// CONFIGURABLE GAME OPTIONS
// changing these values will affect gameplay

export const GameOptions : any = {

    gameSize : {
        width               : 480,      // width of the game, in pixels
        height              : 320       // height of the game, in pixels
    },
    
    gameBackgroundColor     : 0x000000, // game background color

    playerSpeed             : 50,       // player speed, in pixels per second
    gravity                 : 400,      // game gravity
    jumpForce               : 200       // player jump force       
    
}

main.ts

This is where the game is created, with all Phaser related options.

TypeScript
// MAIN GAME FILE

// modules to import
import Phaser from 'phaser';                            // Phaser
import { PreloadAssets } from './scenes/preloadAssets'; // preloadAssets scene
import { PlayGame } from './scenes/playGame';           // playGame scene
import { GameOptions } from './gameOptions';            // game options

// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
    mode        : Phaser.Scale.FIT,                     // adjust size to automatically fit in the window
    autoCenter  : Phaser.Scale.CENTER_BOTH,             // center the game horizontally and vertically
    parent      : 'thegame',                            // DOM id where to render the game
    width       : GameOptions.gameSize.width,           // game width, in pixels
    height      : GameOptions.gameSize.height           // game height, in pixels
}

// game configuration object
const configObject : Phaser.Types.Core.GameConfig = { 
    type            : Phaser.WEBGL,                     // game renderer
    backgroundColor : GameOptions.gameBackgroundColor,  // game background color
    scale           : scaleObject,                      // scale settings
    scene           : [                                 // array with game scenes
        PreloadAssets,                                  // PreloadAssets scene
        PlayGame                                        // PlayGame scene
    ],
    pixelArt        : true                              // set antialiasing as pixel art (no antialiasing)
}

// the game itself
new Phaser.Game(configObject);

scenes > preloadAssets.ts

Here we preload all assets to be used in the game.

TypeScript
// CLASS TO PRELOAD ASSETS

// PreloadAssets class extends Phaser.Scene class
export class PreloadAssets extends Phaser.Scene {
  
    // constructor    
    constructor() {
        super({
            key : 'PreloadAssets'
        });
    }
  
    // method to be called during class preloading
    preload() : void {
 
        // load a spritesheet
        this.load.spritesheet('hero', 'assets/sprites/hero.png', {   // the hero
            frameWidth  : 20,
            frameHeight : 32
        });

        // load a Tiled tilemap
        this.load.tilemapTiledJSON('map', 'assets/maps/level.tmj');
        this.load.image('tiles', 'assets/maps/tiles.png')
    }
  
    // method to be executed when the scene is created
    create() : void {

        // start PlayGame scene
        this.scene.start('PlayGame');
    }
}

scenes > playGame.ts

Main game file, all game logic is stored here.

TypeScript
// THE GAME ITSELF

// modules to import
import { GameOptions }      from '../gameOptions';   // game options   
import { PhysicsPlayer }    from '../physicsPlayer'; // custom class which extends Phaser.GameObjects.Sprite adding some physics features

// PlayGame class extends Phaser.Scene class
export class PlayGame extends Phaser.Scene {

    constructor() {
        super({
            key : 'PlayGame'
        });
    }

    controlKeys : any;              // keys used to move the player
    hero        : PhysicsPlayer;    // the player

    // method to be called once the instance has been created
    create() : void {
        
        // set keyboard controls
        const keyboard : Phaser.Input.Keyboard.KeyboardPlugin = this.input.keyboard as Phaser.Input.Keyboard.KeyboardPlugin; 
        this.controlKeys = keyboard.addKeys({
            'left'  : Phaser.Input.Keyboard.KeyCodes.A,
            'right' : Phaser.Input.Keyboard.KeyCodes.D,
            'jump'  : Phaser.Input.Keyboard.KeyCodes.W
        });

        // define player animations
        this.anims.create({
            key         : 'right',
            frames      : this.anims.generateFrameNumbers('hero', {
                start   : 0,
                end     : 3
            }),
            frameRate   : 10,
            repeat      : -1
        });

        this.anims.create({
            key         : 'left',
            frames      : this.anims.generateFrameNumbers('hero', {
                start   : 4,
                end     : 7
            }),
            frameRate   : 10,
            repeat      : -1
        });

        // create the map and display layer
        const map : Phaser.Tilemaps.Tilemap = this.make.tilemap({
            key : 'map'
        });
        const tiles : Phaser.Tilemaps.Tileset = map.addTilesetImage('tiles', 'tiles') as Phaser.Tilemaps.Tileset;
        const layer : Phaser.Tilemaps.TilemapLayer = map.createLayer(0, tiles, 0, 0) as Phaser.Tilemaps.TilemapLayer;    

        // create the hero and assign properties
        this.hero = new PhysicsPlayer(this, 32, this.game.config.height as number - 48, 'hero');  
        this.hero.gravity = GameOptions.gravity;
        this.hero.jumpForce = GameOptions.jumpForce;
        this.hero.speed = GameOptions.playerSpeed;
        this.hero.layer = layer;  
        this.hero.leftAnimKey = 'left';
        this.hero.rightAnimKey = 'right'; 
    }

    // metod to be called at each frame
    // time: milliseconds passed since the scene has been created
    // delta: milliseconds passed since previous frame
    update(time : number, delta : number) { 

        // check if the player must jump
        if (this.controlKeys.jump.isDown) {
            this.hero.jump();
        }

        // check hero movement direction according to keys pressed
        this.hero.movement = PhysicsPlayer.movingDirection.NONE;
        if (this.controlKeys.right.isDown && !this.controlKeys.left.isDown) {
            this.hero.movement = PhysicsPlayer.movingDirection.RIGHT;
        }
        if (!this.controlKeys.right.isDown && this.controlKeys.left.isDown) {
            this.hero.movement = PhysicsPlayer.movingDirection.LEFT;
        }

        // advance physics simulation
        this.hero.step(delta);
    }
}

physicsPlayer.ts

Custom class to add some basic physics to a Phaser sprite.

Content of terrainType enum should be edited if you use your own Tiled exported level.

TypeScript
// THE PHYSICS SPRITE

// PhysicsPlayer class extends Phaser.GameObjects.Sprite
export class PhysicsPlayer extends Phaser.Physics.Arcade.Sprite {

    jumping         : boolean;                          // is the player jumping?
    jumpForce       : number;                           // jump force
    gravity         : number;                           // gravity affecting the player
    speed           : number;                           // player movement speed
    velocity        : Phaser.Math.Vector2;              // actual player velocity
    movement        : PhysicsPlayer.movingDirection;    // movement direction
    layer           : Phaser.Tilemaps.TilemapLayer;     // Tiled layer where to play
    leftAnimKey     : string;                           // key used for left walk animation
    rightAnimKey    : string;                           // key used for right walk animation

    constructor(scene : Phaser.Scene, posX : number, posY : number, key : string) {
        super(scene, posX, posY, key);
        this.jumping = false;
        this.velocity = new Phaser.Math.Vector2(0, 0);
        scene.add.existing(this);
    }

    // method to make player jump, if not already jumping
    jump() : void {
        if (!this.jumping) {
            this.jumping = true;
            this.velocity.y = this.jumpForce;
        }
    }

    // method to find the closest tile below player's feet
    raycastBottomTile() : Phaser.Tilemaps.Tile {
        const sensorPoint : Phaser.Math.Vector2 = this.getCenter();
        let tile : Phaser.Tilemaps.Tile = this.layer.getTileAtWorldXY(sensorPoint.x, sensorPoint.y, true);
        while (tile.index == PhysicsPlayer.terrainType.NONE) {
            tile = this.layer.getTileAtWorldXY(sensorPoint.x, tile.getCenterY() + tile.height, true);
        }  
        return tile; 
    }

    // method to advance simulation
    // milliseconds: amount of milliseconds passed
    step(milliseconds : number) : void {

        const s : number = milliseconds / 1000;

        // handle jumps
        if (this.jumping) {
            this.setY(this.y - this.velocity.y * s);
            this.velocity.y -= this.gravity * s;
        }

        // handlle movements
        switch (this.movement) {
            case PhysicsPlayer.movingDirection.RIGHT :
                this.velocity.x = this.speed;
                this.anims.play(this.rightAnimKey, true);
                break;
            case PhysicsPlayer.movingDirection.LEFT :
                this.velocity.x = -this.speed;
                this.anims.play(this.leftAnimKey, true);
                break;
            case PhysicsPlayer.movingDirection.NONE :
                this.velocity.x = 0;
                this.anims.stop();
        }

        // update x position
        this.setX(this.x + this.velocity.x * s);

        // get tile below the player
        const tileBelow : Phaser.Tilemaps.Tile = this.raycastBottomTile();

        switch (tileBelow.index) {

            // slope like this one: /
            case PhysicsPlayer.terrainType.SLOPE_FROM_LEFT : 
                var groundY : number = tileBelow.getBottom() - this.displayHeight / 2 - (this.x - tileBelow.getLeft());
                if (!this.jumping) {
                    this.setY(groundY);
                }
                else {
                    if (this.y > groundY) {
                        this.jumping = false;
                        this.setY(groundY);      
                    }
                }
                break;

            // plain terrain
            case PhysicsPlayer.terrainType.PLAIN :  
                var groundY : number = tileBelow.getTop() - this.displayHeight / 2;
                if (!this.jumping) {
                    this.setY(groundY)
                }
                else {
                    if (this.y > tileBelow.getTop() - this.displayHeight / 2) {
                        this.jumping = false;
                        this.setY(groundY);      
                    }    
                }
                break;

            // slope like this one: \
            case PhysicsPlayer.terrainType.SLOPE_FROM_RIGHT :  
                var groundY : number = tileBelow.getBottom() - this.displayHeight / 2 - (tileBelow.getRight() - this.x);
                if (!this.jumping) {
                    this.setY(groundY);
                }
                else {
                    if (this.y >= groundY) {
                        this.jumping = false;
                        this.setY(groundY);      
                    }    
                }
                break;
        }
    }
}

export namespace PhysicsPlayer {
    
    // moving directions
    export enum movingDirection {
        NONE,
        LEFT,
        RIGHT
    }

    // terrain types
    // YOU MAY NEED TO CHANGE THESE VALUES ACCORDIG TO YOUR TILED EXPORTED LEVEL
    export enum terrainType {
        NONE = -1,
        SLOPE_FROM_LEFT = 1,
        PLAIN,
        SLOPE_FROM_RIGHT
    }
}

There is still a lot to do, such as walls to block player movements or holes to make player fall down, but I’ll show you how to build them in next tutorial, meanwhile download the entire project and build something useful out of it.

Don’t know where to start? I have a free guide for you.

Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.