Get the full commented source code of

HTML5 Suika Watermelon Game

Talking about HTML5, Javascript, Phaser and TypeScript.

As you may know, during the last days I am working on a continuous collision detection between a moving circle and a non axis aligned line. While the continuous collision detection against an infinite line is done, a segment line has vertices, and we have to handle vertices collisions.

So I built a moving circle Vs static circle continuous collision detection, because later I’ll assume a vertex is a static circle with radius zero.

But a segment line has two vertices, so I ended building a generic script to handle continuous collision detection between a moving circle and any number of static circles:

Look at the result:

The yellow moving circle has a velocity of (740, 0). This means it will try to move by 740 pixels to the right. But there are a lot of random circles which may block its way making it bounce elsewhere.

As you can see, yellow circle movement is determined before circle starts moving, thanks to continuous collision detection, in this case used as a predictive trajectory system.

To determine the path I used a recursive function which tries to move the circle for the entire velocity, and if it founds a collision, return the collision itself and another call to the same function trying to move the circle at the collision point for the entire remaining velocity, and so on.

Look at the source code, made of one html file, one css file and 3 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">
        </style>
        <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;
    background-color : #31343a;
}

body {
    font : normal 14px arial;
    color : white;
}

canvas {
    touch-action : none;
    -ms-touch-action : none;
}

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 { PlayGame } from './playGame';

// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
    mode : Phaser.Scale.NONE,
    autoCenter : Phaser.Scale.CENTER_HORIZONTALLY,
    parent : 'thegame',
    width : 800,
    height : 600
}

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

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

playGame.ts

Main file, all logic is stored here, included checkCollision method which is the recursive function mentioned above.

import { CollisionResult } from './collisionResult';

// THE GAME ITSELF

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

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

    // method to be executed when the scene has been created
    create() : void {  
        let simulationGraphics : Phaser.GameObjects.Graphics = this.add.graphics();
        let movingCircle : Phaser.Geom.Circle = new Phaser.Geom.Circle(30, 300, 30);
        let circleImage : Phaser.Geom.Circle = new Phaser.Geom.Circle(movingCircle.radius + 1, movingCircle.radius + 1, movingCircle.radius);
        simulationGraphics.lineStyle(2, 0xffff00);
        simulationGraphics.strokeCircleShape(circleImage);
        simulationGraphics.generateTexture('circle', movingCircle.radius * 2 + 2, movingCircle.radius * 2 + 2);
        simulationGraphics.clear();
        let path : Phaser.Curves.Path = new Phaser.Curves.Path(movingCircle.x, movingCircle.y);
        let staticCircles : Phaser.Geom.Circle[] = [];
        simulationGraphics.lineStyle(2, 0xff8800);
        for (let i : number = 0; i < 10; i ++) {
            staticCircles.push(new Phaser.Geom.Circle(Phaser.Math.Between(250, 800), Phaser.Math.Between(0, 600), Phaser.Math.Between(30, 50)));
            simulationGraphics.strokeCircleShape(staticCircles[i]);
        }
        let result : CollisionResult[] = this.checkCollision(movingCircle, new Phaser.Math.Vector2(740, 0), staticCircles);
        result.forEach((collision: CollisionResult, index : number) => {
            let collisionCircle : Phaser.Geom.Circle = new Phaser.Geom.Circle(collision.point.x, collision.point.y, movingCircle.radius);
            path.lineTo(collision.point.x, collision.point.y); 
            simulationGraphics.lineStyle(2, 0x666666);
            simulationGraphics.strokeCircleShape(collisionCircle);    
        })
        simulationGraphics.lineStyle(2, 0xffff00, 1);
        path.draw(simulationGraphics);
        let circleSprite : Phaser.GameObjects.PathFollower = this.add.follower(path, movingCircle.x, movingCircle.y, 'circle');
        circleSprite.startFollow({
            duration: 1500,
            onComplete: () => this.scene.start()
        });
    }

    checkCollision(movingCircle : Phaser.Geom.Circle, circleVelocity : Phaser.Math.Vector2, staticCircles : Phaser.Geom.Circle[]) : CollisionResult[] {
        let velocityLine : Phaser.Geom.Line = new Phaser.Geom.Line(movingCircle.x, movingCircle.y, movingCircle.x + circleVelocity.x, movingCircle.y + circleVelocity.y);
        let closestCircleIndex : number = -1;
        let closestCircleDistance : number = Infinity;
        let collisionResult : CollisionResult = new CollisionResult(new Phaser.Geom.Point(movingCircle.x + circleVelocity.x, movingCircle.y + circleVelocity.y), new Phaser.Math.Vector2(0, 0));
        staticCircles.forEach((staticCircle : Phaser.Geom.Circle, index : number) => {
            let distanceBetweenCircles : number = Phaser.Math.Distance.Between(movingCircle.x, movingCircle.y, staticCircle.x, staticCircle.y);
            let radiiSum : number = staticCircle.radius + movingCircle.radius;
            if (distanceBetweenCircles >= radiiSum) {     
                let shortestDistancePoint : Phaser.Geom.Point = Phaser.Geom.Line.GetNearestPoint(velocityLine, new Phaser.Geom.Point(staticCircle.x, staticCircle.y));
                let shortestDistanceLine : Phaser.Geom.Line = new Phaser.Geom.Line(staticCircle.x, staticCircle.y, shortestDistancePoint.x, shortestDistancePoint.y);
                let shortestDistanceLength : number = Phaser.Geom.Line.Length(shortestDistanceLine);
                if (shortestDistanceLength < radiiSum) {
                    let distanceFromShortestDistancePoint : number = Math.sqrt(radiiSum * radiiSum - shortestDistanceLength * shortestDistanceLength);
                    let newCenter : Phaser.Geom.Point = new Phaser.Geom.Point(shortestDistancePoint.x - distanceFromShortestDistancePoint * (circleVelocity.x / Phaser.Geom.Line.Length(velocityLine)), shortestDistancePoint.y - distanceFromShortestDistancePoint * (circleVelocity.y / Phaser.Geom.Line.Length(velocityLine)));
                    let distanceFromNewCenterToCircle : number = Phaser.Math.Distance.Between(movingCircle.x, movingCircle.y, newCenter.x, newCenter.y); 
                    if (newCenter.x >= velocityLine.left && newCenter.x <= velocityLine.right && newCenter.y >= velocityLine.top && newCenter.y <= velocityLine.bottom && distanceFromNewCenterToCircle < closestCircleDistance) {
                        let circleToDestinationCircleLine : Phaser.Geom.Line = new Phaser.Geom.Line(staticCircle.x, staticCircle.y, newCenter.x, newCenter.y); 
                        let collisionTangent : Phaser.Geom.Line = Phaser.Geom.Line.Rotate(circleToDestinationCircleLine, Math.PI / 2);
                        let reflectionAngle : number = Phaser.Geom.Line.ReflectAngle(velocityLine, collisionTangent);
                        let remainingVelocity : number = Phaser.Math.Distance.Between(newCenter.x, newCenter.y, velocityLine.x2, velocityLine.y2);
                        closestCircleIndex = index;
                        closestCircleDistance = distanceFromNewCenterToCircle;
                        collisionResult = new CollisionResult(newCenter, new Phaser.Math.Vector2(remainingVelocity * Math.cos(reflectionAngle), remainingVelocity * Math.sin(reflectionAngle)));
                    }       
                }
            }  
        });
        if (closestCircleIndex == -1) {
            return [collisionResult];
        }
        else {
            return [collisionResult].concat(this.checkCollision(new Phaser.Geom.Circle(collisionResult.point.x, collisionResult.point.y, movingCircle.radius), new Phaser.Math.Vector2(collisionResult.velocity.x, collisionResult.velocity.y), staticCircles));    
        }
    }
}

collisionResult.ts

Just a custom class to handle a collision, which is made by a point (the new center of the circle) and a vector (the new circle velocity).

export class CollisionResult {
    point : Phaser.Geom.Point;
    velocity : Phaser.Math.Vector2;
    constructor(point : Phaser.Geom.Point, velocity : Phaser.Math.Vector2) {
        this.point = point;
        this.velocity = velocity;
    }
}

And we are finally ready to handle collision between a moving circle and a line segment, which we’ll see next time, meanwhile 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.