Quadtrees vary with each implementation based on the users needs. I've tried to be flexible here with a emphasis on handling objects that will move about a lot. If an object moves within a quadtree then it will generally have to be re-added to the quadtree, so I've implemented that possibility here. Thankfully there's no need to rebuild the quadtree every time an object moves, the object just removes and adds itself back to the tree, and it will only do it if it's moved outside of its containing tlQuadTreeNode.
When I say object, I mean a tlBox, which is a simple axis aligned bounding box type that can be added to the quadtree, the more complex tlCircle, tlLine and tlPolygon which extend tlBox can also be added, but the quadtree will only concern itself with bounding boxes when a query is made on thequadtree.
Using the quadtree is simple enough, create a new quadtree with whatever dimensions you want and then use tlQuadTree.AddBox to add bounding boxes to it. In your main loop you might want to put RunQuadtreeMaintenance which tidies up the quadtree by finding empty partitions and deleting them. Of course the whole point of a quadtree is to find out which objects are within a particular area so that you can do collision checking, rendering, updating or whatever. To do that, you can query the quadtree by simply calling either tlQuadTree.ForEachObjectInArea or tlQuadTree.ForEachObjectInBox which will run a callback function of your choice to perform your specific tasks on them. There's also queries available to check for objects within a radius by using, tlQuadTree.ForEachObjectInRange and tlQuadTree.ForEachObjectInCircle, and also for lines and rays using tlQuadTree.ForEachObjectAlongLine and tlQuadTree.RayCast.
Implementing this quadtree within your game will probably involve including tlBox, tlCircle, tlLine or tlPolygon as a field within your entity/actor etc types. When your actors move about, just make sure you update the position of the Box as well using tlBox.Position or tlBox.Move. When this happens all the necessary updating of the quadtree will happen automatically behind the scenes. Be aware that if an object moves outside of the quadtree bounds it will drop out of the quadtree.
The object is added to each node it overlaps. No object will ever be added to a node that has children, they will be moved down the quadtree to the bottom level of that branch.
tlBoxs are aware if they have already been found and a callback has been made within the same search, so a callback will never be made twice on the same search query.
No, onced a node is partioned and objects moved down, they're there to stay, however if you RunQuadtreeMaintenance then empty nodes will be unpartitioned. I didn't think it was worth the overhead to worry about moving objects back up the tree again.
The quadtree will just concern itself with doing callbacks on objects it finds with rect->rect collision in the case of QueryQuadtreeArea, and circle->rect collision in the case of tlQuadTree.ForEachObjectInRange. Once you've found those objects you can then go on and do more complex collision checking such as poly->poly. If however you only need to check for rect->rect then you can assume a hit straight away as the quadtree will only callback actual hits, potential hits are already excluded automatically if their bounding box is outside the area being queried.
A potential hit would be an object in the same quadtree node that the area check overlaps. So if the area you're checking a collision for overlaps 2 quadnodes, all the objects in those 2 nodes would be considered potential hits. I decided that I may aswell cull any of those bounding boxes that don't overlap the area being checked before doing the callback so that the amount of potential hits is reduced further, and to save wasting the time doing it in the callback function. This applies to both tlQuadTree.ForEachObjectInArea and tlQuadTree.ForEachObjectInRange functions, but as mentioned before, it will only cull according to bounding boxes, you'll have to do a further check in your callback to manage the more complex poly->poly, poly->rect etc., collisions.
When you run a QueryQuadtreeArea (of any type) you can pass an object that will be passed through to the callback function. So the call back function
you create should look like: Function MyCallBackFunction(ObjectFoundInQuadtree:Object, MyData:object) So your data could be anything such as a bullet
or pLayer ship etc., and assuming that object has a tlBox field you can do further collision checks between the 2. If you don't need to pass any
data then just leave it null.
Boxes have a field called collisiontype which you can find out using the property timelinefx.tlBox.CollisionType. This will return either tlBOX_COLLISION, tlCIRCLE_COLLISION, tlPOLY_COLLISION or tlLINE_COLLISION. The chances are though, that you won't need to know the type, as a call to CheckCollisionwill automatically determine the type and perform the appropriate collision check.
Yes you can create as many quadtrees as you want, however, bear in mind that a tlBox can only ever exist in 1 quadtree at a time. In most cases, with the use of Layers, 1 quadtree will probably be enough.
You can put boxes onto different Layers of the quadtree to help organise your collisions and optimise too. Each Layer has it's own separate quadtree which can be defined according to how you need it to be optimised. See SetLayerConfig. So you could put a load of enemy objects onto one Layer and the pLayer objects onto another. This will speed things up when an enemy might have to query the quadtree to see if the pLayer is nearby because the Layer that the pLayer is on won't be so cluttered with other objects. If you need to check for collisions between many objects vs many other objects, then you should try to put those objects onto the same Layer. This way when each object checks for a collision with an object on the same Layer it can skip having to query the quadtree, because each tlbox already knows what quadtree nodes it exists in, so it can check against other objects in the same node straight away without having to drill down into the quadtree each time.
'Import the TimelineFX Module
'We need the following namespaces
Using std..
Using mojo..
Using timelinefx..
Class Game Extends Window
'Field to store the quadtree
Field QTree:tlQuadTree
'field for our box that will used to query the quad tree
Field player:tlBox
'A Field to store the canvas
Field currentCanvas:Canvas
Method New()
'create the quadtree and make it the same size as the window. Here we're making it so that the maximum number of times it can
'be subdivided is 5, and it will sub divide a when 10 objects are added to a quad. These numbers you can change To tweak performance
'It will vary depending on how you use the quadtree
QTree = New tlQuadTree(0, 0, Width, Height, 5, 10)
'Populate the quadtree with a bunch of objects
For Local c:Int = 1 To 1000
Local t:Int = Rnd(3)
Local rect:tlBox
Local x:Float = Rnd() * Width
Local y:Float = Rnd() * Height
Select t
Case 0
'Create a Basic bounding box boundary
rect = New tlBox(x, y, 10, 10, 0)
Case 1
'Create a circle Boundary
rect = New tlCircle(x, y, 5, 0)
Case 2
'Create a polygon boundary
Local verts:= New Float[](- 10.0, -10.0, -15.0, 0.0, -10.0, 10.0, 10.0, 10.0, 15.0, 0.0, 10.0, -10.0)
rect = New tlPolygon(x, y, verts, 0)
End Select
'Add the boundary to the quadtree
QTree.AddBox(rect)
Next
player = New tlBox(0, 0, 50, 50)
End
Method OnRender( canvas:Canvas ) Override
currentCanvas = canvas
App.RequestRender()
canvas.Clear( New Color(0,0,0,1) )
canvas.BlendMode = BlendMode.Alpha
'position the player box
player.Position(Mouse.X, Mouse.Y)
Local time:=App.Millisecs
'when space is pressed, draw everything on the screen. We do this by calling "ForEachObjectInArea", and define the area as the screen size. We also
'pass the DrawScreen interface which will be called by the quadtree if it finds something in the are. We also pass the layers that we want to check.
If Keyboard.KeyDown(Key.Space) QTree.ForEachObjectInArea(0, 0, Width, Height, Self, DrawScreenCallBack, New Int[](0, 1, 2))
'Check for objects within a box using "ForEachObjectWithinBox", passing our player box. We also pass the player as "Data" which is forwarded on to the
'DrawBoxAction so we can use it to find out if it is actually colliding with anything. We pass the DrawBoxAction interface
'we created which will be called when the qaudtree finds something within the bounding box of the object. We also pass the layers that we want to check.
QTree.ForEachObjectInBox(player, self, DrawBoxCallBack, New Int[](0, 1, 2))
'Draw the player box
player.Draw(canvas)
time = App.Millisecs - time
canvas.DrawText(time, 10, 10)
End
End
'These are our call back fuctions where we can decide what to do with each object found in the quadtree query.
'The parameters will contain the object found and any data that you pass through to the query.
Function DrawBoxCallBack:Void(foundobject:Object, data:Object)
'Use casting to create a local rect of whatever boundary object the quad tree has found.
'This could be either a tlBoundary, tlBoundaryCircle, tlBoundaryLine or tlBoundaryPoly
Local rect:tlBox = Cast<tlBox>(foundobject)
'We used the data variable to pass the Game object. This could be
'any object, such as a game entity, which could have a field containing a tlBoundary representing
'its bounding box/poly etc.
Local thegame:= Cast<Game>(data)
'Do a collision check and store the result
Local collisionresult:tlCollisionResult = CheckCollision(thegame.player, rect)
'If there is a collision
If collisionresult.Intersecting = True
'if we can check for the type of collision like this, you can have either tlPOLY_COLLISION, tlBOX_COLLISION, tlLINE_COLLISION,tlCIRCLE_COLLISION
If rect.CollisionType = tlPOLY_COLLISION
'For any polygon collisions rotate the polygon
Cast<tlPolygon>(rect).RotateDegrees(1)
'because the polygon has move it needs to be updated within the quadtree.
Cast<tlPolygon>(rect).UpdateWithinQuadtree()
End If
'Draw the object that collided with the player box in a different colour
thegame.currentCanvas.Color = New Color(0, 1, 0)
rect.Draw(thegame.currentCanvas)
End If
End
Function DrawScreenCallBack:Void(o:Object, data:Object)
'Use casting to create a local rect of whatever boundary object the quad tree has found.
'This could be either a tlBoundary, tlBoundaryCircle, tlBoundaryLine or tlBoundaryPoly
'Note that the Box object has a Data field that you could use to store another object, say your game entity.
Local rect:tlBox = Cast<tlBox>(o)
Local thegame:=Cast<Game>(data)
thegame.currentCanvas.Color = New Color(0.5, 0.5, 0.5)
'Draw the box that was found
rect.Draw(thegame.currentCanvas)
End
Function Main()
New AppInstance
New Game
App.Run()
End
| Public Constructors | |
|---|---|
| New | Create a new tlQuadTree |
| Public Methods | |
|---|---|
| AddBox | Add a new bounding box to the Quadtree |
| AddForRemoval | Add the tlBox to the remove list |
| CleanUp | Remove marked objects |
| Draw | Draw a Layer of the quadtree |
| ForEachObjectAlongLine | Query a quadtree with a tlLine |
| ForEachObjectInArea | Query the Quadtree to find objects with an area |
| ForEachObjectInBox | Query the quadtree to find objects within a tlBox |
| ForEachObjectInCircle | Query the quadtree to find objects within a tlCircle |
| ForEachObjectWithinRange | Query the quadtree to find objects within a certain radius |
| GetHeight | Get the height of the tlQuadtree |
| GetObjectsFound | Find out how many objects were found on the last query |
| GetObjectsInBox | Query the quadtree to find objects within a tlBox |
| GetObjectsInCircle | Query the quadtree to find objects within a tlCircle |
| GetTotalObjects | Find out how many objects are currently in the quadtree |
| GetWidth | Get the width of the tlQuadtree |
| RayCast | Query a quadtree with a ray of given length |
| RunMaintenance | Perform some house keeping on the quadtree |
| SetLayerConfig | Configure a Layer of the quadtree |
| UnHighlight | unhiglight all quad nodes, mainly for debugging purposes |