Do you like my tutorials?

Then consider supporting me on Ko-fi

Talking about Teeter Up game, Box2D, Game development, HTML5, Javascript, Phaser and TypeScript.

During the past few days I’ve been watching a few Netflix video games on my iPhone, and I particularly liked Teeter (Up).

It’s a simple physics game with a ball, a platform, and a hole. What could possibly go wrong?

In this first part of the series, I am focusing on platform movement. There’s a ball, a dynamic body, on a platform, which is a static or a kinematic body.

When the player taps on the left side of the screen, the left side of the bar raises, and the same concept applies to the right side of the screen.

So I am rotating the platform accordingly, around its center, and moving it up using trigonometry.

This is the result:

Tap left and right to raise the bar. Don’t make the ball fall.

To use Box2D powered by Planck.js you should install this package with:

npm install –save planck

If you don’t know how to install a npm package or set up a project this way, I wrote a free minibook explaining everything you need to know to get started.

This first step is very simple, but since I am a nice person, I commented each and every line of code.

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. I also grouped the variables to keep them more organized.

TypeScript
// CONFIGURABLE GAME OPTIONS
// changing these values will affect gameplay
 
export const GameOptions : any = {

    // game canvas size and color
    game : {

        // width of the game, in pixels
        width : 1080,

        // height of the game, in pixels
        height : 1920,

        // game background color
        bgColor : 0x444444
    },

    // Box2D related options
    Box2D : {

        // gravity, in meters per second squared
        gravity : 10,

        // pixels per meters
        worldScale : 120
    },

    // bar properties
    bar : {

        // bar width, in pixels
        width : 1000,

        // bar height, in pixels
        height : 40,

        // bar Y start coordinate, in pixels
        startY : 1800,

        // rotation speed, in degrees per second
        speed : 25
    },

    // ball radius, in pixels
    ballRadius : 25
}

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';
import { PreloadAssets } from './scenes/preloadAssets';
import { PlayGame } from './scenes/playGame';
import { GameOptions } from './gameOptions';

// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
    mode : Phaser.Scale.FIT,
    autoCenter : Phaser.Scale.CENTER_BOTH,
    parent : 'thegame',
    width : GameOptions.game.width,
    height : GameOptions.game.height
}

// game configuration object
const configObject : Phaser.Types.Core.GameConfig = { 
    type : Phaser.AUTO,
    backgroundColor : GameOptions.game.bgColor,
    scale : scaleObject,
    scene : [PreloadAssets, PlayGame]
}

// 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

// 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 images
        this.load.image('ball', 'assets/sprites/ball.png');
        this.load.image('bar', 'assets/sprites/bar.png');
    }
  
    // method to be called once the instance has been created
    create() : void {
 
        // call 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 Planck, { Box, Circle } from 'planck';
import { toMeters, toPixels } from '../planckUtils';
import { GameOptions } from '../gameOptions';

// enum to represent bar side
enum barSide {
    LEFT,
    RIGHT,
    NONE
}

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

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

    world : Planck.World;           // the Box2D world  
    liftingSide : barSide;          // the lifting side of the bar
    bar : Planck.Body;              // the bar itself                           

    // method to be called once the instance has been created
    create() : void {

        // when we start playing, no bar side is lifting
        this.liftingSide = barSide.NONE;

        // create a Box2D world with gravity
        this.world = new Planck.World(new Planck.Vec2(0, GameOptions.Box2D.gravity));

        // set bar and ball sprites
        const barSprite : Phaser.GameObjects.TileSprite = this.add.tileSprite(this.game.config.width as number / 2, GameOptions.bar.startY, GameOptions.bar.width, GameOptions.bar.height, 'bar');
        const ballSprite : Phaser.GameObjects.Sprite = this.add.sprite(this.game.config.width as number / 2, GameOptions.bar.startY - GameOptions.bar.height / 2 - GameOptions.ballRadius, 'ball');
        ballSprite.setDisplaySize(GameOptions.ballRadius * 2, GameOptions.ballRadius * 2);

        // create the bar kinematic body
        this.bar = this.world.createKinematicBody({
            position : new Planck.Vec2(toMeters(barSprite.x), toMeters(barSprite.y))
        })

        // attach a fixture to bar body
        this.bar.createFixture({
            shape : new Box(toMeters(GameOptions.bar.width / 2), toMeters(GameOptions.bar.height / 2)),
            density : 1,
            friction : 0,
            restitution : 0
        })

        // set custom bar body user data
        this.bar.setUserData({
            sprite : barSprite
        })

        // create ball dynamic body
        const ball : Planck.Body = this.world.createDynamicBody({
            position : new Planck.Vec2(toMeters(ballSprite.x), toMeters(ballSprite.y))    
        })
        
        // attach a fixture to ball body
        ball.createFixture({
            shape : new Circle(toMeters(GameOptions.ballRadius)),
            density : 1,
            friction : 0,
            restitution : 0
        })
        
        // set custom ball body user data
        ball.setUserData({
            sprite : ballSprite
        })

        // listener waiting for pointer to be down (pressed)
        this.input.on('pointerdown', (pointer : Phaser.Input.Pointer) => {

            // check lifting side according to pointer x position
            this.liftingSide = Math.floor(pointer.x / (this.game.config.width as number / 2));       
        });

        // listener waiting for pointer to be up (released)
        this.input.on('pointerup', (pointer : Phaser.Input.Pointer) => {

            // bar is not lifting
            this.liftingSide = barSide.NONE;      
        })
    }

    // method to be executed at each frame
    // totalTime : time, in milliseconds, since the game started
    // deltaTime : time, in milliseconds, passed since previous "update" call
    update(totalTime : number, deltaTime : number) : void {  
        
        // advance world simulation
        this.world.step(deltaTime / 1000, 10, 8);
        this.world.clearForces();

        // do we have to lift the bar?
        if (this.liftingSide != barSide.NONE) {

            // determine delta angle
            const deltaAngle : number = Phaser.Math.DegToRad(GameOptions.bar.speed) * deltaTime / 1000;

            // given the angle, determine bar movement
            const barMovement : number = toMeters((GameOptions.bar.width / 2) * Math.sin(deltaAngle));
            
            // get bar position
            const barPosition : Planck.Vec2 = this.bar.getPosition();

            // set new bar angle according to lifting side
            this.bar.setAngle(this.bar.getAngle() + deltaAngle * (this.liftingSide == barSide.LEFT ? 1 : -1));

            // set new bar position
            this.bar.setPosition(new Planck.Vec2(barPosition.x, barPosition.y - barMovement));
        }

        // loop through all bodies
        for (let body : Planck.Body = this.world.getBodyList() as Planck.Body; body; body = body.getNext() as Planck.Body) {
   
            // get body user data
            const userData : any = body.getUserData();

            // get body position
            const bodyPosition : Planck.Vec2 = body.getPosition();

            // get body angle
            const bodyAngle : number = body.getAngle();

            // update sprite position and rotation accordingly
            userData.sprite.setPosition(toPixels(bodyPosition.x), toPixels(bodyPosition.y));
            userData.sprite.setRotation(bodyAngle);
        }
    }  
}

planckUtils.ts

Useful functions to be used in Planck, just to convert pixels to meters and meters to pixels.

TypeScript
// PLANCK UTILITIES

import { GameOptions } from './gameOptions';
 
// simple function to convert pixels to meters
// pixels : amount of pixels to convert
export function toMeters(pixels : number) : number {
    return pixels / GameOptions.Box2D.worldScale;
}
 
// simple function to convert meters to pixels
// meters : amount of meters to convert
export function toPixels(meters : number) : number {
    return meters * GameOptions.Box2D.worldScale;
}

This is going to be a great tutorial series, because the game is simple but open to various challenges. 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.