**Approach (brief)** Use a dynamic quadtree where each node stores references to objects that intersect its AABB. Objects can live in multiple nodes (spanning strategy = store at smallest node that fully contains their bounding box; if none, store at current node). Maintain tree each frame by updating moving objects: lazy reinserts or immediate remove+insert.**Pseudocode**pseudo
struct Node { AABB bounds; list objs; Node[4] children; int depth; }
function insert(node, obj):
if node.isLeaf() and node.objs.size + 1 > CAPACITY and node.depth < MAX_DEPTH:
split(node)
if node.isLeaf():
node.objs.add(obj)
obj.nodes.add(node)
return
// try to push into a child that fully contains obj.bbox
for child in node.children:
if child.bounds.contains(obj.bbox):
insert(child, obj)
return
// spans multiple children -> keep at current node
node.objs.add(obj)
obj.nodes.add(node)
function remove(node, obj):
if obj.nodes.contains(node):
node.objs.remove(obj)
obj.nodes.remove(node)
// attempt merge upward
tryMerge(node)
function split(node):
create 4 children with quarter bounds
move copy of node.objs into appropriate children (re-evaluate containment)
clear node.objs
node.leaf = false
function tryMerge(node):
if node.isLeaf() or any child has children: return
total = sum child.objs.size
if total <= CAPACITY:
collect all child.objs into node.objs and delete children
node.leaf = true
// Per-frame update
function updateFrame(movingObjects):
for obj in movingObjects:
if lazy and small_move(obj): continue
for n in obj.nodes: remove(n, obj) // detach
obj.nodes.clear()
insert(root, obj)
// Frustum culling
function collectVisible(node, frustum, outList):
if not frustum.intersects(node.bounds): return
for obj in node.objs: if frustum.intersects(obj.bbox): outList.add(obj)
if node.isLeaf(): return
for child in node.children: collectVisible(child, frustum, outList)
**Worst-case complexity** - Insert/remove: O(depth + k) where k = number of nodes an object spans. Worst-case O(n) if degenerate (all objects in root). Query (frustum): O(n) worst-case.**Optimizations**- Loose quadtree: enlarge child bounds by factor (e.g., 2x) so moving objects stay in same node longer -> fewer updates.- Lazy reinsertion: only reinsert when object leaves its node's loose bounds or after X frames.- Spatial hashing fallback for very high update rates.- Pool nodes, avoid allocations; store object->node pointers for O(1) removals.- Use AABB vs sphere tests to reduce false positives.**Trade-offs** Loose + lazy reduces update cost but increases overlap during queries (more false positives). Tune CAPACITY, MAX_DEPTH, loose factor, and lazy thresholds for your game’s object density and motion characteristics.