Talking about Game development, HTML5, Javascript, Phaser and TypeScript.
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.
1 2 3 4 5 6 7 8 9 | <! 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | // 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | // 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | 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.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.