UNPKG

@rpgjs/physic

Version:

A deterministic 2D top-down physics library for RPG, sandbox and MMO games

358 lines (357 loc) 10.2 kB
import { createCollider } from "./index18.js"; class SpatialHashCell { constructor() { this.entities = []; } /** * Adds an entity to this cell * * @param entity - Entity to add */ add(entity) { if (this.entities.indexOf(entity) === -1) { this.entities.push(entity); } } /** * Removes an entity from this cell * * @param entity - Entity to remove */ remove(entity) { const index = this.entities.indexOf(entity); if (index !== -1) { const last = this.entities[this.entities.length - 1]; if (last) { this.entities[index] = last; } this.entities.pop(); } } /** * Clears all entities from this cell */ clear() { this.entities.length = 0; } } class SpatialHash { /** * Creates a new spatial hash * * @param cellSize - Size of each cell in world units * @param gridWidth - Number of cells horizontally * @param gridHeight - Number of cells vertically (default: same as width) */ constructor(cellSize, gridWidth, gridHeight) { this.cellSize = cellSize; this.gridWidth = gridWidth; this.gridHeight = gridHeight ?? gridWidth; this.cells = /* @__PURE__ */ new Map(); this.entityCells = /* @__PURE__ */ new WeakMap(); } /** * Converts world coordinates to grid coordinates * * @param x - World X coordinate * @param y - World Y coordinate * @returns Grid coordinates */ worldToGrid(x, y) { return { x: Math.floor(x / this.cellSize), y: Math.floor(y / this.cellSize) }; } /** * Creates a cell key from grid coordinates * * @param gridX - Grid X coordinate * @param gridY - Grid Y coordinate * @returns Numeric cell key */ getKey(gridX, gridY) { return (gridX & 65535) << 16 | gridY & 65535; } /** * Gets or creates a cell at grid coordinates * * @param key - Cell key * @returns Cell instance */ getCell(key) { let cell = this.cells.get(key); if (!cell) { cell = new SpatialHashCell(); this.cells.set(key, cell); } return cell; } /** * Gets all cell keys that an entity's AABB overlaps * * @param entity - Entity to get cells for * @param outKeys - Array to store keys in (to avoid allocation) * @returns Number of keys added */ getEntityKeys(entity, outKeys) { const collider = createCollider(entity); if (!collider) { return 0; } const bounds = collider.getBounds(); const minGrid = this.worldToGrid(bounds.minX, bounds.minY); const maxGrid = this.worldToGrid(bounds.maxX, bounds.maxY); let count = 0; outKeys.length = 0; for (let x = minGrid.x; x <= maxGrid.x; x++) { for (let y = minGrid.y; y <= maxGrid.y; y++) { const wrappedX = (x % this.gridWidth + this.gridWidth) % this.gridWidth; const wrappedY = (y % this.gridHeight + this.gridHeight) % this.gridHeight; outKeys.push(this.getKey(wrappedX, wrappedY)); count++; } } return count; } /** * Inserts an entity into the spatial hash * * @param entity - Entity to insert */ insert(entity) { const newKeys = []; this.getEntityKeys(entity, newKeys); this.entityCells.set(entity, newKeys); for (const key of newKeys) { const cell = this.getCell(key); cell.add(entity); } } /** * Removes an entity from the spatial hash * * @param entity - Entity to remove */ remove(entity) { const keys = this.entityCells.get(entity); if (!keys) { return; } for (const key of keys) { const cell = this.cells.get(key); if (cell) { cell.remove(entity); } } this.entityCells.delete(entity); } /** * Updates an entity's position in the spatial hash * * Removes and re-inserts the entity if it moved to different cells. * * @param entity - Entity to update */ update(entity) { const oldKeys = this.entityCells.get(entity); const newKeys = []; this.getEntityKeys(entity, newKeys); if (oldKeys && oldKeys.length === newKeys.length) { let match = true; for (let i = 0; i < oldKeys.length; i++) { if (oldKeys[i] !== newKeys[i]) { match = false; break; } } if (match) return; } if (oldKeys) { for (const key of oldKeys) { const cell = this.cells.get(key); if (cell) { cell.remove(entity); } } } this.entityCells.set(entity, newKeys); for (const key of newKeys) { const cell = this.getCell(key); cell.add(entity); } } /** * Queries entities near a given entity * * @param entity - Entity to query around * @param results - Optional Set to store results in (avoids allocation) * @returns Set of nearby entities (excluding the query entity) */ query(entity, results = /* @__PURE__ */ new Set()) { let keys = this.entityCells.get(entity); if (!keys) { keys = []; this.getEntityKeys(entity, keys); } results.clear(); for (const key of keys) { const cell = this.cells.get(key); if (cell) { const entities = cell.entities; for (let i = 0; i < entities.length; i++) { const other = entities[i]; if (other && other !== entity) { results.add(other); } } } } return results; } /** * Queries entities in an AABB region * * @param bounds - AABB to query * @returns Set of entities in the region */ queryAABB(bounds) { const minGrid = this.worldToGrid(bounds.minX, bounds.minY); const maxGrid = this.worldToGrid(bounds.maxX, bounds.maxY); const results = /* @__PURE__ */ new Set(); for (let x = minGrid.x; x <= maxGrid.x; x++) { for (let y = minGrid.y; y <= maxGrid.y; y++) { const wrappedX = (x % this.gridWidth + this.gridWidth) % this.gridWidth; const wrappedY = (y % this.gridHeight + this.gridHeight) % this.gridHeight; const key = this.getKey(wrappedX, wrappedY); const cell = this.cells.get(key); if (cell) { for (const entity of cell.entities) { results.add(entity); } } } } return results; } /** * Clears all entities from the spatial hash */ clear() { this.cells.clear(); } /** * Gets statistics about the spatial hash * * @returns Statistics object */ getStats() { let totalEntities = 0; for (const cell of this.cells.values()) { totalEntities += cell.entities.length; } return { totalCells: this.gridWidth * this.gridHeight, usedCells: this.cells.size, totalEntities, averageEntitiesPerCell: this.cells.size > 0 ? totalEntities / this.cells.size : 0 }; } /** * Casts a ray against entities in the spatial hash * * @param ray - Ray to cast * @param mask - Optional collision mask (layer) * @param filter - Optional filter function (return true to include entity) * @returns Raycast hit info if hit, null otherwise */ raycast(ray, mask, filter) { const start = ray.origin; const end = ray.getPoint(Math.min(ray.length, 1e4)); let x0 = start.x; let y0 = start.y; const x1 = end.x; const y1 = end.y; let gx0 = Math.floor(x0 / this.cellSize); let gy0 = Math.floor(y0 / this.cellSize); const gx1 = Math.floor(x1 / this.cellSize); const gy1 = Math.floor(y1 / this.cellSize); const sx = x0 < x1 ? 1 : -1; const sy = y0 < y1 ? 1 : -1; const visitedEntities = /* @__PURE__ */ new Set(); let closestHit = null; const checkCell = (gx, gy) => { const wrappedX = (gx % this.gridWidth + this.gridWidth) % this.gridWidth; const wrappedY = (gy % this.gridHeight + this.gridHeight) % this.gridHeight; const key = this.getKey(wrappedX, wrappedY); const cell = this.cells.get(key); if (cell) { for (const entity of cell.entities) { if (visitedEntities.has(entity)) continue; visitedEntities.add(entity); if (mask !== void 0 && (entity.collisionCategory & mask) === 0) continue; if (filter && !filter(entity)) { continue; } const collider = createCollider(entity); if (collider) { const hit = collider.raycast(ray); if (hit) { if (!closestHit || hit.distance < closestHit.distance) { closestHit = hit; } } } } } }; let x = gx0; let y = gy0; const stepX = sx; const stepY = sy; const tDeltaX = this.cellSize / Math.abs(ray.direction.x); const tDeltaY = this.cellSize / Math.abs(ray.direction.y); let tMaxX = ray.direction.x > 0 ? ((x + 1) * this.cellSize - start.x) / ray.direction.x : (start.x - x * this.cellSize) / -ray.direction.x; if (ray.direction.x < 0) { tMaxX = (start.x - x * this.cellSize) / -ray.direction.x; } else { tMaxX = ((x + 1) * this.cellSize - start.x) / ray.direction.x; } let tMaxY = ray.direction.y > 0 ? ((y + 1) * this.cellSize - start.y) / ray.direction.y : (start.y - y * this.cellSize) / -ray.direction.y; if (ray.direction.y < 0) { tMaxY = (start.y - y * this.cellSize) / -ray.direction.y; } else { tMaxY = ((y + 1) * this.cellSize - start.y) / ray.direction.y; } if (Math.abs(ray.direction.x) < 1e-9) { tMaxX = Infinity; } if (Math.abs(ray.direction.y) < 1e-9) { tMaxY = Infinity; } let steps = 0; const maxSteps = Math.abs(gx1 - gx0) + Math.abs(gy1 - gy0) + 10; while (steps < maxSteps) { checkCell(x, y); if (closestHit) { if (closestHit.distance < Math.min(tMaxX, tMaxY)) { return closestHit; } } if (tMaxX < tMaxY) { tMaxX += tDeltaX; x += stepX; } else { tMaxY += tDeltaY; y += stepY; } steps++; if (closestHit && closestHit.distance < ray.length) ; } return closestHit; } } export { SpatialHash }; //# sourceMappingURL=index14.js.map