187 lines
4.7 KiB
JavaScript
187 lines
4.7 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
|
|
}
|