618 lines
17 KiB
JavaScript
618 lines
17 KiB
JavaScript
// Hierarchical Spatial Hash Grid: HSHG
|
|
//Note that this file has been customized so that HSHG is put into the gdjs object.
|
|
//Thus, it must be included after gd.js
|
|
|
|
/**
|
|
* @namespace
|
|
* @memberof gdjs
|
|
*/
|
|
gdjs.HSHG = gdjs.HSHG || {};
|
|
|
|
(function(root, undefined){
|
|
|
|
//---------------------------------------------------------------------
|
|
// 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
|
|
*/
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
function update_REMOVEALL(){
|
|
// not implemented yet :)
|
|
}
|
|
|
|
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.max[0] < b.min[0] || a.max[1] < b.min[1]){
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function getLongestAABBEdge(min, max){
|
|
return Math.max(
|
|
Math.abs(max[0] - min[0])
|
|
,Math.abs(max[1] - min[1])
|
|
);
|
|
}
|
|
|
|
//---------------------------------------------------------------------
|
|
// ENTITIES
|
|
//---------------------------------------------------------------------
|
|
|
|
/**
|
|
* A hierarchical spatial grid containing objects and allowing fast test collisions between them.
|
|
*
|
|
* @class HSHG
|
|
* @memberof gdjs.HSHG
|
|
* @constructor
|
|
*/
|
|
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 = [];
|
|
}
|
|
|
|
/**
|
|
* Add an object to the grid. The object can be anything as long as it provides a getAABB method.
|
|
* An 'HSHG' property is added to the object, and is then deleted when the object is removed from the HSHG.
|
|
*/
|
|
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
|
|
}; //TODO: recycle existing object if necessary.
|
|
|
|
// 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 = x / this.HIERARCHY_FACTOR;
|
|
if(objSize < x) {
|
|
// find appropriate size
|
|
while( objSize < x ) {
|
|
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 = 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);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Remove an object from the HSHG. The object must be in the HSHG before being removed.
|
|
*/
|
|
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;
|
|
};
|
|
|
|
|
|
/**
|
|
* Must be called when objects have been moved ( typically at each "tick" of the game/simulation ).
|
|
*/
|
|
HSHG.prototype.update = function(){
|
|
this.UPDATE_METHOD.call(this);
|
|
};
|
|
|
|
/**
|
|
* Return a list of objects colliding with theObject.
|
|
* @param {gdjs.RuntimeObject} theObject The object to be tested against.
|
|
*/
|
|
HSHG.prototype.queryForCollisionWith = function(theObject, result){
|
|
|
|
var i, j, k, l, c
|
|
,grid
|
|
,cell
|
|
,objA
|
|
,objB
|
|
,offset
|
|
,adjacentCell
|
|
,biggerGrid
|
|
,objAAABB
|
|
,objAHashInBiggerGrid;
|
|
|
|
result.length = 0;
|
|
|
|
theObject.HSHG.excludeMe = true;
|
|
var theObjectAABB = theObject.getAABB();
|
|
var theObjectHashInItsGrid = theObject.HSHG.grid.toHash(theObjectAABB.min[0], theObjectAABB.min[1]);
|
|
var theObjectCellInItsGrid = theObject.HSHG.grid.allCells[theObjectHashInItsGrid];
|
|
|
|
// default broad test to internal aabb overlap test
|
|
broadOverlapTest = testAABBOverlap;
|
|
|
|
// for all grids ordered by cell size ASC
|
|
for(i = 0; i < this._grids.length; i++){
|
|
grid = this._grids[i];
|
|
|
|
if ( grid.cellSize === theObject.HSHG.grid.cellSize ) { //We're in the grid of theObject:
|
|
|
|
// 1)Test against neighbors:
|
|
|
|
// 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(l = 0; l < cell.objectContainer.length; l++){
|
|
objB = cell.objectContainer[l]; //Note that objB could be theObject.
|
|
if(!objB.HSHG.excludeMe && broadOverlapTest(theObject, objB) === true){
|
|
result.push( 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];
|
|
adjacentCell = grid.allCells[ cell.allCellsIndex + offset ];
|
|
|
|
// Collide all objects in cell with adjacent cell
|
|
for(l = 0; l < adjacentCell.objectContainer.length; l++){
|
|
objB = adjacentCell.objectContainer[l]; //Note that objB could be theObject.
|
|
if(!objB.HSHG.excludeMe && broadOverlapTest(theObject, objB) === true){
|
|
result.push( objB );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2)Test against objects of bigger grids:
|
|
|
|
// For all grids with cellsize larger than the grid of theObject:
|
|
for(k = i + 1; k < this._grids.length; k++){
|
|
biggerGrid = this._grids[k];
|
|
var objectHashInBiggerGrid = biggerGrid.toHash(theObjectAABB.min[0], theObjectAABB.min[1]);
|
|
cell = biggerGrid.allCells[objectHashInBiggerGrid];
|
|
|
|
// Check theObject 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];
|
|
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 theObject: Note that objB is necessarily different from theObject.
|
|
if(broadOverlapTest(theObject, objB) === true){
|
|
result.push( objB );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
break; //All collisions with object have now been registered
|
|
}
|
|
else if ( grid.cellSize < theObject.HSHG.grid.cellSize ) { //We're in a grid with smaller objects
|
|
|
|
// For all objects that are stored in this smaller grid
|
|
for(j = 0; j < grid.allObjects.length; j++){
|
|
|
|
//Get the object of the smaller grid.
|
|
objA = grid.allObjects[j];
|
|
objAAABB = objA.getAABB();
|
|
|
|
//Get its position in the (bigger) grid containing theObject.
|
|
objAHashInBiggerGrid = theObject.HSHG.grid.toHash(objAAABB.min[0], objAAABB.min[1]);
|
|
|
|
//Check if it is near theObject (i.e: Check if the cell of objA is a neighbor of the cell
|
|
//of theObject ).
|
|
var objAIsInAdjacentCellToObject = false;
|
|
for(c = 0; c < theObjectCellInItsGrid.neighborOffsetArray.length; c++){
|
|
offset = theObjectCellInItsGrid.neighborOffsetArray[c];
|
|
if ( objAHashInBiggerGrid === theObjectCellInItsGrid.allCellsIndex + offset ) {
|
|
objAIsInAdjacentCellToObject = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
//If objA is near theObject, trigger a collision test.
|
|
if ( objAIsInAdjacentCellToObject ) {
|
|
//Note that objA is necessarily different from theObject
|
|
if(broadOverlapTest(theObject, objA) === true){
|
|
result.push( objA );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
delete theObject.HSHG.excludeMe;
|
|
};
|
|
|
|
HSHG.update_RECOMPUTE = update_RECOMPUTE;
|
|
HSHG.update_REMOVEALL = update_REMOVEALL;
|
|
|
|
/**
|
|
* Grid
|
|
*
|
|
* @class Grid
|
|
* @memberof gdjs.HSHG
|
|
* @constructor
|
|
* @param cellSize {int} the pixel size of each cell of the grid
|
|
* @param cellCount {int} the total number of cells for the grid (width x height)
|
|
* @param parentHierarchy {HSHG} 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 = []
|
|
,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;
|
|
}
|
|
|
|
return xHash + yHash * 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 a grid
|
|
*
|
|
* @class Cell
|
|
* @memberof gdjs.HSHG
|
|
* @constructor
|
|
* @private
|
|
*/
|
|
function Cell(){
|
|
this.objectContainer = [];
|
|
this.neighborOffsetArray;
|
|
this.occupiedCellsIndex = null;
|
|
this.allCellsIndex = null;
|
|
}
|
|
|
|
//---------------------------------------------------------------------
|
|
// EXPORTS
|
|
//---------------------------------------------------------------------
|
|
|
|
root['HSHG'] = HSHG;
|
|
HSHG._private = {
|
|
Grid: Grid,
|
|
Cell: Cell,
|
|
testAABBOverlap: testAABBOverlap,
|
|
getLongestAABBEdge: getLongestAABBEdge
|
|
};
|
|
|
|
})(gdjs.HSHG);
|