Speed up and simplify TileClass implementation
Use a more memory-efficient layout, reducing memory usage and speeding things up considerably. Patch by: smiley Reviewed By: wraitii Differential Revision: https://code.wildfiregames.com/D4021 This was SVN commit r25622.
This commit is contained in:
parent
01e940217e
commit
6cc6d8c156
@ -1,159 +1,99 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// RangeOp
|
||||
//
|
||||
// Class for efficiently finding number of points within a range
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
function RangeOp(size)
|
||||
{
|
||||
// Get smallest power of 2 which is greater than or equal to size
|
||||
this.nn = 1;
|
||||
while (this.nn < size) {
|
||||
this.nn *= 2;
|
||||
}
|
||||
|
||||
this.vals = new Int16Array(2*this.nn); // int16
|
||||
}
|
||||
|
||||
RangeOp.prototype.set = function(pos, amt)
|
||||
{
|
||||
this.add(pos, amt - this.vals[this.nn + pos]);
|
||||
};
|
||||
|
||||
RangeOp.prototype.add = function(pos, amt)
|
||||
{
|
||||
for(var s = this.nn; s >= 1; s /= 2)
|
||||
{
|
||||
this.vals[s + pos] += amt;
|
||||
pos = Math.floor(pos/2);
|
||||
}
|
||||
};
|
||||
|
||||
RangeOp.prototype.get = function(start, end)
|
||||
{
|
||||
var ret = 0;
|
||||
var i = 1;
|
||||
var nn = this.nn;
|
||||
|
||||
// Count from start to end by powers of 2
|
||||
for (; start+i <= end; i *= 2)
|
||||
{
|
||||
if (start & i)
|
||||
{ // For each bit in start
|
||||
ret += this.vals[nn/i + Math.floor(start/i)];
|
||||
start += i;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
while(i >= 1)
|
||||
{
|
||||
if(start+i <= end)
|
||||
{
|
||||
ret += this.vals[nn/i + Math.floor(start/i)];
|
||||
start += i;
|
||||
}
|
||||
i /= 2;
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
* Class that can be tagged to any tile. Can be used to constrain placers and entity placement to given areas.
|
||||
*/
|
||||
function TileClass(size)
|
||||
{
|
||||
this.size = size;
|
||||
this.inclusionCount = [];
|
||||
this.rangeCount = [];
|
||||
class TileClass {
|
||||
|
||||
for (let i=0; i < size; ++i)
|
||||
constructor(size)
|
||||
{
|
||||
this.inclusionCount[i] = new Int16Array(size); //int16
|
||||
this.rangeCount[i] = new RangeOp(size);
|
||||
this.size = size;
|
||||
this.width = Math.ceil(size / 16); // Need one entry per 16 tiles as each tile takes a single bit
|
||||
this.inclusionGrid = new Uint16Array(this.size * this.width);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given position is part of the tileclass.
|
||||
*/
|
||||
has(position)
|
||||
{
|
||||
if (position.x < 0 || position.x >= this.size || position.y < 0 || position.y >= this.size)
|
||||
return 0;
|
||||
// x >> 4 == Math.floor(x / 16); used to find the integer this x is included in
|
||||
// x & 0xF == x % 16; used to find the bit position of the given x
|
||||
return this.inclusionGrid[position.y * this.width + (position.x >> 4)] & (1 << (position.x & 0xF));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given position to the tileclass.
|
||||
*/
|
||||
add(position)
|
||||
{
|
||||
if (position.x < 0 || position.x >= this.size || position.y < 0 || position.y >= this.size)
|
||||
return;
|
||||
this.inclusionGrid[position.y * this.width + (position.x >> 4)] |= 1 << (position.x & 0xF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given position to the tileclass.
|
||||
*/
|
||||
remove(position)
|
||||
{
|
||||
if (position.x < 0 || position.x >= this.size || position.y < 0 || position.y >= this.size)
|
||||
return;
|
||||
this.inclusionGrid[position.y * this.width + (position.x >> 4)] &= ~(1 << (position.x & 0xF));
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of tiles in the tileclass within the given radius of the given position.
|
||||
* Can return either the total number of members or nonmembers.
|
||||
*/
|
||||
countInRadius(position, radius, returnMembers)
|
||||
{
|
||||
let members = 0;
|
||||
let total = 0;
|
||||
const radius2 = radius * radius;
|
||||
const [x, y] = [position.x, position.y];
|
||||
|
||||
const yMin = Math.max(Math.ceil(y - radius), 0);
|
||||
const yMax = Math.min(Math.floor(y + radius), this.size - 1);
|
||||
for (let iy = yMin; iy <= yMax; ++iy)
|
||||
{
|
||||
const dy = iy - y;
|
||||
const dy2 = dy * dy;
|
||||
const delta = Math.sqrt(radius2 - dy2);
|
||||
const xMin = Math.max(Math.ceil(x - delta), 0);
|
||||
const xMax = Math.min(Math.floor(x + delta), this.size - 1);
|
||||
|
||||
const indexXMin = xMin >> 4;
|
||||
const indexXMax = xMax >> 4;
|
||||
const indexY = iy * this.width;
|
||||
for (let indexX = indexXMin; indexX <= indexXMax; ++indexX)
|
||||
{
|
||||
const imin = indexX == indexXMin ? xMin & 0xF : 0;
|
||||
const imax = indexX == indexXMax ? xMax & 0xF : 15;
|
||||
total += imax - imin + 1;
|
||||
if (this.inclusionGrid[indexY + indexX])
|
||||
for (let i = imin; i <= imax; ++i)
|
||||
if (this.inclusionGrid[indexY + indexX] & (1 << i))
|
||||
++members;
|
||||
}
|
||||
}
|
||||
|
||||
return returnMembers ? members : total - members;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of tiles marked in the tileclass within the given radius of the given position.
|
||||
*/
|
||||
countMembersInRadius(position, radius)
|
||||
{
|
||||
return this.countInRadius(position, radius, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of tiles not marked in the tileclass within the given radius of the given position.
|
||||
*/
|
||||
countNonMembersInRadius(position, radius)
|
||||
{
|
||||
return this.countInRadius(position, radius, false);
|
||||
}
|
||||
}
|
||||
|
||||
TileClass.prototype.has = function(position)
|
||||
{
|
||||
return !!this.inclusionCount[position.x] && !!this.inclusionCount[position.x][position.y];
|
||||
};
|
||||
|
||||
TileClass.prototype.add = function(position)
|
||||
{
|
||||
if (!this.inclusionCount[position.x][position.y] && g_Map.validTile(position))
|
||||
this.rangeCount[position.y].add(position.x, 1);
|
||||
|
||||
++this.inclusionCount[position.x][position.y];
|
||||
};
|
||||
|
||||
TileClass.prototype.remove = function(position)
|
||||
{
|
||||
--this.inclusionCount[position.x][position.y];
|
||||
|
||||
if (!this.inclusionCount[position.x][position.y])
|
||||
this.rangeCount[position.y].add(position.x, -1);
|
||||
};
|
||||
|
||||
TileClass.prototype.countInRadius = function(position, radius, returnMembers)
|
||||
{
|
||||
let members = 0;
|
||||
let nonMembers = 0;
|
||||
let radius2 = Math.square(radius);
|
||||
|
||||
for (let y = position.y - radius; y <= position.y + radius; ++y)
|
||||
{
|
||||
let iy = Math.floor(y);
|
||||
if (radius >= 27) // Switchover point before RangeOp actually performs better than a straight algorithm
|
||||
{
|
||||
if (iy >= 0 && iy < this.size)
|
||||
{
|
||||
let dx = Math.sqrt(Math.square(radius) - Math.square(y - position.y));
|
||||
|
||||
let minX = Math.max(Math.floor(position.x - dx), 0);
|
||||
let maxX = Math.min(Math.floor(position.x + dx), this.size - 1) + 1;
|
||||
|
||||
let newMembers = this.rangeCount[iy].get(minX, maxX);
|
||||
|
||||
members += newMembers;
|
||||
nonMembers += maxX - minX - newMembers;
|
||||
}
|
||||
}
|
||||
else // Simply check the tiles one by one to find the number
|
||||
{
|
||||
let dy = iy - position.y;
|
||||
|
||||
let xMin = Math.max(Math.floor(position.x - radius), 0);
|
||||
let xMax = Math.min(Math.ceil(position.x + radius), this.size - 1);
|
||||
|
||||
for (let ix = xMin; ix <= xMax; ++ix)
|
||||
{
|
||||
let dx = ix - position.x;
|
||||
if (Math.square(dx) + Math.square(dy) <= radius2)
|
||||
{
|
||||
if (this.inclusionCount[ix] && this.inclusionCount[ix][iy] && this.inclusionCount[ix][iy] > 0)
|
||||
++members;
|
||||
else
|
||||
++nonMembers;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (returnMembers)
|
||||
return members;
|
||||
else
|
||||
return nonMembers;
|
||||
};
|
||||
|
||||
TileClass.prototype.countMembersInRadius = function(position, radius)
|
||||
{
|
||||
return this.countInRadius(position, radius, true);
|
||||
};
|
||||
|
||||
TileClass.prototype.countNonMembersInRadius = function(position, radius)
|
||||
{
|
||||
return this.countInRadius(position, radius, false);
|
||||
};
|
||||
|
@ -5,18 +5,18 @@ var g_Map = new RandomMap(0, "blackness");
|
||||
|
||||
// Test that that it checks by value, not by reference
|
||||
{
|
||||
let tileClass = new TileClass(2);
|
||||
let reference1 = new Vector2D(1, 1);
|
||||
let reference2 = new Vector2D(1, 1);
|
||||
const tileClass = new TileClass(2);
|
||||
const reference1 = new Vector2D(1, 1);
|
||||
const reference2 = new Vector2D(1, 1);
|
||||
tileClass.add(reference1);
|
||||
TS_ASSERT(tileClass.has(reference2));
|
||||
}
|
||||
|
||||
// Test out-of-bounds
|
||||
{
|
||||
let tileClass = new TileClass(32);
|
||||
const tileClass = new TileClass(32);
|
||||
|
||||
let absentPoints = [
|
||||
const absentPoints = [
|
||||
new Vector2D(0, 0),
|
||||
new Vector2D(0, 1),
|
||||
new Vector2D(1, 0),
|
||||
@ -26,73 +26,30 @@ var g_Map = new RandomMap(0, "blackness");
|
||||
new Vector2D(0, Infinity)
|
||||
];
|
||||
|
||||
for (let point of absentPoints)
|
||||
for (const point of absentPoints)
|
||||
TS_ASSERT(!tileClass.has(point));
|
||||
}
|
||||
|
||||
// Test multi-insertion
|
||||
{
|
||||
let tileClass = new TileClass(32);
|
||||
let point = new Vector2D(1, 1);
|
||||
|
||||
tileClass.add(point);
|
||||
tileClass.add(point);
|
||||
TS_ASSERT_EQUALS(tileClass.countMembersInRadius(point, 0), 1);
|
||||
|
||||
// Still one point remaining
|
||||
tileClass.remove(point);
|
||||
TS_ASSERT(tileClass.has(point));
|
||||
|
||||
tileClass.remove(point);
|
||||
TS_ASSERT(!tileClass.has(point));
|
||||
}
|
||||
|
||||
// Test multi-insertion removal
|
||||
{
|
||||
let tileClass = new TileClass(32);
|
||||
let point = new Vector2D(2, 7);
|
||||
|
||||
tileClass.add(point);
|
||||
tileClass.add(point);
|
||||
tileClass.remove(point);
|
||||
TS_ASSERT(tileClass.has(point));
|
||||
|
||||
tileClass.remove(point);
|
||||
TS_ASSERT(!tileClass.has(point));
|
||||
}
|
||||
|
||||
// Test multi-insertion removal
|
||||
{
|
||||
let tileClass = new TileClass(55);
|
||||
let point = new Vector2D(5, 4);
|
||||
|
||||
for (let i = 0; i < 50; ++i)
|
||||
tileClass.add(point);
|
||||
|
||||
TS_ASSERT_EQUALS(tileClass.countNonMembersInRadius(point, 1), 4);
|
||||
tileClass.remove(point);
|
||||
TS_ASSERT(tileClass.has(point));
|
||||
TS_ASSERT_EQUALS(tileClass.countNonMembersInRadius(point, 1), 4);
|
||||
tileClass.remove(point);
|
||||
TS_ASSERT(tileClass.has(point));
|
||||
}
|
||||
|
||||
// Test getters
|
||||
{
|
||||
let tileClass = new TileClass(88);
|
||||
const tileClass = new TileClass(88);
|
||||
|
||||
let point = new Vector2D(5, 1);
|
||||
const point = new Vector2D(5, 5);
|
||||
tileClass.add(point);
|
||||
|
||||
let point2 = new Vector2D(4, 9);
|
||||
tileClass.add(point2);
|
||||
const pointBorder = new Vector2D(1, 9);
|
||||
tileClass.add(pointBorder);
|
||||
|
||||
TS_ASSERT_EQUALS(tileClass.countMembersInRadius(point, 0), 1);
|
||||
TS_ASSERT_EQUALS(tileClass.countMembersInRadius(point, 1), 1);
|
||||
TS_ASSERT_EQUALS(tileClass.countMembersInRadius(point, 100), 2);
|
||||
|
||||
TS_ASSERT_EQUALS(tileClass.countNonMembersInRadius(point, 1), 4);
|
||||
TS_ASSERT_EQUALS(tileClass.countNonMembersInRadius(point, 1), 4);
|
||||
TS_ASSERT_EQUALS(tileClass.countNonMembersInRadius(point, 2), 12);
|
||||
TS_ASSERT_EQUALS(tileClass.countNonMembersInRadius(point, 3), 28);
|
||||
|
||||
// Points not on the map are not counted.
|
||||
TS_ASSERT_EQUALS(tileClass.countNonMembersInRadius(pointBorder, 1), 4);
|
||||
TS_ASSERT_EQUALS(tileClass.countNonMembersInRadius(pointBorder, 2), 11);
|
||||
TS_ASSERT_EQUALS(tileClass.countNonMembersInRadius(pointBorder, 3), 22);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user