10.03.2019 - Round 1 results of our "A Love Letter For FreeBASIC" game dev competition have been published. Please be sure to check the results thread: http://games.freebasic.net/forum/index.php?topic=629.0. Don't forget that the competition is continuing with a round 2, lasting till 29th of April, 300 USD first prize. Stay tuned!

Author Topic: Quadtree implementation for use with Chipmunk 2D  (Read 3515 times)

Lithium

  • Recruit
  • **
  • Posts: 28
    • View Profile
Quadtree implementation for use with Chipmunk 2D
« on: February 21, 2012, 08:07:10 PM »
Wrote this for Relativity because:
 - Chipmunk's Hash tables are not good for every size of query
 - Chipmunk's spacial lookup functions are clunky
 - Chipmunk's spacial lookups must be filtered by actual groups or layers

so for 1. using a quadtree fixes that
for 2., this returns an array of results instead of using a callback function
for 3., you attach a meta object to the data field of each body (I called this CollisionObj), which should have an 'index' field which decides which index the object will be placed it, or -1 if it should not be indexed at all

basically you call the update method on your QuadTree instance after calling chipmunk's step function, then you can run closest object, objects within radius, and objects within rectangle queries via methods on the QT instance

code:

Code: [Select]
#define QT_NODE_SIZE 4
#define QT_MAX_DEPTH 20
#define QT_MAX_INDEX 16

#define QT_BRANCH 1
#define QT_LEAF   2

type QTData
    p as cpVect
    d as any ptr
end type

type QTArray
    declare constructor()
    declare destructor()
   
    declare sub push ( byval item as QTData )
   
    length as integer
    max as integer
    items as QTData ptr
end type

#define QTResultSet QTArray

type QTNode
    declare constructor ( byval ntype as integer, byval min as cpVect, byval max as cpVect )
    declare destructor ()
       
    ntype as integer
    links(0 to 3) as QTNode ptr
    pdata as QTArray
    min as cpVect
    max as cpVect
end type

type QuadTree
   
    declare constructor()
    declare destructor()
   
    declare sub eraseAll ()
    declare sub update ( byval space as cpSpace ptr )
   
    declare function addObj ( byval node as QTNode ptr, byref item as QTData, byval level as integer = 0 ) as integer
    declare sub eraseNode ( byval node as QTNode ptr )
   
    declare function closestTo ( byval ind as integer, byval p as cpVect, byval range as double, byval exclude as any ptr = 0 ) as QTResultSet
    declare function inRangeOf ( byval ind as integer, byval p as cpVect, byval range as double ) as QTResultSet
    declare function inBox ( byval ind as integer, byval min as cpVect, byval max as cpVect ) as QTResultSet
    declare sub boxQuery ( byval node as QTNode ptr, byval min as cpVect, byval max as cpVect, byref res as QTResultSet )
   
    index(0 to QT_MAX_INDEX-1) as QTNode ptr
   
end type

constructor QuadTree ()

    for i as integer = 0 to QT_MAX_INDEX-1
        this.index(i) = 0
    next i

end constructor

destructor QuadTree ()

    this.eraseAll()

end destructor

sub QuadTree.eraseAll ()
   
    for i as integer = 0 to QT_MAX_INDEX-1
        if this.index(i) <> 0 then
            this.eraseNode(this.index(i))
            this.index(i) = 0
        end if
    next i
   
end sub

sub QuadTree.update ( byval space as cpSpace ptr )
   
    this.eraseAll()
   
    dim as cpArray ptr bodies = space->bodies
    dim as cpVect min(0 to QT_MAX_INDEX-1)
    dim as cpVect max(0 to QT_MAX_INDEX-1)
    dim as QTArray items(0 to QT_MAX_INDEX-1)
   
    for i as integer = 0 to bodies->num-1
       
        dim as cpBody ptr body = bodies->arr[i]
        dim as CollisionObj ptr co = body->data
               
        if co <> 0 then
           
            if co->index >= 0 and co->index < QT_MAX_INDEX then
           
                dim as QTData item
               
                item.p = body->p
                item.d = co
   
                if items(co->index).length = 0 then
                    min(co->index) = item.p
                    max(co->index) = item.p
                else
                    if item.p.x < min(co->index).x then min(co->index).x = item.p.x
                    if item.p.x > max(co->index).x then max(co->index).x = item.p.x
                    if item.p.y < min(co->index).y then min(co->index).y = item.p.y
                    if item.p.y > max(co->index).y then max(co->index).y = item.p.y
                end if
               
                items(co->index).push(item)
           
            end if
           
        end if
       
    next i
   
    for i as integer = 0 to QT_MAX_INDEX-1
       
        if items(i).length > 0 then
           
            this.index(i) = new QTNode(QT_LEAF, min(i), max(i))
            for j as integer = 0 to items(i).length-1
                this.addObj(this.index(i), items(i).items[j])
            next j
           
        end if
       
    next i
   
end sub

#define QT_P_IN_B(_p, _a, _b) (((_p).x >= (_a).x) and ((_p).y >= (_a).y) and ((_p).x <= (_b).x) and ((_p).y <= (_b).y))
#define QT_B_IN_B(_a1, _b1, _a2, _b2) (((_a1).x <= (_b2).x) and ((_a1).y <= (_b2).y) and ((_b1).x >= (_a2).x) and ((_b1).y >= (_a2).y))

function QuadTree.addObj ( byval node as QTNode ptr, byref item as QTData, byval level as integer = 0 ) as integer
    if QT_P_IN_B(item.p, node->min, node->max) then
        if node->ntype = QT_LEAF then
            if node->pdata.length < QT_NODE_SIZE or level >= QT_MAX_DEPTH then
                node->pdata.push(item)
            else
                dim as QTArray items = node->pdata
                items.push(item)
                *node = *(new QTNode(QT_BRANCH, node->min, node->max))
                for i as integer = 0 to items.length - 1
                    this.addObj(node, items.items[i], level)
                next i
            end if
            return 1
        elseif node->ntype = QT_BRANCH then
            if this.addObj(node->links(0), item, level+1) = 1 then return 1
            if this.addObj(node->links(1), item, level+1) = 1 then return 1
            if this.addObj(node->links(2), item, level+1) = 1 then return 1
            if this.addObj(node->links(3), item, level+1) = 1 then return 1
        end if
    end if
    return 0
end function
   
function QuadTree.closestTo ( byval ind as integer, byval p as cpVect, byval range as double, byval exclude as any ptr = 0 ) as QTResultSet
   
    dim as QTResultSet r1, rout
   
    if ind < 0 or ind >= QT_MAX_INDEX then return rout
    if this.index(ind) = 0 then return rout
   
    dim as cpVect min = cpv(p.x-range, p.y-range), max = cpv(p.x+range, p.y+range)
    this.boxQuery(this.index(ind), min, max, r1)
   
    dim as double range2 = range*range
    dim as double bdist2 = 0
   
    for i as integer = 0 to r1.length-1
        dim as cpVect p2 = r1.items[i].p
        dim as double dx = p2.x-p.x, dy = p2.y-p.y
        dim as double dist2 = dx*dx+dy*dy
        if exclude = r1.items[i].d then continue for
        if rout.length = 0 then
            bdist2 = dist2
            rout.push(r1.items[i])
        elseif dist2 <= bdist2 then
            bdist2 = dist2
            rout.items[0] = r1.items[i]
        end if
    next i
   
    if bdist2 > range2 then rout.length = 0
   
    return rout

end function

function QuadTree.inRangeOf ( byval ind as integer, byval p as cpVect, byval range as double ) as QTResultSet
   
    dim as QTResultSet r1, rout
   
    if ind < 0 or ind >= QT_MAX_INDEX then return rout
    if this.index(ind) = 0 then return rout
   
    dim as cpVect min = cpv(p.x-range, p.y-range), max = cpv(p.x+range, p.y+range)
    this.boxQuery(this.index(ind), min, max, r1)
       
    dim as double range2 = range*range
   
    for i as integer = 0 to r1.length-1
        dim as cpVect p2 = r1.items[i].p
        dim as double dx = p2.x-p.x, dy = p2.y-p.y
        if (dx*dx+dy*dy) <= range2 then rout.push(r1.items[i])
    next i
   
    return rout
   
end function

function QuadTree.inBox ( byval ind as integer, byval min as cpVect, byval max as cpVect ) as QTResultSet
   
    dim as QTResultSet rout
   
    if ind < 0 or ind >= QT_MAX_INDEX then return rout
    if this.index(ind) = 0 then return rout
   
    this.boxQuery(this.index(ind), min, max, rout)
   
    return rout
   
end function

sub QuadTree.boxQuery ( byval node as QTNode ptr, byval min as cpVect, byval max as cpVect, byref res as QTResultSet )
   
    if QT_B_IN_B(node->min, node->max, min, max) then
        if node->ntype = QT_LEAF then
            for i as integer = 0 to node->pdata.length-1
                dim as QTData item = node->pdata.items[i]
                if QT_P_IN_B(item.p, min, max) then
                    res.push(item)
                end if
            next i
        elseif node->ntype = QT_BRANCH then
            this.boxQuery(node->links(0), min, max, res)
            this.boxQuery(node->links(1), min, max, res)
            this.boxQuery(node->links(2), min, max, res)
            this.boxQuery(node->links(3), min, max, res)
        end if
    end if
   
end sub

constructor QTArray ()

    this.length = 0
    this.max = 10
    this.items = allocate(sizeof(QTData)*this.max)

end constructor

destructor QTArray ()

    deallocate this.items

end destructor

sub QTArray.push(byval item as QTData)
   
    if this.length >= this.max then
        this.max *= 10
        dim as QTData ptr newItems = allocate(sizeof(QTData)*this.max)
        for i as integer = 0 to this.length-1
            newItems[i] = this.items[i]
        next i
        deallocate this.items
        this.items = newItems
    end if

    this.items[this.length] = item
    this.length += 1

end sub

constructor QTNode ( byval ntype as integer, byval min as cpVect, byval max as cpVect )

    this.ntype = ntype
     
    this.min = min
    this.max = max
   
    if this.ntype = QT_LEAF then
    elseif this.ntype = QT_BRANCH then
        dim as double cx = (min.x+max.x)*0.5, cy = (min.y+max.y)*0.5
        this.links(0) = new QTNode(QT_LEAF, min, cpv(cx, cy))
        this.links(1) = new QTNode(QT_LEAF, cpv(cx, min.y), cpv(max.x, cy))
        this.links(2) = new QTNode(QT_LEAF, cpv(min.x, cy), cpv(cx, max.y))
        this.links(3) = new QTNode(QT_LEAF, cpv(cx, cy), max)
    end if

end constructor

destructor QTNode ()

end destructor

sub QuadTree.eraseNode ( byval node as QTNode ptr )
       
    if node->ntype = QT_BRANCH then
        this.eraseNode(node->links(0))
        this.eraseNode(node->links(1))
        this.eraseNode(node->links(2))
        this.eraseNode(node->links(3))
        deallocate(node)
    end if
   
end sub