Refactor physics

This commit is contained in:
schmelczerandras 2020-10-05 20:30:42 +02:00
commit c5d97eeea6
10 changed files with 484 additions and 791 deletions

View file

@ -0,0 +1,59 @@
import { vec2 } from 'gl-matrix';
export abstract class BoundingBoxBase {
constructor(
protected _xMin: number = 0,
protected _xMax: number = 0,
protected _yMin: number = 0,
protected _yMax: number = 0
) {}
public get 0(): number {
return this._xMin;
}
public get 1(): number {
return this._xMax;
}
public get 2(): number {
return this._yMin;
}
public get 3(): number {
return this._yMax;
}
public get xMin(): number {
return this._xMin;
}
public get xMax(): number {
return this._xMax;
}
public get yMin(): number {
return this._yMin;
}
public get yMax(): number {
return this._yMax;
}
public get topLeft(): vec2 {
return vec2.fromValues(this._xMin, this._yMax);
}
public get size(): vec2 {
return vec2.fromValues(this._xMax - this._xMin, this._yMax - this._yMin);
}
public intersects(other: BoundingBoxBase): boolean {
return (
this._xMin < other._xMax &&
this._xMax > other._xMin &&
this._yMin < other._yMax &&
this._yMax > other._yMin
);
}
}

View file

@ -0,0 +1,59 @@
import { vec2 } from 'gl-matrix';
import { BoundingBoxBase } from './bounding-box-base';
import { ImmutableBoundingBox } from './immutable-bounding-box';
export class BoundingBox extends BoundingBoxBase {
public get xMin(): number {
return this._xMin;
}
public set xMin(value: number) {
this._xMin = value;
}
public set xMax(value: number) {
this._xMax = value;
}
public get xMax(): number {
return this._xMax;
}
public set yMin(value: number) {
this._yMin = value;
}
public get yMin(): number {
return this._yMin;
}
public set yMax(value: number) {
this._yMax = value;
}
public get yMax(): number {
return this._yMax;
}
public get topLeft(): vec2 {
return vec2.fromValues(this._xMin, this._yMax);
}
public set topLeft(value: vec2) {
this._xMin = value.x;
this._yMax = value.y;
}
public set size(value: vec2) {
this._xMax = this.xMin + value.x;
this._yMin = this.yMax - value.y;
}
public get size(): vec2 {
return vec2.fromValues(this._xMax - this._xMin, this._yMax - this._yMin);
}
public cloneAsImmutable(): ImmutableBoundingBox {
return new ImmutableBoundingBox(this.xMin, this.xMax, this.yMin, this.yMax);
}
}

View file

@ -0,0 +1,3 @@
import { BoundingBoxBase } from './bounding-box-base';
export class ImmutableBoundingBox extends BoundingBoxBase {}

View file

@ -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));
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,90 @@
import { GameObject, Id } from 'shared';
import { BoundingBoxBase } from '../bounding-boxes/bounding-box-base';
import { BoundingBoxList } from './bounding-box-list';
import { BoundingBoxTree } from './bounding-box-tree';
import { Command } from 'shared';
import { Physical } from '../physical';
import { StaticPhysical } from './static-physical-object';
export class PhysicalContainer {
private isTreeInitialized = false;
private staticBoundingBoxesWaitList: Array<StaticPhysical> = [];
private staticBoundingBoxes = new BoundingBoxTree();
private dynamicBoundingBoxes = new BoundingBoxList();
protected objects: Map<Id, GameObject> = new Map();
private objectsGroupedByAbilities: Map<string, Array<GameObject>> = new Map();
public initialize() {
this.staticBoundingBoxes.build(this.staticBoundingBoxesWaitList);
this.isTreeInitialized = true;
}
public addObject(object: Physical) {
this.objects.set(object.gameObject.id, object.gameObject);
for (const command of this.objectsGroupedByAbilities.keys()) {
if (object.gameObject.reactsToCommand(command)) {
this.objectsGroupedByAbilities.get(command).push(object.gameObject);
}
}
this.addPhysical(object);
}
public addPhysical(physical: Physical) {
if (physical.canMove) {
this.dynamicBoundingBoxes.insert(physical);
} else {
if (!this.isTreeInitialized) {
this.staticBoundingBoxesWaitList.push(physical);
} else {
this.staticBoundingBoxes.insert(physical);
}
}
}
public removeObject(object: Physical) {
this.objects.delete(object.gameObject.id);
for (const command of this.objectsGroupedByAbilities.keys()) {
if (object.gameObject.reactsToCommand(command)) {
const array = this.objectsGroupedByAbilities.get(command);
array.splice(
array.findIndex((i) => i.id == object.gameObject.id),
1
);
}
}
this.dynamicBoundingBoxes.remove(object);
}
public sendCommand(e: Command) {
if (!this.objectsGroupedByAbilities.has(e.type)) {
this.createGroupForCommand(e.type);
}
this.objectsGroupedByAbilities.get(e.type).forEach((o, _) => o.sendCommand(e));
}
private createGroupForCommand(commandType: string) {
const objectsReactingToCommand = [];
this.objects.forEach((o, _) => {
if (o.reactsToCommand(commandType)) {
objectsReactingToCommand.push(o);
}
});
this.objectsGroupedByAbilities.set(commandType, objectsReactingToCommand);
}
public findIntersecting(box: BoundingBoxBase): Array<Physical> {
return [
...this.staticBoundingBoxes.findIntersecting(box),
...this.dynamicBoundingBoxes.findIntersecting(box),
];
}
}

View 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;
}

View 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),
};
};
*/

View file

@ -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;
}

View 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;
}