feat: physics++

This commit is contained in:
cha0s 2019-03-23 18:26:35 -05:00
parent 4c8956457f
commit f123d12624
5 changed files with 278 additions and 6 deletions

View File

@ -16,6 +16,7 @@ class EntityListBase {
constructor() {
this.entities_PRIVATE = {};
this.quadTree_PRIVATE = new QuadTree();
this.world_PRIVATE = undefined;
this.state_PRIVATE = I.Map();
this.uuidMap_PRIVATE = {};
}
@ -78,6 +79,14 @@ class EntityListBase {
return this.uuidMap_PRIVATE[uuid];
}
get world() {
return this.world_PRIVATE;
}
set world(world) {
this.world_PRIVATE = world;
}
quadTree() {
return this.quadTree_PRIVATE;
}
@ -110,6 +119,9 @@ class EntityListBase {
entity.tick(elapsed);
}
}
if (this.world_PRIVATE) {
this.world_PRIVATE.tick(elapsed);
}
this.recomputeState();
}

View File

@ -15,6 +15,7 @@ export class Listed extends Trait {
destroy() {
this.removeQuadTreeNodes();
delete this._list;
this.entity.emit('removedFromList');
}
get list() {
@ -24,6 +25,7 @@ export class Listed extends Trait {
set list(list) {
this._list = list;
this.addQuadTreeNodes();
this.entity.emit('addedToList');
}
addQuadTreeNodes() {

View File

@ -24,6 +24,11 @@ class MobileBase extends Trait {
methods() {
return {
applyMovement: (movement) => {
this.entity.x += movement[0];
this.entity.y += movement[1];
},
requestMovement: (vector) => {
if (!this.isMobile) {
return;
@ -44,13 +49,17 @@ class MobileBase extends Trait {
if (Vector.isZero(this.requestedMovement)) {
return;
}
const requestedMovement = Vector.scale(
this.requestedMovement,
elapsed
);
if (this.entity.hasTrait('physical')) {
this.entity.applyImpulse(this.requestedMovement);
}
else {
const requestedMovement = Vector.scale(
this.requestedMovement,
elapsed
);
this.entity.applyMovement(requestedMovement);
}
this.requestedMovement = [0, 0];
this.entity.x += requestedMovement[0];
this.entity.y += requestedMovement[1];
},
}
}

View File

@ -1,4 +1,5 @@
import {compose} from '@avocado/core';
import {ShapeView} from '@avocado/graphics';
import {Vector} from '@avocado/math';
import {shapeFromJSON} from '@avocado/physics';
@ -16,11 +17,74 @@ export class Physical extends decorate(Trait) {
}
initialize() {
this._body = undefined;
this._shape = shapeFromJSON(this.params.get('shape'));
this._shapeView = undefined;
this._world = undefined;
}
destroy() {
if (this._world) {
this._world.removeBody(this._body);
}
}
get body() {
return this._body;
}
get shape() {
return this._shape;
}
set world(world) {
this._world = world;
if (world) {
this._body = world.createBodyForEntity(this.entity);
world.addBody(this._body);
}
}
listeners() {
return {
addedToList: () => {
this.entity.world = this.entity.list.world;
},
positionChanged: () => {
if (this._body) {
this._body.position = this.entity.position;
}
},
traitAdded: (type) => {
if (!this._shapeView && this.entity.container) {
this._shapeView = new ShapeView(this.entity.shape);
this._shapeView.zIndex = 100;
this.entity.container.addChild(this._shapeView);
}
}
};
}
methods() {
return {
applyForce: (force) => {
if (this._world) {
this._body.applyForce(force);
}
},
applyImpulse: (impulse) => {
if (this._world) {
this._body.applyImpulse(impulse);
}
},
}
}
}

185
packages/physics/dummy.js Normal file
View File

@ -0,0 +1,185 @@
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 nodes = [
upperLeft,
upperRight,
lowerLeft,
lowerRight,
].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;
}
}
}