refactor: aabbs/spatial hash
This commit is contained in:
parent
183c8254a2
commit
463d9b5858
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
89
app/ecs-systems/visible-aabbs.js
Normal file
89
app/ecs-systems/visible-aabbs.js
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
|
|
|
@ -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
50
app/util/spatial-hash.js
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user