refactor: aabbs/spatial hash

This commit is contained in:
cha0s 2024-07-02 14:41:54 -05:00
parent 183c8254a2
commit 463d9b5858
8 changed files with 179 additions and 197 deletions

View File

@ -1,35 +0,0 @@
import {System} from '@/ecs/index.js';
export default class CalculateAabbs extends System {
static get priority() {
return {
after: 'IntegratePhysics',
};
}
tick() {
for (const entity of this.ecs.changed(['Position'])) {
const {Position: {x, y}, Sprite, VisibleAabb} = entity;
if (VisibleAabb) {
let size = undefined;
if (Sprite) {
const frame = Sprite.animation
? Sprite.$$sourceJson.animations[Sprite.animation][Sprite.frame]
: '';
size = Sprite.$$sourceJson.frames[frame].sourceSize;
}
/* v8 ignore next 3 */
if (!size) {
throw new Error(`no size for aabb for entity ${entity.id}(${JSON.stringify(entity.toJSON(), null, 2)})`);
}
VisibleAabb.x0 = x - ((size.w ) * (Sprite.anchor.x));
VisibleAabb.x1 = x + ((size.w ) * (1 - Sprite.anchor.x));
VisibleAabb.y0 = y - ((size.h ) * (Sprite.anchor.y));
VisibleAabb.y1 = y + ((size.h ) * (1 - Sprite.anchor.y));
}
}
}
}

View File

@ -1,6 +1,6 @@
import {System} from '@/ecs/index.js';
export default class PlantGrowth extends System {
export default class Interactions extends System {
static queries() {
return {
@ -11,31 +11,27 @@ export default class PlantGrowth extends System {
tick() {
for (const {Direction, Interacts, Position} of this.select('default')) {
Interacts.willInteractWith = 0
const box = {
x: Position.x - 8,
y: Position.y - 8,
w: 16,
h: 16,
}
let x0 = Position.x - 8;
let y0 = Position.y - 8;
if (0 === Direction.direction) {
box.y -= 16
y0 -= 16
}
if (1 === Direction.direction) {
box.x += 16
x0 += 16
}
if (2 === Direction.direction) {
box.y += 16
y0 += 16
}
if (3 === Direction.direction) {
box.x -= 16
x0 -= 16
}
// todo sort
const entities = Array.from(this.ecs.system('UpdateSpatialHash').within(
box.x,
box.y,
box.w,
box.h,
));
const entities = Array.from(this.ecs.system('VisibleAabbs').within({
x0,
x1: x0 + 15,
y0,
y1: y0 + 15,
}));
for (let j = 0; j < entities.length; ++j) {
if (entities[j].Interactive && entities[j].Interactive.interacting) {
Interacts.willInteractWith = entities[0].id;

View File

@ -1,136 +0,0 @@
import {RESOLUTION} from '@/constants.js'
import {System} from '@/ecs/index.js';
class SpatialHash {
constructor({x, y}) {
this.area = {x, y};
this.chunkSize = {x: RESOLUTION.x / 2, y: RESOLUTION.y / 2};
this.chunks = Array(Math.ceil(this.area.x / this.chunkSize.x))
.fill(0)
.map(() => (
Array(Math.ceil(this.area.y / this.chunkSize.y))
.fill(0)
.map(() => [])
));
this.data = {};
}
clamp(x, y) {
return [
Math.max(0, Math.min(x, this.area.x - 1)),
Math.max(0, Math.min(y, this.area.y - 1))
];
}
chunkIndex(x, y) {
const [cx, cy] = this.clamp(x, y);
return [
Math.floor(cx / this.chunkSize.x),
Math.floor(cy / this.chunkSize.y),
];
}
remove(datum) {
if (datum in this.data) {
for (const [cx, cy] of this.data[datum]) {
const chunk = this.chunks[cx][cy];
chunk.splice(chunk.indexOf(datum), 1);
}
}
this.data[datum] = [];
}
update({x0, x1, y0, y1}, datum) {
this.remove(datum);
for (const [x, y] of [[x0, y0], [x0, y1], [x1, y0], [x1, y1]]) {
const [cx, cy] = this.chunkIndex(x, y);
this.data[datum].push([cx, cy]);
this.chunks[cx][cy].push(datum);
}
}
}
export default class UpdateSpatialHash extends System {
static get priority() {
return {
after: 'CalculateAabbs',
};
}
deindex(entities) {
super.deindex(entities);
for (const id of entities) {
this.hash.remove(id);
}
}
reindex(entities) {
for (const id of entities) {
if (1 === id) {
this.hash = new SpatialHash(this.ecs.get(1).AreaSize);
}
}
super.reindex(entities);
for (const id of entities) {
this.updateHash(this.ecs.get(id));
}
}
updateHash(entity) {
if (!entity.VisibleAabb) {
return;
}
this.hash.update(entity.VisibleAabb, entity.id);
}
tick() {
for (const entity of this.ecs.changed(['VisibleAabb'])) {
this.updateHash(entity);
}
}
nearby(entity) {
const [cx0, cy0] = this.hash.chunkIndex(
entity.Position.x - RESOLUTION.x * 0.75,
entity.Position.y - RESOLUTION.x * 0.75,
);
const [cx1, cy1] = this.hash.chunkIndex(
entity.Position.x + RESOLUTION.x * 0.75,
entity.Position.y + RESOLUTION.x * 0.75,
);
const nearby = new Set();
for (let cy = cy0; cy <= cy1; ++cy) {
for (let cx = cx0; cx <= cx1; ++cx) {
this.hash.chunks[cx][cy].forEach((id) => {
nearby.add(this.ecs.get(id));
});
}
}
return nearby;
}
within(x, y, w, h) {
const [cx0, cy0] = this.hash.chunkIndex(x, y);
const [cx1, cy1] = this.hash.chunkIndex(x + w - 1, y + h - 1);
const within = new Set();
for (let cy = cy0; cy <= cy1; ++cy) {
for (let cx = cx0; cx <= cx1; ++cx) {
this.hash.chunks[cx][cy].forEach((id) => {
const entity = this.ecs.get(id);
const {Position} = entity;
if (
Position.x >= x && Position.x < x + w
&& Position.y >= y && Position.y < y + h
) {
within.add(entity);
}
});
}
}
return within;
}
}

View File

@ -0,0 +1,89 @@
import {System} from '@/ecs/index.js';
import {intersects} from '@/util/math.js';
import SpatialHash from '@/util/spatial-hash.js';
export default class VisibleAabbs extends System {
hash;
deindex(entities) {
super.deindex(entities);
for (const id of entities) {
this.hash.remove(id);
}
}
static get priority() {
return {
after: 'IntegratePhysics',
};
}
reindex(entities) {
for (const id of entities) {
if (1 === id) {
this.hash = new SpatialHash(this.ecs.get(1).AreaSize);
}
}
super.reindex(entities);
for (const id of entities) {
this.updateHash(this.ecs.get(id));
}
}
updateHash(entity) {
if (!entity.VisibleAabb) {
return;
}
this.hash.update(entity.VisibleAabb, entity.id);
}
tick() {
for (const entity of this.ecs.changed(['Position'])) {
const {Position: {x, y}, Sprite, VisibleAabb} = entity;
if (VisibleAabb) {
let size = undefined;
if (Sprite) {
const frame = Sprite.animation
? Sprite.$$sourceJson.animations[Sprite.animation][Sprite.frame]
: '';
size = Sprite.$$sourceJson.frames[frame].sourceSize;
}
/* v8 ignore next 3 */
if (!size) {
throw new Error(`no size for aabb for entity ${entity.id}(${JSON.stringify(entity.toJSON(), null, 2)})`);
}
VisibleAabb.x0 = x - Sprite.anchor.x * size.w;
VisibleAabb.x1 = x + (1 - Sprite.anchor.x) * size.w;
VisibleAabb.y0 = y - Sprite.anchor.y * size.h;
VisibleAabb.y1 = y + (1 - Sprite.anchor.y) * size.h;
this.updateHash(entity);
}
}
}
within(query) {
const {x0, x1, y0, y1} = query;
const [cx0, cy0] = this.hash.chunkIndex(x0, y0);
const [cx1, cy1] = this.hash.chunkIndex(x1, y1);
const seen = {};
const within = new Set();
for (let cy = cy0; cy <= cy1; ++cy) {
for (let cx = cx0; cx <= cx1; ++cx) {
for (const id of this.hash.chunks[cx][cy]) {
if (seen[id]) {
continue;
}
seen[id] = true;
const entity = this.ecs.get(id);
if (intersects(query, entity.VisibleAabb)) {
within.add(entity);
}
}
}
}
return within;
}
}

View File

@ -1,4 +1,5 @@
import {
RESOLUTION,
TPS,
} from '@/constants.js';
import Ecs from '@/ecs/ecs.js';
@ -165,8 +166,7 @@ export default class Engine {
'ClampPositions',
'PlantGrowth',
'FollowCamera',
'CalculateAabbs',
'UpdateSpatialHash',
'VisibleAabbs',
'ControlDirection',
'SpriteDirection',
'RunAnimations',
@ -344,7 +344,15 @@ export default class Engine {
const {entity, memory} = this.connectedPlayers.get(connection);
const mainEntityId = entity.id;
const ecs = this.ecses[entity.Ecs.path];
const nearby = ecs.system('UpdateSpatialHash').nearby(entity);
// Entities within half a screen offscreen.
const x0 = entity.Position.x - RESOLUTION.x;
const y0 = entity.Position.y - RESOLUTION.y;
const nearby = ecs.system('VisibleAabbs').within({
x0,
x1: x0 + (RESOLUTION.x * 2),
y0,
y1: y0 + (RESOLUTION.y * 2),
});
// Master entity.
nearby.add(ecs.get(1));
const lastMemory = new Set(memory.values());

View File

@ -1,3 +1,11 @@
export function intersects(l, r) {
if (l.x0 > r.x1) return false;
if (l.y0 > r.y1) return false;
if (l.x1 < r.x0) return false;
if (l.y1 < r.y0) return false;
return true;
}
export function normalizeVector({x, y}) {
if (0 === y && 0 === x) {
return {x: 0, y: 0};

50
app/util/spatial-hash.js Normal file
View File

@ -0,0 +1,50 @@
export default class SpatialHash {
constructor({x, y}) {
this.area = {x, y};
this.chunkSize = {x: 64, y: 64};
this.chunks = Array(Math.ceil(this.area.x / this.chunkSize.x))
.fill(0)
.map(() => (
Array(Math.ceil(this.area.y / this.chunkSize.y))
.fill(0)
.map(() => [])
));
this.data = {};
}
clamp(x, y) {
return [
Math.max(0, Math.min(x, this.area.x - 1)),
Math.max(0, Math.min(y, this.area.y - 1))
];
}
chunkIndex(x, y) {
const [cx, cy] = this.clamp(x, y);
return [
Math.floor(cx / this.chunkSize.x),
Math.floor(cy / this.chunkSize.y),
];
}
remove(datum) {
if (datum in this.data) {
for (const [cx, cy] of this.data[datum]) {
const chunk = this.chunks[cx][cy];
chunk.splice(chunk.indexOf(datum), 1);
}
}
this.data[datum] = [];
}
update({x0, x1, y0, y1}, datum) {
this.remove(datum);
for (const [x, y] of [[x0, y0], [x0, y1], [x1, y0], [x1, y1]]) {
const [cx, cy] = this.chunkIndex(x, y);
this.data[datum].push([cx, cy]);
this.chunks[cx][cy].push(datum);
}
}
}

View File

@ -1,12 +1,14 @@
const filtered = []
for (let i = 0; i < projected.length; ++i) {
const entities = Array.from(ecs.system('UpdateSpatialHash').within(
projected[i].x * layer.tileSize.x,
projected[i].y * layer.tileSize.y,
layer.tileSize.x,
layer.tileSize.y,
));
const x0 = projected[i].x * layer.tileSize.x;
const y0 = projected[i].y * layer.tileSize.y;
const entities = Array.from(ecs.system('VisibleAabbs').within({
x0,
x1: x0 + layer.tileSize.x - 1,
y0,
y1: y0 + layer.tileSize.y - 1,
}));
let hasPlant = false;
for (let j = 0; j < entities.length; ++j) {
if (entities[j].Plant) {