“Serious Scramblers” HTML5 prototype built with Phaser and TypeScript update: simulate continuous collision detection

Read all posts about "" game

In this new part of Serious Scramblers series we are going to try to solve a problem affecting all physics engines, like Phaser’s Arcade, which do not feature Continuous Collision Detection.

What s Continuous Collision Detection? The purpose of Continuous Collision Detection (CCD) is to detect collisions of fast moving bodies that would otherwise be missed.

In our case, if the player falls down too fast, without CCD we’ll see the player passing through platforms. This glitch is called “tunnelling”, like you can see in this animation:

In this case the hero falls too fast for the physics engine to detect the collision with some platforms, and we have a tunnelling effect.

Some physics engines like Box2D feature CCD, using “bullets” in the specific Box2D case, but Arcade Physics does not.

How can we deal with that? There are several ways, which can be more or less complex, but the cheapest one I found is this one:

We assist to tunnelling because the platforms are too tiny to detect a fast body approaching them.

So let’s just create bigger platforms. But what if we want tiny platforms? Then create tiny platform with a big body, like in this example:

Tap and hold left or right to move the character left or right. Once you move, platforms will scroll up. Reach the top of the stage, and it’s game over.

Fall from platform to platform without falling too down, if you reach the bottom of the stage, it’s game over.

As you can see from the debug draw, platform bodies, in purple, are way taller than platform themselves. This will prevent tunnelling: they are simply too big for the player to pass through them.

Once player bottom coordinate is higher than platform top coordinate, which means player is below a platform, we reset platform body to match platform actual size.

And we can have something working like CCD.

Let’s have a look at the 6 TypeScript files and the HTML file used in this prototype. I also highlighted the new lines of code added to the source code of the previous step.

index.html

The webpage which hosts the game, just the bare bones of HTML and main.ts is called.

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

<!DOCTYPE html>
<html>
    <head>
        <style type = "text/css">
            body {
                background: #000000;
                padding: 0px;
                margin: 0px;
            }
        </style>
        <script src = "scripts/main.ts"></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.

In this file I added debug draw to Arcade Physics, to let the engine draw bodies.

// MAIN GAME FILE

// modules to import
import Phaser from 'phaser';
import { PreloadAssets } from './preloadAssets';
import { PlayGame} from './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: 750,
    height: 1334
}

// obhect to initialize Arcade physics
const physicsObject: Phaser.Types.Core.PhysicsConfig = {
    default: 'arcade',
    arcade: {
        gravity: {
            y: GameOptions.gameGravity
        },
        debug: true
    }    
}

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

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

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');
	}
}

gameOptions.ts

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

I increased game gravity to have the player to fall down very fast, to test tunnelling.

// CONFIGURABLE GAME OPTIONS

export const GameOptions = {

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

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

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

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

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

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

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

playGame.ts

The game itself, the biggest class, game logic is stored here, including our creative way to have CCD, changing platforms body size:

// THE GAME ITSELF

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

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

    // group which will contain all platforms
    platformGroup: Phaser.Physics.Arcade.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;

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

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

        // 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.physics.add.group();

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

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

        // 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
            platform.addToGroup(this.platformGroup);

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

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

        // input listener to move the hero
        this.input.on("pointerdown", this.moveHero, this);

        // input listener to stop the hero
        this.input.on("pointerup", this.stopHero, this);
    }

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

        // set platform bounding box size to make it way taller than platform sprite
        platform.setBodySize(platform.displayWidth / platform.scaleX, 400, false);

        // the platform is higher
        platform.isHigher = true;
    }

    // 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 move the hero
    moveHero(e: Phaser.Input.Pointer): void {

        // set hero velocity according to input horizontal coordinate
        this.hero.setVelocityX(GameOptions.heroSpeed * ((e.x > this.gameWidth / 2) ? 1 : -1));

        // is it the first move?
        if(this.hero.firstMove) {

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

            // move platform group
            this.platformGroup.setVelocityY(-GameOptions.platformSpeed);
        }
    }

    // method to stop the hero
    stopHero(): void {

        // ... just stop the hero :)
        this.hero.setVelocityX(0);
    }

    // method to be executed at each frame
    update(): void {

        // handle collision between ball and platforms
        this.physics.world.collide(this.platformGroup, this.hero);

        // get hero lower bounds
        let heroLowerBounds: number = this.hero.getBounds().bottom;

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

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

            // get platform bounds
            let platformBounds: Phaser.Geom.Rectangle = platform.getBounds();

            // if hero bottom side is below platform top side and platform size is higher...
            if (platformBounds.top < heroLowerBounds && platform.isHigher) {

                // reset platform body size
                platform.setBodySize(platform.displayWidth / platform.scaleX, platform.displayHeight, false); 
                
                // platform is no longer higher
                platform.isHigher = false;
            }

            // if a platform leaves the stage to the upper side...
            if (platformBounds.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");
        }
    }
}

playerSprite.ts

Class to define the player Sprite, the main actor of the game, the one players control.

// PLAYER SPRITE CLASS    

// player sprite extends Arcade Sprite class
export default class PlayerSprite extends Phaser.Physics.Arcade.Sprite {

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

        // add physics body to platform
        scene.physics.add.existing(this);
	}
}

platformSprite.ts

Class to define the platforms, we added a property to see if the platform body is higher than actual platform size.

// PLATFORM SPRITE CLASS    

// platform sprite extends RenderTexture class
export default class PlatformSprite extends Phaser.Physics.Arcade.Sprite {

    // platform physics body
    body: Phaser.Physics.Arcade.Body;

    // is the platform body higher?
    isHigher: Boolean = false;

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

        // add physics body to platform
        scene.physics.add.existing(this);
    }

    // method to add the platform to a group and set physics properties
    addToGroup(group: Phaser.Physics.Arcade.Group) {

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

        // platform body does not react to collisions
        this.body.setImmovable(true);

        // platform body is not affected by gravity
        this.body.setAllowGravity(false);

    }
}

And in a few lines we added something working like CCD, and finally we are ready to add multiple camera, enemies and stuff to collect. Meanwhile, download the source code of the entire project.

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

215 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stairs
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers