Understanding physics continuous collision detection using swept AABB method and Minkowski sum – Part 2: both bodies are moving

I had a lot of positive feedback with to post Understanding physics continuous collision detection using swept AABB method and Minkowski sum, but in the example there was only one moving body, while the other did not move.

What if there are two moving bodies? Does Minkowski sum still apply?

Let’s see what happens when two bodies move:

In the above picture, Body A is moving towards Body B, and Body Bis moving towards Body A, and it would be too difficult to say when – or even if – these two bodies collide, if we don’t pretend only one body is moving, with a relative velocity which we can get by subtracting, for instance, Body B velocity from Body A velocity, this way:

Now we can say we have only one body moving, and we can build the Minkowski Sum line explained in previous step:

Allright, we also have a collision point. Let’s strip everything but velocity vector and collision point:

If we define movement start with zero and movement end with one, collision point will be somewhere between 0 and 1 on the velocity vector.

This is the amount of velocity vector needed by both bodies to collide, this way:

So we can start the simulation, which at low velocities works well both with Arcade Physics and Swept AABB:

Click or touch the canvas to make simulation start. You’ll probably won’t notice differences between Arcade Physics and Swept AABB.

Things change at higher speeds:

Click or touch the canvas to make simulation start. While Swept AABB still handles the collision, Arcade Physics fails.

Let’s see the source code of this experiment, built around a simple class:

index.html

The webpage which hosts the game, just the bare bones of HTML.

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

<!DOCTYPE html>
<html>
    <head>
        <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 Scale Manager object.

Here we also initialize the game itself.

// 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.FIT,
    autoCenter: Phaser.Scale.CENTER_BOTH,
    parent: 'thegame',
    width: 800,
    height: 416
}

// game configuration object
const configObject: Phaser.Types.Core.GameConfig = {
    type: Phaser.AUTO,
    scale: scaleObject,
    scene: [PlayGame],
    physics: {
        default: 'arcade'
    }
}

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

playGame.ts

In playGame class we place all stuff on the canvas, make all stuff work and handle user input

// THE GAME ITSELF

import { PhaserSweptAABB } from "./phaserSweptAABB";
import PhysicsBox from "./physicsBox";

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

    // these are my physics box and wall
    box: PhysicsBox;
    wall: PhysicsBox;

    // these are the Arcade physics box and wall
    arcadeBox: Phaser.Physics.Arcade.Sprite;
    arcadeWall: Phaser.Physics.Arcade.Sprite;

    // speed of the projectile, in pixels per second
    projectileSpeed: number = 4000;

    // speed of the wall, in pixels per second
    wallSpeed: number = -2000;

    // our Swept AABB class
    AABB: PhaserSweptAABB;

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

    // method to be called once the class preloads
    preload(): void {

        // load graphic assets
        this.load.image('dot', 'assets/dot.png');
        this.load.image('wall', 'assets/wall.png');

    }

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

        // create a new instance of Swept AABB class
        this.AABB = new PhaserSweptAABB();

        // creation of my physics box and wall
        this.box = new PhysicsBox(this, 20, 128, 'dot');
        this.wall = new PhysicsBox(this, 780, 128, 'wall');

        // creation of Arcade box and wall
        this.arcadeBox = this.physics.add.sprite(20, 288, 'dot');
        this.arcadeWall = this.physics.add.sprite(780, 288, 'wall');

        // set Arcade box and wall immovable
        this.arcadeWall.setImmovable(true);
        this.arcadeBox.setImmovable(true);

        // input listener to fire the bullet
        this.input.on('pointerdown', this.fireBullets, this);

        // just some text to display informaiton
        this.add.text(20, 195, 'Bullet Speed: ' + this.projectileSpeed + ' pixels/s, Wall speed: ' + this.wallSpeed + ' pixels/s', {
            fontSize: '20px'
        });
        this.add.text(20, 12, 'Swept AABB', {
            fontSize: '24px'
        });
        this.add.text(20, 380, 'Arcade Physics', {
            fontSize: '24px'
        });
    }

    // FIRE!!!
    fireBullets() {

        // fire both my box and Arcade physics box
        this.arcadeBox.setVelocity(this.projectileSpeed, 0);
        this.box.setVelocity(this.projectileSpeed, 0);

        // fire both my wall and Arcade physics wall
        this.arcadeWall.setVelocity(this.wallSpeed, 0);
        this.wall.setVelocity(this.wallSpeed, 0);
    }

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

        // Swept AABB collider, to execute only if at least one body is moving
        if (this.box.isMoving() || this.wall.isMoving()) {

            // check collision time, can be any number between 0 (already colliding) and 1 (never colliding in this frame)
            let collisionTime: number = this.AABB.checkCollisionTime(this.box, this.wall, deltaTime);
            
            // update box and wall positions according to collision time
            this.box.updatePosition(deltaTime * collisionTime);
            this.wall.updatePosition(deltaTime * collisionTime);

            // if collision time is less than one, that is there was a collision...
            if (collisionTime < 1) {

                // stop both box and wall
                this.box.stopMoving();
                this.wall.stopMoving();
            }
        }
        
        // Arcade physics collider
        this.physics.world.collide(this.arcadeBox, this.arcadeWall, function(body1: Phaser.GameObjects.GameObject, body2: Phaser.GameObjects.GameObject) {
            
            // just stop both bodies
            let b1: Phaser.Physics.Arcade.Sprite = body1 as Phaser.Physics.Arcade.Sprite;
            let b2: Phaser.Physics.Arcade.Sprite = body2 as Phaser.Physics.Arcade.Sprite
            b1.setVelocity(0, 0);
            b2.setVelocity(0, 0)
        });
    }
}

physicsBox.ts

A simple extension of Phaser Sprite class to store 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);

        // add sprite to the scene
        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;
    }

    // method to check if the body is moving
    isMoving(): boolean {
        return this.velocity.x != 0 || this.velocity.y != 0;
    }

    // method to stop the object
    stopMoving(): void {
        this.setVelocity(0, 0);
    }

    // method to update physics box position - arguments: the amount of milliseconds
    updatePosition(milliseconds: number): void {

        // adjust box velocity
        this.x += this.velocity.x * (milliseconds / 1000);
        this.y += this.velocity.y * (milliseconds / 1000);
    }
}

phaserSweptAABB.ts

The core of the script, the methods responsible of collision detection

import PhysicsBox from "./physicsBox";

export class PhaserSweptAABB {

    // method to check if two bodies collide within a certain time - arguments: the two bodies and a time
    checkCollisionTime(sprite1: PhysicsBox, sprite2: PhysicsBox, time: number): number {
        
        // 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);

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

            // only one intersection point: this is the collision point    
            case 1:

                // get the collision line
                let collisionLine: Phaser.Geom.Line = new Phaser.Geom.Line(sprite1.x, sprite1.y, intersectionPoints[0].x, intersectionPoints[0].y);
                
                // return the ratio between collision line and movement line
                return Phaser.Geom.Line.Length(collisionLine) / Phaser.Geom.Line.Length(movementLine);
            
            // 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, sprite2.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
                let collisionLine2: Phaser.Geom.Line = new Phaser.Geom.Line(sprite1.x, sprite1.y, intersectionPoints[minIndex].x, intersectionPoints[minIndex].y);

                // return the ratio between collision line and movement line
                return Phaser.Geom.Line.Length(collisionLine2) / Phaser.Geom.Line.Length(movementLine);
        }
    }

    // method to perform the Minkowski sum between two Sprites - argument: the sprites
    minkowskiSum(sprite1: PhysicsBox, sprite2: PhysicsBox): 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);
    }
}

And we managed to build a continuous collision detection script capable of handling to fast moving objects. Now we have to put everything together and apply to a real world example, 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

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