Refactor physics
This commit is contained in:
parent
46a48e7c15
commit
c5d97eeea6
40 changed files with 484 additions and 791 deletions
|
|
@ -1,14 +1,11 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { PhysicalGameObject } from '../physical-game-object';
|
||||
|
||||
export abstract class BoundingBoxBase {
|
||||
constructor(
|
||||
public owner: PhysicalGameObject,
|
||||
protected _xMin: number = 0,
|
||||
protected _xMax: number = 0,
|
||||
protected _yMin: number = 0,
|
||||
protected _yMax: number = 0,
|
||||
public readonly isInverted = false
|
||||
protected _yMax: number = 0
|
||||
) {}
|
||||
|
||||
public get 0(): number {
|
||||
|
|
@ -54,12 +54,6 @@ export class BoundingBox extends BoundingBoxBase {
|
|||
}
|
||||
|
||||
public cloneAsImmutable(): ImmutableBoundingBox {
|
||||
return new ImmutableBoundingBox(
|
||||
this.owner,
|
||||
this.xMin,
|
||||
this.xMax,
|
||||
this.yMin,
|
||||
this.yMax
|
||||
);
|
||||
return new ImmutableBoundingBox(this.xMin, this.xMax, this.yMin, this.yMax);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,21 @@
|
|||
import { BoundingBoxBase } from '../bounds/bounding-box-base';
|
||||
import { Physical } from '../physical';
|
||||
import { BoundingBoxBase } from '../bounding-boxes/bounding-box-base';
|
||||
|
||||
export class BoundingBoxList {
|
||||
constructor(private boundingBoxes: Array<BoundingBoxBase> = []) {}
|
||||
constructor(private objects: Array<Physical> = []) {}
|
||||
|
||||
public insert(box: BoundingBoxBase) {
|
||||
this.boundingBoxes.push(box);
|
||||
public insert(object: Physical) {
|
||||
this.objects.push(object);
|
||||
}
|
||||
|
||||
public remove(box: BoundingBoxBase) {
|
||||
this.boundingBoxes.splice(
|
||||
this.boundingBoxes.findIndex((i) => i === box),
|
||||
public remove(object: Physical) {
|
||||
this.objects.splice(
|
||||
this.objects.findIndex((i) => i === object),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
public findIntersecting(box: BoundingBoxBase): Array<BoundingBoxBase> {
|
||||
return this.boundingBoxes.filter((b) => b.intersects(box));
|
||||
public findIntersecting(boundingBox: BoundingBoxBase): Array<Physical> {
|
||||
return this.objects.filter((b) => b.boundingBox.intersects(boundingBox));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,60 +1,60 @@
|
|||
import { BoundingBoxBase } from '../bounding-boxes/bounding-box-base';
|
||||
import { StaticPhysical } from './static-physical-object';
|
||||
// source: https://github.com/ubilabs/kd-tree-javascript/blob/master/kdTree.js
|
||||
|
||||
import { ImmutableBoundingBox } from '../bounds/immutable-bounding-box';
|
||||
|
||||
class Node {
|
||||
public left?: Node = null;
|
||||
public right?: Node = null;
|
||||
constructor(public rectangle: ImmutableBoundingBox, public parent: Node) {}
|
||||
constructor(public object: StaticPhysical, public parent: Node) {}
|
||||
}
|
||||
|
||||
export class BoundingBoxTree {
|
||||
root?: Node;
|
||||
|
||||
constructor(boxes: Array<ImmutableBoundingBox> = []) {
|
||||
this.build(boxes);
|
||||
constructor(objects: Array<StaticPhysical> = []) {
|
||||
this.build(objects);
|
||||
}
|
||||
|
||||
public build(boxes: Array<ImmutableBoundingBox>) {
|
||||
this.root = this.buildRecursive(boxes, 0, null);
|
||||
public build(objects: Array<StaticPhysical>) {
|
||||
this.root = this.buildRecursive(objects, 0, null);
|
||||
}
|
||||
|
||||
private buildRecursive(
|
||||
boxes: Array<ImmutableBoundingBox>,
|
||||
objects: Array<StaticPhysical>,
|
||||
depth: number,
|
||||
parent: Node
|
||||
): Node {
|
||||
if (boxes.length === 0) {
|
||||
if (objects.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (boxes.length === 1) {
|
||||
return new Node(boxes[0], parent);
|
||||
if (objects.length === 1) {
|
||||
return new Node(objects[0], parent);
|
||||
}
|
||||
|
||||
const dimension = depth % 4;
|
||||
|
||||
boxes.sort((a, b) => a[dimension] - b[dimension]);
|
||||
objects.sort((a, b) => a.boundingBox[dimension] - b.boundingBox[dimension]);
|
||||
|
||||
const median = Math.floor(boxes.length / 2);
|
||||
const median = Math.floor(objects.length / 2);
|
||||
|
||||
const node = new Node(boxes[median], parent);
|
||||
node.left = this.buildRecursive(boxes.slice(0, median), depth + 1, node);
|
||||
node.right = this.buildRecursive(boxes.slice(median + 1), depth + 1, node);
|
||||
const node = new Node(objects[median], parent);
|
||||
node.left = this.buildRecursive(objects.slice(0, median), depth + 1, node);
|
||||
node.right = this.buildRecursive(objects.slice(median + 1), depth + 1, node);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
public insert(box: ImmutableBoundingBox) {
|
||||
const [insertPosition, depth] = this.findParent(box, this.root, 0, null);
|
||||
public insert(object: StaticPhysical) {
|
||||
const [insertPosition, depth] = this.findParent(object, this.root, 0, null);
|
||||
|
||||
if (insertPosition === null) {
|
||||
this.root = new Node(box, null);
|
||||
this.root = new Node(object, null);
|
||||
} else {
|
||||
const node = new Node(box, insertPosition);
|
||||
const node = new Node(object, insertPosition);
|
||||
const dimension = depth % 4;
|
||||
|
||||
if (box[dimension] < insertPosition.rectangle[dimension]) {
|
||||
if (object.boundingBox[dimension] < insertPosition.object.boundingBox[dimension]) {
|
||||
insertPosition.left = node;
|
||||
} else {
|
||||
insertPosition.right = node;
|
||||
|
|
@ -62,46 +62,46 @@ export class BoundingBoxTree {
|
|||
}
|
||||
}
|
||||
|
||||
public findIntersecting(box: ImmutableBoundingBox): Array<ImmutableBoundingBox> {
|
||||
const maybeResults = this.findMaybeIntersecting(box, this.root, 0);
|
||||
const results = maybeResults.filter((b) => b.intersects(box));
|
||||
public findIntersecting(boundingBox: BoundingBoxBase): Array<StaticPhysical> {
|
||||
const maybeResults = this.findMaybeIntersecting(boundingBox, this.root, 0);
|
||||
const results = maybeResults.filter((b) => b.boundingBox.intersects(boundingBox));
|
||||
return results;
|
||||
}
|
||||
|
||||
private findMaybeIntersecting(
|
||||
box: ImmutableBoundingBox,
|
||||
boundingBox: BoundingBoxBase,
|
||||
node: Node,
|
||||
depth: number
|
||||
): Array<ImmutableBoundingBox> {
|
||||
): Array<StaticPhysical> {
|
||||
if (node === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (depth % 4 == 0 && box.xMax < node.rectangle.xMin) {
|
||||
return this.findMaybeIntersecting(box, node.left, depth + 1);
|
||||
if (depth % 4 == 0 && boundingBox.xMax < node.object.boundingBox.xMin) {
|
||||
return this.findMaybeIntersecting(boundingBox, node.left, depth + 1);
|
||||
}
|
||||
|
||||
if (depth % 4 == 1 && box.xMin > node.rectangle.xMax) {
|
||||
return this.findMaybeIntersecting(box, node.right, depth + 1);
|
||||
if (depth % 4 == 1 && boundingBox.xMin > node.object.boundingBox.xMax) {
|
||||
return this.findMaybeIntersecting(boundingBox, node.right, depth + 1);
|
||||
}
|
||||
|
||||
if (depth % 4 == 2 && box.yMax < node.rectangle.yMin) {
|
||||
return this.findMaybeIntersecting(box, node.left, depth + 1);
|
||||
if (depth % 4 == 2 && boundingBox.yMax < node.object.boundingBox.yMin) {
|
||||
return this.findMaybeIntersecting(boundingBox, node.left, depth + 1);
|
||||
}
|
||||
|
||||
if (depth % 4 == 3 && box.yMin > node.rectangle.yMax) {
|
||||
return this.findMaybeIntersecting(box, node.right, depth + 1);
|
||||
if (depth % 4 == 3 && boundingBox.yMin > node.object.boundingBox.yMax) {
|
||||
return this.findMaybeIntersecting(boundingBox, node.right, depth + 1);
|
||||
}
|
||||
|
||||
return [
|
||||
node.rectangle,
|
||||
...this.findMaybeIntersecting(box, node.left, depth + 1),
|
||||
...this.findMaybeIntersecting(box, node.right, depth + 1),
|
||||
node.object,
|
||||
...this.findMaybeIntersecting(boundingBox, node.left, depth + 1),
|
||||
...this.findMaybeIntersecting(boundingBox, node.right, depth + 1),
|
||||
];
|
||||
}
|
||||
|
||||
private findParent(
|
||||
box: ImmutableBoundingBox,
|
||||
object: StaticPhysical,
|
||||
node: Node,
|
||||
depth: number,
|
||||
parent: Node
|
||||
|
|
@ -112,10 +112,10 @@ export class BoundingBoxTree {
|
|||
|
||||
const dimension = depth % 4;
|
||||
|
||||
if (box[dimension] < node.rectangle[dimension]) {
|
||||
return this.findParent(box, node.left, depth + 1, node);
|
||||
if (object.boundingBox[dimension] < node.object.boundingBox[dimension]) {
|
||||
return this.findParent(object, node.left, depth + 1, node);
|
||||
}
|
||||
|
||||
return this.findParent(box, node.right, depth + 1, node);
|
||||
return this.findParent(object, node.right, depth + 1, node);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { GameObject, Id } from 'shared';
|
||||
import { BoundingBoxBase } from './bounds/bounding-box-base';
|
||||
import { BoundingBoxBase } from '../bounding-boxes/bounding-box-base';
|
||||
|
||||
import { BoundingBoxList } from './containers/bounding-box-list';
|
||||
import { BoundingBoxTree } from './containers/bounding-box-tree';
|
||||
import { BoundingBoxList } from './bounding-box-list';
|
||||
import { BoundingBoxTree } from './bounding-box-tree';
|
||||
import { Command } from 'shared';
|
||||
import { PhysicalGameObject } from './physical-game-object';
|
||||
import { ImmutableBoundingBox } from './bounds/immutable-bounding-box';
|
||||
import { Physical } from '../physical';
|
||||
import { StaticPhysical } from './static-physical-object';
|
||||
|
||||
export class PhysicalGameObjectContainer {
|
||||
export class PhysicalContainer {
|
||||
private isTreeInitialized = false;
|
||||
private staticBoundingBoxesWaitList: Array<ImmutableBoundingBox> = [];
|
||||
private staticBoundingBoxesWaitList: Array<StaticPhysical> = [];
|
||||
private staticBoundingBoxes = new BoundingBoxTree();
|
||||
private dynamicBoundingBoxes = new BoundingBoxList();
|
||||
|
||||
|
|
@ -21,40 +21,44 @@ export class PhysicalGameObjectContainer {
|
|||
this.isTreeInitialized = true;
|
||||
}
|
||||
|
||||
public addObject(object: PhysicalGameObject, isDynamic) {
|
||||
this.objects.set(object.id, object);
|
||||
public addObject(object: Physical) {
|
||||
this.objects.set(object.gameObject.id, object.gameObject);
|
||||
|
||||
for (const command of this.objectsGroupedByAbilities.keys()) {
|
||||
if (object.reactsToCommand(command)) {
|
||||
this.objectsGroupedByAbilities.get(command).push(object);
|
||||
if (object.gameObject.reactsToCommand(command)) {
|
||||
this.objectsGroupedByAbilities.get(command).push(object.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
if (isDynamic) {
|
||||
this.dynamicBoundingBoxes.insert(object.getBoundingBox());
|
||||
this.addPhysical(object);
|
||||
}
|
||||
|
||||
public addPhysical(physical: Physical) {
|
||||
if (physical.canMove) {
|
||||
this.dynamicBoundingBoxes.insert(physical);
|
||||
} else {
|
||||
if (!this.isTreeInitialized) {
|
||||
this.staticBoundingBoxesWaitList.push(object.getBoundingBox());
|
||||
this.staticBoundingBoxesWaitList.push(physical);
|
||||
} else {
|
||||
this.staticBoundingBoxes.insert(object.getBoundingBox());
|
||||
this.staticBoundingBoxes.insert(physical);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public removeObject(object: PhysicalGameObject) {
|
||||
this.objects.delete(object.id);
|
||||
public removeObject(object: Physical) {
|
||||
this.objects.delete(object.gameObject.id);
|
||||
|
||||
for (const command of this.objectsGroupedByAbilities.keys()) {
|
||||
if (object.reactsToCommand(command)) {
|
||||
if (object.gameObject.reactsToCommand(command)) {
|
||||
const array = this.objectsGroupedByAbilities.get(command);
|
||||
array.splice(
|
||||
array.findIndex((i) => i.id == object.id),
|
||||
array.findIndex((i) => i.id == object.gameObject.id),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.dynamicBoundingBoxes.remove(object.getBoundingBox());
|
||||
this.dynamicBoundingBoxes.remove(object);
|
||||
}
|
||||
|
||||
public sendCommand(e: Command) {
|
||||
|
|
@ -77,10 +81,10 @@ export class PhysicalGameObjectContainer {
|
|||
this.objectsGroupedByAbilities.set(commandType, objectsReactingToCommand);
|
||||
}
|
||||
|
||||
public findIntersecting(box: BoundingBoxBase): Array<PhysicalGameObject> {
|
||||
public findIntersecting(box: BoundingBoxBase): Array<Physical> {
|
||||
return [
|
||||
...this.staticBoundingBoxes.findIntersecting(box),
|
||||
...this.dynamicBoundingBoxes.findIntersecting(box),
|
||||
].map((b) => b.owner);
|
||||
];
|
||||
}
|
||||
}
|
||||
6
backend/src/physics/containers/static-physical-object.ts
Normal file
6
backend/src/physics/containers/static-physical-object.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { Physical } from '../physical';
|
||||
import { ImmutableBoundingBox } from '../bounding-boxes/immutable-bounding-box';
|
||||
|
||||
export interface StaticPhysical extends Physical {
|
||||
readonly boundingBox: ImmutableBoundingBox;
|
||||
}
|
||||
82
backend/src/physics/move-circle.ts
Normal file
82
backend/src/physics/move-circle.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/*import { vec2 } from 'gl-matrix';
|
||||
import { rotate90Deg } from 'shared';
|
||||
import { CirclePhysics } from './bounds/circle-physics';
|
||||
import { PhysicalGameObject } from './physical-game-object';
|
||||
|
||||
export const moveCircle = (
|
||||
circle: CirclePhysics,
|
||||
delta: vec2,
|
||||
possibleIntersectors: Array<PhysicalGameObject>
|
||||
): {
|
||||
realDelta: vec2;
|
||||
hitSurface: boolean;
|
||||
normal?: vec2;
|
||||
tangent?: vec2;
|
||||
} => {
|
||||
circle.center = vec2.add(circle.center, circle.center, delta);
|
||||
|
||||
const intersecting = possibleIntersectors.filter(
|
||||
(b) => b !== circle && circle.areIntersecting(b) && b.canCollide
|
||||
);
|
||||
|
||||
if (intersecting.length === 0) {
|
||||
return {
|
||||
realDelta: delta,
|
||||
hitSurface: false,
|
||||
};
|
||||
}
|
||||
|
||||
const points = circle.getPerimeterPoints(settings.hitDetectionCirclePointCount);
|
||||
|
||||
const distancesOfPoints = points
|
||||
.map((point) => ({
|
||||
point,
|
||||
closest: intersecting
|
||||
.map((i) => ({
|
||||
inverted: i.isInverted,
|
||||
distance: i.owner.distance(point),
|
||||
}))
|
||||
.sort((a, b) => a.distance - b.distance)[0],
|
||||
}))
|
||||
.filter((i) => i.closest);
|
||||
|
||||
const distancesOfIntersectingPoints = distancesOfPoints.filter(
|
||||
(d) =>
|
||||
(d.closest.distance > 0 && d.closest.inverted) ||
|
||||
(d.closest.distance < 0 && !d.closest.inverted)
|
||||
);
|
||||
|
||||
if (distancesOfIntersectingPoints.length === 0) {
|
||||
return {
|
||||
realDelta: delta,
|
||||
hitSurface: false,
|
||||
};
|
||||
}
|
||||
|
||||
const deltas = distancesOfIntersectingPoints.map((pointDistance) => {
|
||||
vec2.subtract(pointDistance.point, circle.center, pointDistance.point);
|
||||
vec2.normalize(pointDistance.point, pointDistance.point);
|
||||
vec2.scale(
|
||||
pointDistance.point,
|
||||
pointDistance.point,
|
||||
(pointDistance.closest.inverted ? 1 : -1) * pointDistance.closest.distance
|
||||
);
|
||||
return pointDistance.point;
|
||||
});
|
||||
|
||||
const approxNormal = deltas.reduce(
|
||||
(sum, current) => vec2.add(sum, sum, current),
|
||||
vec2.create()
|
||||
);
|
||||
vec2.scale(approxNormal, approxNormal, 1 / deltas.length);
|
||||
|
||||
circle.center = vec2.add(circle.center, circle.center, approxNormal);
|
||||
|
||||
return {
|
||||
realDelta: delta,
|
||||
hitSurface: true,
|
||||
normal: vec2.normalize(approxNormal, approxNormal),
|
||||
tangent: rotate90Deg(approxNormal),
|
||||
};
|
||||
};
|
||||
*/
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { GameObject } from 'shared';
|
||||
import { BoundingBoxBase } from './bounds/bounding-box-base';
|
||||
|
||||
export interface PhysicalGameObject extends GameObject {
|
||||
getBoundingBox(): BoundingBoxBase;
|
||||
//distance(target: vec2): number;
|
||||
readonly isInverted: boolean;
|
||||
readonly canCollide: boolean;
|
||||
readonly canMove: boolean;
|
||||
}
|
||||
13
backend/src/physics/physical.ts
Normal file
13
backend/src/physics/physical.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { GameObject } from 'shared';
|
||||
import { BoundingBoxBase } from './bounding-boxes/bounding-box-base';
|
||||
|
||||
export interface Physical {
|
||||
readonly isInverted: boolean;
|
||||
readonly canCollide: boolean;
|
||||
readonly canMove: boolean;
|
||||
readonly boundingBox: BoundingBoxBase;
|
||||
readonly gameObject: GameObject;
|
||||
|
||||
distance(target: vec2): number;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue