import * as I from 'immutable'; import {arrayUnique, compose} from '@avocado/core'; import {Rectangle, QuadTree, Vector} from '@avocado/math'; import {EventEmitter} from '@avocado/mixins'; const decorate = compose( EventEmitter, Vector.Mixin('position', 'x', 'y', { default: [0, 0], }), ); class BodyBase { constructor(entity) { this.force = [0, 0]; this.impulse = [0, 0]; this.contacts = I.Set(); this.entity = entity; this.shape = entity.shape; } applyForce(vector) { this.force = Vector.add(this.force, vector); } applyImpulse(vector) { this.impulse = Vector.add(this.impulse, vector); } } export class Body extends decorate(BodyBase) { constructor(entity) { super(entity); this.position = entity.position; } } export class World { constructor() { this.bodies = []; this.entities = []; this.quadTree = new QuadTree(); this.quadTreeNodes = new WeakMap(); } addBody(body) { this.bodies.push(body); if (body.entity) { this.entities.push(body.entity); } // Add to quad tree. this.addQuadTreeNodes(body); body.on('positionChanged', () => { this.removeQuadTreeNodes(body); this.addQuadTreeNodes(body); }); } addQuadTreeNodes(body) { // 4 points. const aabb = Rectangle.translated(body.shape.aabb, body.position); const width = aabb[2] - .0001; const height = aabb[3] - .0001; const upperLeft = Rectangle.position(aabb); const upperRight = Vector.add(upperLeft, [width, 0]); const lowerLeft = Vector.add(upperLeft, [0, height]); const lowerRight = Vector.add(upperLeft, [width, height]); const points = [ upperLeft, upperRight, lowerLeft, lowerRight, ]; const nodes = points.map((point) => [...point, body]); this.quadTreeNodes.set(body, nodes); for (const node of nodes) { this.quadTree.add(node); } } createBodyForEntity(entity) { return new Body(entity); } removeBody(body) { const index = this.bodies.indexOf(body); if (-1 === index) { return; } if (body.entity) { const entities = this.entities; const entityIndex = entities.indexOf(body.entity); if (-1 !== entityIndex) { entities.splice(entityIndex, 1); } } this.removeQuadTreeNodes(body); this.bodies.splice(index, 1); } removeQuadTreeNodes(body) { const nodes = this.quadTreeNodes.get(body); if (!nodes) { return; } for (const node of nodes) { this.quadTree.remove(node); } } tick(elapsed) { // Apply. for (const entity of this.entities) { const body = entity.body; const impulse = Vector.scale(body.impulse, elapsed); body.position = Vector.add(body.position, impulse); body.position = Vector.add(body.position, body.force); } // Contact checks. const allContacts = new Map(); for (const entity of this.entities) { const body = entity.body; let thisContacts = I.Set(); // Find bodies in AABB. const aabb = Rectangle.translated(body.shape.aabb, body.position); let otherBodies = this.quadTree.search(aabb).map((node) => { return node.data[2]; }); // Not self. otherBodies = otherBodies.filter((otherBody) => { return otherBody !== body; }); // Uniques only. otherBodies = arrayUnique(otherBodies); // TODO: full collision check for (const otherBody of otherBodies) { if (otherBody.entity) { thisContacts = thisContacts.add(otherBody.entity); } } allContacts.set(entity, thisContacts); } // Report collisions. for (const entity of this.entities) { const oldContacts = entity.body.contacts; const newContacts = allContacts.get(entity); for (const contact of newContacts.values()) { if (!oldContacts.has(contact)) { entity.emit('contactStart', contact); } } for (const contact of oldContacts.values()) { if (!newContacts.has(contact)) { entity.emit('contactEnd', contact); } } entity.body.contacts = newContacts; } // Super rudimentary resolving. for (const entity of this.entities) { const body = entity.body; // Contact? Undo impulse. if (entity.body.contacts.size > 0) { const impulse = Vector.scale(body.impulse, elapsed); body.position = Vector.sub(body.position, impulse); body.position = Vector.sub(body.position, body.force); } } // Drop transforms. for (const {body} of this.entities) { body.force = [0, 0]; body.impulse = [0, 0]; } // Propagate position updates. for (const entity of this.entities) { entity.position = entity.body.position; } } }