Do you like my tutorials?

Then consider supporting me on Ko-fi

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

A lot of readers enjoyed my HTML5 Serious Scramblers prototype, but on oldest mobile devices there was an issue with collisions, since Arcade physics does not feature continuous collision detection.

So I started studying a bit about continuous collision detection, and apart from being a quite CPU expensive task, it can be simplified in some particular cases, like platform games, using Swept AABB collision detection.

Then I made two post about Swept AABB collision detection: understanding physics continuous collision detection using swept AABB method and Minkowski sum and understanding physics continuous collision detection using swept AABB method and Minkowski sum – Part 2: both bodies are moving.

Now it’s time to put these concepts into an actual game prototype, like the one you are about to see.

Before we start, remember this is not a physics engine and it’s not meant to replace your favourite physics engine.

It’s just another approach to simple collision detection, useful for you to learn something light and new.

Let’s see this first example:

Focus on the canvas, then move the yellow box with LEFT and RIGHT arrow keys, don’t fall down and don’t let platforms bring you to the top of the screen.

Everything is sooooooo slow, just to let you see collision detection works.

Now, let’s speed up a lot the whole stuff:

Try to play this one: apart from being almost unplayable, collision detection at this speed would be impossible without continuous collision detection, but I managed to do it with Swept AABB.

Finally, a playable prototype, the possible base for future improvements:

Did you see it? A continuous collision detection written in a few lines. Let’s see the source code:

index.html

The web page which hosts the game, just the bare bones of HTML and main.ts is called. No changes have been made.

Also look at the thegame div, this is where the game runs.

<!DOCTYPE html>
<html>
    <head>
        <style>
            body {
                margin: 0px;
            }
        </style>
        <script src = "main.js"></script>
    </head>
<body>
    <div id = "thegame"></div>
</body>
</html>

main.ts

The main TypeScript file, the one called by index.html.
Here we import most of the game libraries and define both Scale Manager object and Physics object.

Here we also initialize the game itself.

// 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: 750,
    height: 1334
}

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

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

gameOptions.ts

Game options which can be changed to tune the gameplay are stored in a separate module, ready to be reused.

// CONFIGURABLE GAME OPTIONS

export const GameOptions = {

    // first platform vertical position. 0 = top of the screen, 1 = bottom of the screen
    firstPlatformPosition: 4 / 10,

    // game gravity, which only affects the hero
    gameGravity: 600,

    // hero speed, in pixels per second
    heroSpeed: 80,

    // platform speed, in pixels per second
    platformSpeed: 20,

    // platform length range, in pixels
    platformLengthRange: [150, 250],

    // platform horizontal distance range from the center of the stage, in pixels
    platformHorizontalDistanceRange: [0, 250],

    // platform vertical distance range, in pixels
    platformVerticalDistanceRange: [150, 300]
}

preloadAssets.ts

Class to preload all assets used in the game.

// CLASS TO PRELOAD ASSETS

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

    // constructor    
    constructor() {
        super({
            key: 'PreloadAssets'
        });
    }

    // preloading assets, the good old way
    preload(): void {
        this.load.image('hero', 'assets/hero.png');
        this.load.image('platform', 'assets/platform.png');
	}

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

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

physicsBox.ts

Class which extends Sprite class and adds physics properties such as velocity.

// PhysicsBox class extends Phaser Sprite class
export default class PhysicsBox extends Phaser.GameObjects.Sprite {

    // vector containing x and y velocity
    velocity: Phaser.Math.Vector2;

    // constructor - arguments: the scene, x and y position and texture key
    constructor(scene: Phaser.Scene, x: number, y: number, key: string) {
        super(scene, x, y, key);
        scene.add.existing(this);

        // physics object has no velocity at the beginning
        this.velocity = new Phaser.Math.Vector2(0, 0);
    }

    // set body velocity - arguments: x and y velocity
    setVelocity(x: number, y: number): void {

        // update velocity property
        this.velocity.x = x;
        this.velocity.y = y;
    }
}

playerSprite.ts

Player extends PhysicsBox which extends Sprite:

// PLAYER SPRITE CLASS    

import PhysicsBox from "./physicsBox";

// player sprite extends PhysicsBox class
export default class PlayerSprite extends PhysicsBox {

    // is the first time player is moving
    firstMove: Boolean = true;

    // constructor
	constructor(scene: Phaser.Scene, x: number, y: number, key: string) {
		super(scene, x, y, key);

        // add the player to the scnee
        scene.add.existing(this);
	}
}

platformSprite.ts

Same thing for the platforms: they extend PhysicsBox class which extends Sprite:

// PLATFORM SPRITE CLASS    

import PhysicsBox from "./physicsBox";

// platform sprite extends PhysicsBox class
export default class PlatformSprite extends PhysicsBox {

    // constructor
    constructor(scene: Phaser.Scene, x: number, y: number, key: string) {
        super(scene, x, y, key);

        // add the platform to the scnee
        scene.add.existing(this);
    }
}

playGame.ts

The game itself, the biggest class, game logic is stored here.

// THE GAME ITSELF

// modules to import
import { GameOptions } from './gameOptions';
import PlayerSprite from './playerSprite';
import PlatformSprite from './platformSprite';
import SweptAABB from './sweptAABB';
import SweptAABBCollisionInfo from './sweptAABBCollisionInfo';

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

    // group which will contain all platforms
    platformGroup: Phaser.GameObjects.Group;

    // the hero of the game
    hero: PlayerSprite;

    // here we store game width once for all
    gameWidth: number;

    // here we store game height once for all
    gameHeight: number;

    // instance of arrow keys to control the player
    arrowKeys: Phaser.Types.Input.Keyboard.CursorKeys;

    // our Swept AABB class
    AABB: SweptAABB;

    // flag to check if any left button has been pressed
    leftPressed: boolean;
 
    // flag to check if any right button has been pressed
    rightPressed: boolean;

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

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

        // new arrow keys instance
        this.arrowKeys = this.input.keyboard.createCursorKeys();
 
        // new Swept AABB instance
        this.AABB = new SweptAABB();

        // save game width value
        this.gameWidth = this.game.config.width as number;

        // save game height value
        this.gameHeight = this.game.config.height as number;

        // create a new physics group
        this.platformGroup = this.add.group();

        // create starting platform
        let platform: PlatformSprite = new PlatformSprite(this, this.gameWidth / 2, this.gameHeight * GameOptions.firstPlatformPosition, "platform");

        // add platform to platform group
        this.platformGroup.add(platform);

        // add the hero
        this.hero = new PlayerSprite(this, this.gameWidth / 2, 0, "hero");

        // place the hero on top of the platform
        this.AABB.placeOnTop(this.hero, platform)

        // we are going to create 10 more platforms which we'll reuse to save resources
        for(let i = 0; i < 10; i ++) {

            // platform creation, as a member of platformGroup physics group
            let platform = new PlatformSprite(this, 0, 0, "platform");

            // add platform to platform group
            this.platformGroup.add(platform);

            // position the platform
            this.positionPlatform(platform);
        }
    }

    // method to position a platform
    positionPlatform(platform: PlatformSprite):void {

        // vertical position
        platform.y = this.getLowestPlatform() + this.randomValue(GameOptions.platformVerticalDistanceRange);

        // horizontal position
        platform.x = this.gameWidth / 2 + this.randomValue(GameOptions.platformHorizontalDistanceRange) * Phaser.Math.RND.sign();

        // platform width
        platform.displayWidth = this.randomValue(GameOptions.platformLengthRange);
    }

    // method to get the lowest platform, returns the position of the lowest platform, in pixels
    getLowestPlatform(): number {

        // lowest platform value is initially set to zero
        let lowestPlatform = 0;

        // get all platforms
        let platforms: PlatformSprite[] = this.platformGroup.getChildren() as PlatformSprite[];

        // loop through all platforms
        for (let platform of platforms) {

            // get the highest value between lowestPlatform and platform y coordinate
            lowestPlatform = Math.max(lowestPlatform, platform.y);
        };

        // return lowest platform coordinate
        return lowestPlatform;
    }

    // method to toss a random value between two elements in an array
    randomValue(a: number[]): number {

        // return a random integer between the first and the second item of the array
        return Phaser.Math.Between(a[0], a[1]);
    }

    // method to be executed at each frame
    update(t: number, dt: number): void {

        // left and right arrow keys aren't been pressed, yet
        this.leftPressed = false;
        this.rightPressed = false;

        // is left arrow key down?
        if (this.arrowKeys.left.isDown) {
 
            // left button is being pressed
            this.leftPressed = true;
        }
 
        // is right arrow key down?
        if (this.arrowKeys.right.isDown) {
             
            // right button has been pressed
            this.rightPressed = true;
        }

        // is hero first move and was there an input?
        if (this.hero.firstMove && (this.leftPressed || this.rightPressed)) {

            // it's no longer the first move
            this.hero.firstMove = false;

            // get all platforms
            let platforms: PlatformSprite[] = this.platformGroup.getChildren() as PlatformSprite[];

            // loop through all platforms
            for (let platform of platforms) {

                // start to move the platforms
                platform.setVelocity(0, GameOptions.platformSpeed * -1);
            }

            // start to apply the gravity to hero
            this.hero.setVelocity(0, GameOptions.gameGravity);
        }

        // isn't hero first move?
        if (!this.hero.firstMove) {

            // set hero horizontal velocity according to arrow keys pressed
            this.hero.velocity.x = this.leftPressed ? -GameOptions.heroSpeed : (this.rightPressed ? GameOptions.heroSpeed : 0);

            // check colliding platform, if any, once the hero moved
            let collidingPlatform: (SweptAABBCollisionInfo | null) = this.AABB.moveActor(this.hero, this.hero.velocity.x, this.hero.velocity.y, this.platformGroup, dt);
            
            // get all platforms
            let platforms: PlatformSprite[] = this.platformGroup.getChildren() as PlatformSprite[];

            // loop through all platforms
            for (let platform of platforms) {

                // move the platform
                this.AABB.moveObject(platform, platform.velocity.x, platform.velocity.y, dt);

                // if current platform is the colliding platform...
                if (collidingPlatform && platform == collidingPlatform.collisionActor) {

                    // check which side of the platform collide
                    switch(collidingPlatform.collisionSide) {

                        // top side
                        case this.AABB.TOP_SIDE:

                            // place hero on top of the platform
                            this.AABB.placeOnTop(this.hero, platform); 
                            break;

                        // left side
                        case this.AABB.LEFT_SIDE:

                            // if the hero is moving right... 
                            if (this.hero.velocity.x > 0) {

                                // ... place the hero on the left side of the platform
                                this.AABB.placeOnLeft(this.hero, platform); 
                            }
                            break;

                        // right side    
                        case this.AABB.RIGHT_SIDE:

                            // if the hero is moving left... 
                            if (this.hero.velocity.x < 0) {

                                // ... place the hero on the right side of the platform 
                                this.AABB.placeOnRight(this.hero, platform); 
                            }
                            break;
                    }       
                }

                // if a platform leaves the stage to the upper side...
                if (platform.getBounds().bottom < 0) {

                    // ... recycle the platform
                    this.positionPlatform(platform);
                }
            }

            // if the hero falls down or leaves the stage from the top...
            if(this.hero.y > this.gameHeight || this.hero.y < 0) {

                // restart the scene
                this.scene.start("PlayGame");
            }
        }
    }
}

sweptAABB.ts

Swept AABB logic is stored here. The concepts explained in understanding physics continuous collision detection using swept AABB method and Minkowski sum and understanding physics continuous collision detection using swept AABB method and Minkowski sum – Part 2: both bodies are moving are applied here.

// SWEPT AABB CLASS

// modules to import
import PhysicsBox from "./physicsBox";
import SweptAABBCollisionInfo from "./sweptAABBCollisionInfo";

// class to handle Swept AABB physics
export default class SweptAABB {

    // a series of constants to 
    TOP_SIDE: number = 0;
    RIGHT_SIDE: number = 1;
    BOTTOM_SIDE: number = 2;
    LEFT_SIDE: number = 3;
    
    // method to place a physics object on top of another physics object
    // arguments: the two physics objects
    placeOnTop(s1: Phaser.GameObjects.Sprite, s2: Phaser.GameObjects.Sprite): void {
        s1.y = s2.getBounds().top - s1.displayHeight * (1 - s1.originY);
    }

    // method to place a physics object on the left of another physics object
    // arguments: the two physics objects
    placeOnLeft(s1: Phaser.GameObjects.Sprite, s2: Phaser.GameObjects.Sprite): void {
        s1.x = s2.getBounds().left - s1.displayWidth * (1 - s1.originX);
    }

    // method to place a physics object on the right of another physics object
    // arguments: the two physics objects
    placeOnRight(s1: Phaser.GameObjects.Sprite, s2: Phaser.GameObjects.Sprite): void {
        s1.x = s2.getBounds().right + s1.displayWidth * (1 - s1.originX);
    }

    // method to move an object
    // arguments: the sprite, the horizontal speed, the vertical speed and the amount of time, in milliseconds
    moveObject(s: Phaser.GameObjects.Sprite, xSpeed: number, ySpeed: number, ms: number): void {
        s.x += xSpeed * ms / 1000;
        s.y += ySpeed * ms / 1000;
    }

    // method to move an actor. Actors, unlike objects, may collide with other objects
    // argument: the main sprite, horizontal and vertical speed, collision group and the amount of time, in milliseconds
    moveActor(s: PhysicsBox, xSpeed: number, ySpeed: number, collisionGroup: Phaser.GameObjects.Group, ms: number): SweptAABBCollisionInfo | null {
 
        // get all possible collisions
        let possibleCollisions: PhysicsBox[] = collisionGroup.getChildren() as PhysicsBox[];

        // minimum collision time is 1 (no collision)
        let minimumCollisionTime: number = 1;

        // current colliding body is null at the moment, since there isn't any collision yet
        let collidingBody: SweptAABBCollisionInfo | null = null;

        // loop through all possible collisions
        for (let possibleCollision of possibleCollisions) {

            // get collision information
            let collisionInfo: SweptAABBCollisionInfo | null  = this.collisionInformation(s, possibleCollision, ms);

            // if there is a collision and its collision time is less than current minumum collision time...
            if (collisionInfo && collisionInfo.collisionTime < minimumCollisionTime) {

                // update minimum collision time
                minimumCollisionTime = collisionInfo.collisionTime;  

                // update colliding body
                collidingBody = collisionInfo;
            }
        }

        // move the actor
        this.moveObject(s, xSpeed, ySpeed, ms);

        // return the colliding body, if any, or null
        return (minimumCollisionTime < 1) ? collidingBody : null;    
    }

    // method to check if two bodies collide within a certain time - arguments: the two bodies and a time
    collisionInformation(sprite1: PhysicsBox, sprite2: PhysicsBox, time: number): SweptAABBCollisionInfo | null {
         
        // determine relative speed subtracting the two speed vectors
        let relativeSpeed: Phaser.Math.Vector2 = new Phaser.Math.Vector2(sprite1.velocity.x, sprite1.velocity.y).subtract(new Phaser.Math.Vector2(sprite2.velocity.x, sprite2.velocity.y));
 
        // get movement line, from box origin to box relative destination
        let movementLine: Phaser.Geom.Line =  new Phaser.Geom.Line(sprite1.x, sprite1.y, sprite1.x + relativeSpeed.x * (time / 1000), sprite1.y + relativeSpeed.y * (time / 1000));
        
        // Minkowski rectangle built inflating the sprite bodies
        let minkowskiRectangle: Phaser.Geom.Rectangle = this.minkowskiSum(sprite1, sprite2);
 
        // array to store all intersection points between movement line and Minkowski rectangle
        let intersectionPoints: Phaser.Geom.Point[] = [];
         
        // get all intersection points between movement line and Minkowski rectangle, then store them into intersectionPoints array
        Phaser.Geom.Intersects.GetLineToRectangle(movementLine, minkowskiRectangle, intersectionPoints);

        // here we will store collision line
        let collisionLine: Phaser.Geom.Line;

        // here we will store collision side
        let collisionSide: number;

        // here we will store collision time
        let collisionTime: number;
 
        // different cases according to intersection points vector length
        switch (intersectionPoints.length) {
 
            // no intersection points: return 1, that is objects can move for the entire interval
            case 0:
                return null;
 
            // only one intersection point: this is the collision point    
            case 1:
 
                // get the collision line
                collisionLine = new Phaser.Geom.Line(sprite1.x, sprite1.y, intersectionPoints[0].x, intersectionPoints[0].y);

                // get the collision side
                collisionSide = this.getCollisionSide(minkowskiRectangle, intersectionPoints[0]);

                // get the collision time
                collisionTime = Phaser.Geom.Line.Length(collisionLine) / Phaser.Geom.Line.Length(movementLine);
               
                // return the ratio between collision line and movement line
                return new SweptAABBCollisionInfo(sprite2, collisionTime, intersectionPoints[0], collisionSide); 
             
            // more than one intersection point: collision point is the closest to moving body
            default:
                 
                // set the minimum distance to Infinity, the highest number
                let minDistance: number = Infinity;
 
                // set minimum index to zero (first element)
                let minIndex: number = 0;
 
                // looping through all instersection points
                for (let i: number = 0; i < intersectionPoints.length; i ++) {
 
                    // get distance between body and points
                    let distance: number = Phaser.Math.Distance.Between(sprite1.x, sprite1.y, intersectionPoints[i].x, intersectionPoints[i].y);
                     
                    // is distance less then minimum distance?
                    if (distance < minDistance) {
 
                        // update minimum index
                        minIndex = i;
 
                        // update minimum distance
                        minDistance = distance;
                    } 
                }
 
                // get the collision line
                collisionLine = new Phaser.Geom.Line(sprite1.x, sprite1.y, intersectionPoints[minIndex].x, intersectionPoints[minIndex].y);

                // get the collision side
                collisionSide = this.getCollisionSide(minkowskiRectangle, intersectionPoints[minIndex]);

                // get the collision time
                collisionTime = Phaser.Geom.Line.Length(collisionLine) / Phaser.Geom.Line.Length(movementLine);
               
                // return the ratio between collision line and movement line
                return new SweptAABBCollisionInfo(sprite2, collisionTime, intersectionPoints[minIndex], collisionSide); 
        }
    }

    // method to get the collision side 
    // arguments: the minkowsky rectangle and a point
    getCollisionSide(minkowsky: Phaser.Geom.Rectangle, point: Phaser.Geom.Point): number {
        
        // is the point along top side?
        if (minkowsky.top == point.y) {
            return this.TOP_SIDE;
        }

        // is the point along bottom side?
        if (minkowsky.bottom == point.y) {
            return this.BOTTOM_SIDE;
        }

        // is the point along left side?
        if (minkowsky.left == point.x) {
            return this.LEFT_SIDE;
        }

        // is the point along right side?
        if (minkowsky.right == point.x) {
            return this.RIGHT_SIDE;
        }

        // no collision side, should be impossible to reach this line
        return -1;
    }

    // method to perform the Minkowski sum between two Sprites - argument: the sprites
    minkowskiSum(sprite1: Phaser.GameObjects.Sprite, sprite2: Phaser.GameObjects.Sprite): Phaser.Geom.Rectangle {
         
        // get bounding boxes
        let spriteBounds1: Phaser.Geom.Rectangle = sprite1.getBounds();
        let spriteBounds2: Phaser.Geom.Rectangle = sprite2.getBounds();
 
        // new rectangle leftmost point
        let newLeft: number = spriteBounds2.left - spriteBounds1.width / 2;
 
        // new rectangle upper point
        let newTop: number = spriteBounds2.top - spriteBounds1.height / 2;
 
        // new rectangle width
        let newWidth: number = spriteBounds1.width + spriteBounds2.width;
 
        // new rectangle height
        let newHeight: number = spriteBounds1.height + spriteBounds2.height;
 
        // return the inflated rectangle
        return new Phaser.Geom.Rectangle(newLeft, newTop, newWidth, newHeight);
    }
}

sweptAABBCollisionInfo.ts

Just a simple class to store collision information:

// modules to import
import PhysicsBox from "./physicsBox";

// class to handle Swept AABB collision information
export default class SweptAABBCollisionInfo {

    // colliding actor
    collisionActor: PhysicsBox;

    // collision time, from 0 (beginning of the frame, already colliding) to 1 (no collision)
    collisionTime: number;

    // collision point
    collisionPoint: Phaser.Geom.Point;

    // collision side
    collisionSide: number;
    
    // constructor - arguments: the actor, the time, the point and the side
    constructor(actor: PhysicsBox, time: number, point: Phaser.Geom.Point, side: number) {
        this.collisionActor = actor;
        this.collisionTime = time;
        this.collisionPoint = point;
        this.collisionSide = side; 
    }
}

And that’s it. It may not be the easier way to build a simple platformer, but learning the basics of AABB collision detection will help you in future development in case you won’t be able to rely on physics engines. Download the source code of the entire project.

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