import Component from '@/ecs/component.js'; import {distance, intersects, transform} 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() { $$aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity}; $$aabbs = []; $$collisionStart; $$collisionEnd; $$intersections = new Map(); get aabb() { const {Position: {x: px, y: py}} = ecs.get(this.entity); return { x0: this.$$aabb.x0 + px, x1: this.$$aabb.x1 + px, y0: this.$$aabb.y0 + py, y1: this.$$aabb.y1 + py, }; } get aabbs() { const {Position: {x: px, y: py}} = ecs.get(this.entity); const aabbs = []; for (const aabb of this.$$aabbs) { aabbs.push({ x0: aabb.x0 + px, x1: aabb.x1 + px, y0: aabb.y0 + py, y1: aabb.y1 + py, }) } return aabbs; } endIntersections(other, intersections) { const otherEntity = ecs.get(other.entity); const thisEntity = ecs.get(this.entity); for (const intersection of intersections) { const [body, otherBody] = [ intersection.entity.bodies[intersection.i], intersection.other.bodies[intersection.j], ]; if (this.$$collisionEnd) { const script = this.$$collisionEnd.clone(); script.context.other = otherEntity; script.context.pair = [body, otherBody]; const ticker = script.ticker(); ecs.addDestructionDependency(thisEntity.id, ticker); ecs.addDestructionDependency(otherEntity.id, ticker); thisEntity.Ticking.add(ticker); } if (other.$$collisionEnd) { const script = other.$$collisionEnd.clone(); script.context.other = thisEntity; script.context.pair = [otherBody, body]; const ticker = script.ticker(); ecs.addDestructionDependency(thisEntity.id, ticker); ecs.addDestructionDependency(otherEntity.id, ticker); otherEntity.Ticking.add(ticker); } } this.$$intersections.delete(other); other.$$intersections.delete(this); } checkCollision(other) { const otherEntity = ecs.get(other.entity); const thisEntity = ecs.get(this.entity); const intersections = this.intersectionsWith(other); const activeIntersections = this.$$intersections.get(other) || new Set(); if (0 === intersections.length) { // had none; have none if (0 === activeIntersections.size) { return; } this.endIntersections(other, intersections); return; } for (const intersection of intersections) { // new pair - start const [body, otherBody] = [ intersection.entity.bodies[intersection.i], intersection.other.bodies[intersection.j], ]; let hasMatchingIntersection = false; for (const activeIntersection of activeIntersections) { if ( ( activeIntersection.entity === intersection.entity && activeIntersection.other === intersection.other && activeIntersection.i === intersection.i && activeIntersection.j === intersection.j ) || ( activeIntersection.entity === intersection.other && activeIntersection.other === intersection.entity && activeIntersection.i === intersection.j && activeIntersection.j === intersection.i ) ) { hasMatchingIntersection = true; break; } } if (!hasMatchingIntersection) { if (this.$$collisionStart) { const script = this.$$collisionStart.clone(); script.context.entity = thisEntity; script.context.other = otherEntity; script.context.pair = [body, otherBody]; const ticker = script.ticker(); ecs.addDestructionDependency(otherEntity.id, ticker); ecs.addDestructionDependency(thisEntity.id, ticker); thisEntity.Ticking.add(ticker); } if (other.$$collisionStart) { const script = other.$$collisionStart.clone(); script.context.entity = otherEntity; script.context.other = thisEntity; script.context.pair = [otherBody, body]; const ticker = script.ticker(); ecs.addDestructionDependency(otherEntity.id, ticker); ecs.addDestructionDependency(thisEntity.id, ticker); otherEntity.Ticking.add(ticker); } activeIntersections.add(intersection); } // undo restricted movement if (!body.unstoppable && otherBody.impassable) { const j = this.bodies.indexOf(body); const oj = other.bodies.indexOf(otherBody); const aabb = this.$$aabbs[j]; const otherAabb = other.aabbs[oj]; const {Position} = thisEntity; if (!intersects( { x0: aabb.x0 + Position.lastX, x1: aabb.x1 + Position.lastX, y0: aabb.y0 + Position.y, y1: aabb.y1 + Position.y, }, otherAabb, )) { Position.x = Position.lastX } else if (!intersects( { x0: aabb.x0 + Position.x, x1: aabb.x1 + Position.x, y0: aabb.y0 + Position.lastY, y1: aabb.y1 + Position.lastY, }, otherAabb, )) { Position.y = Position.lastY } else { Position.x = Position.lastX Position.y = Position.lastY } break; } } if (activeIntersections.size > 0) { this.$$intersections.set(other, activeIntersections); other.$$intersections.set(this, activeIntersections); } } closest(aabb) { const entity = ecs.get(this.entity); return Array.from(ecs.system('MaintainColliderHash').within(aabb)) .filter((other) => other !== entity) .sort(({Position: l}, {Position: r}) => { return distance(entity.Position, l) > distance(entity.Position, r) ? -1 : 1; }); } destroy() { for (const [other] of this.$$intersections) { other.$$intersections.delete(this); } this.$$intersections.clear(); } intersectionsWith(other) { const {aabb, aabbs} = this; const {aabb: otherAabb, aabbs: otherAabbs} = other; const intersections = []; if (!intersects(aabb, otherAabb)) { return intersections; } for (const i in aabbs) { const aabb = aabbs[i]; const body = this.bodies[i]; for (const j in otherAabbs) { const otherAabb = otherAabbs[j]; const otherBody = other.bodies[j]; if (body.group === otherBody.group && body.group < 0) { continue; } if (body.group !== otherBody.group || 0 === body.group) { if (0 === (otherBody.mask & body.bits) || 0 === (body.mask & otherBody.bits)) { continue; } } // todo accuracy if (intersects(aabb, otherAabb)) { intersections.push({ entity: this, other, i, j, }); } } } return intersections; } 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; } updateAabbs() { this.$$aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity}; this.$$aabbs = []; const {bodies} = this; const {Direction: {direction = 0} = {}} = ecs.get(this.entity) || {}; for (const body of bodies) { let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity; for (const point of transform(body.points, {rotation: direction})) { const {x, y} = point; 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, }); } } } } async load(instance) { for (const i in instance.bodies) { instance.bodies[i] = { ...this.constructor.schema.constructor.defaultValue( this.constructor.schema.specification.concrete.properties.bodies.concrete.subtype, ), ...instance.bodies[i], }; } instance.updateAabbs(); // heavy handed... if ('undefined' !== typeof window) { return; } instance.$$collisionEnd = await this.ecs.readScript( instance.collisionEndScript, { ecs: this.ecs, }, ); instance.$$collisionStart = await this.ecs.readScript( instance.collisionStartScript, { ecs: this.ecs, }, ); } static properties = { bodies: { type: 'array', subtype: { type: 'object', properties: { bits: {defaultValue: 0x00000001, type: 'uint32'}, impassable: {type: 'uint8'}, group: {type: 'int32'}, mask: {defaultValue: 0xFFFFFFFF, type: 'uint32'}, points: { type: 'array', subtype: vector2d('int16'), }, unstoppable: {type: 'uint8'}, }, }, }, collisionEndScript: {type: 'string'}, collisionStartScript: {type: 'string'}, }; }