feat: physics++
This commit is contained in:
parent
4c8956457f
commit
f123d12624
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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];
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
185
packages/physics/dummy.js
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user