lance-gg
Version:
A Node.js based real-time multiplayer game server
603 lines (499 loc) • 18.7 kB
text/typescript
// Hierarchical Spatial Hash Grid: HSHG
// source: https://gist.github.com/kirbysayshi/1760774
// ---------------------------------------------------------------------
// GLOBAL FUNCTIONS
// ---------------------------------------------------------------------
/**
* Updates every object's position in the grid, but only if
* the hash value for that object has changed.
* This method DOES NOT take into account object expansion or
* contraction, just position, and does not attempt to change
* the grid the object is currently in; it only (possibly) changes
* the cell.
*
* If the object has significantly changed in size, the best bet is to
* call removeObject() and addObject() sequentially, outside of the
* normal update cycle of HSHG.
*
* @return void desc
*/
function update_RECOMPUTE() {
var i,
obj,
grid,
meta,
objAABB,
newObjHash;
// for each object
for (i = 0; i < this._globalObjects.length; i++) {
obj = this._globalObjects[i];
meta = obj.HSHG;
grid = meta.grid;
// recompute hash
objAABB = obj.getAABB();
newObjHash = grid.toHash(objAABB.min[0], objAABB.min[1]);
if (newObjHash !== meta.hash) {
// grid position has changed, update!
grid.removeObject(obj);
grid.addObject(obj, newObjHash);
}
}
}
// not implemented yet :)
function update_REMOVEALL() {
}
function testAABBOverlap(objA, objB) {
var a = objA.getAABB(),
b = objB.getAABB();
// if(a.min[0] > b.max[0] || a.min[1] > b.max[1] || a.min[2] > b.max[2]
// || a.max[0] < b.min[0] || a.max[1] < b.min[1] || a.max[2] < b.min[2]){
if (a.min[0] > b.max[0] || a.min[1] > b.max[1] ||
a.max[0] < b.min[0] || a.max[1] < b.min[1]) {
return false;
}
return true;
}
function getLongestAABBEdge(min, max) {
return Math.max(
Math.abs(max[0] - min[0])
, Math.abs(max[1] - min[1])
// ,Math.abs(max[2] - min[2])
);
}
// ---------------------------------------------------------------------
// ENTITIES
// ---------------------------------------------------------------------
function HSHG() {
this.MAX_OBJECT_CELL_DENSITY = 1 / 8; // objects / cells
this.INITIAL_GRID_LENGTH = 256; // 16x16
this.HIERARCHY_FACTOR = 2;
this.HIERARCHY_FACTOR_SQRT = Math.SQRT2;
this.UPDATE_METHOD = update_RECOMPUTE; // or update_REMOVEALL
this._grids = [];
this._globalObjects = [];
}
// HSHG.prototype.init = function(){
// this._grids = [];
// this._globalObjects = [];
// }
HSHG.prototype.addObject = function (obj) {
var x, i,
cellSize,
objAABB = obj.getAABB(),
objSize = getLongestAABBEdge(objAABB.min, objAABB.max),
oneGrid, newGrid;
// for HSHG metadata
obj.HSHG = {
globalObjectsIndex: this._globalObjects.length
};
// add to global object array
this._globalObjects.push(obj);
if (this._grids.length == 0) {
// no grids exist yet
cellSize = objSize * this.HIERARCHY_FACTOR_SQRT;
newGrid = new Grid(cellSize, this.INITIAL_GRID_LENGTH, this);
newGrid.initCells();
newGrid.addObject(obj);
this._grids.push(newGrid);
} else {
x = 0;
// grids are sorted by cellSize, smallest to largest
for (i = 0; i < this._grids.length; i++) {
oneGrid = this._grids[i];
x = oneGrid.cellSize;
if (objSize < x) {
x /= this.HIERARCHY_FACTOR;
if (objSize < x) {
// find appropriate size
while (objSize < x) {
x /= this.HIERARCHY_FACTOR;
}
newGrid = new Grid(x * this.HIERARCHY_FACTOR, this.INITIAL_GRID_LENGTH, this);
newGrid.initCells();
// assign obj to grid
newGrid.addObject(obj);
// insert grid into list of grids directly before oneGrid
this._grids.splice(i, 0, newGrid);
} else {
// insert obj into grid oneGrid
oneGrid.addObject(obj);
}
return;
}
}
while (objSize >= x) {
x *= this.HIERARCHY_FACTOR;
}
newGrid = new Grid(x, this.INITIAL_GRID_LENGTH, this);
newGrid.initCells();
// insert obj into grid
newGrid.addObject(obj);
// add newGrid as last element in grid list
this._grids.push(newGrid);
}
};
HSHG.prototype.removeObject = function (obj) {
var meta = obj.HSHG,
globalObjectsIndex,
replacementObj;
if (meta === undefined) {
throw Error(obj + ' was not in the HSHG.');
return;
}
// remove object from global object list
globalObjectsIndex = meta.globalObjectsIndex;
if (globalObjectsIndex === this._globalObjects.length - 1) {
this._globalObjects.pop();
} else {
replacementObj = this._globalObjects.pop();
replacementObj.HSHG.globalObjectsIndex = globalObjectsIndex;
this._globalObjects[globalObjectsIndex] = replacementObj;
}
meta.grid.removeObject(obj);
// remove meta data
delete obj.HSHG;
};
HSHG.prototype.update = function () {
this.UPDATE_METHOD.call(this);
};
HSHG.prototype.queryForCollisionPairs = function (broadOverlapTestCallback) {
var i, j, k, l, c,
grid,
cell,
objA,
objB,
offset,
adjacentCell,
biggerGrid,
objAAABB,
objAHashInBiggerGrid,
possibleCollisions: any = [];
// default broad test to internal aabb overlap test
let broadOverlapTest = broadOverlapTestCallback || testAABBOverlap;
// for all grids ordered by cell size ASC
for (i = 0; i < this._grids.length; i++) {
grid = this._grids[i];
// for each cell of the grid that is occupied
for (j = 0; j < grid.occupiedCells.length; j++) {
cell = grid.occupiedCells[j];
// collide all objects within the occupied cell
for (k = 0; k < cell.objectContainer.length; k++) {
objA = cell.objectContainer[k];
for (l = k + 1; l < cell.objectContainer.length; l++) {
objB = cell.objectContainer[l];
if (broadOverlapTest(objA, objB) === true) {
possibleCollisions.push([objA, objB]);
}
}
}
// for the first half of all adjacent cells (offset 4 is the current cell)
for (c = 0; c < 4; c++) {
offset = cell.neighborOffsetArray[c];
// if(offset === null) { continue; }
adjacentCell = grid.allCells[cell.allCellsIndex + offset];
// collide all objects in cell with adjacent cell
for (k = 0; k < cell.objectContainer.length; k++) {
objA = cell.objectContainer[k];
for (l = 0; l < adjacentCell.objectContainer.length; l++) {
objB = adjacentCell.objectContainer[l];
if (broadOverlapTest(objA, objB) === true) {
possibleCollisions.push([objA, objB]);
}
}
}
}
}
// forall objects that are stored in this grid
for (j = 0; j < grid.allObjects.length; j++) {
objA = grid.allObjects[j];
objAAABB = objA.getAABB();
// for all grids with cellsize larger than grid
for (k = i + 1; k < this._grids.length; k++) {
biggerGrid = this._grids[k];
objAHashInBiggerGrid = biggerGrid.toHash(objAAABB.min[0], objAAABB.min[1]);
cell = biggerGrid.allCells[objAHashInBiggerGrid];
// check objA against every object in all cells in offset array of cell
// for all adjacent cells...
for (c = 0; c < cell.neighborOffsetArray.length; c++) {
offset = cell.neighborOffsetArray[c];
// if(offset === null) { continue; }
adjacentCell = biggerGrid.allCells[cell.allCellsIndex + offset];
// for all objects in the adjacent cell...
for (l = 0; l < adjacentCell.objectContainer.length; l++) {
objB = adjacentCell.objectContainer[l];
// test against object A
if (broadOverlapTest(objA, objB) === true) {
possibleCollisions.push([objA, objB]);
}
}
}
}
}
}
// return list of object pairs
return possibleCollisions;
};
HSHG.update_RECOMPUTE = update_RECOMPUTE;
HSHG.update_REMOVEALL = update_REMOVEALL;
/**
* Grid
*
* @constructor
* @param int cellSize the pixel size of each cell of the grid
* @param int cellCount the total number of cells for the grid (width x height)
* @param HSHG parentHierarchy the HSHG to which this grid belongs
* @return void
*/
function Grid(cellSize, cellCount, parentHierarchy) {
this.cellSize = cellSize;
this.inverseCellSize = 1 / cellSize;
this.rowColumnCount = ~~Math.sqrt(cellCount);
this.xyHashMask = this.rowColumnCount - 1;
this.occupiedCells = [];
this.allCells = Array(this.rowColumnCount * this.rowColumnCount);
this.allObjects = [];
this.sharedInnerOffsets = [];
this._parentHierarchy = parentHierarchy || null;
}
Grid.prototype.initCells = function () {
// TODO: inner/unique offset rows 0 and 2 may need to be
// swapped due to +y being "down" vs "up"
var i,
gridLength = this.allCells.length,
x, y,
wh = this.rowColumnCount,
isOnRightEdge, isOnLeftEdge, isOnTopEdge, isOnBottomEdge,
innerOffsets = [
// y+ down offsets
// -1 + -wh, -wh, -wh + 1,
// -1, 0, 1,
// wh - 1, wh, wh + 1
// y+ up offsets
wh - 1, wh, wh + 1,
-1, 0, 1,
-1 + -wh, -wh, -wh + 1
],
leftOffset, rightOffset, topOffset, bottomOffset,
uniqueOffsets: any[] = [],
cell;
this.sharedInnerOffsets = innerOffsets;
// init all cells, creating offset arrays as needed
for (i = 0; i < gridLength; i++) {
cell = new Cell();
// compute row (y) and column (x) for an index
y = ~~(i / this.rowColumnCount);
x = ~~(i - (y * this.rowColumnCount));
// reset / init
isOnRightEdge = false;
isOnLeftEdge = false;
isOnTopEdge = false;
isOnBottomEdge = false;
// right or left edge cell
if ((x + 1) % this.rowColumnCount == 0) {
isOnRightEdge = true;
} else if (x % this.rowColumnCount == 0) {
isOnLeftEdge = true;
}
// top or bottom edge cell
if ((y + 1) % this.rowColumnCount == 0) {
isOnTopEdge = true;
} else if (y % this.rowColumnCount == 0) {
isOnBottomEdge = true;
}
// if cell is edge cell, use unique offsets, otherwise use inner offsets
if (isOnRightEdge || isOnLeftEdge || isOnTopEdge || isOnBottomEdge) {
// figure out cardinal offsets first
rightOffset = isOnRightEdge === true ? -wh + 1 : 1;
leftOffset = isOnLeftEdge === true ? wh - 1 : -1;
topOffset = isOnTopEdge === true ? -gridLength + wh : wh;
bottomOffset = isOnBottomEdge === true ? gridLength - wh : -wh;
// diagonals are composites of the cardinals
uniqueOffsets = [
// y+ down offset
// leftOffset + bottomOffset, bottomOffset, rightOffset + bottomOffset,
// leftOffset, 0, rightOffset,
// leftOffset + topOffset, topOffset, rightOffset + topOffset
// y+ up offset
leftOffset + topOffset, topOffset, rightOffset + topOffset,
leftOffset, 0, rightOffset,
leftOffset + bottomOffset, bottomOffset, rightOffset + bottomOffset
];
cell.neighborOffsetArray = uniqueOffsets;
} else {
cell.neighborOffsetArray = this.sharedInnerOffsets;
}
cell.allCellsIndex = i;
this.allCells[i] = cell;
}
};
Grid.prototype.toHash = function (x, y, z) {
var i, xHash, yHash, zHash;
if (x < 0) {
i = (-x) * this.inverseCellSize;
xHash = this.rowColumnCount - 1 - (~~i & this.xyHashMask);
} else {
i = x * this.inverseCellSize;
xHash = ~~i & this.xyHashMask;
}
if (y < 0) {
i = (-y) * this.inverseCellSize;
yHash = this.rowColumnCount - 1 - (~~i & this.xyHashMask);
} else {
i = y * this.inverseCellSize;
yHash = ~~i & this.xyHashMask;
}
// if(z < 0){
// i = (-z) * this.inverseCellSize;
// zHash = this.rowColumnCount - 1 - ( ~~i & this.xyHashMask );
// } else {
// i = z * this.inverseCellSize;
// zHash = ~~i & this.xyHashMask;
// }
return xHash + yHash * this.rowColumnCount;
// + zHash * this.rowColumnCount * this.rowColumnCount;
};
Grid.prototype.addObject = function (obj, hash) {
var objAABB,
objHash,
targetCell;
// technically, passing this in this should save some computational effort when updating objects
if (hash !== undefined) {
objHash = hash;
} else {
objAABB = obj.getAABB();
objHash = this.toHash(objAABB.min[0], objAABB.min[1]);
}
targetCell = this.allCells[objHash];
if (targetCell.objectContainer.length === 0) {
// insert this cell into occupied cells list
targetCell.occupiedCellsIndex = this.occupiedCells.length;
this.occupiedCells.push(targetCell);
}
// add meta data to obj, for fast update/removal
obj.HSHG.objectContainerIndex = targetCell.objectContainer.length;
obj.HSHG.hash = objHash;
obj.HSHG.grid = this;
obj.HSHG.allGridObjectsIndex = this.allObjects.length;
// add obj to cell
targetCell.objectContainer.push(obj);
// we can assume that the targetCell is already a member of the occupied list
// add to grid-global object list
this.allObjects.push(obj);
// do test for grid density
if (this.allObjects.length / this.allCells.length > this._parentHierarchy.MAX_OBJECT_CELL_DENSITY) {
// grid must be increased in size
this.expandGrid();
}
};
Grid.prototype.removeObject = function (obj) {
var meta = obj.HSHG,
hash,
containerIndex,
allGridObjectsIndex,
cell,
replacementCell,
replacementObj;
hash = meta.hash;
containerIndex = meta.objectContainerIndex;
allGridObjectsIndex = meta.allGridObjectsIndex;
cell = this.allCells[hash];
// remove object from cell object container
if (cell.objectContainer.length === 1) {
// this is the last object in the cell, so reset it
cell.objectContainer.length = 0;
// remove cell from occupied list
if (cell.occupiedCellsIndex === this.occupiedCells.length - 1) {
// special case if the cell is the newest in the list
this.occupiedCells.pop();
} else {
replacementCell = this.occupiedCells.pop();
replacementCell.occupiedCellsIndex = cell.occupiedCellsIndex;
this.occupiedCells[cell.occupiedCellsIndex] = replacementCell;
}
cell.occupiedCellsIndex = null;
} else {
// there is more than one object in the container
if (containerIndex === cell.objectContainer.length - 1) {
// special case if the obj is the newest in the container
cell.objectContainer.pop();
} else {
replacementObj = cell.objectContainer.pop();
replacementObj.HSHG.objectContainerIndex = containerIndex;
cell.objectContainer[containerIndex] = replacementObj;
}
}
// remove object from grid object list
if (allGridObjectsIndex === this.allObjects.length - 1) {
this.allObjects.pop();
} else {
replacementObj = this.allObjects.pop();
replacementObj.HSHG.allGridObjectsIndex = allGridObjectsIndex;
this.allObjects[allGridObjectsIndex] = replacementObj;
}
};
Grid.prototype.expandGrid = function () {
var i, j,
currentCellCount = this.allCells.length,
currentRowColumnCount = this.rowColumnCount,
currentXYHashMask = this.xyHashMask,
newCellCount = currentCellCount * 4, // double each dimension
newRowColumnCount = ~~Math.sqrt(newCellCount),
newXYHashMask = newRowColumnCount - 1,
allObjects = this.allObjects.slice(0), // duplicate array, not objects contained
aCell,
push = Array.prototype.push;
// remove all objects
for (i = 0; i < allObjects.length; i++) {
this.removeObject(allObjects[i]);
}
// reset grid values, set new grid to be 4x larger than last
this.rowColumnCount = newRowColumnCount;
this.allCells = Array(this.rowColumnCount * this.rowColumnCount);
this.xyHashMask = newXYHashMask;
// initialize new cells
this.initCells();
// re-add all objects to grid
for (i = 0; i < allObjects.length; i++) {
this.addObject(allObjects[i]);
}
};
/**
* A cell of the grid
*
* @constructor
* @return void desc
*/
function Cell() {
this.objectContainer = [];
this.neighborOffsetArray;
this.occupiedCellsIndex = null;
this.allCellsIndex = null;
}
// ---------------------------------------------------------------------
// EXPORTS
// ---------------------------------------------------------------------
HSHG._private = {
Grid: Grid,
Cell: Cell,
testAABBOverlap: testAABBOverlap,
getLongestAABBEdge: getLongestAABBEdge
};
class HSHGDetector {
private hshg: any;
constructor() {
this.hshg = new HSHG();
}
addObject(o: any) {
this.hshg.addObject(o);
}
removeObject(o: any) {
this.hshg.removeObject(o);
}
update() {
this.hshg.update();
}
queryForCollisionPairs(): any[] {
return this.hshg.queryForCollisionPairs();
}
}
export { HSHGDetector }