covid19-demo/games.gdevelop-app.com/vincenzo/game-adbfc30b-3d8b-43b1-87a.../libs/hshg.js

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);