Understanding continuous collision detection between a moving circle and a static circle

The journey to understanding continuous collision detection between a moving circle and a non axis aligned line is almost done, so let’s have a small recap:

In first step, we saw how to manage continuous collision detection between a moving circle and a static infinite line.

In second step we saw the same thing, but with a segment line, also handling ball bounce.

Being a segment line, it has vertices, so we need to manage collision between the circle and the vertices.

Before seeing this part, we have to handle continuous collision detection between a moving circle and a static circle.

This is important, because later we’ll assume a vertex is a circle with radius = zero.

Let’s see the interactive example:

You can drag all interactive spots to change both circles position and radius, as well as moving circle velocity.

What’s the logic behind it?

1 – First, we need to find the closest point on the movement vector of the moving circle from the center of the static circle, and get the distance from the center of the static circle to the closest point.

2 – If the distance is greater than the sum of the two circles radii, then there’s no collision.

3 – If the distance is smaller then the sum of the two circles radii, then there might be a collision if the new center of the moving circle is on the line of the vector movement.

4 – If there is a collision, the rebound is calculated on the tangent of the moving circle on collision point, which is a line perpendicular to the line which connects the static circle center and the moving circle center.

Everything has been made using Phaser, so I am releasing the source code of the simulation.

A proper class will be released once it will be able to handle circle Vs vertex collision.

The source code is made of one html file, a css file and three 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 { PreloadAssets } from './preloadAssets';
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 : [PreloadAssets, PlayGame]
}

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

preloadAssets.ts

Here we preload all assets to be used in the game, actually just the interactive spot.

// CLASS TO PRELOAD ASSETS

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

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

    // method to be execute during class preloading
    preload(): void {

        // this is how we preload an image
        this.load.image('point', 'assets/point.png');
	}

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

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

playGame.ts

Main file, all logic is stored here.

// THE GAME ITSELF

enum anchorPoint {
    CircleCenter,
    CircleRadius,
    CircleVelocity,
    StaticCircleCenter,
    StaticCircleRadius
}

enum intersectionType {
    None,
    Simple,
    Strict
}

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

    simulationGraphics : Phaser.GameObjects.Graphics;
    pointsArray : Phaser.GameObjects.Sprite[];
    obstacleSegment : Phaser.Geom.Line;
    staticCircle : Phaser.Geom.Circle;
    movingCircle : Phaser.Geom.Circle;
    destinationCircle : Phaser.Geom.Circle;
    velocityLine : Phaser.Geom.Line;

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

    // method to be executed when the scene has been created
    create() : void {  
        this.obstacleSegment = new Phaser.Geom.Line(483, 381, 223, 410);
        this.movingCircle = new Phaser.Geom.Circle(80, 200, 50);
        this.staticCircle = new Phaser.Geom.Circle(345, 355, 50)
        this.velocityLine = new Phaser.Geom.Line(80, 200, 422, 512);
        this.destinationCircle = new Phaser.Geom.Circle(422, 512, 50);
        const pointColors : number[] =  [0x00ff00, 0x008800, 0xff0000, 0x880000, 0x0000ff, 0x000088, 0x000088];
        this.pointsArray = [
            this.add.sprite(this.movingCircle.x, this.movingCircle.y, 'point'),
            this.add.sprite(this.movingCircle.x, this.movingCircle.y - this.movingCircle.radius, 'point'),
            this.add.sprite(this.velocityLine.x2, this.velocityLine.y2, 'point'),
            this.add.sprite(this.staticCircle.x, this.staticCircle.y, 'point'),
            this.add.sprite(this.staticCircle.x, this.staticCircle.y - this.staticCircle.radius, 'point')
        ];
        this.pointsArray.forEach((point : Phaser.GameObjects.Sprite, index : number) => {
            point.setInteractive();  
            point.setData('type', index);
        })
       
        this.simulationGraphics = this.add.graphics();
     
        this.input.setDraggable(this.pointsArray);
        this.input.on('drag', this.dragPoint, this);
        this.drawAndExplain(); 
    }

    dragPoint(pointer : Phaser.Input.Pointer, point : Phaser.GameObjects.Sprite, posX : number, posY : number) : void {
        point.setPosition(posX, posY);
        switch (point.getData('type')) {
            case anchorPoint.StaticCircleCenter :
                this.staticCircle.x = posX;
                this.staticCircle.y = posY;
                this.pointsArray[anchorPoint.StaticCircleRadius].setPosition(posX, posY - this.staticCircle.radius);
                break;
            case anchorPoint.StaticCircleRadius :
                point.setPosition(this.staticCircle.x, Math.min(point.y, this.staticCircle.y - 1));
                this.staticCircle.radius = this.staticCircle.y -point.y;
                break;
            case anchorPoint.CircleVelocity :
                this.velocityLine.x2 = posX;
                this.velocityLine.y2 = posY;
                this.destinationCircle.x = posX;
                this.destinationCircle.y = posY;
                break;
            case anchorPoint.CircleRadius :
                point.setPosition(this.movingCircle.x, Math.min(point.y, this.movingCircle.y - 1));
                this.movingCircle.radius = this.movingCircle.y -point.y;
                this.destinationCircle.radius = this.movingCircle.y -point.y;
                break; 
            case anchorPoint.CircleCenter :
                this.velocityLine.setTo(posX, posY, this.velocityLine.x2 + posX - this.movingCircle.x, this.velocityLine.y2 + posY - this.movingCircle.y)
                this.movingCircle.x = posX;
                this.movingCircle.y = posY;
                this.destinationCircle.x = this.velocityLine.x2;
                this.destinationCircle.y = this.velocityLine.y2;
                this.pointsArray[anchorPoint.CircleRadius].setPosition(posX, posY - this.movingCircle.radius);
                this.pointsArray[anchorPoint.CircleVelocity].setPosition(this.velocityLine.x2, this.velocityLine.y2);
                break;
        }
        this.drawAndExplain(); 
    }

    drawAndExplain() : void { 
        this.simulationGraphics.clear();
        this.styleAndStroke(0xffff00, this.movingCircle);
        this.styleAndStroke(0xff8800, this.staticCircle);
        let distanceBetweenCircles : number = Phaser.Math.Distance.Between(this.movingCircle.x, this.movingCircle.y, this.staticCircle.x, this.staticCircle.y);
        if (distanceBetweenCircles < this.movingCircle.radius + this.destinationCircle.radius) {
            return;  
        }
        this.styleAndStroke(0x888888, this.destinationCircle);
        this.styleAndStroke(0x00a2ff, this.velocityLine);
        let shortestDistancePoint : Phaser.Geom.Point = Phaser.Geom.Line.GetNearestPoint(this.velocityLine, new Phaser.Geom.Point(this.staticCircle.x, this.staticCircle.y));
        let shortestDistanceLine : Phaser.Geom.Line = new Phaser.Geom.Line(this.staticCircle.x, this.staticCircle.y, shortestDistancePoint.x, shortestDistancePoint.y);
        let radiiSum : number = this.staticCircle.radius + this.movingCircle.radius;
        let shortestDistanceLength : number = Phaser.Geom.Line.Length(shortestDistanceLine);
        if (shortestDistanceLength >= radiiSum) {
            this.styleAndStroke(0xff00ff, this.destinationCircle);  
        }
        else {
            let distanceFromShortestDistancePoint : number = Math.sqrt(radiiSum * radiiSum - shortestDistanceLength * shortestDistanceLength);
            let newCenter : Phaser.Geom.Point = new Phaser.Geom.Point(shortestDistancePoint.x - distanceFromShortestDistancePoint * (Phaser.Geom.Line.Width(this.velocityLine) / Phaser.Geom.Line.Length(this.velocityLine)), shortestDistancePoint.y -  distanceFromShortestDistancePoint * (Phaser.Geom.Line.Height(this.velocityLine) / Phaser.Geom.Line.Length(this.velocityLine)));
            let distanceFromNewCenterToCircle : number = Phaser.Math.Distance.Between(this.movingCircle.x, this.movingCircle.y, newCenter.x, newCenter.y);
            if (distanceFromNewCenterToCircle > Phaser.Geom.Line.Length(this.velocityLine)) {
                this.styleAndStroke(0xff00ff, this.destinationCircle); 
            }
            else {
                let destinationCircle : Phaser.Geom.Circle = new Phaser.Geom.Circle(newCenter.x, newCenter.y, this.movingCircle.radius);
                this.styleAndStroke(0xff00ff, destinationCircle);
                let circleToDestinationCircleLine : Phaser.Geom.Line = new Phaser.Geom.Line(this.staticCircle.x, this.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(this.velocityLine, collisionTangent);
                let remainingVelocity : number = Phaser.Math.Distance.Between(newCenter.x, newCenter.y, this.velocityLine.x2, this.velocityLine.y2);
                let reboundLine : Phaser.Geom.Line = new Phaser.Geom.Line(newCenter.x, newCenter.y, newCenter.x + remainingVelocity * Math.cos(reflectionAngle), newCenter.y + remainingVelocity * Math.sin(reflectionAngle));
                this.styleAndStroke(0x00a2ff, reboundLine);
                let reboundCircle : Phaser.Geom.Circle = new Phaser.Geom.Circle(reboundLine.x2, reboundLine.y2, this.movingCircle.radius);
                this.styleAndStroke(0xff00ff, reboundCircle); 
            }
        }
    }

    styleAndStroke(color : number, geom : Phaser.Geom.Circle | Phaser.Geom.Line) : void {
        this.simulationGraphics.lineStyle(2, color);
        if (geom instanceof Phaser.Geom.Circle) {
            this.simulationGraphics.strokeCircleShape(geom);   
        }
        else {
            this.simulationGraphics.strokeLineShape(geom);   
        } 
    }
}

We are about to have our little continuous collision detection routine to code simple games without physics engines, 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

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