Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
96bf604
Demo for collisions detection has been copied from spinning-shapes an…
maerch Nov 24, 2014
4192987
Encapsulates determination of potential collision pairs
maerch Nov 24, 2014
1ec7013
Introducing shape objects which enables to plugin your own shapes for…
maerch Nov 25, 2014
5a817de
Merge branch 'collision-shapes' into quadtree
maerch Nov 26, 2014
15cf5e1
Initial quad tree in testing mode
maerch Dec 3, 2014
0428479
Tests comparing naive and quad approach
maerch Dec 4, 2014
f87bfe8
Merged removal of uncollision methods
maerch Dec 5, 2014
516494b
Repairs tests
maerch Dec 5, 2014
7fc8ef3
Bugfix && quad settings depend on number of entities
maerch Dec 8, 2014
665a932
Move rendering of quadtree to demo
maerch Dec 8, 2014
e97ea7f
Adapt to quad changes
maerch Dec 8, 2014
4fbdf36
Adapt to quad changes
maerch Dec 8, 2014
7d4de01
Use shapes for collision detection
maerch Dec 8, 2014
3b86b06
Merged removing of collision records
maerch Dec 8, 2014
7fa75b8
Removed last occurrence of collideRecords
maerch Dec 8, 2014
1e4ca88
Refactoring && calculation of world size based on entities (dynamic s…
maerch Dec 8, 2014
96fad4b
Fixes tests
maerch Dec 9, 2014
644c8cb
Cleanup
maerch Dec 9, 2014
f0bed74
v0.6.0
maerch Dec 9, 2014
ab682ed
Adds bounding box changes for v0.6.0
maerch Dec 9, 2014
b396879
Using else-if
maerch Dec 16, 2014
b896d21
id instead of freeId
maerch Dec 17, 2014
cd71849
Using singleton shapes && simplify shape creation
maerch Dec 17, 2014
16bdcef
Removing from the end of current collision pairs
maerch Dec 17, 2014
9a9854f
Pass quad tree settings via the constructor (which fixes also a bug)
maerch Dec 17, 2014
85ab5f8
Using singleton shapes && simplify shape creation
maerch Dec 17, 2014
b139f7e
Use point object with x and y property in the quad tree constructor
maerch Dec 18, 2014
452ecc7
Does not expose quad tree details in collider anymore
maerch Dec 18, 2014
352b310
Use 'isSetupForCollision'
maerch Dec 18, 2014
2d5e88f
Revert shape changes
maerch Dec 18, 2014
791f869
Use object as return value of #getDimensions
maerch Dec 18, 2014
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
0.6.0 / 2014-12-09

[NEW] Coquette is using a quad tree for collision detection now.
[NEW] Support for own shapes has been added. Your own shape needs a function called `isIntersecting` which takes another Shape and returns true or false.

[BREAKING CHANGE] To enable collision detection for an entity the `boundingBox` property has to return an instance of a Shape, either `Collider.Shape.Rectangle` or `Collider.Shape.Circle`. Both constructors take the entity as a parameter.

0.5.2 / 2014-12-01

[FIX] Fix Left Right Space to destroy ALL asteroids in group when one member hit with bullet.
Expand Down
2 changes: 1 addition & 1 deletion coquette-min.js

Large diffs are not rendered by default.

262 changes: 232 additions & 30 deletions coquette.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,44 @@
return obj.center !== undefined && obj.size !== undefined;
};

var Shape = {
isIntersecting: function(e1, e2) {
var s1 = getBoundingBox(e1);
var s2 = getBoundingBox(e2);

var circle = Collider.prototype.CIRCLE;
var rectangle = Collider.prototype.RECTANGLE;

if (s1 === rectangle && s2 === rectangle) {
return Maths.rectanglesIntersecting(e1, e2);
} else
if (s1 === circle && s2 === rectangle) {
return Maths.circleAndRectangleIntersecting(e1, e2);
} else
if (s1 === rectangle && s2 === circle) {
return Maths.circleAndRectangleIntersecting(e2, e1);
} else
if (s1 === circle && s2 === circle) {
return Maths.circlesIntersecting(e1, e2);
} else return undefined;
}
}

var RectangleShape = function() {}
RectangleShape.prototype = Shape;

var CircleShape = function() {}
CircleShape.prototype = Shape;

Collider.prototype = {
_currentCollisionPairs: [],

update: function() {
this._currentCollisionPairs = [];

// get all entity pairs to test for collision
var ent = this.c.entities.all();
for (var i = 0, len = ent.length; i < len; i++) {
for (var j = i + 1; j < len; j++) {
this._currentCollisionPairs.push([ent[i], ent[j]]);
}
}
this._currentCollisionPairs = quadTreeCollisionPairs.apply(this, [this.c.entities.all()]);

// test collisions
while (this._currentCollisionPairs.length > 0) {
var pair = this._currentCollisionPairs.shift();
if (this.isColliding(pair[0], pair[1])) {
this.collision(pair[0], pair[1]);
}
this.collision(pair[0], pair[1]);
}
},

Expand Down Expand Up @@ -80,32 +98,97 @@
}
},

isIntersecting: function(obj1, obj2) {
var shape1 = getBoundingBox(obj1);
var shape2 = getBoundingBox(obj2);

var result;
if((result = shape1.isIntersecting(obj1, obj2)) !== undefined) {
return result;
} else
if((result = shape2.isIntersecting(obj1, obj2)) !== undefined) {
return result;
} else {
throw new Error("Unsupported bounding box shapes for collision detection.");
}
},

isColliding: function(obj1, obj2) {
return obj1 !== obj2 &&
isSetupForCollisions(obj1) &&
isSetupForCollisions(obj2) &&
this.isIntersecting(obj1, obj2);
},

isIntersecting: function(obj1, obj2) {
var obj1BoundingBox = getBoundingBox(obj1);
var obj2BoundingBox = getBoundingBox(obj2);

if (obj1BoundingBox === this.RECTANGLE && obj2BoundingBox === this.RECTANGLE) {
return Maths.rectanglesIntersecting(obj1, obj2);
} else if (obj1BoundingBox === this.CIRCLE && obj2BoundingBox === this.RECTANGLE) {
return Maths.circleAndRectangleIntersecting(obj1, obj2);
} else if (obj1BoundingBox === this.RECTANGLE && obj2BoundingBox === this.CIRCLE) {
return Maths.circleAndRectangleIntersecting(obj2, obj1);
} else if (obj1BoundingBox === this.CIRCLE && obj2BoundingBox === this.CIRCLE) {
return Maths.circlesIntersecting(obj1, obj2);
} else {
throw "Objects being collision tested have unsupported bounding box types."
RECTANGLE: new RectangleShape(),
CIRCLE: new CircleShape()
};

var getDimensions = function(entities) {
var maxx, minx, maxy, miny;

entities.forEach(function(entity) {
if(isSetupForCollisions(entity)) {
if(maxx === undefined || entity.center.x > maxx) {
maxx = entity.center.x;
}
if(minx === undefined || entity.center.x < minx) {
minx = entity.center.x;
}
if(maxy === undefined || entity.center.y > maxy) {
maxy = entity.center.y;
}
if(miny === undefined || entity.center.y < miny) {
miny = entity.center.y;
}
}
},
});

RECTANGLE: 0,
CIRCLE: 1
var width = maxx - minx;
var height = maxy - miny;

return {size: {x: width, y: height }, center: {x: minx + width/2, y: miny + height/2}}
};

var quadTreeCollisionPairs = function(entities) {
var dimensions = getDimensions(entities);

var p1 = {x: dimensions.center.x - dimensions.size.x/2,
y: dimensions.center.y - dimensions.size.y/2};
var p2 = {x: dimensions.center.x + dimensions.size.x/2,
y: dimensions.center.y + dimensions.size.y/2};

this.quadTree = new Quadtree(p1, p2, {
maxObj: Math.max(Math.round(entities.length/4), 1),
maxLevel: 5
});
var quadTree = this.quadTree;
entities.forEach(function(entity) {
quadTree.insert(entity);
});

quadTree.allCollisionPairs = allCollisionPairs.bind(this);
return quadTree.collisions();
};

var allCollisionPairs = function(ent) {
var potentialCollisionPairs = [];

// get all entity pairs to test for collision
for (var i = 0, len = ent.length; i < len; i++) {
for (var j = i + 1; j < len; j++) {
potentialCollisionPairs.push([ent[i], ent[j]]);
}
}

var collisionPairs = [];
potentialCollisionPairs.forEach(function(pair) {
if(this.isColliding(pair[0], pair[1])) {
collisionPairs.push(pair);
}
}.bind(this));

return collisionPairs;
};

var getBoundingBox = function(obj) {
Expand Down Expand Up @@ -351,6 +434,122 @@
RADIANS_TO_DEGREES: 0.01745
};

function Quadtree(p1, p2, settings, level) {
this.p1 = p1;
this.p2 = p2;

var width = this.p2.x-this.p1.x;
var height = this.p2.y-this.p1.y;

this.objects = [];
this.nodes = [];
this.rectangles = [];
this.leaf = true;
this.settings = settings;

this.level = level || 1;
}

Quadtree.prototype.insert = function(object) {
var x = object.center.x;
var y = object.center.y;
if(isNaN(x) || isNaN(y)) return;

if(this.leaf) {
if(this.objects.length<this.settings.maxObj || this.level === this.settings.maxLevel) {
this.objects.push(object);
return this;
} else {
this.split();
return this.insert(object);
}
} else {
for(var i=0; i<this.nodes.length; i++) {
if(Collider.prototype.isIntersecting(this.rectangles[i], object)) {
this.nodes[i].insert(object);
}
}
}
}

Quadtree.prototype.split = function() {
var x1 = this.p1.x, x2 = this.p2.x, y1 = this.p1.y, y2 = this.p2.y, level = this.level;

this.leaf = false;
var hx = (x2-x1)/2+x1;
var hy = (y2-y1)/2+y1;
this.nodes[0] = new Quadtree({x: x1, y: y1} , {x: hx, y: hy}, this.settings, level+1);
this.nodes[1] = new Quadtree({x: hx, y: y1} , {x: x2, y: hy}, this.settings, level+1);
this.nodes[2] = new Quadtree({x: x1, y: hy} , {x: hx, y: y2}, this.settings, level+1);
this.nodes[3] = new Quadtree({x: hx, y: hy} , {x: x2, y: y2}, this.settings, level+1);

var width = this.p2.x-this.p1.x;
var height = this.p2.y-this.p1.y;
// Is always the same - thanks symmetry
var size = {x: width/2,
y: height/2}

this.rectangles[0] = {
center:
{x: width/4 + this.p1.x,
y: height/4 + this.p1.y},
size: size,
boundingBox: Collider.prototype.RECTANGLE};
this.rectangles[1] = {
center:
{x: width/4*3 + this.p1.x,
y: height/4 + this.p1.y},
size: size,
boundingBox: Collider.prototype.RECTANGLE};
this.rectangles[2] = {
center:
{x: width/4 + this.p1.x,
y: height/4*3 + this.p1.y},
size: size,
boundingBox: Collider.prototype.RECTANGLE};
this.rectangles[3] = {
center:
{x: width/4*3 + this.p1.x,
y: height/4*3 + this.p1.y},
size: size,
boundingBox: Collider.prototype.RECTANGLE};

for(var i=0; i<this.objects.length; i++) {
var object = this.objects[i];
this.insert(object);
}
this.objects.length = 0;
}

Quadtree.prototype.visit = function(callback) {
if(!callback(this.objects, this) && !this.leaf) {
this.nodes.forEach(function(node) {
node.visit(callback);
});
}
}

Quadtree.prototype.collisions = function() {
var collisions = [];
var scanned = {};
this.visit(function(objects, quad) {
this.allCollisionPairs(objects).forEach(function(pair) {
var pairId = uniquePairId(pair);
if(!scanned[pairId]) {
collisions.push(pair);
scanned[pairId] = true;
}
});
return false;
}.bind(this));
return collisions;
}

function uniquePairId(pair) {
return [Math.min(pair[0]._id, pair[1]._id),
Math.max(pair[0]._id, pair[1]._id)].toString();
}

exports.Collider = Collider;
exports.Collider.Maths = Maths;
})(typeof exports === 'undefined' ? this.Coquette : exports);
Expand Down Expand Up @@ -816,6 +1015,8 @@
})(typeof exports === 'undefined' ? this.Coquette : exports);

;(function(exports) {
var id = 0;

function Entities(coquette, game) {
this.c = coquette;
this.game = game;
Expand Down Expand Up @@ -849,6 +1050,7 @@

create: function(Constructor, settings) {
var entity = new Constructor(this.game, settings || {});
entity._id = id++;
this.c.collider.createEntity(entity);
this._entities.push(entity);
return entity;
Expand Down
Loading