diff --git a/app/ecs-components/collider.js b/app/ecs-components/collider.js new file mode 100644 index 0000000..f55de3a --- /dev/null +++ b/app/ecs-components/collider.js @@ -0,0 +1,75 @@ +import Component from '@/ecs/component.js'; +import {intersects} from '@/util/math.js'; + +import vector2d from './helpers/vector-2d'; + +export default class Collider extends Component { + instanceFromSchema() { + const {ecs} = this; + return class ColliderInstance extends super.instanceFromSchema() { + isCollidingWith(other) { + const {aabb, aabbs} = this; + const {aabb: otherAabb, aabbs: otherAabbs} = other; + if (!intersects(aabb, otherAabb)) { + return false; + } + for (const aabb of aabbs) { + for (const otherAabb of otherAabbs) { + if (intersects(aabb, otherAabb)) { + return true; + } + } + } + return false; + } + isWithin(query) { + const {aabb, aabbs} = this; + if (!intersects(aabb, query)) { + return false; + } + for (const aabb of aabbs) { + if (intersects(aabb, query)) { + return true; + } + } + return false; + } + recalculateAabbs() { + const {Position: {x: px, y: py}} = ecs.get(this.entity); + this.aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity}; + this.aabbs = []; + const {bodies} = this; + for (const points of bodies) { + let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity; + for (const point of points) { + const x = point.x + px; + const y = point.y + py; + if (x < x0) x0 = x; + if (x < this.aabb.x0) this.aabb.x0 = x; + if (x > x1) x1 = x; + if (x > this.aabb.x1) this.aabb.x1 = x; + if (y < y0) y0 = y; + if (y < this.aabb.y0) this.aabb.y0 = y; + if (y > y1) y1 = y; + if (y > this.aabb.y1) this.aabb.y1 = y; + } + this.aabbs.push({ + x0: x0 > x1 ? x1 : x0, + x1: x0 > x1 ? x0 : x1, + y0: y0 > y1 ? y1 : y0, + y1: y0 > y1 ? y0 : y1, + }); + } + } + } + } + static properties = { + bodies: { + type: 'array', + subtype: { + type: 'array', + subtype: vector2d('int16'), + }, + }, + }; +} diff --git a/app/ecs-systems/colliders.js b/app/ecs-systems/colliders.js new file mode 100644 index 0000000..1cf52e0 --- /dev/null +++ b/app/ecs-systems/colliders.js @@ -0,0 +1,90 @@ +import {System} from '@/ecs/index.js'; +import SpatialHash from '@/util/spatial-hash.js'; + +export default class Colliders 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.Collider) { + return; + } + entity.Collider.recalculateAabbs(); + this.hash.update(entity.Collider.aabb, entity.id); + } + + tick() { + const seen = {}; + for (const entity of this.ecs.changed(['Position'])) { + if (seen[entity.id]) { + continue; + } + seen[entity.id] = true; + if (!entity.Collider) { + continue; + } + this.updateHash(entity); + for (const other of this.within(entity.Collider.aabb)) { + if (seen[other.id]) { + continue; + } + seen[other.id] = true; + if (!other.Collider) { + continue; + } + if (entity.Collider.isCollidingWith(other.Collider)) { + console.log('collide', entity, other); + } + } + } + } + + 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 (entity.Collider.isWithin(query)) { + within.add(entity); + } + } + } + } + return within; + } + +} diff --git a/app/engine.js b/app/engine.js index 63483b4..b372d75 100644 --- a/app/engine.js +++ b/app/engine.js @@ -167,6 +167,7 @@ export default class Engine { 'PlantGrowth', 'FollowCamera', 'VisibleAabbs', + 'Collliders', 'ControlDirection', 'SpriteDirection', 'RunAnimations', diff --git a/app/util/math.js b/app/util/math.js index 51fc013..1d20762 100644 --- a/app/util/math.js +++ b/app/util/math.js @@ -1,3 +1,9 @@ +export function distance({x: lx, y: ly}, {x: rx, y: ry}) { + const xd = lx - rx; + const yd = ly - ry; + return Math.sqrt(xd * xd + yd * yd); +} + export function intersects(l, r) { if (l.x0 > r.x1) return false; if (l.y0 > r.y1) return false; diff --git a/public/assets/tomato-seeds/start.js b/public/assets/tomato-seeds/start.js index 0b5d511..804d2a2 100644 --- a/public/assets/tomato-seeds/start.js +++ b/public/assets/tomato-seeds/start.js @@ -9,6 +9,16 @@ if (projected?.length > 0) { const [, direction] = Sprite.animation.split(':') const plant = { + Collider: { + bodies: [ + [ + {x: -8, y: -8}, + {x: 8, y: -8}, + {x: -8, y: 8}, + {x: 8, y: 8}, + ], + ], + }, Interactive: { interactScript: '/assets/tomato-plant/interact.js', },