From 128bdccd7933715eaf7a5487d5b2d032a401fb8e Mon Sep 17 00:00:00 2001 From: Richard Davey Date: Wed, 4 Sep 2013 13:54:55 +0100 Subject: [PATCH] Sorted out the QuadTree issues and resolved a small but vital bug in separateX, also re-organised the collision flags in Body. --- examples/quadtree.php | 19 +++- examples/quadtree2.php | 94 ++++++++++++++++++ src/geom/Rectangle.js | 11 ++- src/math/QuadTree.js | 142 ++++++++++++++-------------- src/physics/arcade/ArcadePhysics.js | 74 ++++++++++----- src/physics/arcade/Body.js | 34 ++++--- src/utils/Debug.js | 12 +-- 7 files changed, 265 insertions(+), 121 deletions(-) create mode 100644 examples/quadtree2.php diff --git a/examples/quadtree.php b/examples/quadtree.php index 2948f88a..afee3f4c 100644 --- a/examples/quadtree.php +++ b/examples/quadtree.php @@ -25,11 +25,14 @@ function create() { + game.world.setSize(2000, 2000); + aliens = []; - for (var i = 0; i < 100; i++) + for (var i = 0; i < 1000; i++) { var s = game.add.sprite(game.world.randomX, game.world.randomY, 'baddie'); + s.name = 'alien' + s; s.body.collideWorldBounds = true; s.body.bounce.setTo(1, 1); s.body.velocity.setTo(10 + Math.random() * 10, 10 + Math.random() * 10); @@ -51,6 +54,9 @@ total = game.physics.quadTree.retrieve(ship); + // Get the ships top-most ID. If the length of that ID is 1 then we can ignore every other result, + // it's simply not colliding with anything :) + for (var i = 0; i < total.length; i++) { total[i].sprite.alpha = 1; @@ -80,12 +86,19 @@ for (var i = 0; i < aliens.length; i++) { - game.debug.renderRectangle(aliens[i].bounds); + // game.debug.renderRectangle(aliens[i].bounds); } game.debug.renderText(total.length, 32, 32); game.debug.renderQuadTree(game.physics.quadTree); - game.debug.renderRectangle(ship); + // game.debug.renderRectangle(ship); + + game.debug.renderText('Index: ' + ship.body.quadTreeIndex, 32, 80); + + for (var i = 0; i < ship.body.quadTreeIDs.length; i++) + { + game.debug.renderText('ID: ' + ship.body.quadTreeIDs[i], 32, 100 + (i * 20)); + } } diff --git a/examples/quadtree2.php b/examples/quadtree2.php new file mode 100644 index 00000000..e582f3dd --- /dev/null +++ b/examples/quadtree2.php @@ -0,0 +1,94 @@ + + + + phaser.js - a new beginning + + + + + + + + \ No newline at end of file diff --git a/src/geom/Rectangle.js b/src/geom/Rectangle.js index 7d0238ed..5350e7ea 100644 --- a/src/geom/Rectangle.js +++ b/src/geom/Rectangle.js @@ -656,11 +656,15 @@ Phaser.Rectangle.containsPoint = function (a, point) { * @return {bool} A value of true if the Rectangle object contains the specified point; otherwise false. */ Phaser.Rectangle.containsRect = function (a, b) { + // If the given rect has a larger volume than this one then it can never contain it - if(a.volume > b.volume) { + if (a.volume > b.volume) + { return false; } + return (a.x >= b.x && a.y >= b.y && a.right <= b.right && a.bottom <= b.bottom); + }; /** @@ -685,9 +689,10 @@ Phaser.Rectangle.equals = function (a, b) { */ Phaser.Rectangle.intersection = function (a, b, out) { - if (typeof out === "undefined") { out = new Phaser.Rectangle(); } + out = out || new Phaser.Rectangle; - if (Phaser.Rectangle.intersects(a, b)) { + if (Phaser.Rectangle.intersects(a, b)) + { out.x = Math.max(a.x, b.x); out.y = Math.max(a.y, b.y); out.width = Math.min(a.right, b.right) - out.x; diff --git a/src/math/QuadTree.js b/src/math/QuadTree.js index 3eeaf7cf..6ede0dc7 100644 --- a/src/math/QuadTree.js +++ b/src/math/QuadTree.js @@ -3,7 +3,11 @@ * @version 1.0 * @author Timo Hausmann * - * Optimised to reduce temp. var creation and increase performance by Richard Davey + * @version 1.2, September 4th 2013 + * @author Richard Davey + * The original code was a conversion of the Java code posted to GameDevTuts. However I've tweaked + * it massively to add node indexing, removed lots of temp. var creation and significantly + * increased performance as a result. * * Original version at https://github.com/timohausmann/quadtree-js/ */ @@ -37,8 +41,12 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * @param Integer maxLevels (optional) total max levels inside root QuadTree (default: 4) * @param Integer level (optional) deepth level, required for subnodes */ -Phaser.QuadTree = function (x, y, width, height, maxObjects, maxLevels, level) { +Phaser.QuadTree = function (physicsManager, x, y, width, height, maxObjects, maxLevels, level) { + this.physicsManager = physicsManager; + this.ID = physicsManager.quadTreeID; + physicsManager.quadTreeID++; + this.maxObjects = maxObjects || 10; this.maxLevels = maxLevels || 4; this.level = level || 0; @@ -66,67 +74,19 @@ Phaser.QuadTree.prototype = { */ split: function() { - this.level + 1; + this.level++; // top right node - this.nodes[0] = new Phaser.QuadTree(this.bounds.right, this.bounds.y, this.bounds.subWidth, this.bounds.subHeight, this.maxObjects, this.maxLevels, this.level); + this.nodes[0] = new Phaser.QuadTree(this.physicsManager, this.bounds.right, this.bounds.y, this.bounds.subWidth, this.bounds.subHeight, this.maxObjects, this.maxLevels, this.level); // top left node - this.nodes[1] = new Phaser.QuadTree(this.bounds.x, this.bounds.y, this.bounds.subWidth, this.bounds.subHeight, this.maxObjects, this.maxLevels, this.level); + this.nodes[1] = new Phaser.QuadTree(this.physicsManager, this.bounds.x, this.bounds.y, this.bounds.subWidth, this.bounds.subHeight, this.maxObjects, this.maxLevels, this.level); // bottom left node - this.nodes[2] = new Phaser.QuadTree(this.bounds.x, this.bounds.bottom, this.bounds.subWidth, this.bounds.subHeight, this.maxObjects, this.maxLevels, this.level); + this.nodes[2] = new Phaser.QuadTree(this.physicsManager, this.bounds.x, this.bounds.bottom, this.bounds.subWidth, this.bounds.subHeight, this.maxObjects, this.maxLevels, this.level); // bottom right node - this.nodes[3] = new Phaser.QuadTree(this.bounds.right, this.bounds.bottom, this.bounds.subWidth, this.bounds.subHeight, this.maxObjects, this.maxLevels, this.level); - - }, - - /* - * Determine which node the object belongs to - * @param Object pRect bounds of the area to be checked, with x, y, width, height - * @return Integer index of the subnode (0-3), or -1 if pRect cannot completely fit within a subnode and is part of the parent node - */ - getIndex: function (rect) { - - var index = -1; - - // var verticalMidpoint = this.bounds.x + (this.bounds.width / 2); - var verticalMidpoint = this.bounds.right; - // var horizontalMidpoint = this.bounds.y + (this.bounds.height / 2); - var horizontalMidpoint = this.bounds.bottom; - - var topQuadrant = (rect.y < this.bounds.bottom && rect.bottom < this.bounds.bottom); - var bottomQuadrant = (rect.y > this.bounds.bottom); - - // rect can completely fit within the left quadrants - if (rect.x < verticalMidpoint && rect.right < verticalMidpoint) - { - if (topQuadrant) - { - // rect can completely fit within the top quadrants - index = 1; - } - else if (bottomQuadrant) - { - // rect can completely fit within the bottom quadrants - index = 2; - } - } - else if (rect.x > verticalMidpoint) - { - // rect can completely fit within the right quadrants - if (topQuadrant) - { - index = 0; - } - else if (bottomQuadrant) - { - index = 3; - } - } - - return index; + this.nodes[3] = new Phaser.QuadTree(this.physicsManager, this.bounds.right, this.bounds.bottom, this.bounds.subWidth, this.bounds.subHeight, this.maxObjects, this.maxLevels, this.level); }, @@ -142,7 +102,6 @@ Phaser.QuadTree.prototype = { var index; // if we have subnodes ... - // if (typeof this.nodes[0] !== 'undefined') if (this.nodes[0] != null) { index = this.getIndex(body.bounds); @@ -156,10 +115,9 @@ Phaser.QuadTree.prototype = { this.objects.push(body); - if (this.objects.length > this.maxObjects && this.level < this.maxLevels ) + if (this.objects.length > this.maxObjects && this.level < this.maxLevels) { // Split if we don't already have subnodes - // if (typeof this.nodes[0] === 'undefined') if (this.nodes[0] == null) { this.split(); @@ -170,7 +128,7 @@ Phaser.QuadTree.prototype = { { index = this.getIndex(this.objects[i].bounds); - if (index !== -1 ) + if (index !== -1) { // this is expensive - see what we can do about it this.nodes[index].insert(this.objects.splice(i, 1)[0]); @@ -183,6 +141,48 @@ Phaser.QuadTree.prototype = { } }, + /* + * Determine which node the object belongs to + * @param Object pRect bounds of the area to be checked, with x, y, width, height + * @return Integer index of the subnode (0-3), or -1 if pRect cannot completely fit within a subnode and is part of the parent node + */ + getIndex: function (rect) { + + // default is that rect doesn't fit, i.e. it straddles the internal quadrants + var index = -1; + + if (rect.x < this.bounds.right && rect.right < this.bounds.right) + { + if ((rect.y < this.bounds.bottom && rect.bottom < this.bounds.bottom)) + { + // rect fits within the top-left quadrant of this quadtree + index = 1; + } + else if ((rect.y > this.bounds.bottom)) + { + // rect fits within the bottom-left quadrant of this quadtree + index = 2; + } + } + else if (rect.x > this.bounds.right) + { + // rect can completely fit within the right quadrants + if ((rect.y < this.bounds.bottom && rect.bottom < this.bounds.bottom)) + { + // rect fits within the top-right quadrant of this quadtree + index = 0; + } + else if ((rect.y > this.bounds.bottom)) + { + // rect fits within the bottom-right quadrant of this quadtree + index = 3; + } + } + + return index; + + }, + /* * Return all objects that could collide with the given object * @param Object pRect bounds of the object to be checked, with x, y, width, height @@ -190,25 +190,27 @@ Phaser.QuadTree.prototype = { */ retrieve: function (sprite) { - var index = this.getIndex(sprite.body.bounds); var returnObjects = this.objects; - - // if we have subnodes ... - // if (typeof this.nodes[0] !== 'undefined') + + sprite.body.quadTreeIndex = this.getIndex(sprite.body.bounds); + + // Temp store for the node IDs this sprite is in, we can use this for fast elimination later + sprite.body.quadTreeIDs.push(this.ID); + if (this.nodes[0]) { // if rect fits into a subnode .. - if (index !== -1) + if (sprite.body.quadTreeIndex !== -1) { - returnObjects = returnObjects.concat(this.nodes[index].retrieve(sprite)); + returnObjects = returnObjects.concat(this.nodes[sprite.body.quadTreeIndex].retrieve(sprite)); } else { - // if rect does not fit into a subnode, check it against all subnodes - for (var i = 0, len = this.nodes.length; i < len; i++) - { - returnObjects = returnObjects.concat(this.nodes[i].retrieve(sprite)); - } + // if rect does not fit into a subnode, check it against all subnodes (unrolled for speed) + returnObjects = returnObjects.concat(this.nodes[0].retrieve(sprite)); + returnObjects = returnObjects.concat(this.nodes[1].retrieve(sprite)); + returnObjects = returnObjects.concat(this.nodes[2].retrieve(sprite)); + returnObjects = returnObjects.concat(this.nodes[3].retrieve(sprite)); } } diff --git a/src/physics/arcade/ArcadePhysics.js b/src/physics/arcade/ArcadePhysics.js index 47ddc246..97e0bf43 100644 --- a/src/physics/arcade/ArcadePhysics.js +++ b/src/physics/arcade/ArcadePhysics.js @@ -19,20 +19,21 @@ Phaser.Physics.Arcade = function (game) { */ this.maxLevels = 4; - this.LEFT = 0x0001; - this.RIGHT = 0x0010; - this.UP = 0x0100; - this.DOWN = 0x1000; this.NONE = 0; - this.CEILING = this.UP; - this.FLOOR = this.DOWN; - this.WALL = this.LEFT | this.RIGHT; - this.ANY = this.LEFT | this.RIGHT | this.UP | this.DOWN; + this.UP = 1; + this.DOWN = 2; + this.LEFT = 3; + this.RIGHT = 4; + this.CEILING = 1; + this.FLOOR = 2; + this.WALL = 5; + this.ANY = 6; this.OVERLAP_BIAS = 4; this.TILE_OVERLAP = false; - this.quadTree = null; + this.quadTree = null; + this.quadTreeID = 0; // avoid gc spikes by caching these values for re-use this._obj1Bounds = new Phaser.Rectangle; @@ -140,7 +141,8 @@ Phaser.Physics.Arcade.prototype = { preUpdate: function () { // Create our tree which all of the Physics bodies will add themselves to - this.quadTree = new Phaser.QuadTree(this.game.world.bounds.x, this.game.world.bounds.y, this.game.world.bounds.width, this.game.world.bounds.height, this.maxObjects, this.maxLevels); + this.quadTreeID = 0; + this.quadTree = new Phaser.QuadTree(this, this.game.world.bounds.x, this.game.world.bounds.y, this.game.world.bounds.width, this.game.world.bounds.height, this.maxObjects, this.maxLevels); }, @@ -162,7 +164,31 @@ Phaser.Physics.Arcade.prototype = { */ overlap: function (object1, object2, notifyCallback, processCallback) { - return result; + object2 = object2 || null; + notifyCallback = notifyCallback || null; + processCallback = processCallback || this.separate; + + // Get the ships top-most ID. If the length of that ID is 1 then we can ignore every other result, + // it's simply not colliding with anything :) + var potentials = this.quadTree.retrieve(object1); + var output = []; + + for (var i = 0, len = potentials.length; i < len; i++) + { + if (this.separate(object1.body, potentials[i]).body) + { + output.push(potentials[i]); + } + } + + if (output.length > 0) + { + return output; + } + else + { + return null; + } }, @@ -207,32 +233,32 @@ Phaser.Physics.Arcade.prototype = { this._maxOverlap = object1.deltaAbsX() + object2.deltaAbsX() + this.OVERLAP_BIAS; // If they did overlap (and can), figure out by how much and flip the corresponding flags - if (object1.deltaAbsX() > object2.deltaAbsX()) + if (object1.deltaX() > object2.deltaX()) { this._overlap = object1.x + object1.width - object2.x; - if ((this._overlap > this._maxOverlap) || !(object1.allowCollisions & this.RIGHT) || !(object2.allowCollisions & this.LEFT)) + if ((this._overlap > this._maxOverlap) || object1.allowCollision.right == false || object2.allowCollision.left == false) { this._overlap = 0; } else { - object1.touching |= this.RIGHT; - object2.touching |= this.LEFT; + object1.touching.right = true; + object2.touching.left = true; } } else if (object1.deltaX() < object2.deltaX()) { this._overlap = object1.x - object2.width - object2.x; - if ((-this._overlap > this._maxOverlap) || !(object1.allowCollisions & this.LEFT) || !(object2.allowCollisions & this.RIGHT)) + if ((-this._overlap > this._maxOverlap) || object1.allowCollision.left == false || object2.allowCollision.right == false) { this._overlap = 0; } else { - object1.touching |= this.LEFT; - object2.touching |= this.RIGHT; + object1.touching.left = true; + object2.touching.right = true; } } } @@ -309,28 +335,28 @@ Phaser.Physics.Arcade.prototype = { { this._overlap = object1.y + object1.height - object2.y; - if ((this._overlap > this._maxOverlap) || !(object1.allowCollisions & this.DOWN) || !(object2.allowCollisions & this.UP)) + if ((this._overlap > this._maxOverlap) || object1.allowCollision.down == false || object2.allowCollision.up == false) { this._overlap = 0; } else { - object1.touching |= this.DOWN; - object2.touching |= this.UP; + object1.touching.down = true; + object2.touching.up = true; } } else if (object1.deltaY() < object2.deltaY()) { this._overlap = object1.y - object2.height - object2.y; - if ((-this._overlap > this._maxOverlap) || !(object1.allowCollisions & this.UP) || !(object2.allowCollisions & this.DOWN)) + if ((-this._overlap > this._maxOverlap) || object1.allowCollision.up == false || object2.allowCollision.down == false) { this._overlap = 0; } else { - object1.touching |= this.UP; - object2.touching |= this.DOWN; + object1.touching.up = true; + object2.touching.down = true; } } } diff --git a/src/physics/arcade/Body.js b/src/physics/arcade/Body.js index 5c0a8a11..9f81acf2 100644 --- a/src/physics/arcade/Body.js +++ b/src/physics/arcade/Body.js @@ -38,23 +38,16 @@ Phaser.Physics.Arcade.Body = function (sprite) { this.maxAngular = 1000; this.mass = 1; - // Handy consts - this.LEFT = 0x0001; - this.RIGHT = 0x0010; - this.UP = 0x0100; - this.DOWN = 0x1000; - this.NONE = 0; - this.CEILING = this.UP; - this.FLOOR = this.DOWN; - this.WALL = this.LEFT | this.RIGHT; - this.ANY = this.LEFT | this.RIGHT | this.UP | this.DOWN; + this.quadTreeIndex = []; + + // Allow collision + this.allowCollision = { any: true, up: true, down: true, left: true, right: true }; + this.touching = { none: true, up: false, down: false, left: false, right: false }; + this.wasTouching = { none: true, up: false, down: false, left: false, right: false }; this.immovable = false; this.moves = true; - this.touching = 0; - this.wasTouching = 0; this.rotation = 0; - this.allowCollisions = this.ANY; this.allowRotation = false; this.allowGravity = true; @@ -85,6 +78,19 @@ Phaser.Physics.Arcade.Body.prototype = { update: function () { + // Store and reset collision flags + this.wasTouching.none = this.touching.none; + this.wasTouching.up = this.touching.up; + this.wasTouching.down = this.touching.down; + this.wasTouching.left = this.touching.left; + this.wasTouching.right = this.touching.right; + + this.touching.none = true; + this.touching.up = false; + this.touching.down = false; + this.touching.left = false; + this.touching.right = false; + this.lastX = this.x; this.lastY = this.y; @@ -95,6 +101,8 @@ Phaser.Physics.Arcade.Body.prototype = { if (this.allowCollisions & this.ANY) { + this.quadTreeIDs = []; + this.quadTreeIndex = -1; this.game.physics.quadTree.insert(this); } diff --git a/src/utils/Debug.js b/src/utils/Debug.js index 3fd0d5f0..d5f24484 100644 --- a/src/utils/Debug.js +++ b/src/utils/Debug.js @@ -117,6 +117,7 @@ Phaser.Utils.Debug.prototype = { { this.context.strokeStyle = color; this.context.strokeRect(bounds.x, bounds.y, bounds.width, bounds.height); + this.renderText(quadtree.ID + ' / ' + quadtree.objects.length, bounds.x + 4, bounds.y + 16, 'rgb(0,200,0)', '12px Courier'); } else { @@ -145,13 +146,8 @@ Phaser.Utils.Debug.prototype = { if (showBounds) { - this.context.beginPath(); - this.context.moveTo(sprite.bounds.x, sprite.bounds.y); - this.context.lineTo(sprite.bounds.x + sprite.bounds.width, sprite.bounds.y); - this.context.lineTo(sprite.bounds.x + sprite.bounds.width, sprite.bounds.y + sprite.bounds.height); - this.context.lineTo(sprite.bounds.x, sprite.bounds.y + sprite.bounds.height); - this.context.closePath(); this.context.strokeStyle = 'rgba(255,0,255,0.5)'; + this.context.strokeRect(sprite.bounds.x, sprite.bounds.y, sprite.bounds.width, sprite.bounds.height); this.context.stroke(); } @@ -514,8 +510,8 @@ Phaser.Utils.Debug.prototype = { return; } - if (typeof color === "undefined") { color = 'rgb(255,255,255)'; } - if (typeof font === "undefined") { font = '16px Courier'; } + color = color || 'rgb(255,255,255)'; + font = font || '16px Courier'; this.start(); this.context.font = font;