UNPKG

blaze-2d

Version:

A fast and simple WebGL 2 2D game engine written in TypeScript

307 lines 10.2 kB
import AABB from "./aabb"; import AABBNode from "./aabbNode"; /** * Represents a tree data structure which contains nodes that store {@link AABB}s. * * This tree is used to speed up the physics engine's broadphase. * * @see [Intro to AABB Trees](https://www.azurefromthetrenches.com/introductory-guide-to-aabb-tree-collision-detection/) * @see [Physics Broadphase - AABB Tree](http://allenchou.net/2014/02/game-physics-broadphase-dynamic-aabb-tree/) */ export default class AABBTree { constructor() { this.margin = 0.1; this.leafMarginScale = 0.2; this.leafMarginSlop = 0.05; this.leafMarginMin = 0.02; this.leafMarginMax = 10; /** * The number of nodes which had to be reinserted in the last tree update. */ this.insertionsLastUpdate = 0; } /** * Updates the tree. */ update() { if (!this.root) return; // console.log(this.root); if (this.root.isLeaf()) { this.root.updateAABB(this.margin); return; } // update nodes and find invalid nodes const invalidNodes = []; const stack = [this.root]; while (stack.length > 0) { const node = stack.pop(); if (node.isLeaf()) { const margin = this.updateAABBMargin(node); const fat = node.aabb; const tight = new AABB(fat.obj, 0); if (!fat.contains(tight)) { if (!margin) this.updateAABBMargin(node, true); invalidNodes.push(node); } } else { stack.push(node.left, node.right); } } this.insertionsLastUpdate = 0; // re-insert invalid nodes for (const node of invalidNodes) { // remove node this.removeNode(node); // if (this.root === node) console.log(node) // re-insert node this.addNode(node, this.root); this.insertionsLastUpdate++; } } /** * Updates a leaf node's aabb's margin based on the aabb's bound {@link CollisionObject}. * * @param leaf The leaf node to update * @param force Force the margin to be updated even when there is an insufficient margin difference * @returns The new margin or false if the margin wasn't updated. */ updateAABBMargin(leaf, force = false) { const vel = leaf.aabb.obj.velocity; const newMargin = Math.max(Math.min(Math.max(Math.abs(vel[0]), Math.abs(vel[1])) * this.leafMarginScale, this.leafMarginMax), this.leafMarginMin); if (force || Math.abs(newMargin - leaf.aabb.margin) > this.leafMarginSlop) { // console.log("margin update"); // Logger.log("aabbTree", `Margin updated [old: ${node.aabb.margin}] [new: ${newMargin}]`); // console.log(newMargin); leaf.updateAABB(newMargin); return newMargin; } return false; } /** * Obtains all the pairs of {@link CollisionObject}s which could possibly be colliding. * * @returns An array containing the {@link CollisionPair}s */ collectPairs() { if (!this.root || this.root.isLeaf()) return []; this.pairs = []; this.clearChildrenCrossedFlag(); this.collectPairsHelper(this.root.left, this.root.right); return this.pairs; } /** * Sets the `childrenCrossed` flag to false on all nodes in the tree. */ clearChildrenCrossedFlag() { if (!this.root) return; if (this.root.isLeaf()) this.root.childrenCrossed = false; const stack = [this.root]; while (stack.length > 0) { const node = stack.pop(); node.childrenCrossed = false; if (!node.isLeaf()) { stack.push(node.left, node.right); } } } crossChildren(n) { if (n.childrenCrossed) return; this.collectPairsHelper(n.left, n.right); n.childrenCrossed = true; } /** * Collects {@link CollisionPair}s. * * @param a An {@link AABBNode} to check for pairs * @param b An {@link AABBNode} to check for pairs */ collectPairsHelper(a, b) { if (a.isLeaf()) { // 2 leaves so check for collision if (b.isLeaf()) { // found potential collision if (!a.aabb.obj.filter.reject(b.aabb.obj.filter) && a.aabb.intersects(b.aabb)) this.pairs.push({ a: a.aabb.obj, b: b.aabb.obj }); } else { // leaf / branch, 2 cross checks this.crossChildren(b); this.collectPairsHelper(a, b.left); this.collectPairsHelper(a, b.right); } } else { // branch / leaf, 2 cross checks if (b.isLeaf()) { this.crossChildren(a); this.collectPairsHelper(a.left, b); this.collectPairsHelper(a.right, b); } else { // 2 branches, 4 cross checks this.crossChildren(a); this.crossChildren(b); this.collectPairsHelper(a.left, b.left); this.collectPairsHelper(a.left, b.right); this.collectPairsHelper(a.right, b.left); this.collectPairsHelper(a.right, b.right); } } } /** * Picks aabbs in the tree. * * @param point The point to pick {@link AABB}s at. * @returns The picked aabbs */ pick(point) { if (!this.root) return []; const picked = []; const stack = [this.root]; while (stack.length > 0) { const node = stack.pop(); if (node.isLeaf()) { if (node.aabb.intersects(point)) picked.push(node.aabb); } else { stack.push(node.left, node.right); } } return picked; } /** * Raycasts against aabbs in the tree. * * @param ray The ray to cast. */ raycast(ray) { if (!this.root) return []; const result = []; const stack = [this.root]; while (stack.length > 0) { const node = stack.pop(); if (node.aabb.intersects(ray)) { if (node.isLeaf()) { result.push(node.aabb); } else { stack.push(node.left, node.right); } } } return result; } /** * Inserts an aabb into the tree. * * @param aabb The {@link AABB} to insert */ add(aabb) { if (!this.root) { this.root = new AABBNode(aabb); return; } const node = new AABBNode(aabb); this.addNode(node, this.root); } /** * Removes the leaf node containing the {@link AABB} bounding the given {@link CollisionObject}. * * @param obj The {@link CollisionObject} to remove */ remove(obj) { var _a; if (!this.root) return; const stack = [this.root]; while (stack.length > 0) { const node = stack.pop(); // remove node // TODO make this comparison a passed in function and use function overloading to allow removing of CollisionObject, AABB, Node if (((_a = node.aabb) === null || _a === void 0 ? void 0 : _a.obj) === obj) { if (!node.isLeaf()) return; this.removeNode(node); } if (!node.isLeaf()) stack.push(node.left, node.right); } } /** * Inserts a node into the tree. * * @param node The {@link AABBNode} to insert */ addNode(node, parent) { // parent is leaf so simply split if (parent.isLeaf()) { const pParent = parent.parent; const newParent = new AABBNode(parent, node, this.margin); if (pParent) pParent.replaceChild(parent, newParent); else if (parent === this.root) this.root = newParent; // console.log(this.root); return; } // console.log("not leaf"); // parent is branch, compute area diffs between pre-insert and post-insert const left = parent.left.aabb; const right = parent.right.aabb; const leftNext = new AABB(left, node.aabb, this.margin); const rightNext = new AABB(right, node.aabb, this.margin); const areaDiffLeft = leftNext.area() - left.area(); const areaDiffRight = rightNext.area() - right.area(); // insert into child that gives less area increase if (areaDiffLeft < areaDiffRight) this.addNode(node, parent.left); else this.addNode(node, parent.right); // update parent AABB parent.updateAABB(this.margin); } /** * Remove the given node and replace the parent with the node's sibling * * @param node The node to remove */ removeNode(node) { const parent = node.parent; if (node === this.root) { this.root = undefined; return; } const sibling = node.sibling(); if (!parent.parent) { this.root = sibling; sibling.parent = undefined; } else { parent.parent.replaceChild(parent, sibling); } node.parent = undefined; } getHeight(node) { if (!this.root) return 0; if (!node) { return this.getHeight(this.root); } if (node.isLeaf()) return 0; const leftHeight = this.getHeight(node.left); const rightHeight = this.getHeight(node.right); return Math.max(leftHeight, rightHeight) + 1; } } //# sourceMappingURL=aabbTree.js.map