From 96bf604bea673c1cfdb5200f693e8c5116866d24 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Mon, 24 Nov 2014 13:44:45 -0500 Subject: [PATCH 01/28] Demo for collisions detection has been copied from spinning-shapes and extended by statistics about collision detection --- demos/collisions/game.js | 200 ++++++++++++++++++++++++++++++++++++ demos/collisions/index.html | 18 ++++ 2 files changed, 218 insertions(+) create mode 100644 demos/collisions/game.js create mode 100644 demos/collisions/index.html diff --git a/demos/collisions/game.js b/demos/collisions/game.js new file mode 100644 index 0000000..b7e42dd --- /dev/null +++ b/demos/collisions/game.js @@ -0,0 +1,200 @@ +;(function(exports) { + var GOLDEN_RATIO = 1.61803398875; + + var Collisions = function() { + var autoFocus = false; + this.c = new Coquette(this, "collisions-canvas", + 800, 500 / GOLDEN_RATIO, "white", autoFocus); + + var update = this.c.collider.update; + + // Calculate statistics for collision detection + // by intercepting function calls on the collider. + this.c.collider.update = function() { + var scanned = 0; + var colliding = 0; + + var isColliding = this.isColliding; + this.isColliding = function() { + scanned++; + var result = isColliding.apply(this, arguments); + if(result) colliding++ + return result; + } + + var start = +new Date(); + update.apply(this); + var end = +new Date(); + var diff = end - start; + + this.isColliding = isColliding; + + collisionStatistics.executionTime(diff); + collisionStatistics.scannedEntityPairs(scanned); + collisionStatistics.collidingEntityPairs(colliding); + } + + }; + + Collisions.prototype = { + update: function() { + var viewSize = this.c.renderer.getViewSize(); + var viewCenter = this.c.renderer.getViewCenter(); + + if (this.c.entities.all().length < 50) { // not enough shapes + var dirFromCenter = randomDirection(); + var Shape = Math.random() > 0.5 ? Rectangle : Circle; + this.c.entities.create(Shape, { // make one + center: offscreenPosition(dirFromCenter, viewSize, viewCenter), + vec: movingOnscreenVec(dirFromCenter) + }); + } + + // destroy entities that are off screen + var entities = this.c.entities.all(); + for (var i = 0; i < entities.length; i++) { + if (isOutOfView(entities[i], viewSize, viewCenter)) { + this.c.entities.destroy(entities[i]); + } + } + }, + }; + + var Rectangle = function(game, settings) { + this.c = game.c; + this.angle = Math.random() * 360; + this.center = settings.center; + this.size = { x: 70, y: 70 / GOLDEN_RATIO }; + this.vec = settings.vec; + this.turnSpeed = 2 * Math.random() - 1; + + mixin(makeCurrentCollidersCountable, this); + }; + + Rectangle.prototype = { + update: function() { + // move + this.center.x += this.vec.x; + this.center.y += this.vec.y; + + this.angle += this.turnSpeed; // turn + }, + + draw: function(ctx) { + if (this.colliderCount > 0) { + ctx.lineWidth = 2; + } else { + ctx.lineWidth = 1; + } + + ctx.strokeStyle = "black"; + ctx.strokeRect(this.center.x - this.size.x / 2, this.center.y - this.size.y / 2, + this.size.x, this.size.y); + }, + + }; + + var Circle = function(game, settings) { + this.c = game.c; + this.boundingBox = this.c.collider.CIRCLE; + this.center = settings.center; + this.size = { x: 55, y: 55 }; + this.vec = settings.vec; + + mixin(makeCurrentCollidersCountable, this); + }; + + Circle.prototype = { + update: function() { + // move + this.center.x += this.vec.x; + this.center.y += this.vec.y; + }, + + draw: function(ctx) { + if (this.colliderCount > 0) { + ctx.lineWidth = 2; + } else { + ctx.lineWidth = 1; + } + + ctx.beginPath(); + ctx.arc(this.center.x, this.center.y, this.size.x / 2, 0, Math.PI * 2, true); + ctx.closePath(); + ctx.strokeStyle = "black"; + ctx.stroke(); + }, + + }; + + var randomDirection = function() { + return Coquette.Collider.Maths.unitVector({ x:Math.random() - .5, y:Math.random() - .5 }); + }; + + var movingOnscreenVec = function(dirFromCenter) { + return { x: -dirFromCenter.x * 3 * Math.random(), y: -dirFromCenter.y * 3 * Math.random() } + }; + + var offscreenPosition = function(dirFromCenter, viewSize, viewCenter) { + return { + x: viewCenter.x + dirFromCenter.x * viewSize.x, + y: viewCenter.y + dirFromCenter.y * viewSize.y, + }; + }; + + var isOutOfView = function(obj, viewSize, viewCenter) { + return Coquette.Collider.Maths.distance(obj.center, viewCenter) > + Math.max(viewSize.x, viewSize.y); + }; + + var mixin = function(from, to) { + for (var i in from) { + to[i] = from[i]; + } + }; + + var collisionStatistics = { + + collisionsEl: undefined, + scannedEl: undefined, + timeEl: undefined, + + collidingEntityPairs: function(collidingEntities) { + if(!this.collisionsEl) { + this.collisionsEl = document.getElementById("collisions"); + } + this.collisionsEl.innerHTML = collidingEntities; + }, + + scannedEntityPairs: function(scannedEntities) { + if(!this.scannedEl) { + this.scannedEl = document.getElementById("scanned"); + } + this.scannedEl.innerHTML = scannedEntities; + }, + + executionTime: function(time) { + if(!this.timeEl) { + this.timeEl = document.getElementById("time"); + } + this.timeEl.innerHTML = time; + } + } + + var makeCurrentCollidersCountable = { + colliderCount: 0, // number of other shapes currently touching this shape + + collision: function(_, type) { + if (type === this.c.collider.INITIAL) { + this.colliderCount++; + } + }, + + uncollision: function() { + this.colliderCount--; + } + + }; + + exports.Collisions = Collisions; +})(this); diff --git a/demos/collisions/index.html b/demos/collisions/index.html new file mode 100644 index 0000000..be15cb4 --- /dev/null +++ b/demos/collisions/index.html @@ -0,0 +1,18 @@ + + + + + + + + + +
0 collisions.
+
0 elements scanned.
+
0 ms for collision detection.
+ + From 419298787bfa20e3cab18342ba3f5f1136aaf6cf Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Mon, 24 Nov 2014 14:09:45 -0500 Subject: [PATCH 02/28] Encapsulates determination of potential collision pairs --- src/collider.js | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/collider.js b/src/collider.js index 132beaf..2152d9b 100644 --- a/src/collider.js +++ b/src/collider.js @@ -1,6 +1,7 @@ ;(function(exports) { var Collider = function(coquette) { this.c = coquette; + this._getPotentialCollisionPairs = allCollisionPairs; }; // if no entities have uncollision(), skip expensive record keeping for uncollisions @@ -22,15 +23,7 @@ _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 = this._getPotentialCollisionPairs(this.c.entities.all()); // test collisions while (this._currentCollisionPairs.length > 0) { @@ -144,6 +137,23 @@ CIRCLE: 1 }; + var quadTreeCollisionPairs = function(ent) { + + }; + + var allCollisionPairs = function(ent) { + var collisionPairs = []; + + // 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++) { + collisionPairs.push([ent[i], ent[j]]); + } + } + + return collisionPairs; + }; + var getBoundingBox = function(obj) { return obj.boundingBox || Collider.prototype.RECTANGLE; }; From 1ec70132127f4501d0affbb1ad05ae34a39b8cf1 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Tue, 25 Nov 2014 17:25:08 -0500 Subject: [PATCH 03/28] Introducing shape objects which enables to plugin your own shapes for collision detection --- src/collider.js | 66 +++++++++++++++++++++++++++++---------- src/quadtree.js | 82 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 16 deletions(-) create mode 100644 src/quadtree.js diff --git a/src/collider.js b/src/collider.js index 132beaf..ffffeed 100644 --- a/src/collider.js +++ b/src/collider.js @@ -17,6 +17,45 @@ return obj.center !== undefined && obj.size !== undefined; }; + var RectangleShape = function(entity) { + this.entity = entity; + } + + RectangleShape.prototype = { + isIntersecting: function(anotherShape) { + if(anotherShape instanceof CircleShape) { + return Maths.circleAndRectangleIntersecting(anotherShape.entity, this.entity); + } + if(anotherShape instanceof RectangleShape) { + return Maths.rectanglesIntersecting(this.entity, anotherShape.entity); + } + if(anotherShape.isIntersecting) { + return anotherShape.isIntersecting(this); + } + throw "Objects being collision tested have unsupported bounding box types." + } + } + + var CircleShape = function(entity) { + this.entity = entity; + } + + CircleShape.prototype = { + isIntersecting: function(anotherShape) { + if(anotherShape instanceof CircleShape) { + return Maths.circlesIntersecting(this.entity, anotherShape.entity); + } + if(anotherShape instanceof RectangleShape) { + return Maths.circleAndRectangleIntersecting(this.entity, anotherShape.entity); + } + if(anotherShape.isIntersecting) { + return anotherShape.isIntersecting(this); + } + throw "Objects being collision tested have unsupported bounding box types." + } + } + + Collider.prototype = { _collideRecords: [], _currentCollisionPairs: [], @@ -121,27 +160,22 @@ }, 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." - } + var Shape, shape1, shape2; + + Shape = getBoundingBox(obj1); + shape1 = new Shape(obj1); + + Shape = getBoundingBox(obj2); + shape2 = new Shape(obj2); + + return shape1.isIntersecting(shape2); }, INITIAL: 0, SUSTAINED: 1, - RECTANGLE: 0, - CIRCLE: 1 + RECTANGLE: RectangleShape, + CIRCLE: CircleShape }; var getBoundingBox = function(obj) { diff --git a/src/quadtree.js b/src/quadtree.js new file mode 100644 index 0000000..e837878 --- /dev/null +++ b/src/quadtree.js @@ -0,0 +1,82 @@ +function Quadtree(x1, y1, x2, y2, level) { + this.x1 = x1; + this.x2 = x2; + this.y1 = y1; + this.y2 = y2; + + this.objects = []; + this.nodes = []; + this.leaf = true; + + this.level = level || 1; +} + +Quadtree.prototype = { + MAX_OBJECTS: 5, + MAX_LEVEL: 10 +} + +Quadtree.prototype.insert = function(object) { + var x = object.x; + var y = object.y; + if(isNaN(x) || isNaN(y)) return; + + if(this.leaf) { + if(this.objects.length= x1) && (o.x < x2) && (o.y >= y1) && (o.y < y2)) { + found.push(o); + } + }); + return qx1 >= x2 || qy1 >= y2 || qx2 < x1 || qy2 < y1; + }); + return found; +} + +module.exports = Quadtree; From 15cf5e1d186bda51bbbd1105501e5ffe8e242b2a Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Wed, 3 Dec 2014 11:39:57 -0500 Subject: [PATCH 04/28] Initial quad tree in testing mode --- demos/collisions/game.js | 221 ++++++++++++++++----------------- demos/collisions/index.html | 3 +- src/collider.js | 241 +++++++++++++++++++++++++++++++----- src/entities.js | 3 + src/renderer.js | 19 +++ 5 files changed, 337 insertions(+), 150 deletions(-) diff --git a/demos/collisions/game.js b/demos/collisions/game.js index b7e42dd..292bafa 100644 --- a/demos/collisions/game.js +++ b/demos/collisions/game.js @@ -1,72 +1,110 @@ ;(function(exports) { - var GOLDEN_RATIO = 1.61803398875; - var Collisions = function() { - var autoFocus = false; - this.c = new Coquette(this, "collisions-canvas", - 800, 500 / GOLDEN_RATIO, "white", autoFocus); + var maxNumberOfShape = 100; + var width = 800; + var height = 500; - var update = this.c.collider.update; - // Calculate statistics for collision detection - // by intercepting function calls on the collider. - this.c.collider.update = function() { - var scanned = 0; - var colliding = 0; - - var isColliding = this.isColliding; - this.isColliding = function() { - scanned++; - var result = isColliding.apply(this, arguments); - if(result) colliding++ - return result; - } + var time = { + size: 10, + data: [], + pointer: 0, + startTime: 0, - var start = +new Date(); - update.apply(this); - var end = +new Date(); - var diff = end - start; + timeEl: undefined, + + start: function() { + this.startTime = +new Date(); + }, - this.isColliding = isColliding; + end: function() { + var endTime = +new Date(); + this.add(endTime - this.startTime); - collisionStatistics.executionTime(diff); - collisionStatistics.scannedEntityPairs(scanned); - collisionStatistics.collidingEntityPairs(colliding); + if(!this.timeEl) { + this.timeEl = document.getElementById("time"); + } + this.timeEl.innerHTML = this.avg(); + }, + + add: function(diff) { + this.data[this.pointer] = diff; + this.pointer = ++this.pointer % this.size; + }, + + avg: function() { + var sum = 0; + this.data.forEach(function(time) { + sum += time; + }); + return Math.round(sum/this.data.length); } + } + + var Collisions = function() { + var autoFocus = true; + var c = new Coquette(this, "collisions-canvas", + width, height, "white", autoFocus); + this.c = c; + + // Measuring time for calculating collisions + var update = this.c.collider.update; + this.c.collider.update = function() { + time.start(); + update.apply(this); + time.end(); + }; }; Collisions.prototype = { + entityEl: undefined, + update: function() { - var viewSize = this.c.renderer.getViewSize(); + var viewSize = this.c.renderer.getViewSize(); var viewCenter = this.c.renderer.getViewCenter(); - if (this.c.entities.all().length < 50) { // not enough shapes - var dirFromCenter = randomDirection(); + if (this.c.inputter.isPressed(this.c.inputter.SPACE)) { + this.c.collider._toggleCollisionStrategy(); + } + + var x1 = viewCenter.x - viewSize.x/2; + var x2 = viewCenter.x + viewSize.x/2; + var y1 = viewCenter.y - viewSize.y/2; + var y2 = viewCenter.y + viewSize.y/2; + + var entities = this.c.entities.all(); + if(!this.entityEl) { + this.entityEl = document.getElementById("entities"); + } + this.entityEl.innerHTML = entities.length; + + if (entities.length < maxNumberOfShape) { // not enough shapes var Shape = Math.random() > 0.5 ? Rectangle : Circle; this.c.entities.create(Shape, { // make one - center: offscreenPosition(dirFromCenter, viewSize, viewCenter), - vec: movingOnscreenVec(dirFromCenter) + center: randomPosition(x1, y1, x2, y2), + vec: randomVec() }); } - // destroy entities that are off screen - var entities = this.c.entities.all(); - for (var i = 0; i < entities.length; i++) { - if (isOutOfView(entities[i], viewSize, viewCenter)) { - this.c.entities.destroy(entities[i]); - } - } + this.c.entities.all().forEach(function(entity) { + // Wrap it! + if(entity.center.x > x2) entity.center.x = x1; + if(entity.center.x < x1) entity.center.x = x2; + if(entity.center.y > y2) entity.center.y = y1; + if(entity.center.y < y1) entity.center.y = y2; + }); }, }; var Rectangle = function(game, settings) { - this.c = game.c; - this.angle = Math.random() * 360; - this.center = settings.center; - this.size = { x: 70, y: 70 / GOLDEN_RATIO }; - this.vec = settings.vec; - this.turnSpeed = 2 * Math.random() - 1; + this.c = game.c; + this.boundingBox = new Coquette.Collider.Shape.Rectangle(this); + this.angle = Math.random() * 360; + this.center = settings.center; + this.size = { x: 20, y: 10}; + this.vec = settings.vec; + this.turnSpeed = 2 * Math.random() - 1; mixin(makeCurrentCollidersCountable, this); }; @@ -78,16 +116,13 @@ this.center.y += this.vec.y; this.angle += this.turnSpeed; // turn + this.lineWidth = this.colliderCount>0 ? 2 : 1; + this.colliderCount = 0; }, draw: function(ctx) { - if (this.colliderCount > 0) { - ctx.lineWidth = 2; - } else { - ctx.lineWidth = 1; - } - ctx.strokeStyle = "black"; + ctx.lineWidth = this.lineWidth; ctx.strokeRect(this.center.x - this.size.x / 2, this.center.y - this.size.y / 2, this.size.x, this.size.y); }, @@ -95,11 +130,11 @@ }; var Circle = function(game, settings) { - this.c = game.c; - this.boundingBox = this.c.collider.CIRCLE; - this.center = settings.center; - this.size = { x: 55, y: 55 }; - this.vec = settings.vec; + this.c = game.c; + this.boundingBox = new Coquette.Collider.Shape.Circle(this); + this.center = settings.center; + this.size = { x: 10, y: 10 }; + this.vec = settings.vec; mixin(makeCurrentCollidersCountable, this); }; @@ -109,16 +144,14 @@ // move this.center.x += this.vec.x; this.center.y += this.vec.y; + + this.lineWidth = this.colliderCount>0 ? 2 : 1; + this.colliderCount = 0; }, draw: function(ctx) { - if (this.colliderCount > 0) { - ctx.lineWidth = 2; - } else { - ctx.lineWidth = 1; - } - ctx.beginPath(); + ctx.lineWidth = this.lineWidth; ctx.arc(this.center.x, this.center.y, this.size.x / 2, 0, Math.PI * 2, true); ctx.closePath(); ctx.strokeStyle = "black"; @@ -127,25 +160,17 @@ }; - var randomDirection = function() { - return Coquette.Collider.Maths.unitVector({ x:Math.random() - .5, y:Math.random() - .5 }); - }; - - var movingOnscreenVec = function(dirFromCenter) { - return { x: -dirFromCenter.x * 3 * Math.random(), y: -dirFromCenter.y * 3 * Math.random() } - }; - - var offscreenPosition = function(dirFromCenter, viewSize, viewCenter) { - return { - x: viewCenter.x + dirFromCenter.x * viewSize.x, - y: viewCenter.y + dirFromCenter.y * viewSize.y, - }; - }; + var randomPosition = function(x1, y1, x2, y2) { + var randx = Math.round(Math.random() * (x2-x1) + x1); + var randy = Math.round(Math.random() * (y2-y1) + y1); + return { x: randx, y: randy }; + } - var isOutOfView = function(obj, viewSize, viewCenter) { - return Coquette.Collider.Maths.distance(obj.center, viewCenter) > - Math.max(viewSize.x, viewSize.y); - }; + var randomVec = function() { + var randx = Math.round(Math.random() * 10 - 5); + var randy = Math.round(Math.random() * 10 - 5); + return { x: randx, y: randy }; + } var mixin = function(from, to) { for (var i in from) { @@ -153,47 +178,13 @@ } }; - var collisionStatistics = { - - collisionsEl: undefined, - scannedEl: undefined, - timeEl: undefined, - - collidingEntityPairs: function(collidingEntities) { - if(!this.collisionsEl) { - this.collisionsEl = document.getElementById("collisions"); - } - this.collisionsEl.innerHTML = collidingEntities; - }, - - scannedEntityPairs: function(scannedEntities) { - if(!this.scannedEl) { - this.scannedEl = document.getElementById("scanned"); - } - this.scannedEl.innerHTML = scannedEntities; - }, - - executionTime: function(time) { - if(!this.timeEl) { - this.timeEl = document.getElementById("time"); - } - this.timeEl.innerHTML = time; - } - } - var makeCurrentCollidersCountable = { colliderCount: 0, // number of other shapes currently touching this shape collision: function(_, type) { - if (type === this.c.collider.INITIAL) { - this.colliderCount++; - } + this.colliderCount++; }, - uncollision: function() { - this.colliderCount--; - } - }; exports.Collisions = Collisions; diff --git a/demos/collisions/index.html b/demos/collisions/index.html index be15cb4..61c5abb 100644 --- a/demos/collisions/index.html +++ b/demos/collisions/index.html @@ -11,8 +11,7 @@ -
0 collisions.
-
0 elements scanned.
+
0 entities.
0 ms for collision detection.
diff --git a/src/collider.js b/src/collider.js index 03f1036..fc96444 100644 --- a/src/collider.js +++ b/src/collider.js @@ -1,7 +1,8 @@ ;(function(exports) { var Collider = function(coquette) { this.c = coquette; - this._getPotentialCollisionPairs = allCollisionPairs; + this._getCollisionPairs = quadTreeCollisionPairs; + }; // if no entities have uncollision(), skip expensive record keeping for uncollisions @@ -61,20 +62,24 @@ _collideRecords: [], _currentCollisionPairs: [], - update: function() { - this._currentCollisionPairs = this._getPotentialCollisionPairs(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]); - } else { - this.removeOldCollision(this.getCollideRecordIds(pair[0], pair[1])[0]); - } + _toggleCollisionStrategy: function() { + if(this._getCollisionPairs == quadTreeCollisionPairs) { + this._getCollisionPairs = allCollisionPairs; + console.log("Using all pairs for collision"); + } else { + this._getCollisionPairs = quadTreeCollisionPairs; + console.log("Using quad tree for collision"); } }, + update: function() { + this._collideRecords = []; // Just for now until I merge Mary's changes + var collisionPairs = this._getCollisionPairs(this.c.entities.all()); + collisionPairs.forEach(function(pair) { + this.collision(pair[0], pair[1]); + }.bind(this)); + }, + collision: function(entity1, entity2) { var collisionType; if (!isUncollisionOn(this.c.entities.all())) { @@ -147,49 +152,89 @@ } }, - isColliding: function(obj1, obj2) { - return isSetupForCollisions(obj1) && isSetupForCollisions(obj2) && - this.isIntersecting(obj1, obj2); - }, - - isIntersecting: function(obj1, obj2) { - var Shape, shape1, shape2; - Shape = getBoundingBox(obj1); - shape1 = new Shape(obj1); + INITIAL: 0, + SUSTAINED: 1, - Shape = getBoundingBox(obj2); - shape2 = new Shape(obj2); + RECTANGLE: 0, + CIRCLE: 1 + }; - return shape1.isIntersecting(shape2); - }, + var isColliding = function(obj1, obj2) { + return isSetupForCollisions(obj1) && isSetupForCollisions(obj2) && + isIntersecting(obj1, obj2); + }; - INITIAL: 0, - SUSTAINED: 1, + var isIntersecting = function(obj1, obj2) { + var shape1 = getBoundingBox(obj1); + var shape2 = getBoundingBox(obj2); - RECTANGLE: RectangleShape, - CIRCLE: CircleShape + return shape1.isIntersecting(shape2); }; - var quadTreeCollisionPairs = function(ent) { + var testPerformance = function(f, of) { + var start = +new Date(); + f(); + var end = +new Date(); + var diff = end - start; + //console.log(of, diff); + } + var quadTreeCollisionPairs = function(entities) { + var viewSize = this.c.renderer.getViewSize(); + var viewCenter = this.c.renderer.getViewCenter(); + + var x1 = viewCenter.x - viewSize.x/2; + var y1 = viewCenter.y - viewSize.y/2; + var x2 = viewCenter.x + viewSize.x/2; + var y2 = viewCenter.y + viewSize.y/2; + + this.quadTree = new Quadtree(x1, y1, x2, y2); + var quadTree = this.quadTree; + testPerformance(function() { + entities.forEach(function(entity) { + quadTree.insert(entity); + }); + }, "Insertion") + + var collisionPairs = []; + var scannedEntities = {}; + testPerformance(function() { + entities.forEach(function(entity) { + var collisions = quadTree.collisions(getBoundingBox(entity)); + collisions.forEach(function(collision) { + if(!scannedEntities[collision._id]) { + collisionPairs.push([entity, collision]); + } + }); + scannedEntities[entity._id] = true; + }); + }, "Retrieving") + return collisionPairs; }; var allCollisionPairs = function(ent) { - var collisionPairs = []; + 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++) { - collisionPairs.push([ent[i], ent[j]]); + potentialCollisionPairs.push([ent[i], ent[j]]); } } + var collisionPairs = []; + potentialCollisionPairs.forEach(function(pair) { + if (isColliding(pair[0], pair[1])) { + collisionPairs.push(pair); + } + }); + return collisionPairs; }; var getBoundingBox = function(obj) { - return obj.boundingBox || Collider.prototype.RECTANGLE; + return obj.boundingBox || new Coquette.Collider.Shape.Rectangle(obj); }; var notifyEntityOfCollision = function(entity, other, type) { @@ -437,6 +482,136 @@ RADIANS_TO_DEGREES: 0.01745 }; + function Quadtree(x1, y1, x2, y2, level) { + this.x1 = x1; + this.x2 = x2; + this.y1 = y1; + this.y2 = y2; + + var width = this.x2-this.x1; + var height = this.y2-this.y1; + this.rectangle = this.createRectangle(x1, y1, x2, y2); + if(this.rectangle.entity.size.y < 0) { + console.log("PANIK") + } + this.objects = []; + this.nodes = []; + this.rectangles = []; + this.leaf = true; + + this.level = level || 1; + } + + Quadtree.prototype = { + MAX_OBJECTS: 1, + MAX_LEVEL: 7 + } + + 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 Date: Thu, 4 Dec 2014 15:40:28 -0500 Subject: [PATCH 05/28] Tests comparing naive and quad approach --- demos/collisions/game.js | 132 +++++++++++++++++++++++++++++++----- demos/collisions/index.html | 9 ++- src/collider.js | 58 ++++++---------- 3 files changed, 142 insertions(+), 57 deletions(-) diff --git a/demos/collisions/game.js b/demos/collisions/game.js index 292bafa..655d4f3 100644 --- a/demos/collisions/game.js +++ b/demos/collisions/game.js @@ -1,18 +1,18 @@ ;(function(exports) { - var maxNumberOfShape = 100; var width = 800; var height = 500; + var Timer = function() { + this.size = 10; + this.data = []; + this.pointer = 0; + this.startTime = 0; - var time = { - size: 10, - data: [], - pointer: 0, - startTime: 0, - - timeEl: undefined, + this.timeEl = undefined; + } + Timer.prototype = { start: function() { this.startTime = +new Date(); }, @@ -41,18 +41,113 @@ } } + var Test = function(settings) { + this.time = {}; + this.timer = new Timer(); + this.settings = settings; + this.quad = false; + } + + Test.prototype.onStartCollisionDetection = function(collider) { + if(!this.start) { + collider._useQuadtree(this.quad, this.settings.quad); + this.start = +new Date(); + } + this.timer.start(); + } + + Test.prototype.onEndCollisionDetection = function() { + this.timer.end(); + var currentTime = +new Date(); + if(currentTime - this.start > this.settings.duration) { + this.start = undefined; + if(!this.quad) { + this.time.all = this.timer.avg(); + this.timer = new Timer(); + this.quad = true; + } else { + this.time.quad = this.timer.avg(); + this.quad = false; + testSuite.nextTest(); + } + } + } + + var TestSuite = function() { + this.tests = []; + this.current = 0; + + this.tableEl; + } + + TestSuite.prototype = { + addTest: function(test) { + this.tests.push(test); + }, + currentTest: function() { + return this.tests[this.current]; + }, + nextTest: function() { + this.logTest(); + this.current = ((this.current+1) % this.tests.length); + return this.currentTest(); + }, + hasNextTest: function() { + return this.tests.lengththis.current+1) { + this.tableEl.deleteRow(this.current+1); + } + var row = this.tableEl.insertRow(this.current+1); + + var cell1 = row.insertCell(0); + var cell2 = row.insertCell(1); + var cell3 = row.insertCell(2); + + // Add some text to the new cells: + cell1.innerHTML = JSON.stringify(this.currentTest().settings); + if(this.currentTest().time.all < this.currentTest().time.quad) { + cell2.innerHTML = "" + this.currentTest().time.all + "ms"; + cell3.innerHTML = this.currentTest().time.quad + "ms"; + } else { + cell2.innerHTML = this.currentTest().time.all + "ms"; + cell3.innerHTML = "" + this.currentTest().time.quad + "ms"; + + } + } + }; + + var testSuite = new TestSuite(); + var entitiesCount = [50, 250, 500]; + var maxObj = [1, 3, 5, 10, 20]; + var maxLevel = [1, 3, 5, 10, 20]; + + entitiesCount.forEach(function(count) { + maxObj.forEach(function(obj) { + maxLevel.forEach(function(level) { + testSuite.addTest(new Test({entities: count, duration: 5000, + quad: { + maxObjects: obj, maxLevel: level + }})); + }); + }); + }); + var Collisions = function() { var autoFocus = true; var c = new Coquette(this, "collisions-canvas", width, height, "white", autoFocus); this.c = c; - // Measuring time for calculating collisions var update = this.c.collider.update; this.c.collider.update = function() { - time.start(); + testSuite.currentTest().onStartCollisionDetection(this); update.apply(this); - time.end(); + testSuite.currentTest().onEndCollisionDetection(this); }; }; @@ -64,10 +159,6 @@ var viewSize = this.c.renderer.getViewSize(); var viewCenter = this.c.renderer.getViewCenter(); - if (this.c.inputter.isPressed(this.c.inputter.SPACE)) { - this.c.collider._toggleCollisionStrategy(); - } - var x1 = viewCenter.x - viewSize.x/2; var x2 = viewCenter.x + viewSize.x/2; var y1 = viewCenter.y - viewSize.y/2; @@ -79,7 +170,9 @@ } this.entityEl.innerHTML = entities.length; - if (entities.length < maxNumberOfShape) { // not enough shapes + var currentTestSettings = testSuite.currentTest().settings; + // Create if too less + for(var i=0; i<(currentTestSettings.entities-entities.length); i++) { var Shape = Math.random() > 0.5 ? Rectangle : Circle; this.c.entities.create(Shape, { // make one center: randomPosition(x1, y1, x2, y2), @@ -87,8 +180,13 @@ }); } + // Destroy if too many + for(var i=0; i<(entities.length-currentTestSettings.entities); i++) { + this.c.entities.destroy(entities[i]); + } + + // Wrap it! this.c.entities.all().forEach(function(entity) { - // Wrap it! if(entity.center.x > x2) entity.center.x = x1; if(entity.center.x < x1) entity.center.x = x2; if(entity.center.y > y2) entity.center.y = y1; diff --git a/demos/collisions/index.html b/demos/collisions/index.html index 61c5abb..450c583 100644 --- a/demos/collisions/index.html +++ b/demos/collisions/index.html @@ -11,7 +11,12 @@ -
0 entities.
-
0 ms for collision detection.
+
0ms for testing 0 entities.
+

Test results

+
+ + +
SettingsNaiveQuad
+
diff --git a/src/collider.js b/src/collider.js index fc96444..f57ff52 100644 --- a/src/collider.js +++ b/src/collider.js @@ -2,7 +2,6 @@ var Collider = function(coquette) { this.c = coquette; this._getCollisionPairs = quadTreeCollisionPairs; - }; // if no entities have uncollision(), skip expensive record keeping for uncollisions @@ -57,18 +56,17 @@ } } - Collider.prototype = { _collideRecords: [], _currentCollisionPairs: [], - _toggleCollisionStrategy: function() { - if(this._getCollisionPairs == quadTreeCollisionPairs) { - this._getCollisionPairs = allCollisionPairs; - console.log("Using all pairs for collision"); - } else { + _useQuadtree: function(useQuadtree, quadSettings) { + if(useQuadtree) { this._getCollisionPairs = quadTreeCollisionPairs; - console.log("Using quad tree for collision"); + this._quadSettings = quadSettings; + } else { + this._getCollisionPairs = allCollisionPairs; + this.quadTree = undefined; } }, @@ -152,7 +150,6 @@ } }, - INITIAL: 0, SUSTAINED: 1, @@ -172,14 +169,6 @@ return shape1.isIntersecting(shape2); }; - var testPerformance = function(f, of) { - var start = +new Date(); - f(); - var end = +new Date(); - var diff = end - start; - //console.log(of, diff); - } - var quadTreeCollisionPairs = function(entities) { var viewSize = this.c.renderer.getViewSize(); var viewCenter = this.c.renderer.getViewCenter(); @@ -190,26 +179,23 @@ var y2 = viewCenter.y + viewSize.y/2; this.quadTree = new Quadtree(x1, y1, x2, y2); + this.quadTree.settings = this._quadSettings; var quadTree = this.quadTree; - testPerformance(function() { - entities.forEach(function(entity) { - quadTree.insert(entity); - }); - }, "Insertion") + entities.forEach(function(entity) { + quadTree.insert(entity); + }); var collisionPairs = []; var scannedEntities = {}; - testPerformance(function() { - entities.forEach(function(entity) { - var collisions = quadTree.collisions(getBoundingBox(entity)); - collisions.forEach(function(collision) { - if(!scannedEntities[collision._id]) { - collisionPairs.push([entity, collision]); - } - }); - scannedEntities[entity._id] = true; + entities.forEach(function(entity) { + var collisions = quadTree.collisions(getBoundingBox(entity)); + collisions.forEach(function(collision) { + if(!scannedEntities[collision._id]) { + collisionPairs.push([entity, collision]); + } }); - }, "Retrieving") + scannedEntities[entity._id] = true; + }); return collisionPairs; }; @@ -498,22 +484,18 @@ this.nodes = []; this.rectangles = []; this.leaf = true; + this.settings = {maxObj: 1, maxLevel: 5}; this.level = level || 1; } - Quadtree.prototype = { - MAX_OBJECTS: 1, - MAX_LEVEL: 7 - } - 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 Date: Fri, 5 Dec 2014 12:52:37 -0500 Subject: [PATCH 06/28] Repairs tests --- spec/collider.spec.js | 319 ++++++++---------------------------------- 1 file changed, 60 insertions(+), 259 deletions(-) diff --git a/spec/collider.spec.js b/spec/collider.spec.js index 7ca9303..b7e5de7 100644 --- a/spec/collider.spec.js +++ b/spec/collider.spec.js @@ -3,13 +3,18 @@ var Renderer = require('../src/renderer').Renderer; var Entities = require('../src/entities').Entities; var Maths = Collider.Maths; -var mockObj = function(centerX, centerY, sizeX, sizeY, boundingBox, angle) { - return { +var mockObj = function(centerX, centerY, sizeX, sizeY, Shape, angle) { + var obj = { center: { x:centerX, y:centerY }, size: { x:sizeX, y:sizeY }, - boundingBox: boundingBox, angle: angle === undefined ? 0 : angle }; + + if(Shape) { + obj.boundingBox: new Shape(obj); + } + + return obj; }; var mock = function(thingToMockHost, thingToMockAttribute, mock) { @@ -25,6 +30,7 @@ describe('collider', function() { var MockCoquette = function() { this.entities = new Entities(this); this.collider = new Collider(this); + this.renderer = {}; }; var Thing = function(__, settings) { @@ -37,9 +43,12 @@ describe('collider', function() { describe('create', function() { it('should not add coll pair for new entity with itself', function() { var c = new MockCoquette(); - var unmock = mock(c.collider, "isColliding", function(a, b) { + var unmockCollider = mock(c.collider, "isColliding", function(a, b) { return true; }); + var unmockRenderer = mock(c.renderer, "getViewSize", function() { + return {x: 500, y: 500}; + }) var haveCreatedEntity = false; var others = []; @@ -57,7 +66,8 @@ describe('collider', function() { expect(others[0].id).toEqual(0); expect(others[1].id).toEqual(1); expect(others.length).toEqual(2); - unmock(); + unmockCollider(); + unmockRenderer(); }); it('should do collisions for new entity during current round of coll detection', function() { @@ -82,40 +92,6 @@ describe('collider', function() { expect(createdEntityCollisions).toEqual(2); unmock(); }); - - it('should support uncolls by ordering pairs for created entity in same way as pairs for existing entities', function() { - var c = new MockCoquette(); - var unmock = mock(c.collider, "isColliding", function(a, b) { - return true; - }); - - // start with two entities, do coll det and in process create third entity - var haveCreatedEntity = false; - var uncollisions = 0; - var createdEntity; - c.entities.create(Thing, { id: 0, collision: function() { - if (!haveCreatedEntity) { - haveCreatedEntity = true; - createdEntity = c.entities.create(Thing, { id: 2, uncollision: function() { - uncollisions++; - }}); - } - }}); - c.entities.create(Thing, { id: 1 }); - c.collider.update(); - - // produce an uncollision - unmock(); - var unmock = mock(c.collider, "isColliding", function(a, b) { - return false; - }); - c.collider.update(); - - // check uncollision happened - expect(uncollisions).toEqual(2); - - unmock(); - }); }); describe('destroy', function() { @@ -225,74 +201,17 @@ describe('collider', function() { c.collider.update(); unmock(); }); - - it('should fire uncollision on uncollision', function() { - var c = new MockCoquette(); - var uncollisions = 0; - var unmock = mock(c.collider, "isColliding", function() { return true; }); - c.entities.create(Thing, { uncollision: function() { uncollisions++; }}); - c.entities.create(Thing); - c.collider.update(); - mock(c.collider, "isColliding", function() { return false; }) - c.collider.update(); - expect(uncollisions).toEqual(1); - unmock(); - }); - - it('should not fire uncollision on sustained non coll', function() { - var c = new MockCoquette(); - var uncollisions = 0; - var unmock = mock(c.collider, "isColliding", function() { return true; }); - c.entities.create(Thing, { uncollision: function() { uncollisions++; }}); - c.entities.create(Thing); - c.collider.update(); - mock(c.collider, "isColliding", function() { return false; }) - c.collider.update(); - expect(uncollisions).toEqual(1); - c.collider.update(); - expect(uncollisions).toEqual(1); - unmock(); - }); - }); - - describe('destroyEntity()', function() { - it('should fire uncollision if colliding', function() { - var c = new MockCoquette(); - var uncollisions = 0; - var unmock = mock(c.collider, "isColliding", function() { return true; }); - c.entities.create(Thing, { uncollision: function() { uncollisions++; }}); - c.entities.create(Thing); - c.collider.update(); - expect(uncollisions).toEqual(0); - c.collider.destroyEntity(c.entities._entities[0]); - expect(uncollisions).toEqual(1); - unmock(); - }); - - it('should not fire uncollision if not colliding', function() { - var c = new MockCoquette(); - var uncollisions = 0; - var unmock = mock(c.collider, "isColliding", function() { return false; }); - c.entities.create(Thing, { uncollision: function() { uncollisions++; }}); - c.collider.update(); - c.collider.destroyEntity(c.entities._entities[0]); - expect(uncollisions).toEqual(0); - unmock(); - }); }); describe('collision()', function() { - it('should keep on banging out INITIAL colls if no uncollision fns', function() { + it('should keep on banging out collision callbacks', function() { var c = new MockCoquette(); var unmock = mock(c.collider, "isColliding", function() { return true }); var collisions = 0; c.entities.create(Thing, { - collision: function(__, type) { + collision: function() { collisions++; - if (type !== c.collider.INITIAL) { - throw "arg"; - } } }); c.entities.create(Thing); @@ -302,46 +221,6 @@ describe('collider', function() { expect(collisions).toEqual(3); unmock(); }); - - it('should do initial INITIAL coll if entity uncollision fn', function() { - var c = new MockCoquette(); - - var unmock = mock(c.collider, "isColliding", function() { return true }); - var initial = 0; - c.entities.create(Thing, { - uncollision: function() {}, - collision: function(__, type) { - if (type === c.collider.INITIAL) { - initial++; - } - } - }); - c.entities.create(Thing); - c.collider.update(); - expect(initial).toEqual(1); - unmock(); - }); - - it('should bang out sustained colls if colls are sustained and entity has uncollision fn', function() { - var c = new MockCoquette(); - - var unmock = mock(c.collider, "isColliding", function() { return true }); - var sustained = 0; - c.entities.create(Thing, { - uncollision: function() {}, - collision: function(__, type) { - if (type === c.collider.SUSTAINED) { - sustained++; - } - } - }); - c.entities.create(Thing); - c.collider.update(); - c.collider.update(); - c.collider.update(); - expect(sustained).toEqual(2); - unmock(); - }); }); }); @@ -385,7 +264,7 @@ describe('collider', function() { var correctObj = mockObj(5, 5, 10, 10); var c = new Collider(); it('should return true for two objects with center and size', function() { - expect(c.isColliding(correctObj, correctObj)).toEqual(true); + expect(c.isColliding(correctObj, mockObj(5, 5, 10, 10))).toEqual(true); }); it('should return false when center missing', function() { @@ -432,18 +311,18 @@ describe('collider', function() { }); it('should return true: rotated top right just touching rotated top left', function() { - expect(new Collider().isIntersecting(mockObj(207, 222, 70, 43, Collider.prototype.RECTANGLE, 325), - mockObj(280, 235, 70, 43, Collider.prototype.RECTANGLE, -484))).toEqual(true); + expect(new Collider().isIntersecting(mockObj(207, 222, 70, 43, Collider.Shape.Rectangle, 325), + mockObj(280, 235, 70, 43, Collider.Shape.Rectangle, -484))).toEqual(true); }); it('should return true: rotated bottom right just touching rotated top left', function() { - expect(new Collider().isIntersecting(mockObj(238, 205, 70, 43, Collider.prototype.RECTANGLE, 280), - mockObj(207, 133, 70, 43, Collider.prototype.RECTANGLE, 93))).toEqual(true); + expect(new Collider().isIntersecting(mockObj(238, 205, 70, 43, Collider.Shape.Rectangle, 280), + mockObj(207, 133, 70, 43, Collider.Shape.Rectangle, 93))).toEqual(true); }); it('should return true: rotated top right just touching rotated bottom left', function() { - expect(new Collider().isIntersecting(mockObj(349, 171, 70, 43, Collider.prototype.RECTANGLE, 113), - mockObj(409, 123, 70, 43, Collider.prototype.RECTANGLE, 649))).toEqual(true); + expect(new Collider().isIntersecting(mockObj(349, 171, 70, 43, Collider.Shape.Rectangle, 113), + mockObj(409, 123, 70, 43, Collider.Shape.Rectangle, 649))).toEqual(true); }); }); @@ -469,18 +348,18 @@ describe('collider', function() { }); it('should return false: rotated top right just missing rotated top left', function() { - expect(new Collider().isIntersecting(mockObj(199, 223, 70, 43, Collider.prototype.RECTANGLE, 325), - mockObj(283, 237, 70, 43, Collider.prototype.RECTANGLE, -484))).toEqual(false); + expect(new Collider().isIntersecting(mockObj(199, 223, 70, 43, Collider.Shape.Rectangle, 325), + mockObj(283, 237, 70, 43, Collider.Shape.Rectangle, -484))).toEqual(false); }); it('should return false: rotated bottom right just missing rotated top left', function() { - expect(new Collider().isIntersecting(mockObj(242, 213, 70, 43, Collider.prototype.RECTANGLE, 280), - mockObj(207, 133, 70, 43, Collider.prototype.RECTANGLE, 93))).toEqual(false); + expect(new Collider().isIntersecting(mockObj(242, 213, 70, 43, Collider.Shape.Rectangle, 280), + mockObj(207, 133, 70, 43, Collider.Shape.Rectangle, 93))).toEqual(false); }); it('should return true: rotated top right just missing rotated bottom left', function() { - expect(new Collider().isIntersecting(mockObj(340, 177, 70, 43, Collider.prototype.RECTANGLE, 113), - mockObj(409, 123, 70, 43, Collider.prototype.RECTANGLE, 649))).toEqual(false); + expect(new Collider().isIntersecting(mockObj(340, 177, 70, 43, Collider.Shape.Rectangle, 113), + mockObj(409, 123, 70, 43, Collider.Shape.Rectangle, 649))).toEqual(false); }); }); }); @@ -488,25 +367,25 @@ describe('collider', function() { describe('circle and rectangle', function() { describe('collisions', function() { it('should return true: circles side by side just overlapping', function() { - expect(new Collider().isIntersecting(mockObj(332, 180, 55, 55, Collider.prototype.CIRCLE), - mockObj(282, 182, 55, 55, Collider.prototype.CIRCLE))).toEqual(true); + expect(new Collider().isIntersecting(mockObj(332, 180, 55, 55, Collider.Shape.Circle), + mockObj(282, 182, 55, 55, Collider.Shape.Circle))).toEqual(true); }); it('should return true: circles one on top of the other just overlapping', function() { - expect(new Collider().isIntersecting(mockObj(291, 192, 55, 55, Collider.prototype.CIRCLE), - mockObj(289, 241, 55, 55, Collider.prototype.CIRCLE))).toEqual(true); + expect(new Collider().isIntersecting(mockObj(291, 192, 55, 55, Collider.Shape.Circle), + mockObj(289, 241, 55, 55, Collider.Shape.Circle))).toEqual(true); }); }); describe('non-collisions', function() { it('should return false: circles side by side just missing', function() { - expect(new Collider().isIntersecting(mockObj(345, 180, 55, 55, Collider.prototype.CIRCLE), - mockObj(282, 182, 55, 55, Collider.prototype.CIRCLE))).toEqual(false); + expect(new Collider().isIntersecting(mockObj(345, 180, 55, 55, Collider.Shape.Circle), + mockObj(282, 182, 55, 55, Collider.Shape.Circle))).toEqual(false); }); it('should return false: circles one on top of the other just missing', function() { - expect(new Collider().isIntersecting(mockObj(291, 186, 55, 55, Collider.prototype.CIRCLE), - mockObj(289, 249, 55, 55, Collider.prototype.CIRCLE))).toEqual(false); + expect(new Collider().isIntersecting(mockObj(291, 186, 55, 55, Collider.Shape.Circle), + mockObj(289, 249, 55, 55, Collider.Shape.Circle))).toEqual(false); }); }); }); @@ -515,49 +394,49 @@ describe('collider', function() { describe('collisions', function() { it('should return true for circ+rect that are colliding', function() { var collider = new Collider(); - var obj1 = mockObj(10, 10, 10, 10, collider.CIRCLE); - var obj2 = mockObj(14, 14, 10, 10, collider.RECTANGLE); + var obj1 = mockObj(10, 10, 10, 10, collider.Shape.Circle); + var obj2 = mockObj(14, 14, 10, 10, collider.Shape.Rectangle); expect(collider.isIntersecting(obj1, obj2)).toEqual(true); }); it('should return true: rotated top right just touching circle', function() { - expect(new Collider().isIntersecting(mockObj(208, 181, 55, 55, Collider.prototype.CIRCLE), - mockObj(153, 216, 70, 43, Collider.prototype.RECTANGLE, 123))).toEqual(true); + expect(new Collider().isIntersecting(mockObj(208, 181, 55, 55, Collider.Shape.Circle), + mockObj(153, 216, 70, 43, Collider.Shape.Rectangle, 123))).toEqual(true); }); it('should return true: rotated bottom right just touching circle', function() { - expect(new Collider().isIntersecting(mockObj(163, 277, 55, 55, Collider.prototype.CIRCLE), - mockObj(153, 216, 70, 43, Collider.prototype.RECTANGLE, 123))).toEqual(true); + expect(new Collider().isIntersecting(mockObj(163, 277, 55, 55, Collider.Shape.Circle), + mockObj(153, 216, 70, 43, Collider.Shape.Rectangle, 123))).toEqual(true); }); it('should return true: rotated top left side just touching circle', function() { - expect(new Collider().isIntersecting(mockObj(112, 193, 55, 55, Collider.prototype.CIRCLE), - mockObj(153, 216, 70, 43, Collider.prototype.RECTANGLE, 123))).toEqual(true); + expect(new Collider().isIntersecting(mockObj(112, 193, 55, 55, Collider.Shape.Circle), + mockObj(153, 216, 70, 43, Collider.Shape.Rectangle, 123))).toEqual(true); }); }); describe('non-collisions', function() { it('should return false for circ+rect that are not colliding', function() { var collider = new Collider(); - var obj1 = mockObj(10, 10, 10, 10, collider.CIRCLE); - var obj2 = mockObj(19, 19, 10, 10, collider.RECTANGLE); + var obj1 = mockObj(10, 10, 10, 10, collider.Shape.Circle); + var obj2 = mockObj(19, 19, 10, 10, collider.Shape.Rectangle); var intersecting = collider.isIntersecting(obj1, obj2); expect(intersecting).toEqual(false); }); it('should return false: rotated top right just missing circle', function() { - expect(new Collider().isIntersecting(mockObj(223, 180, 55, 55, Collider.prototype.CIRCLE), - mockObj(153, 216, 70, 43, Collider.prototype.RECTANGLE, 123))).toEqual(false); + expect(new Collider().isIntersecting(mockObj(223, 180, 55, 55, Collider.Shape.Circle), + mockObj(153, 216, 70, 43, Collider.Shape.Rectangle, 123))).toEqual(false); }); it('should return false: rotated bottom right just missing circle', function() { - expect(new Collider().isIntersecting(mockObj(166, 288, 55, 55, Collider.prototype.CIRCLE), - mockObj(153, 216, 70, 43, Collider.prototype.RECTANGLE, 123))).toEqual(false); + expect(new Collider().isIntersecting(mockObj(166, 288, 55, 55, Collider.Shape.Circle), + mockObj(153, 216, 70, 43, Collider.Shape.Rectangle, 123))).toEqual(false); }); it('should return false: rotated top left side just missing circle', function() { - expect(new Collider().isIntersecting(mockObj(105, 186, 55, 55, Collider.prototype.CIRCLE), - mockObj(153, 216, 70, 43, Collider.prototype.RECTANGLE, 123))).toEqual(false); + expect(new Collider().isIntersecting(mockObj(105, 186, 55, 55, Collider.Shape.Circle), + mockObj(153, 216, 70, 43, Collider.Shape.Rectangle, 123))).toEqual(false); }); }); }); @@ -566,12 +445,12 @@ describe('collider', function() { var collider = new Collider(); var obj1 = mockObj(5, 5, 1, 1, "la"); - var obj2 = mockObj(0, 0, 10, 10, collider.CIRCLE); + var obj2 = mockObj(0, 0, 10, 10, collider.Shape.Circle); expect(function() { collider.isIntersecting(obj1, obj2); }).toThrow(); - var obj1 = mockObj(5, 5, 1, 1, Collider.CIRCLE); + var obj1 = mockObj(5, 5, 1, 1, Collider.Shape.Circle); var obj2 = mockObj(0, 0, 10, 10, "la"); expect(function() { collider.isIntersecting(obj1, obj2); @@ -582,94 +461,16 @@ describe('collider', function() { it('should only return true when circle+rect in right order to collide', function() { var collider = new Collider(); - var obj1 = mockObj(38, 38, 10, 10, collider.CIRCLE); - var obj2 = mockObj(20, 20, 30, 30, collider.RECTANGLE); + var obj1 = mockObj(38, 38, 10, 10, collider.Shape.Circle); + var obj2 = mockObj(20, 20, 30, 30, collider.Shape.Rectangle); expect(collider.isIntersecting(obj1, obj2)).toEqual(true); // same dimensions, swap shape type and get no collision - var obj1 = mockObj(38, 38, 10, 10, collider.RECTANGLE); - var obj2 = mockObj(20, 20, 30, 30, collider.CIRCLE); + var obj1 = mockObj(38, 38, 10, 10, collider.Shape.Rectangle); + var obj2 = mockObj(20, 20, 30, 30, collider.Shape.Circle); expect(collider.isIntersecting(obj1, obj2)).toEqual(false); }); }); }); }); - - describe('regressions', function() { - it('should not re-report coll as result of entity reorder', function() { - // In progress collisions recorded inside collider. When checking to see - // if collision already recorded, assumed two entities would be in same order in - // record. This assumption valid if entities always compared in same order. - // But, this was occasionally not the case after zindex sort following entity - // creation. - - var MockCoquette = function() { - this.entities = new Entities(this); - this.collider = new Collider(this); - this.renderer = new Renderer(this, {}, { - style: {}, - getContext: function() { } - }); - }; - - var Entity = function(__, settings) { - for (var i in settings) { - this[i] = settings[i]; - } - }; - - // prove that sorting on entities with zindexes of zeroes reorders them - // (this was how the entities got reordered) - - var c = new MockCoquette(); - c.entities.create(Entity, { zindex: 0, id: 0 }); - c.entities.create(Entity, { zindex: 0, id: 1 }); - expect(c.entities.all()[0].id).toEqual(0); - expect(c.entities.all()[1].id).toEqual(1); - - c.entities._entities.sort(function(a, b) { - return (a.zindex || 0) < (b.zindex || 0) ? -1 : 1; - }); - expect(c.entities.all()[0].id).toEqual(1); - expect(c.entities.all()[1].id).toEqual(0); - - // prove that Entities.create no longer sorts on zindex - - c = new MockCoquette(); - c.entities.create(Entity, { zindex: 1 }); - c.entities.create(Entity, { zindex: 0 }); - expect(c.entities.all()[0].zindex).toEqual(1); - expect(c.entities.all()[1].zindex).toEqual(0); - - // prove that reordering entities produces the bug - - c = new MockCoquette(); - var initial = 0; - c.entities.create(Entity, { - uncollision: function() {}, // switch off repeated collision reporting - collision: function(__, type) { - if (type === c.collider.INITIAL) { - initial++; - } - } - }); - c.entities.create(Entity); - - var restoreIsIntersecting = mock(c.collider, 'isColliding', function() { - return true; - }); - - c.collider.update(); - expect(initial).toEqual(1); - c.collider.update(); - expect(initial).toEqual(1); // collision not re-reported - - var temp = c.entities._entities[0]; - c.entities._entities[0] = c.entities._entities[1]; - c.entities._entities[1] = temp; // reorder entities - c.collider.update(); - expect(initial).toEqual(2); // boom - restoreIsIntersecting(); - }); - }); }); From 7fc8ef3dc82a1de268080b0b425035de23d4318b Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Mon, 8 Dec 2014 12:13:26 -0500 Subject: [PATCH 07/28] Bugfix && quad settings depend on number of entities --- demos/collisions/game.js | 31 ++++++++++-------------- src/collider.js | 51 +++++++++++++++++++--------------------- 2 files changed, 36 insertions(+), 46 deletions(-) diff --git a/demos/collisions/game.js b/demos/collisions/game.js index 63bb3d9..3ffb60b 100644 --- a/demos/collisions/game.js +++ b/demos/collisions/game.js @@ -3,6 +3,8 @@ var width = 800; var height = 500; + var shapeSize = 15; + var Timer = function() { this.size = 10; this.data = []; @@ -45,12 +47,14 @@ this.time = {}; this.timer = new Timer(); this.settings = settings; - this.quad = false; + if(settings.quad !== undefined) { + this.quad = settings.quad; + } } Test.prototype.onStartCollisionDetection = function(collider) { if(!this.start) { - collider._useQuadtree(this.quad, this.settings.quad); + collider._useQuadtree(this.quad); this.start = +new Date(); } this.timer.start(); @@ -122,19 +126,8 @@ }; var testSuite = new TestSuite(); - var entitiesCount = [50, 250, 500]; - var maxObj = [1, 3, 5, 10, 20]; - var maxLevel = [1, 3, 5, 10, 20]; - - entitiesCount.forEach(function(count) { - maxObj.forEach(function(obj) { - maxLevel.forEach(function(level) { - testSuite.addTest(new Test({entities: count, duration: 10000, - quad: { - maxObjects: obj, maxLevel: level - }})); - }); - }); + [50, 100, 250, 500].forEach(function(count) { + testSuite.addTest(new Test({entities: count, duration: 5000, quad: false})); }); var Collisions = function() { @@ -200,7 +193,7 @@ this.boundingBox = new Coquette.Collider.Shape.Rectangle(this); this.angle = Math.random() * 360; this.center = settings.center; - this.size = { x: 20, y: 10}; + this.size = { x: shapeSize*2, y: shapeSize}; this.vec = settings.vec; this.turnSpeed = 2 * Math.random() - 1; @@ -231,7 +224,7 @@ this.c = game.c; this.boundingBox = new Coquette.Collider.Shape.Circle(this); this.center = settings.center; - this.size = { x: 10, y: 10 }; + this.size = { x: shapeSize, y: shapeSize }; this.vec = settings.vec; mixin(makeCurrentCollidersCountable, this); @@ -265,8 +258,8 @@ } var randomVec = function() { - var randx = Math.round(Math.random() * 10 - 5); - var randy = Math.round(Math.random() * 10 - 5); + var randx = Math.round(Math.random() * 5 - 2.5); + var randy = Math.round(Math.random() * 5 - 2.5); return { x: randx, y: randy }; } diff --git a/src/collider.js b/src/collider.js index edf8e71..6dc2475 100644 --- a/src/collider.js +++ b/src/collider.js @@ -50,10 +50,9 @@ _collideRecords: [], _currentCollisionPairs: [], - _useQuadtree: function(useQuadtree, quadSettings) { + _useQuadtree: function(useQuadtree) { if(useQuadtree) { this._getCollisionPairs = quadTreeCollisionPairs; - this._quadSettings = quadSettings; } else { this._getCollisionPairs = allCollisionPairs; this.quadTree = undefined; @@ -149,24 +148,16 @@ var y2 = viewCenter.y + viewSize.y/2; this.quadTree = new Quadtree(x1, y1, x2, y2); - this.quadTree.settings = this._quadSettings; + this.quadTree.settings = { + maxObj: Math.max(Math.round(entities.length/4), 1), + maxLevel: 5 + }; var quadTree = this.quadTree; entities.forEach(function(entity) { quadTree.insert(entity); }); - var collisionPairs = []; - var scannedEntities = {}; - entities.forEach(function(entity) { - var collisions = quadTree.collisions(getBoundingBox(entity)); - collisions.forEach(function(collision) { - if(!scannedEntities[collision._id]) { - collisionPairs.push([entity, collision]); - } - }); - scannedEntities[entity._id] = true; - }); - return collisionPairs; + return quadTree.collisions(); }; var allCollisionPairs = function(ent) { @@ -441,9 +432,7 @@ var width = this.x2-this.x1; var height = this.y2-this.y1; this.rectangle = this.createRectangle(x1, y1, x2, y2); - if(this.rectangle.entity.size.y < 0) { - console.log("PANIK") - } + this.objects = []; this.nodes = []; this.rectangles = []; @@ -534,24 +523,32 @@ } Quadtree.prototype.visit = function(callback) { - if(!callback(this.objects, this.rectangle) && !this.leaf) { + if(!callback(this.objects, this) && !this.leaf) { this.nodes.forEach(function(node) { node.visit(callback); }); } } - Quadtree.prototype.collisions = function(shape) { - var found = []; - this.visit(function(objects, quadRectangle) { - objects.forEach(function(o) { - if(shape.entity._id !== o._id && shape.isIntersecting(getBoundingBox(o))) { - found.push(o); + Quadtree.prototype.collisions = function() { + var collisions = []; + var scanned = {}; + this.visit(function(objects, quad) { + allCollisionPairs(objects).forEach(function(pair) { + var pairId = uniquePairId(pair); + if(!scanned[pairId]) { + collisions.push(pair); + scanned[pairId] = true; } }); - return !quadRectangle.isIntersecting(shape); + return false; }); - return found; + 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; From 665a932927d5af54d86561407bbc4a11f0f8a648 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Mon, 8 Dec 2014 12:14:13 -0500 Subject: [PATCH 08/28] Move rendering of quadtree to demo --- demos/collisions/game.js | 30 ++++++++++++++++++++++++++++-- src/renderer.js | 19 ------------------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/demos/collisions/game.js b/demos/collisions/game.js index 3ffb60b..6d99218 100644 --- a/demos/collisions/game.js +++ b/demos/collisions/game.js @@ -136,13 +136,25 @@ width, height, "white", autoFocus); this.c = c; - var update = this.c.collider.update; + var colliderUpdate = this.c.collider.update; this.c.collider.update = function() { testSuite.currentTest().onStartCollisionDetection(this); - update.apply(this); + colliderUpdate.apply(this); testSuite.currentTest().onEndCollisionDetection(this); }; + var rendererUpdate = this.c.renderer.update; + this.c.renderer.update = function(intercal) { + rendererUpdate.apply(this); + var ctx = this.getCtx(); + + // draw quad tree + if(this.c.collider.quadTree) { + drawQuad(this.c.collider.quadTree, ctx); + } + + } + }; Collisions.prototype = { @@ -251,6 +263,20 @@ }; + var levelToColor = ['green', 'red', 'orange', 'yellow', 'brown', 'purple', 'blue']; + var drawQuad = function(quadtree, ctx) { + ctx.lineWidth = 1; + ctx.strokeStyle = levelToColor[quadtree.level-1]; + var x1 = quadtree.x1; + var y1 = quadtree.y1; + var x2 = quadtree.x2; + var y2 = quadtree.y2; + ctx.strokeRect(x1, y1, x2-x1, y2-y1); + quadtree.nodes.forEach(function(node) { + drawQuad(node, ctx); + }); + } + var randomPosition = function(x1, y1, x2, y2) { var randx = Math.round(Math.random() * (x2-x1) + x1); var randy = Math.round(Math.random() * (y2-y1) + y1); diff --git a/src/renderer.js b/src/renderer.js index 235527a..bf9e6d6 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -50,11 +50,6 @@ this._viewSize.x, this._viewSize.y); - // draw quad tree - if(this.c.collider.quadTree) { - drawQuad(this.c.collider.quadTree, ctx); - } - // draw game and entities var drawables = [this.game] .concat(this.c.entities.all().sort(zindexSort)); @@ -90,20 +85,6 @@ } }; - var levelToColor = ['green', 'red', 'orange', 'yellow', 'brown', 'purple', 'blue']; - var drawQuad = function(quadtree, ctx) { - ctx.lineWidth = 1; - ctx.strokeStyle = levelToColor[quadtree.level-1]; - var x1 = quadtree.x1; - var y1 = quadtree.y1; - var x2 = quadtree.x2; - var y2 = quadtree.y2; - ctx.strokeRect(x1, y1, x2-x1, y2-y1); - quadtree.nodes.forEach(function(node) { - drawQuad(node, ctx); - }); - } - var viewOffset = function(viewCenter, viewSize) { return { x: -(viewCenter.x - viewSize.x / 2), From e97ea7f72658cef6ab5d7c53ceb35910aecf7546 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Mon, 8 Dec 2014 13:06:25 -0500 Subject: [PATCH 09/28] Adapt to quad changes --- demos/spinning-shapes/game.js | 2 +- src/collider.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/demos/spinning-shapes/game.js b/demos/spinning-shapes/game.js index bc7855c..452cecc 100644 --- a/demos/spinning-shapes/game.js +++ b/demos/spinning-shapes/game.js @@ -79,7 +79,7 @@ var Circle = function(game, settings) { this.c = game.c; - this.boundingBox = this.c.collider.CIRCLE; + this.boundingBox = new Coquette.Collider.Shape.Circle(this); this.center = settings.center; this.size = { x: 55, y: 55 }; this.vec = settings.vec; diff --git a/src/collider.js b/src/collider.js index 6dc2475..7050dcb 100644 --- a/src/collider.js +++ b/src/collider.js @@ -119,7 +119,11 @@ }, isIntersecting: function(obj1, obj2) { - isIntersecting(obj1, obj2); + return isIntersecting(obj1, obj2); + }, + + isColliding: function(obj1, obj2) { + return isColliding(obj1, obj2); }, RECTANGLE: 0, From 4fbdf36698bea1bb98bc4f9d2c7336951b63fd82 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Mon, 8 Dec 2014 13:25:41 -0500 Subject: [PATCH 10/28] Adapt to quad changes --- demos/leftrightspace/asteroid.js | 2 +- demos/leftrightspace/bullet.js | 2 +- demos/leftrightspace/player.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/demos/leftrightspace/asteroid.js b/demos/leftrightspace/asteroid.js index f577cb2..b8d52db 100644 --- a/demos/leftrightspace/asteroid.js +++ b/demos/leftrightspace/asteroid.js @@ -2,7 +2,7 @@ var Asteroid = function(game, settings) { this.game = game; this.center = settings.center; - this.boundingBox = this.game.c.collider.CIRCLE; + this.boundingBox = new Coquette.Collider.Shape.Circle(this); if (settings.radius === undefined) { this.size = { x: 60, y: 60 }; diff --git a/demos/leftrightspace/bullet.js b/demos/leftrightspace/bullet.js index b18416f..6d1a7cd 100644 --- a/demos/leftrightspace/bullet.js +++ b/demos/leftrightspace/bullet.js @@ -1,7 +1,7 @@ ;(function(exports) { var Bullet = function(game, settings) { this.game = game; - this.boundingBox = game.c.collider.CIRCLE; + this.boundingBox = new Coquette.Collider.Shape.Circle(this); this.center = settings.center; this.vel = settings.vector; }; diff --git a/demos/leftrightspace/player.js b/demos/leftrightspace/player.js index b5b0e98..18b34cd 100644 --- a/demos/leftrightspace/player.js +++ b/demos/leftrightspace/player.js @@ -1,7 +1,7 @@ ;(function(exports) { var Player = function(game, settings) { this.game = game; - this.boundingBox = game.c.collider.CIRCLE; + this.boundingBox = new Coquette.Collider.Shape.Circle(this); this.center = settings.center; this.vel = { x:0, y:0 }; // bullshit this.pathInset = this.game.c.renderer.getViewSize().x / 2; From 7d4de01b6ecf8bb2cdc04459c1046921d5f96147 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Mon, 8 Dec 2014 13:30:26 -0500 Subject: [PATCH 11/28] Use shapes for collision detection --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1649f5c..b3b9138 100644 --- a/README.md +++ b/README.md @@ -292,7 +292,7 @@ To make an entity support collisions, put these attributes on it: * `center`: The center of the entity, e.g. `{ x: 10, y: 20 }`. * `size`: The size of the entity, e.g. `{ x: 50, y: 30 }`. -* `boundingBox`: The shape that best approximates the shape of the entity, either `c.collider.RECTANGLE` or `c.collider.CIRCLE`. +* `boundingBox`: The shape that best approximates the shape of the entity, either an instance of `Coquette.Collider.Shape.Rectangle` or `Coquette.Collider.Shape.Circle` with the entity passed to the constructor. * `angle`: The orientation of the entity in degrees, e.g. `30`. And, optionally, this method: @@ -305,7 +305,7 @@ For example: var Player = function() { this.center = { x: 10, y: 20 }; this.size = { x: 50, y: 50 }; - this.boundingBox = c.collider.CIRCLE; + this.boundingBox = new Coquette.Collider.Shape.Circle(this); this.angle = 0; }; From 7fa75b8d0d3772b4e24398a71398044d91c7a1c2 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Mon, 8 Dec 2014 16:45:04 -0500 Subject: [PATCH 12/28] Removed last occurrence of collideRecords --- src/collider.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/collider.js b/src/collider.js index 8dafda6..c0314c7 100644 --- a/src/collider.js +++ b/src/collider.js @@ -59,7 +59,6 @@ }, update: function() { - this._collideRecords = []; // Just for now until I merge Mary's changes var collisionPairs = this._getCollisionPairs(this.c.entities.all()); collisionPairs.forEach(function(pair) { this.collision(pair[0], pair[1]); From 1e4ca88c3432b9adc08f45882430931710c0d400 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Mon, 8 Dec 2014 17:17:52 -0500 Subject: [PATCH 13/28] Refactoring && calculation of world size based on entities (dynamic size of quad tree) --- src/collider.js | 71 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/src/collider.js b/src/collider.js index c0314c7..a777d04 100644 --- a/src/collider.js +++ b/src/collider.js @@ -90,7 +90,10 @@ }, isIntersecting: function(obj1, obj2) { - return isIntersecting(obj1, obj2); + var shape1 = getBoundingBox(obj1); + var shape2 = getBoundingBox(obj2); + + return shape1.isIntersecting(shape2); }, isColliding: function(obj1, obj2) { @@ -104,26 +107,45 @@ CIRCLE: 1 }; - var isColliding = function(obj1, obj2) { - return isSetupForCollisions(obj1) && isSetupForCollisions(obj2) && - isIntersecting(obj1, obj2); - }; + var getDimensions = function(entities) { + var maxx, minx, maxy, miny; + + entities.forEach(function(entity) { + if(entity.center) { + 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; + } + } + }); - var isIntersecting = function(obj1, obj2) { - var shape1 = getBoundingBox(obj1); - var shape2 = getBoundingBox(obj2); + var width = maxx - minx; + var height = maxy - miny; - return shape1.isIntersecting(shape2); + var worldSize = {x: width, y: height }; + var worldCenter = {x: minx + width/2, y: miny + height/2}; + return [worldSize, worldCenter]; }; var quadTreeCollisionPairs = function(entities) { - var viewSize = this.c.renderer.getViewSize(); - var viewCenter = this.c.renderer.getViewCenter(); + var dimensions = getDimensions(entities); + + var worldSize = dimensions[0]; + var worldCenter = dimensions[1]; + + var x1 = worldCenter.x - worldSize.x/2; + var y1 = worldCenter.y - worldSize.y/2; + var x2 = worldCenter.x + worldSize.x/2; + var y2 = worldCenter.y + worldSize.y/2; - var x1 = viewCenter.x - viewSize.x/2; - var y1 = viewCenter.y - viewSize.y/2; - var x2 = viewCenter.x + viewSize.x/2; - var y2 = viewCenter.y + viewSize.y/2; this.quadTree = new Quadtree(x1, y1, x2, y2); this.quadTree.settings = { @@ -135,6 +157,7 @@ quadTree.insert(entity); }); + quadTree.allCollisionPairs = allCollisionPairs.bind(this); return quadTree.collisions(); }; @@ -150,10 +173,10 @@ var collisionPairs = []; potentialCollisionPairs.forEach(function(pair) { - if (isColliding(pair[0], pair[1])) { + if(this.isColliding(pair[0], pair[1])) { collisionPairs.push(pair); } - }); + }.bind(this)); return collisionPairs; }; @@ -459,22 +482,22 @@ var size = {x: width/2, y: height/2} - this.rectangles[0] = new Coquette.Collider.Shape.Rectangle({ + this.rectangles[0] = new Collider.Shape.Rectangle({ center: {x: width/4 + this.x1, y: height/4 + this.y1}, size: size}); - this.rectangles[1] = new Coquette.Collider.Shape.Rectangle({ + this.rectangles[1] = new Collider.Shape.Rectangle({ center: {x: width/4*3 + this.x1, y: height/4 + this.y1}, size: size}); - this.rectangles[2] = new Coquette.Collider.Shape.Rectangle({ + this.rectangles[2] = new Collider.Shape.Rectangle({ center: {x: width/4 + this.x1, y: height/4*3 + this.y1}, size: size}); - this.rectangles[3] = new Coquette.Collider.Shape.Rectangle({ + this.rectangles[3] = new Collider.Shape.Rectangle({ center: {x: width/4*3 + this.x1, y: height/4*3 + this.y1}, @@ -490,7 +513,7 @@ Quadtree.prototype.createRectangle = function(x1, y1, x2, y2) { var width = this.x2-this.x1; var height = this.y2-this.y1; - return new Coquette.Collider.Shape.Rectangle({ + return new Collider.Shape.Rectangle({ center: {x: width/2 + x1, y: height/2 + y1}, @@ -512,7 +535,7 @@ var collisions = []; var scanned = {}; this.visit(function(objects, quad) { - allCollisionPairs(objects).forEach(function(pair) { + this.allCollisionPairs(objects).forEach(function(pair) { var pairId = uniquePairId(pair); if(!scanned[pairId]) { collisions.push(pair); @@ -520,7 +543,7 @@ } }); return false; - }); + }.bind(this)); return collisions; } From 96fad4b1ade5b4207625cb6698f7d661740aa99a Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Tue, 9 Dec 2014 11:13:06 -0500 Subject: [PATCH 14/28] Fixes tests --- coquette-min.js | 2 +- coquette.js | 302 +++++++++++++++++++++++++++++++++++++----- spec/collider.spec.js | 132 ++++++++++++------ src/collider.js | 13 +- 4 files changed, 366 insertions(+), 83 deletions(-) diff --git a/coquette-min.js b/coquette-min.js index d289900..123fd67 100644 --- a/coquette-min.js +++ b/coquette-min.js @@ -1 +1 @@ -(function(exports){var Coquette=function(game,canvasId,width,height,backgroundColor,autoFocus){var canvas=document.getElementById(canvasId);this.renderer=new Coquette.Renderer(this,game,canvas,width,height,backgroundColor);this.inputter=new Coquette.Inputter(this,canvas,autoFocus);this.entities=new Coquette.Entities(this,game);this.runner=new Coquette.Runner(this);this.collider=new Coquette.Collider(this);var self=this;this.ticker=new Coquette.Ticker(this,function(interval){self.collider.update(interval);self.runner.update(interval);if(game.update!==undefined){game.update(interval)}self.entities.update(interval);self.renderer.update(interval);self.inputter.update()})};exports.Coquette=Coquette})(this);(function(exports){var Collider=function(coquette){this.c=coquette};var isSetupForCollisions=function(obj){return obj.center!==undefined&&obj.size!==undefined};Collider.prototype={_currentCollisionPairs:[],update:function(){this._currentCollisionPairs=[];var ent=this.c.entities.all();for(var i=0,len=ent.length;i0){var pair=this._currentCollisionPairs.shift();if(this.isColliding(pair[0],pair[1])){this.collision(pair[0],pair[1])}}},collision:function(entity1,entity2){notifyEntityOfCollision(entity1,entity2);notifyEntityOfCollision(entity2,entity1)},createEntity:function(entity){var ent=this.c.entities.all();for(var i=0,len=ent.length;i=0;i--){if(this._currentCollisionPairs[i][0]===entity||this._currentCollisionPairs[i][1]===entity){this._currentCollisionPairs.splice(i,1)}}},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:0,CIRCLE:1};var getBoundingBox=function(obj){return obj.boundingBox||Collider.prototype.RECTANGLE};var notifyEntityOfCollision=function(entity,other){if(entity.collision!==undefined){entity.collision(other)}};var rotated=function(obj){return obj.angle!==undefined&&obj.angle!==0};var getAngle=function(obj){return obj.angle===undefined?0:obj.angle};var Maths={circlesIntersecting:function(obj1,obj2){return Maths.distance(obj1.center,obj2.center)rectangleObj.center.x+rectangleObj.size.x/2){closest.x=rectangleObj.center.x+rectangleObj.size.x/2}else{closest.x=unrotatedCircleCenter.x}if(unrotatedCircleCenter.yrectangleObj.center.y+rectangleObj.size.y/2){closest.y=rectangleObj.center.y+rectangleObj.size.y/2}else{closest.y=unrotatedCircleCenter.y}return this.distance(unrotatedCircleCenter,closest)obj2.center.x+obj2.size.x/2){return false}else if(obj1.center.y-obj1.size.y/2>obj2.center.y+obj2.size.y/2){return false}else if(obj1.center.y+obj1.size.y/2current){min=current}if(current>max){max=current}}return{min:min,max:max}},rectangleCorners:function(obj){var corners=[{x:obj.center.x-obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y+obj.size.y/2},{x:obj.center.x-obj.size.x/2,y:obj.center.y+obj.size.y/2}];var angle=getAngle(obj)*Maths.RADIANS_TO_DEGREES;for(var i=0;i0){var run=this._runs.shift();run.fn(run.obj)}},add:function(obj,fn){this._runs.push({obj:obj,fn:fn})}};exports.Runner=Runner})(typeof exports==="undefined"?this.Coquette:exports);(function(exports){var interval=16;function Ticker(coquette,gameLoop){setupRequestAnimationFrame();var nextTickFn;this.stop=function(){nextTickFn=function(){}};this.start=function(){var prev=(new Date).getTime();var tick=function(){var now=(new Date).getTime();var interval=now-prev;prev=now;gameLoop(interval);requestAnimationFrame(nextTickFn)};nextTickFn=tick;requestAnimationFrame(nextTickFn)};this.start()}var setupRequestAnimationFrame=function(){var lastTime=0;var vendors=["ms","moz","webkit","o"];for(var x=0;x=0;i--){if(this._currentCollisionPairs[i][0]===entity||this._currentCollisionPairs[i][1]===entity){this._currentCollisionPairs.splice(i,1)}}},isIntersecting:function(obj1,obj2){var shape1=getBoundingBox(obj1);var shape2=getBoundingBox(obj2);return shape1.isIntersecting(shape2)},isColliding:function(obj1,obj2){return obj1!==obj2&&isSetupForCollisions(obj1)&&isSetupForCollisions(obj2)&&this.isIntersecting(obj1,obj2)},RECTANGLE:0,CIRCLE:1};var getDimensions=function(entities){var maxx,minx,maxy,miny;entities.forEach(function(entity){if(entity.center){if(maxx===undefined||entity.center.x>maxx){maxx=entity.center.x}if(minx===undefined||entity.center.xmaxy){maxy=entity.center.y}if(miny===undefined||entity.center.yrectangleObj.center.x+rectangleObj.size.x/2){closest.x=rectangleObj.center.x+rectangleObj.size.x/2}else{closest.x=unrotatedCircleCenter.x}if(unrotatedCircleCenter.yrectangleObj.center.y+rectangleObj.size.y/2){closest.y=rectangleObj.center.y+rectangleObj.size.y/2}else{closest.y=unrotatedCircleCenter.y}return this.distance(unrotatedCircleCenter,closest)obj2.center.x+obj2.size.x/2){return false}else if(obj1.center.y-obj1.size.y/2>obj2.center.y+obj2.size.y/2){return false}else if(obj1.center.y+obj1.size.y/2current){min=current}if(current>max){max=current}}return{min:min,max:max}},rectangleCorners:function(obj){var corners=[{x:obj.center.x-obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y+obj.size.y/2},{x:obj.center.x-obj.size.x/2,y:obj.center.y+obj.size.y/2}];var angle=getAngle(obj)*Maths.RADIANS_TO_DEGREES;for(var i=0;i0){var run=this._runs.shift();run.fn(run.obj)}},add:function(obj,fn){this._runs.push({obj:obj,fn:fn})}};exports.Runner=Runner})(typeof exports==="undefined"?this.Coquette:exports);(function(exports){var interval=16;function Ticker(coquette,gameLoop){setupRequestAnimationFrame();var nextTickFn;this.stop=function(){nextTickFn=function(){}};this.start=function(){var prev=(new Date).getTime();var tick=function(){var now=(new Date).getTime();var interval=now-prev;prev=now;gameLoop(interval);requestAnimationFrame(nextTickFn)};nextTickFn=tick;requestAnimationFrame(nextTickFn)};this.start()}var setupRequestAnimationFrame=function(){var lastTime=0;var vendors=["ms","moz","webkit","o"];for(var x=0;x 0) { - var pair = this._currentCollisionPairs.shift(); - if (this.isColliding(pair[0], pair[1])) { - this.collision(pair[0], pair[1]); - } + Collider.prototype = { + _currentCollisionPairs: [], + + _useQuadtree: function(useQuadtree) { + if(useQuadtree) { + this._getCollisionPairs = quadTreeCollisionPairs; + } else { + this._getCollisionPairs = allCollisionPairs; + this.quadTree = undefined; } }, + update: function() { + var collisionPairs = this._getCollisionPairs(this.c.entities.all()); + collisionPairs.forEach(function(pair) { + this.collision(pair[0], pair[1]); + }.bind(this)); + }, + collision: function(entity1, entity2) { notifyEntityOfCollision(entity1, entity2); notifyEntityOfCollision(entity2, entity1); @@ -80,6 +115,13 @@ } }, + isIntersecting: function(obj1, obj2) { + var shape1 = getBoundingBox(obj1); + var shape2 = getBoundingBox(obj2); + + return shape1.isIntersecting(shape2); + }, + isColliding: function(obj1, obj2) { return obj1 !== obj2 && isSetupForCollisions(obj1) && @@ -87,29 +129,86 @@ 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: 0, CIRCLE: 1 }; + var getDimensions = function(entities) { + var maxx, minx, maxy, miny; + + entities.forEach(function(entity) { + if(entity.center) { + 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; + } + } + }); + + var width = maxx - minx; + var height = maxy - miny; + + var worldSize = {x: width, y: height }; + var worldCenter = {x: minx + width/2, y: miny + height/2}; + return [worldSize, worldCenter]; + }; + + var quadTreeCollisionPairs = function(entities) { + var dimensions = getDimensions(entities); + + var worldSize = dimensions[0]; + var worldCenter = dimensions[1]; + + var x1 = worldCenter.x - worldSize.x/2; + var y1 = worldCenter.y - worldSize.y/2; + var x2 = worldCenter.x + worldSize.x/2; + var y2 = worldCenter.y + worldSize.y/2; + + + this.quadTree = new Quadtree(x1, y1, x2, y2); + this.quadTree.settings = { + 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) { - return obj.boundingBox || Collider.prototype.RECTANGLE; + return obj.boundingBox || new Collider.Shape.Rectangle(obj); }; var notifyEntityOfCollision = function(entity, other) { @@ -351,8 +450,140 @@ RADIANS_TO_DEGREES: 0.01745 }; + function Quadtree(x1, y1, x2, y2, level) { + this.x1 = x1; + this.x2 = x2; + this.y1 = y1; + this.y2 = y2; + + var width = this.x2-this.x1; + var height = this.y2-this.y1; + this.rectangle = this.createRectangle(x1, y1, x2, y2); + + this.objects = []; + this.nodes = []; + this.rectangles = []; + this.leaf = true; + this.settings = {maxObj: 1, maxLevel: 5}; + + 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= 0; i--){ if (this._currentCollisionPairs[i][0] === entity || this._currentCollisionPairs[i][1] === entity) { - this._currentCollisionPairs.splice(i, 1); + this._currentCollisionPairs.splice(i, 1, undefined); } } }, From 644c8cbf34434a0e100f5bf8ed3d8c0011c2092f Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Tue, 9 Dec 2014 11:23:21 -0500 Subject: [PATCH 15/28] Cleanup --- src/quadtree.js | 82 ------------------------------------------------- 1 file changed, 82 deletions(-) delete mode 100644 src/quadtree.js diff --git a/src/quadtree.js b/src/quadtree.js deleted file mode 100644 index e837878..0000000 --- a/src/quadtree.js +++ /dev/null @@ -1,82 +0,0 @@ -function Quadtree(x1, y1, x2, y2, level) { - this.x1 = x1; - this.x2 = x2; - this.y1 = y1; - this.y2 = y2; - - this.objects = []; - this.nodes = []; - this.leaf = true; - - this.level = level || 1; -} - -Quadtree.prototype = { - MAX_OBJECTS: 5, - MAX_LEVEL: 10 -} - -Quadtree.prototype.insert = function(object) { - var x = object.x; - var y = object.y; - if(isNaN(x) || isNaN(y)) return; - - if(this.leaf) { - if(this.objects.length= x1) && (o.x < x2) && (o.y >= y1) && (o.y < y2)) { - found.push(o); - } - }); - return qx1 >= x2 || qy1 >= y2 || qx2 < x1 || qy2 < y1; - }); - return found; -} - -module.exports = Quadtree; From f0bed743b933ada734c92244ccb70167c6c0e00c Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Tue, 9 Dec 2014 11:33:48 -0500 Subject: [PATCH 16/28] v0.6.0 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a34e95..3315bad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/package.json b/package.json index f50f197..03495fa 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "coquette", "description": "A micro framework for JavaScript games.", "author": "Mary Rose Cook (http://maryrosecook.com/)", - "version": "0.5.4", + "version": "0.6.0", "repository": { "type": "git", "url": "https://github.com/maryrosecook/coquette.git" From ab682ed82bab7cb1d5d49b504af54d9af80ab290 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Tue, 9 Dec 2014 11:36:51 -0500 Subject: [PATCH 17/28] Adds bounding box changes for v0.6.0 --- index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index a264103..388d48d 100644 --- a/index.html +++ b/index.html @@ -373,7 +373,7 @@
Entity setup
  • center: The center of the entity, e.g. { x: 10, y: 20 }.
  • size: The size of the entity, e.g. { x: 50, y: 30 }.
  • -
  • boundingBox: The shape that best approximates the shape of the entity, either c.collider.RECTANGLE or c.collider.CIRCLE.
  • +
  • boundingBox: The shape that best approximates the shape of the entity, either an instance of Coquette.Collider.Shape.Rectangle or Coquette.Collider.Shape.Circle with the entity passed to the constructor.
  • angle: The orientation of the entity in degrees, e.g. 30.
@@ -389,7 +389,7 @@
Entity setup
var Player = function() { this.center = { x: 10, y: 20 }; this.size = { x: 50, y: 50 }; - this.boundingBox = c.collider.CIRCLE; + this.boundingBox = new Collider.Shape.Circle(this); this.angle = 0; }; From b3968791703a5b6b4e1ae692ff740f49e1bd9e9a Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Tue, 16 Dec 2014 13:26:42 -0500 Subject: [PATCH 18/28] Using else-if --- src/collider.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/collider.js b/src/collider.js index 7f44e5d..3b1bc81 100644 --- a/src/collider.js +++ b/src/collider.js @@ -16,14 +16,11 @@ isIntersecting: function(anotherShape) { if(anotherShape instanceof CircleShape) { return Maths.circleAndRectangleIntersecting(anotherShape.entity, this.entity); - } - if(anotherShape instanceof RectangleShape) { + } else if(anotherShape instanceof RectangleShape) { return Maths.rectanglesIntersecting(this.entity, anotherShape.entity); - } - if(anotherShape.isIntersecting) { + } else if(anotherShape.isIntersecting) { return anotherShape.isIntersecting(this); - } - throw "Objects being collision tested have unsupported bounding box types." + } else throw "Objects being collision tested have unsupported bounding box types." } } From b896d21815c1be5ebeaf5fa3bdfea24128630c90 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Wed, 17 Dec 2014 12:05:44 -0500 Subject: [PATCH 19/28] id instead of freeId --- src/entities.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/entities.js b/src/entities.js index b81117d..77ff55d 100644 --- a/src/entities.js +++ b/src/entities.js @@ -1,5 +1,5 @@ ;(function(exports) { - var freeId = 0; + var id = 0; function Entities(coquette, game) { this.c = coquette; @@ -34,7 +34,7 @@ create: function(Constructor, settings) { var entity = new Constructor(this.game, settings || {}); - entity._id = freeId++; + entity._id = id++; this.c.collider.createEntity(entity); this._entities.push(entity); return entity; From cd71849928aa5f2a161c6c598cea22bbff9dc10c Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Wed, 17 Dec 2014 16:47:38 -0500 Subject: [PATCH 20/28] Using singleton shapes && simplify shape creation --- coquette-min.js | 2 +- coquette.js | 130 ++++++++++++++----------------- demos/collisions/game.js | 4 +- demos/leftrightspace/asteroid.js | 2 +- demos/leftrightspace/bullet.js | 2 +- demos/leftrightspace/player.js | 2 +- demos/spinning-shapes/game.js | 2 +- spec/collider.spec.js | 95 +++++++++++----------- src/collider.js | 110 ++++++++++++-------------- 9 files changed, 160 insertions(+), 189 deletions(-) diff --git a/coquette-min.js b/coquette-min.js index 123fd67..1e2b70c 100644 --- a/coquette-min.js +++ b/coquette-min.js @@ -1 +1 @@ -(function(exports){var Coquette=function(game,canvasId,width,height,backgroundColor,autoFocus){var canvas=document.getElementById(canvasId);this.renderer=new Coquette.Renderer(this,game,canvas,width,height,backgroundColor);this.inputter=new Coquette.Inputter(this,canvas,autoFocus);this.entities=new Coquette.Entities(this,game);this.runner=new Coquette.Runner(this);this.collider=new Coquette.Collider(this);var self=this;this.ticker=new Coquette.Ticker(this,function(interval){self.collider.update(interval);self.runner.update(interval);if(game.update!==undefined){game.update(interval)}self.entities.update(interval);self.renderer.update(interval);self.inputter.update()})};exports.Coquette=Coquette})(this);(function(exports){var Collider=function(coquette){this.c=coquette;this._getCollisionPairs=quadTreeCollisionPairs};var isSetupForCollisions=function(obj){return obj.center!==undefined&&obj.size!==undefined};var RectangleShape=function(entity){this.entity=entity};RectangleShape.prototype={isIntersecting:function(anotherShape){if(anotherShape instanceof CircleShape){return Maths.circleAndRectangleIntersecting(anotherShape.entity,this.entity)}if(anotherShape instanceof RectangleShape){return Maths.rectanglesIntersecting(this.entity,anotherShape.entity)}if(anotherShape.isIntersecting){return anotherShape.isIntersecting(this)}throw"Objects being collision tested have unsupported bounding box types."}};var CircleShape=function(entity){this.entity=entity};CircleShape.prototype={isIntersecting:function(anotherShape){if(anotherShape instanceof CircleShape){return Maths.circlesIntersecting(this.entity,anotherShape.entity)}if(anotherShape instanceof RectangleShape){return Maths.circleAndRectangleIntersecting(this.entity,anotherShape.entity)}if(anotherShape.isIntersecting){return anotherShape.isIntersecting(this)}throw"Objects being collision tested have unsupported bounding box types."}};Collider.prototype={_currentCollisionPairs:[],_useQuadtree:function(useQuadtree){if(useQuadtree){this._getCollisionPairs=quadTreeCollisionPairs}else{this._getCollisionPairs=allCollisionPairs;this.quadTree=undefined}},update:function(){var collisionPairs=this._getCollisionPairs(this.c.entities.all());collisionPairs.forEach(function(pair){this.collision(pair[0],pair[1])}.bind(this))},collision:function(entity1,entity2){notifyEntityOfCollision(entity1,entity2);notifyEntityOfCollision(entity2,entity1)},createEntity:function(entity){var ent=this.c.entities.all();for(var i=0,len=ent.length;i=0;i--){if(this._currentCollisionPairs[i][0]===entity||this._currentCollisionPairs[i][1]===entity){this._currentCollisionPairs.splice(i,1)}}},isIntersecting:function(obj1,obj2){var shape1=getBoundingBox(obj1);var shape2=getBoundingBox(obj2);return shape1.isIntersecting(shape2)},isColliding:function(obj1,obj2){return obj1!==obj2&&isSetupForCollisions(obj1)&&isSetupForCollisions(obj2)&&this.isIntersecting(obj1,obj2)},RECTANGLE:0,CIRCLE:1};var getDimensions=function(entities){var maxx,minx,maxy,miny;entities.forEach(function(entity){if(entity.center){if(maxx===undefined||entity.center.x>maxx){maxx=entity.center.x}if(minx===undefined||entity.center.xmaxy){maxy=entity.center.y}if(miny===undefined||entity.center.yrectangleObj.center.x+rectangleObj.size.x/2){closest.x=rectangleObj.center.x+rectangleObj.size.x/2}else{closest.x=unrotatedCircleCenter.x}if(unrotatedCircleCenter.yrectangleObj.center.y+rectangleObj.size.y/2){closest.y=rectangleObj.center.y+rectangleObj.size.y/2}else{closest.y=unrotatedCircleCenter.y}return this.distance(unrotatedCircleCenter,closest)obj2.center.x+obj2.size.x/2){return false}else if(obj1.center.y-obj1.size.y/2>obj2.center.y+obj2.size.y/2){return false}else if(obj1.center.y+obj1.size.y/2current){min=current}if(current>max){max=current}}return{min:min,max:max}},rectangleCorners:function(obj){var corners=[{x:obj.center.x-obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y+obj.size.y/2},{x:obj.center.x-obj.size.x/2,y:obj.center.y+obj.size.y/2}];var angle=getAngle(obj)*Maths.RADIANS_TO_DEGREES;for(var i=0;i0){var run=this._runs.shift();run.fn(run.obj)}},add:function(obj,fn){this._runs.push({obj:obj,fn:fn})}};exports.Runner=Runner})(typeof exports==="undefined"?this.Coquette:exports);(function(exports){var interval=16;function Ticker(coquette,gameLoop){setupRequestAnimationFrame();var nextTickFn;this.stop=function(){nextTickFn=function(){}};this.start=function(){var prev=(new Date).getTime();var tick=function(){var now=(new Date).getTime();var interval=now-prev;prev=now;gameLoop(interval);requestAnimationFrame(nextTickFn)};nextTickFn=tick;requestAnimationFrame(nextTickFn)};this.start()}var setupRequestAnimationFrame=function(){var lastTime=0;var vendors=["ms","moz","webkit","o"];for(var x=0;x=0;i--){if(this._currentCollisionPairs[i][0]===entity||this._currentCollisionPairs[i][1]===entity){this._currentCollisionPairs.splice(i,1,undefined)}}},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)},RECTANGLE:new RectangleShape,CIRCLE:new CircleShape};var getDimensions=function(entities){var maxx,minx,maxy,miny;entities.forEach(function(entity){if(entity.center){if(maxx===undefined||entity.center.x>maxx){maxx=entity.center.x}if(minx===undefined||entity.center.xmaxy){maxy=entity.center.y}if(miny===undefined||entity.center.yrectangleObj.center.x+rectangleObj.size.x/2){closest.x=rectangleObj.center.x+rectangleObj.size.x/2}else{closest.x=unrotatedCircleCenter.x}if(unrotatedCircleCenter.yrectangleObj.center.y+rectangleObj.size.y/2){closest.y=rectangleObj.center.y+rectangleObj.size.y/2}else{closest.y=unrotatedCircleCenter.y}return this.distance(unrotatedCircleCenter,closest)obj2.center.x+obj2.size.x/2){return false}else if(obj1.center.y-obj1.size.y/2>obj2.center.y+obj2.size.y/2){return false}else if(obj1.center.y+obj1.size.y/2current){min=current}if(current>max){max=current}}return{min:min,max:max}},rectangleCorners:function(obj){var corners=[{x:obj.center.x-obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y+obj.size.y/2},{x:obj.center.x-obj.size.x/2,y:obj.center.y+obj.size.y/2}];var angle=getAngle(obj)*Maths.RADIANS_TO_DEGREES;for(var i=0;i0){var run=this._runs.shift();run.fn(run.obj)}},add:function(obj,fn){this._runs.push({obj:obj,fn:fn})}};exports.Runner=Runner})(typeof exports==="undefined"?this.Coquette:exports);(function(exports){var interval=16;function Ticker(coquette,gameLoop){setupRequestAnimationFrame();var nextTickFn;this.stop=function(){nextTickFn=function(){}};this.start=function(){var prev=(new Date).getTime();var tick=function(){var now=(new Date).getTime();var interval=now-prev;prev=now;gameLoop(interval);requestAnimationFrame(nextTickFn)};nextTickFn=tick;requestAnimationFrame(nextTickFn)};this.start()}var setupRequestAnimationFrame=function(){var lastTime=0;var vendors=["ms","moz","webkit","o"];for(var x=0;x= 0; i--){ if (this._currentCollisionPairs[i][0] === entity || this._currentCollisionPairs[i][1] === entity) { - this._currentCollisionPairs.splice(i, 1); + this._currentCollisionPairs.splice(i, 1, undefined); } } }, @@ -119,7 +113,15 @@ var shape1 = getBoundingBox(obj1); var shape2 = getBoundingBox(obj2); - return shape1.isIntersecting(shape2); + 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) { @@ -129,8 +131,8 @@ this.isIntersecting(obj1, obj2); }, - RECTANGLE: 0, - CIRCLE: 1 + RECTANGLE: new RectangleShape(), + CIRCLE: new CircleShape() }; var getDimensions = function(entities) { @@ -208,7 +210,7 @@ }; var getBoundingBox = function(obj) { - return obj.boundingBox || new Collider.Shape.Rectangle(obj); + return obj.boundingBox || Collider.prototype.RECTANGLE; }; var notifyEntityOfCollision = function(entity, other) { @@ -458,7 +460,6 @@ var width = this.x2-this.x1; var height = this.y2-this.y1; - this.rectangle = this.createRectangle(x1, y1, x2, y2); this.objects = []; this.nodes = []; @@ -484,7 +485,7 @@ } } else { for(var i=0; i Date: Wed, 17 Dec 2014 17:04:45 -0500 Subject: [PATCH 21/28] Removing from the end of current collision pairs --- coquette-min.js | 2 +- coquette.js | 11 +++++------ src/collider.js | 11 +++++------ 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/coquette-min.js b/coquette-min.js index 1e2b70c..36daa26 100644 --- a/coquette-min.js +++ b/coquette-min.js @@ -1 +1 @@ -(function(exports){var Coquette=function(game,canvasId,width,height,backgroundColor,autoFocus){var canvas=document.getElementById(canvasId);this.renderer=new Coquette.Renderer(this,game,canvas,width,height,backgroundColor);this.inputter=new Coquette.Inputter(this,canvas,autoFocus);this.entities=new Coquette.Entities(this,game);this.runner=new Coquette.Runner(this);this.collider=new Coquette.Collider(this);var self=this;this.ticker=new Coquette.Ticker(this,function(interval){self.collider.update(interval);self.runner.update(interval);if(game.update!==undefined){game.update(interval)}self.entities.update(interval);self.renderer.update(interval);self.inputter.update()})};exports.Coquette=Coquette})(this);(function(exports){var Collider=function(coquette){this.c=coquette;this._getCollisionPairs=quadTreeCollisionPairs};var isSetupForCollisions=function(obj){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:[],_useQuadtree:function(useQuadtree){if(useQuadtree){this._getCollisionPairs=quadTreeCollisionPairs}else{this._getCollisionPairs=allCollisionPairs;this.quadTree=undefined}},update:function(){this._currentCollisionPairs=this._getCollisionPairs(this.c.entities.all());for(var i=0;i=0;i--){if(this._currentCollisionPairs[i][0]===entity||this._currentCollisionPairs[i][1]===entity){this._currentCollisionPairs.splice(i,1,undefined)}}},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)},RECTANGLE:new RectangleShape,CIRCLE:new CircleShape};var getDimensions=function(entities){var maxx,minx,maxy,miny;entities.forEach(function(entity){if(entity.center){if(maxx===undefined||entity.center.x>maxx){maxx=entity.center.x}if(minx===undefined||entity.center.xmaxy){maxy=entity.center.y}if(miny===undefined||entity.center.yrectangleObj.center.x+rectangleObj.size.x/2){closest.x=rectangleObj.center.x+rectangleObj.size.x/2}else{closest.x=unrotatedCircleCenter.x}if(unrotatedCircleCenter.yrectangleObj.center.y+rectangleObj.size.y/2){closest.y=rectangleObj.center.y+rectangleObj.size.y/2}else{closest.y=unrotatedCircleCenter.y}return this.distance(unrotatedCircleCenter,closest)obj2.center.x+obj2.size.x/2){return false}else if(obj1.center.y-obj1.size.y/2>obj2.center.y+obj2.size.y/2){return false}else if(obj1.center.y+obj1.size.y/2current){min=current}if(current>max){max=current}}return{min:min,max:max}},rectangleCorners:function(obj){var corners=[{x:obj.center.x-obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y+obj.size.y/2},{x:obj.center.x-obj.size.x/2,y:obj.center.y+obj.size.y/2}];var angle=getAngle(obj)*Maths.RADIANS_TO_DEGREES;for(var i=0;i0){var run=this._runs.shift();run.fn(run.obj)}},add:function(obj,fn){this._runs.push({obj:obj,fn:fn})}};exports.Runner=Runner})(typeof exports==="undefined"?this.Coquette:exports);(function(exports){var interval=16;function Ticker(coquette,gameLoop){setupRequestAnimationFrame();var nextTickFn;this.stop=function(){nextTickFn=function(){}};this.start=function(){var prev=(new Date).getTime();var tick=function(){var now=(new Date).getTime();var interval=now-prev;prev=now;gameLoop(interval);requestAnimationFrame(nextTickFn)};nextTickFn=tick;requestAnimationFrame(nextTickFn)};this.start()}var setupRequestAnimationFrame=function(){var lastTime=0;var vendors=["ms","moz","webkit","o"];for(var x=0;x0){var pair=this._currentCollisionPairs.shift();this.collision(pair[0],pair[1])}},collision:function(entity1,entity2){notifyEntityOfCollision(entity1,entity2);notifyEntityOfCollision(entity2,entity1)},createEntity:function(entity){var ent=this.c.entities.all();for(var i=0,len=ent.length;i=0;i--){if(this._currentCollisionPairs[i][0]===entity||this._currentCollisionPairs[i][1]===entity){this._currentCollisionPairs.splice(i,1)}}},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)},RECTANGLE:new RectangleShape,CIRCLE:new CircleShape};var getDimensions=function(entities){var maxx,minx,maxy,miny;entities.forEach(function(entity){if(entity.center){if(maxx===undefined||entity.center.x>maxx){maxx=entity.center.x}if(minx===undefined||entity.center.xmaxy){maxy=entity.center.y}if(miny===undefined||entity.center.yrectangleObj.center.x+rectangleObj.size.x/2){closest.x=rectangleObj.center.x+rectangleObj.size.x/2}else{closest.x=unrotatedCircleCenter.x}if(unrotatedCircleCenter.yrectangleObj.center.y+rectangleObj.size.y/2){closest.y=rectangleObj.center.y+rectangleObj.size.y/2}else{closest.y=unrotatedCircleCenter.y}return this.distance(unrotatedCircleCenter,closest)obj2.center.x+obj2.size.x/2){return false}else if(obj1.center.y-obj1.size.y/2>obj2.center.y+obj2.size.y/2){return false}else if(obj1.center.y+obj1.size.y/2current){min=current}if(current>max){max=current}}return{min:min,max:max}},rectangleCorners:function(obj){var corners=[{x:obj.center.x-obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y+obj.size.y/2},{x:obj.center.x-obj.size.x/2,y:obj.center.y+obj.size.y/2}];var angle=getAngle(obj)*Maths.RADIANS_TO_DEGREES;for(var i=0;i0){var run=this._runs.shift();run.fn(run.obj)}},add:function(obj,fn){this._runs.push({obj:obj,fn:fn})}};exports.Runner=Runner})(typeof exports==="undefined"?this.Coquette:exports);(function(exports){var interval=16;function Ticker(coquette,gameLoop){setupRequestAnimationFrame();var nextTickFn;this.stop=function(){nextTickFn=function(){}};this.start=function(){var prev=(new Date).getTime();var tick=function(){var now=(new Date).getTime();var interval=now-prev;prev=now;gameLoop(interval);requestAnimationFrame(nextTickFn)};nextTickFn=tick;requestAnimationFrame(nextTickFn)};this.start()}var setupRequestAnimationFrame=function(){var lastTime=0;var vendors=["ms","moz","webkit","o"];for(var x=0;x 0) { + var pair = this._currentCollisionPairs.shift(); + this.collision(pair[0], pair[1]); } }, @@ -104,7 +103,7 @@ for(var i = this._currentCollisionPairs.length - 1; i >= 0; i--){ if (this._currentCollisionPairs[i][0] === entity || this._currentCollisionPairs[i][1] === entity) { - this._currentCollisionPairs.splice(i, 1, undefined); + this._currentCollisionPairs.splice(i, 1); } } }, diff --git a/src/collider.js b/src/collider.js index 1be1340..921160d 100644 --- a/src/collider.js +++ b/src/collider.js @@ -51,11 +51,10 @@ update: function() { this._currentCollisionPairs = this._getCollisionPairs(this.c.entities.all()); - for(var i=0; i 0) { + var pair = this._currentCollisionPairs.shift(); + this.collision(pair[0], pair[1]); } }, @@ -78,7 +77,7 @@ for(var i = this._currentCollisionPairs.length - 1; i >= 0; i--){ if (this._currentCollisionPairs[i][0] === entity || this._currentCollisionPairs[i][1] === entity) { - this._currentCollisionPairs.splice(i, 1, undefined); + this._currentCollisionPairs.splice(i, 1); } } }, From 9a9854f6d8e6bfab1fd535f0627dbca0cf18b24f Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Wed, 17 Dec 2014 17:09:44 -0500 Subject: [PATCH 22/28] Pass quad tree settings via the constructor (which fixes also a bug) --- coquette-min.js | 2 +- coquette.js | 17 ++++++++--------- src/collider.js | 17 ++++++++--------- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/coquette-min.js b/coquette-min.js index 36daa26..d4d615f 100644 --- a/coquette-min.js +++ b/coquette-min.js @@ -1 +1 @@ -(function(exports){var Coquette=function(game,canvasId,width,height,backgroundColor,autoFocus){var canvas=document.getElementById(canvasId);this.renderer=new Coquette.Renderer(this,game,canvas,width,height,backgroundColor);this.inputter=new Coquette.Inputter(this,canvas,autoFocus);this.entities=new Coquette.Entities(this,game);this.runner=new Coquette.Runner(this);this.collider=new Coquette.Collider(this);var self=this;this.ticker=new Coquette.Ticker(this,function(interval){self.collider.update(interval);self.runner.update(interval);if(game.update!==undefined){game.update(interval)}self.entities.update(interval);self.renderer.update(interval);self.inputter.update()})};exports.Coquette=Coquette})(this);(function(exports){var Collider=function(coquette){this.c=coquette;this._getCollisionPairs=quadTreeCollisionPairs};var isSetupForCollisions=function(obj){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:[],_useQuadtree:function(useQuadtree){if(useQuadtree){this._getCollisionPairs=quadTreeCollisionPairs}else{this._getCollisionPairs=allCollisionPairs;this.quadTree=undefined}},update:function(){this._currentCollisionPairs=this._getCollisionPairs(this.c.entities.all());while(this._currentCollisionPairs.length>0){var pair=this._currentCollisionPairs.shift();this.collision(pair[0],pair[1])}},collision:function(entity1,entity2){notifyEntityOfCollision(entity1,entity2);notifyEntityOfCollision(entity2,entity1)},createEntity:function(entity){var ent=this.c.entities.all();for(var i=0,len=ent.length;i=0;i--){if(this._currentCollisionPairs[i][0]===entity||this._currentCollisionPairs[i][1]===entity){this._currentCollisionPairs.splice(i,1)}}},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)},RECTANGLE:new RectangleShape,CIRCLE:new CircleShape};var getDimensions=function(entities){var maxx,minx,maxy,miny;entities.forEach(function(entity){if(entity.center){if(maxx===undefined||entity.center.x>maxx){maxx=entity.center.x}if(minx===undefined||entity.center.xmaxy){maxy=entity.center.y}if(miny===undefined||entity.center.yrectangleObj.center.x+rectangleObj.size.x/2){closest.x=rectangleObj.center.x+rectangleObj.size.x/2}else{closest.x=unrotatedCircleCenter.x}if(unrotatedCircleCenter.yrectangleObj.center.y+rectangleObj.size.y/2){closest.y=rectangleObj.center.y+rectangleObj.size.y/2}else{closest.y=unrotatedCircleCenter.y}return this.distance(unrotatedCircleCenter,closest)obj2.center.x+obj2.size.x/2){return false}else if(obj1.center.y-obj1.size.y/2>obj2.center.y+obj2.size.y/2){return false}else if(obj1.center.y+obj1.size.y/2current){min=current}if(current>max){max=current}}return{min:min,max:max}},rectangleCorners:function(obj){var corners=[{x:obj.center.x-obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y+obj.size.y/2},{x:obj.center.x-obj.size.x/2,y:obj.center.y+obj.size.y/2}];var angle=getAngle(obj)*Maths.RADIANS_TO_DEGREES;for(var i=0;i0){var run=this._runs.shift();run.fn(run.obj)}},add:function(obj,fn){this._runs.push({obj:obj,fn:fn})}};exports.Runner=Runner})(typeof exports==="undefined"?this.Coquette:exports);(function(exports){var interval=16;function Ticker(coquette,gameLoop){setupRequestAnimationFrame();var nextTickFn;this.stop=function(){nextTickFn=function(){}};this.start=function(){var prev=(new Date).getTime();var tick=function(){var now=(new Date).getTime();var interval=now-prev;prev=now;gameLoop(interval);requestAnimationFrame(nextTickFn)};nextTickFn=tick;requestAnimationFrame(nextTickFn)};this.start()}var setupRequestAnimationFrame=function(){var lastTime=0;var vendors=["ms","moz","webkit","o"];for(var x=0;x0){var pair=this._currentCollisionPairs.shift();this.collision(pair[0],pair[1])}},collision:function(entity1,entity2){notifyEntityOfCollision(entity1,entity2);notifyEntityOfCollision(entity2,entity1)},createEntity:function(entity){var ent=this.c.entities.all();for(var i=0,len=ent.length;i=0;i--){if(this._currentCollisionPairs[i][0]===entity||this._currentCollisionPairs[i][1]===entity){this._currentCollisionPairs.splice(i,1)}}},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)},RECTANGLE:new RectangleShape,CIRCLE:new CircleShape};var getDimensions=function(entities){var maxx,minx,maxy,miny;entities.forEach(function(entity){if(entity.center){if(maxx===undefined||entity.center.x>maxx){maxx=entity.center.x}if(minx===undefined||entity.center.xmaxy){maxy=entity.center.y}if(miny===undefined||entity.center.yrectangleObj.center.x+rectangleObj.size.x/2){closest.x=rectangleObj.center.x+rectangleObj.size.x/2}else{closest.x=unrotatedCircleCenter.x}if(unrotatedCircleCenter.yrectangleObj.center.y+rectangleObj.size.y/2){closest.y=rectangleObj.center.y+rectangleObj.size.y/2}else{closest.y=unrotatedCircleCenter.y}return this.distance(unrotatedCircleCenter,closest)obj2.center.x+obj2.size.x/2){return false}else if(obj1.center.y-obj1.size.y/2>obj2.center.y+obj2.size.y/2){return false}else if(obj1.center.y+obj1.size.y/2current){min=current}if(current>max){max=current}}return{min:min,max:max}},rectangleCorners:function(obj){var corners=[{x:obj.center.x-obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y+obj.size.y/2},{x:obj.center.x-obj.size.x/2,y:obj.center.y+obj.size.y/2}];var angle=getAngle(obj)*Maths.RADIANS_TO_DEGREES;for(var i=0;i0){var run=this._runs.shift();run.fn(run.obj)}},add:function(obj,fn){this._runs.push({obj:obj,fn:fn})}};exports.Runner=Runner})(typeof exports==="undefined"?this.Coquette:exports);(function(exports){var interval=16;function Ticker(coquette,gameLoop){setupRequestAnimationFrame();var nextTickFn;this.stop=function(){nextTickFn=function(){}};this.start=function(){var prev=(new Date).getTime();var tick=function(){var now=(new Date).getTime();var interval=now-prev;prev=now;gameLoop(interval);requestAnimationFrame(nextTickFn)};nextTickFn=tick;requestAnimationFrame(nextTickFn)};this.start()}var setupRequestAnimationFrame=function(){var lastTime=0;var vendors=["ms","moz","webkit","o"];for(var x=0;x Date: Wed, 17 Dec 2014 16:47:38 -0500 Subject: [PATCH 23/28] Using singleton shapes && simplify shape creation --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b3b9138..1649f5c 100644 --- a/README.md +++ b/README.md @@ -292,7 +292,7 @@ To make an entity support collisions, put these attributes on it: * `center`: The center of the entity, e.g. `{ x: 10, y: 20 }`. * `size`: The size of the entity, e.g. `{ x: 50, y: 30 }`. -* `boundingBox`: The shape that best approximates the shape of the entity, either an instance of `Coquette.Collider.Shape.Rectangle` or `Coquette.Collider.Shape.Circle` with the entity passed to the constructor. +* `boundingBox`: The shape that best approximates the shape of the entity, either `c.collider.RECTANGLE` or `c.collider.CIRCLE`. * `angle`: The orientation of the entity in degrees, e.g. `30`. And, optionally, this method: @@ -305,7 +305,7 @@ For example: var Player = function() { this.center = { x: 10, y: 20 }; this.size = { x: 50, y: 50 }; - this.boundingBox = new Coquette.Collider.Shape.Circle(this); + this.boundingBox = c.collider.CIRCLE; this.angle = 0; }; From b139f7e787e1bb3e4b617f0993915dac32e9114d Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Thu, 18 Dec 2014 09:48:34 -0500 Subject: [PATCH 24/28] Use point object with x and y property in the quad tree constructor --- coquette-min.js | 2 +- coquette.js | 52 +++++++++++++++++++--------------------- demos/collisions/game.js | 8 +++---- src/collider.js | 52 +++++++++++++++++++--------------------- 4 files changed, 55 insertions(+), 59 deletions(-) diff --git a/coquette-min.js b/coquette-min.js index d4d615f..5ed96d1 100644 --- a/coquette-min.js +++ b/coquette-min.js @@ -1 +1 @@ -(function(exports){var Coquette=function(game,canvasId,width,height,backgroundColor,autoFocus){var canvas=document.getElementById(canvasId);this.renderer=new Coquette.Renderer(this,game,canvas,width,height,backgroundColor);this.inputter=new Coquette.Inputter(this,canvas,autoFocus);this.entities=new Coquette.Entities(this,game);this.runner=new Coquette.Runner(this);this.collider=new Coquette.Collider(this);var self=this;this.ticker=new Coquette.Ticker(this,function(interval){self.collider.update(interval);self.runner.update(interval);if(game.update!==undefined){game.update(interval)}self.entities.update(interval);self.renderer.update(interval);self.inputter.update()})};exports.Coquette=Coquette})(this);(function(exports){var Collider=function(coquette){this.c=coquette;this._getCollisionPairs=quadTreeCollisionPairs};var isSetupForCollisions=function(obj){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:[],_useQuadtree:function(useQuadtree){if(useQuadtree){this._getCollisionPairs=quadTreeCollisionPairs}else{this._getCollisionPairs=allCollisionPairs;this.quadTree=undefined}},update:function(){this._currentCollisionPairs=this._getCollisionPairs(this.c.entities.all());while(this._currentCollisionPairs.length>0){var pair=this._currentCollisionPairs.shift();this.collision(pair[0],pair[1])}},collision:function(entity1,entity2){notifyEntityOfCollision(entity1,entity2);notifyEntityOfCollision(entity2,entity1)},createEntity:function(entity){var ent=this.c.entities.all();for(var i=0,len=ent.length;i=0;i--){if(this._currentCollisionPairs[i][0]===entity||this._currentCollisionPairs[i][1]===entity){this._currentCollisionPairs.splice(i,1)}}},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)},RECTANGLE:new RectangleShape,CIRCLE:new CircleShape};var getDimensions=function(entities){var maxx,minx,maxy,miny;entities.forEach(function(entity){if(entity.center){if(maxx===undefined||entity.center.x>maxx){maxx=entity.center.x}if(minx===undefined||entity.center.xmaxy){maxy=entity.center.y}if(miny===undefined||entity.center.yrectangleObj.center.x+rectangleObj.size.x/2){closest.x=rectangleObj.center.x+rectangleObj.size.x/2}else{closest.x=unrotatedCircleCenter.x}if(unrotatedCircleCenter.yrectangleObj.center.y+rectangleObj.size.y/2){closest.y=rectangleObj.center.y+rectangleObj.size.y/2}else{closest.y=unrotatedCircleCenter.y}return this.distance(unrotatedCircleCenter,closest)obj2.center.x+obj2.size.x/2){return false}else if(obj1.center.y-obj1.size.y/2>obj2.center.y+obj2.size.y/2){return false}else if(obj1.center.y+obj1.size.y/2current){min=current}if(current>max){max=current}}return{min:min,max:max}},rectangleCorners:function(obj){var corners=[{x:obj.center.x-obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y+obj.size.y/2},{x:obj.center.x-obj.size.x/2,y:obj.center.y+obj.size.y/2}];var angle=getAngle(obj)*Maths.RADIANS_TO_DEGREES;for(var i=0;i0){var run=this._runs.shift();run.fn(run.obj)}},add:function(obj,fn){this._runs.push({obj:obj,fn:fn})}};exports.Runner=Runner})(typeof exports==="undefined"?this.Coquette:exports);(function(exports){var interval=16;function Ticker(coquette,gameLoop){setupRequestAnimationFrame();var nextTickFn;this.stop=function(){nextTickFn=function(){}};this.start=function(){var prev=(new Date).getTime();var tick=function(){var now=(new Date).getTime();var interval=now-prev;prev=now;gameLoop(interval);requestAnimationFrame(nextTickFn)};nextTickFn=tick;requestAnimationFrame(nextTickFn)};this.start()}var setupRequestAnimationFrame=function(){var lastTime=0;var vendors=["ms","moz","webkit","o"];for(var x=0;x0){var pair=this._currentCollisionPairs.shift();this.collision(pair[0],pair[1])}},collision:function(entity1,entity2){notifyEntityOfCollision(entity1,entity2);notifyEntityOfCollision(entity2,entity1)},createEntity:function(entity){var ent=this.c.entities.all();for(var i=0,len=ent.length;i=0;i--){if(this._currentCollisionPairs[i][0]===entity||this._currentCollisionPairs[i][1]===entity){this._currentCollisionPairs.splice(i,1)}}},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)},RECTANGLE:new RectangleShape,CIRCLE:new CircleShape};var getDimensions=function(entities){var maxx,minx,maxy,miny;entities.forEach(function(entity){if(entity.center){if(maxx===undefined||entity.center.x>maxx){maxx=entity.center.x}if(minx===undefined||entity.center.xmaxy){maxy=entity.center.y}if(miny===undefined||entity.center.yrectangleObj.center.x+rectangleObj.size.x/2){closest.x=rectangleObj.center.x+rectangleObj.size.x/2}else{closest.x=unrotatedCircleCenter.x}if(unrotatedCircleCenter.yrectangleObj.center.y+rectangleObj.size.y/2){closest.y=rectangleObj.center.y+rectangleObj.size.y/2}else{closest.y=unrotatedCircleCenter.y}return this.distance(unrotatedCircleCenter,closest)obj2.center.x+obj2.size.x/2){return false}else if(obj1.center.y-obj1.size.y/2>obj2.center.y+obj2.size.y/2){return false}else if(obj1.center.y+obj1.size.y/2current){min=current}if(current>max){max=current}}return{min:min,max:max}},rectangleCorners:function(obj){var corners=[{x:obj.center.x-obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y+obj.size.y/2},{x:obj.center.x-obj.size.x/2,y:obj.center.y+obj.size.y/2}];var angle=getAngle(obj)*Maths.RADIANS_TO_DEGREES;for(var i=0;i0){var run=this._runs.shift();run.fn(run.obj)}},add:function(obj,fn){this._runs.push({obj:obj,fn:fn})}};exports.Runner=Runner})(typeof exports==="undefined"?this.Coquette:exports);(function(exports){var interval=16;function Ticker(coquette,gameLoop){setupRequestAnimationFrame();var nextTickFn;this.stop=function(){nextTickFn=function(){}};this.start=function(){var prev=(new Date).getTime();var tick=function(){var now=(new Date).getTime();var interval=now-prev;prev=now;gameLoop(interval);requestAnimationFrame(nextTickFn)};nextTickFn=tick;requestAnimationFrame(nextTickFn)};this.start()}var setupRequestAnimationFrame=function(){var lastTime=0;var vendors=["ms","moz","webkit","o"];for(var x=0;x Date: Thu, 18 Dec 2014 11:11:19 -0500 Subject: [PATCH 25/28] Does not expose quad tree details in collider anymore --- coquette-min.js | 2 +- coquette.js | 12 +------ demos/collisions/game.js | 64 ++++++++++++++++++++++++++----------- demos/collisions/index.html | 25 +++++++++++++-- src/collider.js | 12 +------ 5 files changed, 71 insertions(+), 44 deletions(-) diff --git a/coquette-min.js b/coquette-min.js index 5ed96d1..9c29110 100644 --- a/coquette-min.js +++ b/coquette-min.js @@ -1 +1 @@ -(function(exports){var Coquette=function(game,canvasId,width,height,backgroundColor,autoFocus){var canvas=document.getElementById(canvasId);this.renderer=new Coquette.Renderer(this,game,canvas,width,height,backgroundColor);this.inputter=new Coquette.Inputter(this,canvas,autoFocus);this.entities=new Coquette.Entities(this,game);this.runner=new Coquette.Runner(this);this.collider=new Coquette.Collider(this);var self=this;this.ticker=new Coquette.Ticker(this,function(interval){self.collider.update(interval);self.runner.update(interval);if(game.update!==undefined){game.update(interval)}self.entities.update(interval);self.renderer.update(interval);self.inputter.update()})};exports.Coquette=Coquette})(this);(function(exports){var Collider=function(coquette){this.c=coquette;this._getCollisionPairs=quadTreeCollisionPairs};var isSetupForCollisions=function(obj){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:[],_useQuadtree:function(useQuadtree){if(useQuadtree){this._getCollisionPairs=quadTreeCollisionPairs}else{this._getCollisionPairs=allCollisionPairs;this.quadTree=undefined}},update:function(){this._currentCollisionPairs=this._getCollisionPairs(this.c.entities.all());while(this._currentCollisionPairs.length>0){var pair=this._currentCollisionPairs.shift();this.collision(pair[0],pair[1])}},collision:function(entity1,entity2){notifyEntityOfCollision(entity1,entity2);notifyEntityOfCollision(entity2,entity1)},createEntity:function(entity){var ent=this.c.entities.all();for(var i=0,len=ent.length;i=0;i--){if(this._currentCollisionPairs[i][0]===entity||this._currentCollisionPairs[i][1]===entity){this._currentCollisionPairs.splice(i,1)}}},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)},RECTANGLE:new RectangleShape,CIRCLE:new CircleShape};var getDimensions=function(entities){var maxx,minx,maxy,miny;entities.forEach(function(entity){if(entity.center){if(maxx===undefined||entity.center.x>maxx){maxx=entity.center.x}if(minx===undefined||entity.center.xmaxy){maxy=entity.center.y}if(miny===undefined||entity.center.yrectangleObj.center.x+rectangleObj.size.x/2){closest.x=rectangleObj.center.x+rectangleObj.size.x/2}else{closest.x=unrotatedCircleCenter.x}if(unrotatedCircleCenter.yrectangleObj.center.y+rectangleObj.size.y/2){closest.y=rectangleObj.center.y+rectangleObj.size.y/2}else{closest.y=unrotatedCircleCenter.y}return this.distance(unrotatedCircleCenter,closest)obj2.center.x+obj2.size.x/2){return false}else if(obj1.center.y-obj1.size.y/2>obj2.center.y+obj2.size.y/2){return false}else if(obj1.center.y+obj1.size.y/2current){min=current}if(current>max){max=current}}return{min:min,max:max}},rectangleCorners:function(obj){var corners=[{x:obj.center.x-obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y+obj.size.y/2},{x:obj.center.x-obj.size.x/2,y:obj.center.y+obj.size.y/2}];var angle=getAngle(obj)*Maths.RADIANS_TO_DEGREES;for(var i=0;i0){var run=this._runs.shift();run.fn(run.obj)}},add:function(obj,fn){this._runs.push({obj:obj,fn:fn})}};exports.Runner=Runner})(typeof exports==="undefined"?this.Coquette:exports);(function(exports){var interval=16;function Ticker(coquette,gameLoop){setupRequestAnimationFrame();var nextTickFn;this.stop=function(){nextTickFn=function(){}};this.start=function(){var prev=(new Date).getTime();var tick=function(){var now=(new Date).getTime();var interval=now-prev;prev=now;gameLoop(interval);requestAnimationFrame(nextTickFn)};nextTickFn=tick;requestAnimationFrame(nextTickFn)};this.start()}var setupRequestAnimationFrame=function(){var lastTime=0;var vendors=["ms","moz","webkit","o"];for(var x=0;x0){var pair=this._currentCollisionPairs.shift();this.collision(pair[0],pair[1])}},collision:function(entity1,entity2){notifyEntityOfCollision(entity1,entity2);notifyEntityOfCollision(entity2,entity1)},createEntity:function(entity){var ent=this.c.entities.all();for(var i=0,len=ent.length;i=0;i--){if(this._currentCollisionPairs[i][0]===entity||this._currentCollisionPairs[i][1]===entity){this._currentCollisionPairs.splice(i,1)}}},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)},RECTANGLE:new RectangleShape,CIRCLE:new CircleShape};var getDimensions=function(entities){var maxx,minx,maxy,miny;entities.forEach(function(entity){if(entity.center){if(maxx===undefined||entity.center.x>maxx){maxx=entity.center.x}if(minx===undefined||entity.center.xmaxy){maxy=entity.center.y}if(miny===undefined||entity.center.yrectangleObj.center.x+rectangleObj.size.x/2){closest.x=rectangleObj.center.x+rectangleObj.size.x/2}else{closest.x=unrotatedCircleCenter.x}if(unrotatedCircleCenter.yrectangleObj.center.y+rectangleObj.size.y/2){closest.y=rectangleObj.center.y+rectangleObj.size.y/2}else{closest.y=unrotatedCircleCenter.y}return this.distance(unrotatedCircleCenter,closest)obj2.center.x+obj2.size.x/2){return false}else if(obj1.center.y-obj1.size.y/2>obj2.center.y+obj2.size.y/2){return false}else if(obj1.center.y+obj1.size.y/2current){min=current}if(current>max){max=current}}return{min:min,max:max}},rectangleCorners:function(obj){var corners=[{x:obj.center.x-obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y+obj.size.y/2},{x:obj.center.x-obj.size.x/2,y:obj.center.y+obj.size.y/2}];var angle=getAngle(obj)*Maths.RADIANS_TO_DEGREES;for(var i=0;i0){var run=this._runs.shift();run.fn(run.obj)}},add:function(obj,fn){this._runs.push({obj:obj,fn:fn})}};exports.Runner=Runner})(typeof exports==="undefined"?this.Coquette:exports);(function(exports){var interval=16;function Ticker(coquette,gameLoop){setupRequestAnimationFrame();var nextTickFn;this.stop=function(){nextTickFn=function(){}};this.start=function(){var prev=(new Date).getTime();var tick=function(){var now=(new Date).getTime();var interval=now-prev;prev=now;gameLoop(interval);requestAnimationFrame(nextTickFn)};nextTickFn=tick;requestAnimationFrame(nextTickFn)};this.start()}var setupRequestAnimationFrame=function(){var lastTime=0;var vendors=["ms","moz","webkit","o"];for(var x=0;x 0) { var pair = this._currentCollisionPairs.shift(); diff --git a/demos/collisions/game.js b/demos/collisions/game.js index 8dfe444..f076163 100644 --- a/demos/collisions/game.js +++ b/demos/collisions/game.js @@ -54,7 +54,6 @@ Test.prototype.onStartCollisionDetection = function(collider) { if(!this.start) { - collider._useQuadtree(this.quad); this.start = +new Date(); } this.timer.start(); @@ -81,7 +80,7 @@ this.tests = []; this.current = 0; - this.tableEl; + this.tbodyEl; } TestSuite.prototype = { @@ -100,28 +99,27 @@ return this.tests.lengththis.current+1) { - this.tableEl.deleteRow(this.current+1); + if(this.tbodyEl.rows.length>this.current) { + this.tbodyEl.deleteRow(this.current); } - var row = this.tableEl.insertRow(this.current+1); + var row = this.tbodyEl.insertRow(this.current); var cell1 = row.insertCell(0); var cell2 = row.insertCell(1); var cell3 = row.insertCell(2); // Add some text to the new cells: - cell1.innerHTML = JSON.stringify(this.currentTest().settings); + cell1.innerHTML = this.currentTest().settings.entities; if(this.currentTest().time.all < this.currentTest().time.quad) { - cell2.innerHTML = "" + this.currentTest().time.all + "ms"; - cell3.innerHTML = this.currentTest().time.quad + "ms"; + cell2.className = "faster"; } else { - cell2.innerHTML = this.currentTest().time.all + "ms"; - cell3.innerHTML = "" + this.currentTest().time.quad + "ms"; - + cell3.className = "faster"; } + cell2.innerHTML = this.currentTest().time.all + "ms"; + cell3.innerHTML = this.currentTest().time.quad + "ms"; } }; @@ -136,10 +134,20 @@ width, height, "white", autoFocus); this.c = c; - var colliderUpdate = this.c.collider.update; - this.c.collider.update = function() { + var collider = this.c.collider; + var colliderUpdate = collider.update; + collider.update = function() { testSuite.currentTest().onStartCollisionDetection(this); - colliderUpdate.apply(this); + if(testSuite.currentTest().quad) { + colliderUpdate.apply(collider); + } else { + var ent = this.c.entities.all(); + collider._currentCollisionPairs = allCollisionPairs.apply(collider, [ent]); + while (collider._currentCollisionPairs.length > 0) { + var pair = collider._currentCollisionPairs.shift(); + collider.collision(pair[0], pair[1]); + } + } testSuite.currentTest().onEndCollisionDetection(this); }; @@ -149,8 +157,8 @@ var ctx = this.getCtx(); // draw quad tree - if(this.c.collider.quadTree) { - drawQuad(this.c.collider.quadTree, ctx); + if(testSuite.currentTest().quad && collider.quadTree) { + drawQuad(collider.quadTree, ctx); } } @@ -304,5 +312,25 @@ }; + 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; + }; + exports.Collisions = Collisions; })(this); diff --git a/demos/collisions/index.html b/demos/collisions/index.html index 450c583..46c10cc 100644 --- a/demos/collisions/index.html +++ b/demos/collisions/index.html @@ -8,14 +8,33 @@ new Collisions(); }); + -
0ms for testing 0 entities.
-

Test results

+

0ms for testing 0 entities.

- + +
SettingsNaiveQuad
EntitiesWithout quadtreeWith quadtree
.........
diff --git a/src/collider.js b/src/collider.js index eeec36e..2e92551 100644 --- a/src/collider.js +++ b/src/collider.js @@ -1,7 +1,6 @@ ;(function(exports) { var Collider = function(coquette) { this.c = coquette; - this._getCollisionPairs = quadTreeCollisionPairs; }; var isSetupForCollisions = function(obj) { @@ -40,17 +39,8 @@ Collider.prototype = { _currentCollisionPairs: [], - _useQuadtree: function(useQuadtree) { - if(useQuadtree) { - this._getCollisionPairs = quadTreeCollisionPairs; - } else { - this._getCollisionPairs = allCollisionPairs; - this.quadTree = undefined; - } - }, - update: function() { - this._currentCollisionPairs = this._getCollisionPairs(this.c.entities.all()); + this._currentCollisionPairs = quadTreeCollisionPairs.apply(this, [this.c.entities.all()]); while (this._currentCollisionPairs.length > 0) { var pair = this._currentCollisionPairs.shift(); From 352b3108a70a65c0397b86b13e4d71e45e9e8384 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Thu, 18 Dec 2014 11:13:29 -0500 Subject: [PATCH 26/28] Use 'isSetupForCollision' --- coquette-min.js | 2 +- coquette.js | 2 +- src/collider.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/coquette-min.js b/coquette-min.js index 9c29110..c528857 100644 --- a/coquette-min.js +++ b/coquette-min.js @@ -1 +1 @@ -(function(exports){var Coquette=function(game,canvasId,width,height,backgroundColor,autoFocus){var canvas=document.getElementById(canvasId);this.renderer=new Coquette.Renderer(this,game,canvas,width,height,backgroundColor);this.inputter=new Coquette.Inputter(this,canvas,autoFocus);this.entities=new Coquette.Entities(this,game);this.runner=new Coquette.Runner(this);this.collider=new Coquette.Collider(this);var self=this;this.ticker=new Coquette.Ticker(this,function(interval){self.collider.update(interval);self.runner.update(interval);if(game.update!==undefined){game.update(interval)}self.entities.update(interval);self.renderer.update(interval);self.inputter.update()})};exports.Coquette=Coquette})(this);(function(exports){var Collider=function(coquette){this.c=coquette};var isSetupForCollisions=function(obj){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=quadTreeCollisionPairs.apply(this,[this.c.entities.all()]);while(this._currentCollisionPairs.length>0){var pair=this._currentCollisionPairs.shift();this.collision(pair[0],pair[1])}},collision:function(entity1,entity2){notifyEntityOfCollision(entity1,entity2);notifyEntityOfCollision(entity2,entity1)},createEntity:function(entity){var ent=this.c.entities.all();for(var i=0,len=ent.length;i=0;i--){if(this._currentCollisionPairs[i][0]===entity||this._currentCollisionPairs[i][1]===entity){this._currentCollisionPairs.splice(i,1)}}},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)},RECTANGLE:new RectangleShape,CIRCLE:new CircleShape};var getDimensions=function(entities){var maxx,minx,maxy,miny;entities.forEach(function(entity){if(entity.center){if(maxx===undefined||entity.center.x>maxx){maxx=entity.center.x}if(minx===undefined||entity.center.xmaxy){maxy=entity.center.y}if(miny===undefined||entity.center.yrectangleObj.center.x+rectangleObj.size.x/2){closest.x=rectangleObj.center.x+rectangleObj.size.x/2}else{closest.x=unrotatedCircleCenter.x}if(unrotatedCircleCenter.yrectangleObj.center.y+rectangleObj.size.y/2){closest.y=rectangleObj.center.y+rectangleObj.size.y/2}else{closest.y=unrotatedCircleCenter.y}return this.distance(unrotatedCircleCenter,closest)obj2.center.x+obj2.size.x/2){return false}else if(obj1.center.y-obj1.size.y/2>obj2.center.y+obj2.size.y/2){return false}else if(obj1.center.y+obj1.size.y/2current){min=current}if(current>max){max=current}}return{min:min,max:max}},rectangleCorners:function(obj){var corners=[{x:obj.center.x-obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y+obj.size.y/2},{x:obj.center.x-obj.size.x/2,y:obj.center.y+obj.size.y/2}];var angle=getAngle(obj)*Maths.RADIANS_TO_DEGREES;for(var i=0;i0){var run=this._runs.shift();run.fn(run.obj)}},add:function(obj,fn){this._runs.push({obj:obj,fn:fn})}};exports.Runner=Runner})(typeof exports==="undefined"?this.Coquette:exports);(function(exports){var interval=16;function Ticker(coquette,gameLoop){setupRequestAnimationFrame();var nextTickFn;this.stop=function(){nextTickFn=function(){}};this.start=function(){var prev=(new Date).getTime();var tick=function(){var now=(new Date).getTime();var interval=now-prev;prev=now;gameLoop(interval);requestAnimationFrame(nextTickFn)};nextTickFn=tick;requestAnimationFrame(nextTickFn)};this.start()}var setupRequestAnimationFrame=function(){var lastTime=0;var vendors=["ms","moz","webkit","o"];for(var x=0;x0){var pair=this._currentCollisionPairs.shift();this.collision(pair[0],pair[1])}},collision:function(entity1,entity2){notifyEntityOfCollision(entity1,entity2);notifyEntityOfCollision(entity2,entity1)},createEntity:function(entity){var ent=this.c.entities.all();for(var i=0,len=ent.length;i=0;i--){if(this._currentCollisionPairs[i][0]===entity||this._currentCollisionPairs[i][1]===entity){this._currentCollisionPairs.splice(i,1)}}},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)},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.xmaxy){maxy=entity.center.y}if(miny===undefined||entity.center.yrectangleObj.center.x+rectangleObj.size.x/2){closest.x=rectangleObj.center.x+rectangleObj.size.x/2}else{closest.x=unrotatedCircleCenter.x}if(unrotatedCircleCenter.yrectangleObj.center.y+rectangleObj.size.y/2){closest.y=rectangleObj.center.y+rectangleObj.size.y/2}else{closest.y=unrotatedCircleCenter.y}return this.distance(unrotatedCircleCenter,closest)obj2.center.x+obj2.size.x/2){return false}else if(obj1.center.y-obj1.size.y/2>obj2.center.y+obj2.size.y/2){return false}else if(obj1.center.y+obj1.size.y/2current){min=current}if(current>max){max=current}}return{min:min,max:max}},rectangleCorners:function(obj){var corners=[{x:obj.center.x-obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y+obj.size.y/2},{x:obj.center.x-obj.size.x/2,y:obj.center.y+obj.size.y/2}];var angle=getAngle(obj)*Maths.RADIANS_TO_DEGREES;for(var i=0;i0){var run=this._runs.shift();run.fn(run.obj)}},add:function(obj,fn){this._runs.push({obj:obj,fn:fn})}};exports.Runner=Runner})(typeof exports==="undefined"?this.Coquette:exports);(function(exports){var interval=16;function Ticker(coquette,gameLoop){setupRequestAnimationFrame();var nextTickFn;this.stop=function(){nextTickFn=function(){}};this.start=function(){var prev=(new Date).getTime();var tick=function(){var now=(new Date).getTime();var interval=now-prev;prev=now;gameLoop(interval);requestAnimationFrame(nextTickFn)};nextTickFn=tick;requestAnimationFrame(nextTickFn)};this.start()}var setupRequestAnimationFrame=function(){var lastTime=0;var vendors=["ms","moz","webkit","o"];for(var x=0;x maxx) { maxx = entity.center.x; } diff --git a/src/collider.js b/src/collider.js index 2e92551..1c4855c 100644 --- a/src/collider.js +++ b/src/collider.js @@ -102,7 +102,7 @@ var maxx, minx, maxy, miny; entities.forEach(function(entity) { - if(entity.center) { + if(isSetupForCollisions(entity)) { if(maxx === undefined || entity.center.x > maxx) { maxx = entity.center.x; } From 2d5e88fdec98f7faec24ebb2f09a8d837a91062e Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Thu, 18 Dec 2014 11:21:21 -0500 Subject: [PATCH 27/28] Revert shape changes --- index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 388d48d..a264103 100644 --- a/index.html +++ b/index.html @@ -373,7 +373,7 @@
Entity setup
  • center: The center of the entity, e.g. { x: 10, y: 20 }.
  • size: The size of the entity, e.g. { x: 50, y: 30 }.
  • -
  • boundingBox: The shape that best approximates the shape of the entity, either an instance of Coquette.Collider.Shape.Rectangle or Coquette.Collider.Shape.Circle with the entity passed to the constructor.
  • +
  • boundingBox: The shape that best approximates the shape of the entity, either c.collider.RECTANGLE or c.collider.CIRCLE.
  • angle: The orientation of the entity in degrees, e.g. 30.
@@ -389,7 +389,7 @@
Entity setup
var Player = function() { this.center = { x: 10, y: 20 }; this.size = { x: 50, y: 50 }; - this.boundingBox = new Collider.Shape.Circle(this); + this.boundingBox = c.collider.CIRCLE; this.angle = 0; }; From 791f8695c5046afb69deb8257c1b296083ae9955 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Thu, 18 Dec 2014 11:21:57 -0500 Subject: [PATCH 28/28] Use object as return value of #getDimensions --- coquette-min.js | 2 +- coquette.js | 16 +++++----------- src/collider.js | 16 +++++----------- 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/coquette-min.js b/coquette-min.js index c528857..527fa41 100644 --- a/coquette-min.js +++ b/coquette-min.js @@ -1 +1 @@ -(function(exports){var Coquette=function(game,canvasId,width,height,backgroundColor,autoFocus){var canvas=document.getElementById(canvasId);this.renderer=new Coquette.Renderer(this,game,canvas,width,height,backgroundColor);this.inputter=new Coquette.Inputter(this,canvas,autoFocus);this.entities=new Coquette.Entities(this,game);this.runner=new Coquette.Runner(this);this.collider=new Coquette.Collider(this);var self=this;this.ticker=new Coquette.Ticker(this,function(interval){self.collider.update(interval);self.runner.update(interval);if(game.update!==undefined){game.update(interval)}self.entities.update(interval);self.renderer.update(interval);self.inputter.update()})};exports.Coquette=Coquette})(this);(function(exports){var Collider=function(coquette){this.c=coquette};var isSetupForCollisions=function(obj){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=quadTreeCollisionPairs.apply(this,[this.c.entities.all()]);while(this._currentCollisionPairs.length>0){var pair=this._currentCollisionPairs.shift();this.collision(pair[0],pair[1])}},collision:function(entity1,entity2){notifyEntityOfCollision(entity1,entity2);notifyEntityOfCollision(entity2,entity1)},createEntity:function(entity){var ent=this.c.entities.all();for(var i=0,len=ent.length;i=0;i--){if(this._currentCollisionPairs[i][0]===entity||this._currentCollisionPairs[i][1]===entity){this._currentCollisionPairs.splice(i,1)}}},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)},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.xmaxy){maxy=entity.center.y}if(miny===undefined||entity.center.yrectangleObj.center.x+rectangleObj.size.x/2){closest.x=rectangleObj.center.x+rectangleObj.size.x/2}else{closest.x=unrotatedCircleCenter.x}if(unrotatedCircleCenter.yrectangleObj.center.y+rectangleObj.size.y/2){closest.y=rectangleObj.center.y+rectangleObj.size.y/2}else{closest.y=unrotatedCircleCenter.y}return this.distance(unrotatedCircleCenter,closest)obj2.center.x+obj2.size.x/2){return false}else if(obj1.center.y-obj1.size.y/2>obj2.center.y+obj2.size.y/2){return false}else if(obj1.center.y+obj1.size.y/2current){min=current}if(current>max){max=current}}return{min:min,max:max}},rectangleCorners:function(obj){var corners=[{x:obj.center.x-obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y+obj.size.y/2},{x:obj.center.x-obj.size.x/2,y:obj.center.y+obj.size.y/2}];var angle=getAngle(obj)*Maths.RADIANS_TO_DEGREES;for(var i=0;i0){var run=this._runs.shift();run.fn(run.obj)}},add:function(obj,fn){this._runs.push({obj:obj,fn:fn})}};exports.Runner=Runner})(typeof exports==="undefined"?this.Coquette:exports);(function(exports){var interval=16;function Ticker(coquette,gameLoop){setupRequestAnimationFrame();var nextTickFn;this.stop=function(){nextTickFn=function(){}};this.start=function(){var prev=(new Date).getTime();var tick=function(){var now=(new Date).getTime();var interval=now-prev;prev=now;gameLoop(interval);requestAnimationFrame(nextTickFn)};nextTickFn=tick;requestAnimationFrame(nextTickFn)};this.start()}var setupRequestAnimationFrame=function(){var lastTime=0;var vendors=["ms","moz","webkit","o"];for(var x=0;x0){var pair=this._currentCollisionPairs.shift();this.collision(pair[0],pair[1])}},collision:function(entity1,entity2){notifyEntityOfCollision(entity1,entity2);notifyEntityOfCollision(entity2,entity1)},createEntity:function(entity){var ent=this.c.entities.all();for(var i=0,len=ent.length;i=0;i--){if(this._currentCollisionPairs[i][0]===entity||this._currentCollisionPairs[i][1]===entity){this._currentCollisionPairs.splice(i,1)}}},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)},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.xmaxy){maxy=entity.center.y}if(miny===undefined||entity.center.yrectangleObj.center.x+rectangleObj.size.x/2){closest.x=rectangleObj.center.x+rectangleObj.size.x/2}else{closest.x=unrotatedCircleCenter.x}if(unrotatedCircleCenter.yrectangleObj.center.y+rectangleObj.size.y/2){closest.y=rectangleObj.center.y+rectangleObj.size.y/2}else{closest.y=unrotatedCircleCenter.y}return this.distance(unrotatedCircleCenter,closest)obj2.center.x+obj2.size.x/2){return false}else if(obj1.center.y-obj1.size.y/2>obj2.center.y+obj2.size.y/2){return false}else if(obj1.center.y+obj1.size.y/2current){min=current}if(current>max){max=current}}return{min:min,max:max}},rectangleCorners:function(obj){var corners=[{x:obj.center.x-obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y-obj.size.y/2},{x:obj.center.x+obj.size.x/2,y:obj.center.y+obj.size.y/2},{x:obj.center.x-obj.size.x/2,y:obj.center.y+obj.size.y/2}];var angle=getAngle(obj)*Maths.RADIANS_TO_DEGREES;for(var i=0;i0){var run=this._runs.shift();run.fn(run.obj)}},add:function(obj,fn){this._runs.push({obj:obj,fn:fn})}};exports.Runner=Runner})(typeof exports==="undefined"?this.Coquette:exports);(function(exports){var interval=16;function Ticker(coquette,gameLoop){setupRequestAnimationFrame();var nextTickFn;this.stop=function(){nextTickFn=function(){}};this.start=function(){var prev=(new Date).getTime();var tick=function(){var now=(new Date).getTime();var interval=now-prev;prev=now;gameLoop(interval);requestAnimationFrame(nextTickFn)};nextTickFn=tick;requestAnimationFrame(nextTickFn)};this.start()}var setupRequestAnimationFrame=function(){var lastTime=0;var vendors=["ms","moz","webkit","o"];for(var x=0;x