Note: New Version: Last updated on 30th of July
//Author: Johannes Pauw
//Released under the MIT license (to match Adam Atomic, since this quadtree is built specifically for flixel).
//Go look the license up on the internets if you want specifics, but the basic idea is that you can do absolutely anything
//you want with it, without restriction, for commercial or private use.
package com.doginlake.quadtree
{
import com.adamatomic.flixel.FlxArray;
import com.adamatomic.flixel.FlxCore;
import com.adamatomic.flixel.FlxG;
import com.adamatomic.flixel.FlxSprite;
//@desc The primary node class for the quadtree
public class DogTree
{
private var x:Number;
private var y:Number;
private var width:Number;
private var height:Number;
private var NE:DogTree;
private var NW:DogTree;
private var SE:DogTree;
private var SW:DogTree;
private var parent:DogTree;
private var objects:FlxArray;
private var boundingArray:FlxArray;
private var removalArray:FlxArray;
public function DogTree(depthLeft:int, x:Number, y:Number, width:Number, height:Number, parent:DogTree = null, boundingArray:Array = null)
{
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.parent = parent;
this.removalArray = new FlxArray();
if (parent == null)
{
this.boundingArray = new FlxArray();
}
this.objects = new FlxArray();
depthLeft--;
if (depthLeft > 0)
{
NW = new DogTree(depthLeft, x, y, width / 2, height / 2, this, boundingArray);
NE = new DogTree(depthLeft, x + width / 2, y, width / 2, height / 2, this, boundingArray);
SW = new DogTree(depthLeft, x, y + height / 2, width / 2, height / 2, this, boundingArray);
SE = new DogTree(depthLeft, x + width / 2, y + height / 2, width / 2, height / 2, this, boundingArray);
}
}
//Check if this node has children. If it does, see if the object will fit completely within the bounds of one of the children. If not, add to this node.
public function add(core:FlxCore):void
{
if (NE != null)
{
if (core.x > NW.x && core.y > NW.y && core.x + core.width < NW.x + NW.width && core.y + core.height < NW.y + NW.height)
{
NW.add(core);
}
else if (core.x > NE.x && core.y > NE.y && core.x + core.width < NE.x + NE.width && core.y + core.height < NE.y + NE.height)
{
NE.add(core);
}
else if (core.x > SW.x && core.y > SW.y && core.x + core.width < SW.x + SW.width && core.y + core.height < SW.y + SW.height)
{
SW.add(core);
}
else if (core.x > SE.x && core.y > SE.y && core.x + core.width < SE.x + SE.width && core.y + core.height < SE.y + SE.height)
{
SE.add(core);
}
else
{
objects.add(core);
core._dogNode = this;
}
}
else
{
objects.add(core);
core._dogNode = this;
}
}
//Check if this object needs to go to a new node
public function update(core:FlxCore):void
{
if (NE != null)
{
if (core.x > NW.x && core.y > NW.y && core.x + core.width < NW.x + NW.width && core.y + core.height < NW.y + NW.height)
{
objects.remove(core, true);
NW.add(core);
}
else if (core.x > NE.x && core.y > NE.y && core.x + core.width < NE.x + NE.width && core.y + core.height < NE.y + NE.height)
{
objects.remove(core, true);
NE.add(core);
}
else if (core.x > SW.x && core.y > SW.y && core.x + core.width < SW.x + SW.width && core.y + core.height < SW.y + SW.height)
{
objects.remove(core, true);
SW.add(core);
}
else if (core.x > SE.x && core.y > SE.y && core.x + core.width < SE.x + SE.width && core.y + core.height < SE.y + SE.height)
{
objects.remove(core, true);
SE.add(core);
}
else if (core.x > x && core.y > y && core.x + core.width < x + width && core.y + core.height < y + height)
{
}
else if (parent != null)
{
objects.remove(core, true);
parent.secondaryUpdate(core);
}
}
else if (core.x > x && core.y > y && core.x + core.width < x + width && core.y + core.height < y + height)
{
}
else if (parent != null)
{
objects.remove(core, true);
parent.secondaryUpdate(core);
}
}
public function secondaryUpdate(core:FlxCore):void
{
if (NE != null)
{
if (core.x > NW.x && core.y > NW.y && core.x + core.width < NW.x + NW.width && core.y + core.height < NW.y + NW.height)
{
NW.add(core);
}
else if (core.x > NE.x && core.y > NE.y && core.x + core.width < NE.x + NE.width && core.y + core.height < NE.y + NE.height)
{
NE.add(core);
}
else if (core.x > SW.x && core.y > SW.y && core.x + core.width < SW.x + SW.width && core.y + core.height < SW.y + SW.height)
{
SW.add(core);
}
else if (core.x > SE.x && core.y > SE.y && core.x + core.width < SE.x + SE.width && core.y + core.height < SE.y + SE.height)
{
SE.add(core);
}
else if (core.x > x && core.y > y && core.x + core.width < x + width && core.y + core.height < y + height)
{
core._dogNode = this;
objects.add(core);
}
else
{
if (parent != null)
{
parent.secondaryUpdate(core);
}
else
{
core._dogNode = this;
objects.add(core);
}
}
}
else if (core.x > x && core.y > y && core.x + core.width < x + width && core.y + core.height < y + height)
{
core._dogNode = this;
objects.add(core);
}
else
{
if (parent != null)
{
parent.secondaryUpdate(core);
}
else
{
core._dogNode = this;
objects.add(core);
}
}
}
//Find objects that may collide with the inputted variables.
public function findBoundingCollisions(boundingX:Number, boundingY:Number, boundingHeight:Number, boundingWidth:Number, array:FlxArray = null):FlxArray
{
var core:FlxCore;
if (array == null)
{
boundingArray.clear();
array = boundingArray;
}
if (objects.length > 0)
{
for (var i:int = 0; i < objects.length; i++)
{
core = objects[i] as FlxCore;
if (!core.dead)
{
array.add(core);
}
}
}
if (NE != null)
{
if (!(boundingX + boundingWidth < NW.x || boundingX > NW.x + NW.width || boundingY + boundingHeight < NW.y || boundingY > NW.y + NW.height))
{
array = NW.findBoundingCollisions(boundingX, boundingY, boundingHeight, boundingWidth, array);
}
if (!(boundingX + boundingWidth < NE.x || boundingX > NE.x + NE.width || boundingY + boundingHeight < NE.y || boundingY > NE.y + NE.height))
{
array = NE.findBoundingCollisions(boundingX, boundingY, boundingHeight, boundingWidth, array);
}
if (!(boundingX + boundingWidth < SW.x || boundingX > SW.x + SW.width || boundingY + boundingHeight < SW.y || boundingY > SW.y + SW.height))
{
array = SW.findBoundingCollisions(boundingX, boundingY, boundingHeight, boundingWidth, array);
}
if (!(boundingX + boundingWidth < SE.x || boundingX > SE.x + SE.width || boundingY + boundingHeight < SE.y || boundingY > SE.y + SE.height))
{
array = SE.findBoundingCollisions(boundingX, boundingY, boundingHeight, boundingWidth, array);
}
}
return array;
}
//Check if this single FlxCore object collides with any of the objects in the tree
public function singleCollision(core:FlxSprite, Collide:Function = null ):void
{
FlxG.collideArray(findBoundingCollisions(core.x, core.y, core.height, core.width), core, Collide);
}
//Check if anything in this array of FlxCore objects collides with any of the objects in the tree
public function arrayCollision(array:FlxArray, Collide:Function = null):void
{
var core:FlxSprite;
for (var i:uint = 0; i < array.length; i++)
{
core = array[i];
if (!core.dead)
{
FlxG.collideArray(findBoundingCollisions(core.x, core.y, core.height, core.width), core, Collide);
}
}
}
//Check if this single FlxCore object overlaps with any of the objects in the tree
public function singleOverlap(core:FlxCore, Collide:Function):void
{
FlxG.overlapArray(findBoundingCollisions(core.x, core.y, core.height, core.width), core, Collide);
}
//Check if anything in this array of FlxCore objects overlaps with any of the objects in the tree
public function arrayOverlap(array:FlxArray, Collide:Function):void
{
var core:FlxCore;
for (var i:uint = 0; i < array.length; i++)
{
core = array[i];
if (!core.dead)
{
FlxG.overlapArray(findBoundingCollisions(core.x, core.y, core.height, core.width), core, Collide);
}
}
}
}
}
Test program to play around with. Use your 'B' key to switch between standard and quadtree collision detection. It'll notify you in the logs which one you're using.
package collidetest
{
import com.adamatomic.flixel.*;
import com.doginlake.quadtree.DogTree;
import flash.geom.Point;
public class Test extends FlxState
{
private var _tileTree:DogTree;
private var _tileArray:FlxArray;
private var _fieldWidth:Number;
private var _fieldHeight:Number;
private var _useTree:Boolean;
private var _numSprites:int;
private var _spriteBaseSize:int;
private var _spriteSizeRange:int;
function Test():void
{
super();
_fieldWidth = 1500;
_fieldHeight = 1500;
_numSprites = 250;
_spriteBaseSize = 1;
_spriteSizeRange = 20;
_tileTree = new DogTree(9, 0, 0, _fieldWidth, _fieldHeight);
_tileArray = new FlxArray();
_useTree = true;
var sprite:FlxSprite;
var spriteXLoc:Number;
var spriteYLoc:Number;
var spriteSize:Number;
for (var i:int = 0; i < _numSprites; i++)
{
spriteSize = Math.random() * _spriteSizeRange+_spriteBaseSize;
spriteXLoc = Math.random() * (_fieldWidth-spriteSize);
spriteYLoc = Math.random() * (_fieldHeight-spriteSize);
sprite = new FlxSprite(null, spriteXLoc, spriteYLoc, false, false, spriteSize, spriteSize, 0xff777777);
sprite.velocity.x = Math.random() * 50;
sprite.velocity.y = Math.random() * 50;
if (Math.random() > .5)
{
sprite.velocity.x = -sprite.velocity.x
}
if (Math.random() > .5)
{
sprite.velocity.y = -sprite.velocity.y
}
this.add(sprite);
_tileTree.add(sprite);
_tileArray.add(sprite);
}
}
override public function update():void
{
var sprite:FlxSprite;
for (var i:int = 0; i < _tileArray.length; i++)
{
sprite = _tileArray[i] as FlxSprite;
if (sprite.x < 0 || sprite.x + sprite.width > _fieldWidth)
{
sprite.velocity.x = -sprite.velocity.x;
}
if (sprite.y < 0 || sprite.y + sprite.height > _fieldHeight)
{
sprite.velocity.y = -sprite.velocity.y;
}
}
if (FlxG.justPressed(FlxG.B))
{
_useTree = !_useTree;
if (_useTree)
{
FlxG.log("Using DogTree");
}
else
{
FlxG.log("Not Using DogTree");
}
}
if (_useTree)
{
for (var j:int = 0; j < _tileArray.length; j++)
{
sprite = _tileArray[j] as FlxSprite;
sprite._dogNode.update(sprite);
}
_tileTree.arrayOverlap(_tileArray, overlaps);
}
else
{
FlxG.overlapArrays(_tileArray, _tileArray, overlaps);
}
super.update();
}
public function overlaps(core1:FlxCore, core2:FlxCore):void
{
core1.flicker(.1);
core2.flicker(.1);
}
}
}
Finally, you have to add
public var _dogNode:DogTree;
to FlxCore
Make sure to only call the update method on objects that are in the tree if they are able to move.
I haven't tested it very extensively, so there could still be bugs.
It showed moderate speed increases for me when implemented in MODE -- I think the increases were more significant when I was working in haXe (which is what I built Polygonal Fury with, where I took this code from) because I was able to inline methods, which reduced the number of calls dramatically.
It's also likely that there are ways that you could speed this up more. If anyone cares to, go right ahead. For my purposes, this is fast enough.
Update: 30th July
I've heavily modified the quadtree, it's somewhat faster now. I've gotten *very* mixed results with it -- In the example program I supplied, the settings that it starts with show a significant increase in performance over the standard collision detection. However, if you set the size of the field much smaller, or increase the size of the sprites, or do a variety of other things, standard collision detection starts to be faster.
It says on PolygonalLabs.de (a fantastic site mentioned later in this thread as well) at
http://lab.polygonal.de/ds/ under the 'Linked List' heading that object access in AS3 is faster than array access. Taking that into account, along with *much* faster object insertion/removal, it seems likely that linked list would be a better way to go to store objects in the quadtree itself. I used linked lists after reading that bit on that site for Polygonal Fury, and I definitely saw speed increases when using it on there. I'm currently working on implementing a simple linked list that stores flxcore objects, and integrating it into DogTree. I still hope to keep the changes in other flixel files down to that one line in flxcore though.