179 lines
5.0 KiB
JavaScript
179 lines
5.0 KiB
JavaScript
import * as I from 'immutable';
|
|
|
|
import {arrayUnique} from '@avocado/core';
|
|
import {Rectangle, QuadTree, Vector} from '@avocado/math';
|
|
|
|
import {Body} from './body';
|
|
import {AbstractWorld} from '../abstract/world';
|
|
|
|
export class World extends AbstractWorld {
|
|
|
|
constructor() {
|
|
super();
|
|
this.bodies = [];
|
|
this.quadTree = new QuadTree();
|
|
this.quadTreeNodes = new Map();
|
|
}
|
|
|
|
addBody(body) {
|
|
this.bodies.push(body);
|
|
// Add to quad tree.
|
|
this.addQuadTreeNodes(body);
|
|
body.on('positionChanged', () => {
|
|
this.removeQuadTreeNodes(body);
|
|
this.addQuadTreeNodes(body);
|
|
});
|
|
}
|
|
|
|
addQuadTreeNodes(body) {
|
|
// 4 points.
|
|
const aabb = body.aabb;
|
|
const points = Rectangle.toPoints(aabb);
|
|
const nodes = points.map((point) => [...point, body]);
|
|
this.quadTreeNodes.set(body, nodes);
|
|
for (const node of nodes) {
|
|
this.quadTree.add(node);
|
|
}
|
|
}
|
|
|
|
createBody(shape) {
|
|
return new Body(shape);
|
|
}
|
|
|
|
removeBody(body) {
|
|
super.removeBody(body);
|
|
const index = this.bodies.indexOf(body);
|
|
if (-1 === index) {
|
|
return;
|
|
}
|
|
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.values()) {
|
|
const body = entity.body;
|
|
const translation = Vector.add(
|
|
body.impulse,
|
|
body.force,
|
|
);
|
|
body.position = Vector.add(body.position, translation);
|
|
}
|
|
// Contact checks.
|
|
const allContacts = new Map();
|
|
const checkedSoFar = new Map();
|
|
for (const entity of this.entities.values()) {
|
|
const body = entity.body;
|
|
// Find bodies in AABB.
|
|
const aabb = body.aabb;
|
|
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
|
|
const checkSet = new Set();
|
|
checkedSoFar.set(entity, checkSet);
|
|
for (const otherBody of otherBodies) {
|
|
const otherEntity = this.entities.get(otherBody);
|
|
if (otherEntity) {
|
|
// Only one check per pair.
|
|
const otherCheckSet = checkedSoFar.get(otherEntity);
|
|
if (otherCheckSet) {
|
|
if (otherCheckSet.has(entity)) {
|
|
continue;
|
|
}
|
|
}
|
|
// Self contacts.
|
|
let contacts = allContacts.get(entity);
|
|
if (!contacts) {
|
|
contacts = I.Set();
|
|
}
|
|
contacts = contacts.add(otherEntity);
|
|
allContacts.set(entity, contacts);
|
|
// Other contacts.
|
|
let otherContacts = allContacts.get(otherEntity);
|
|
if (!otherContacts) {
|
|
otherContacts = I.Set();
|
|
}
|
|
otherContacts = otherContacts.add(entity);
|
|
allContacts.set(otherEntity, otherContacts);
|
|
// Mark as checked.
|
|
checkSet.add(otherEntity);
|
|
}
|
|
}
|
|
if (!allContacts.get(entity)) {
|
|
allContacts.set(entity, I.Set());
|
|
}
|
|
}
|
|
// Report collisions.
|
|
for (const entity of this.entities.values()) {
|
|
const oldContacts = entity.body.contacts;
|
|
const newContacts = allContacts.get(entity);
|
|
for (const contact of newContacts.values()) {
|
|
if (!oldContacts.has(contact)) {
|
|
entity.emit('collisionStart', contact);
|
|
}
|
|
}
|
|
for (const contact of oldContacts.values()) {
|
|
if (!newContacts.has(contact)) {
|
|
entity.emit('collisionEnd', contact);
|
|
}
|
|
}
|
|
entity.body.contacts = newContacts;
|
|
}
|
|
// Super rudimentary resolving.
|
|
for (const entity of this.entities.values()) {
|
|
const body = entity.body;
|
|
// Contact? Undo impulse.
|
|
const contacts = body.contacts;
|
|
if (contacts.size > 0) {
|
|
const translation = Vector.add(
|
|
body.impulse,
|
|
body.force,
|
|
);
|
|
body.position = Vector.sub(body.position, translation);
|
|
// Check all contacts and see if impulse vector can be fixed.
|
|
for (const axe of [0, 1]) {
|
|
const anyContact = false;
|
|
const potentialTranslation = [0, 0];
|
|
potentialTranslation[axe] = translation[axe];
|
|
body.position = Vector.add(body.position, potentialTranslation);
|
|
for (const contact of contacts) {
|
|
const contactBody = contact.body;
|
|
if (Rectangle.intersects(body.aabb, contactBody.aabb)) {
|
|
anyContact = true;
|
|
break;
|
|
}
|
|
}
|
|
if (anyContact) {
|
|
body.position = Vector.sub(body.position, potentialTranslation);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Drop transforms.
|
|
for (const {body} of this.entities.values()) {
|
|
body.force = [0, 0];
|
|
body.impulse = [0, 0];
|
|
}
|
|
super.tick(elapsed);
|
|
}
|
|
|
|
}
|