Talking about HTML5, Javascript, Phaser and TypeScript.
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.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.