Refactor physics
This commit is contained in:
parent
46a48e7c15
commit
c5d97eeea6
40 changed files with 484 additions and 791 deletions
|
|
@ -5,7 +5,7 @@ import cors from 'cors';
|
||||||
import { applyArrayPlugins, Random, TransportEvents, deserializeCommand } from 'shared';
|
import { applyArrayPlugins, Random, TransportEvents, deserializeCommand } from 'shared';
|
||||||
import './index.html';
|
import './index.html';
|
||||||
import { Player } from './players/player';
|
import { Player } from './players/player';
|
||||||
import { PhysicalGameObjectContainer } from './physics/physical-game-object-container';
|
import { PhysicalContainer } from './physics/containers/physical-container';
|
||||||
import { createDungeon } from './map/create-dungeon';
|
import { createDungeon } from './map/create-dungeon';
|
||||||
import { glMatrix } from 'gl-matrix';
|
import { glMatrix } from 'gl-matrix';
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@ applyArrayPlugins();
|
||||||
|
|
||||||
Random.seed = 42;
|
Random.seed = 42;
|
||||||
|
|
||||||
const objects = new PhysicalGameObjectContainer();
|
const objects = new PhysicalContainer();
|
||||||
|
|
||||||
createDungeon(objects);
|
createDungeon(objects);
|
||||||
createDungeon(objects);
|
createDungeon(objects);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { vec2, vec3 } from 'gl-matrix';
|
import { vec2, vec3 } from 'gl-matrix';
|
||||||
import { Random } from 'shared';
|
import { Random } from 'shared';
|
||||||
import { LampPhysics } from '../objects/lamp-physics';
|
import { LampPhysical } from '../objects/lamp-physical';
|
||||||
import { TunnelPhysics } from '../objects/tunnel-physics';
|
import { TunnelPhysical } from '../objects/tunnel-physical';
|
||||||
import { PhysicalGameObjectContainer } from '../physics/physical-game-object-container';
|
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||||
|
|
||||||
export const createDungeon = (objects: PhysicalGameObjectContainer) => {
|
export const createDungeon = (objects: PhysicalContainer) => {
|
||||||
let previousRadius = 350;
|
let previousRadius = 350;
|
||||||
let previousEnd = vec2.create();
|
let previousEnd = vec2.create();
|
||||||
|
|
||||||
|
|
@ -16,26 +16,25 @@ export const createDungeon = (objects: PhysicalGameObjectContainer) => {
|
||||||
const currentEnd = vec2.fromValues(i, height);
|
const currentEnd = vec2.fromValues(i, height);
|
||||||
const currentToRadius = Random.getRandom() * 300 + 150;
|
const currentToRadius = Random.getRandom() * 300 + 150;
|
||||||
|
|
||||||
const tunnel = new TunnelPhysics(
|
const tunnel = new TunnelPhysical(
|
||||||
previousEnd,
|
previousEnd,
|
||||||
currentEnd,
|
currentEnd,
|
||||||
previousRadius,
|
previousRadius,
|
||||||
currentToRadius
|
currentToRadius
|
||||||
);
|
);
|
||||||
|
|
||||||
objects.addObject(tunnel, false);
|
objects.addObject(tunnel);
|
||||||
|
|
||||||
if (++tunnelsCountSinceLastLight > 3 && Random.getRandom() > 0.7) {
|
if (++tunnelsCountSinceLastLight > 3 && Random.getRandom() > 0.7) {
|
||||||
objects.addObject(
|
objects.addObject(
|
||||||
new LampPhysics(
|
new LampPhysical(
|
||||||
currentEnd,
|
currentEnd,
|
||||||
vec3.normalize(
|
vec3.normalize(
|
||||||
vec3.create(),
|
vec3.create(),
|
||||||
vec3.fromValues(Random.getRandom(), 0, Random.getRandom())
|
vec3.fromValues(Random.getRandom(), 0, Random.getRandom())
|
||||||
),
|
),
|
||||||
0.5
|
0.5
|
||||||
),
|
)
|
||||||
false
|
|
||||||
);
|
);
|
||||||
tunnelsCountSinceLastLight = 0;
|
tunnelsCountSinceLastLight = 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
94
backend/src/objects/character-physical.ts
Normal file
94
backend/src/objects/character-physical.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { vec2 } from 'gl-matrix';
|
||||||
|
import { id, CharacterBase } from 'shared';
|
||||||
|
|
||||||
|
import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box';
|
||||||
|
|
||||||
|
import { CirclePhysical } from './circle-physical';
|
||||||
|
import { Physical } from '../physics/physical';
|
||||||
|
|
||||||
|
export class CharacterPhysical extends CharacterBase implements Physical {
|
||||||
|
public readonly canCollide = true;
|
||||||
|
public readonly isInverted = false;
|
||||||
|
public readonly canMove = true;
|
||||||
|
|
||||||
|
private static readonly headOffset = vec2.fromValues(0, 40);
|
||||||
|
private static readonly leftFootOffset = vec2.fromValues(-20, -10);
|
||||||
|
private static readonly rightFootOffset = vec2.fromValues(20, -10);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(
|
||||||
|
id(),
|
||||||
|
new CirclePhysical(vec2.clone(CharacterPhysical.headOffset), 50, null),
|
||||||
|
new CirclePhysical(vec2.clone(CharacterPhysical.leftFootOffset), 20, null),
|
||||||
|
new CirclePhysical(vec2.clone(CharacterPhysical.rightFootOffset), 20, null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _boundingBox?: ImmutableBoundingBox;
|
||||||
|
|
||||||
|
public get boundingBox(): ImmutableBoundingBox {
|
||||||
|
if (!this._boundingBox) {
|
||||||
|
this._boundingBox = (this.head as CirclePhysical).boundingBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._boundingBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get gameObject(): CharacterPhysical {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo
|
||||||
|
public distance(_: vec2): number {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
public step() {
|
||||||
|
const movementForce = vec2.fromValues(right - left, up - down);
|
||||||
|
this.head.applyForce(movementForce, deltaTime);
|
||||||
|
this.head.applyForce(settings.gravitationalForce, deltaTime);
|
||||||
|
|
||||||
|
const bodyCenter = vec2.sub(vec2.create(), this.head.center, this.headOffset);
|
||||||
|
|
||||||
|
const leftFootPositon = vec2.add(vec2.create(), bodyCenter, this.leftFootOffset);
|
||||||
|
const rightFootPositon = vec2.add(vec2.create(), bodyCenter, this.rightFootOffset);
|
||||||
|
|
||||||
|
const leftFootDelta = vec2.sub(vec2.create(), this.leftFoot.center, leftFootPositon);
|
||||||
|
const rightFootDelta = vec2.sub(
|
||||||
|
vec2.create(),
|
||||||
|
this.rightFoot.center,
|
||||||
|
rightFootPositon
|
||||||
|
);
|
||||||
|
|
||||||
|
vec2.scale(leftFootDelta, leftFootDelta, 0.0006);
|
||||||
|
vec2.scale(rightFootDelta, rightFootDelta, 0.0006);
|
||||||
|
|
||||||
|
this.head.applyForce(leftFootDelta, deltaTime);
|
||||||
|
this.head.applyForce(rightFootDelta, deltaTime);
|
||||||
|
|
||||||
|
vec2.scale(leftFootDelta, leftFootDelta, -0.5);
|
||||||
|
vec2.scale(rightFootDelta, rightFootDelta, -0.5);
|
||||||
|
|
||||||
|
this.leftFoot.applyForce(movementForce, deltaTime);
|
||||||
|
this.rightFoot.applyForce(movementForce, deltaTime);
|
||||||
|
this.leftFoot.applyForce(leftFootDelta, deltaTime);
|
||||||
|
this.rightFoot.applyForce(rightFootDelta, deltaTime);
|
||||||
|
this.leftFoot.applyForce(settings.gravitationalForce, deltaTime);
|
||||||
|
this.rightFoot.applyForce(settings.gravitationalForce, deltaTime);
|
||||||
|
|
||||||
|
this.head.step(deltaTime);
|
||||||
|
this.leftFoot.step(deltaTime);
|
||||||
|
this.rightFoot.step(deltaTime);
|
||||||
|
|
||||||
|
this.flashlight.center = vec2.add(
|
||||||
|
vec2.create(),
|
||||||
|
this.head.center,
|
||||||
|
vec2.fromValues(0, 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
public toJSON(): any {
|
||||||
|
const { type, id, head, leftFoot, rightFoot } = this;
|
||||||
|
return [type, id, head, leftFoot, rightFoot];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { id, CharacterBase } from 'shared';
|
|
||||||
|
|
||||||
import { ImmutableBoundingBox } from '../physics/bounds/immutable-bounding-box';
|
|
||||||
import { PhysicalGameObject } from '../physics/physical-game-object';
|
|
||||||
import { CirclePhysics } from './circle-physics';
|
|
||||||
|
|
||||||
export class CharacterPhysics extends CharacterBase implements PhysicalGameObject {
|
|
||||||
public readonly canCollide = true;
|
|
||||||
public readonly isInverted = false;
|
|
||||||
public readonly canMove = true;
|
|
||||||
|
|
||||||
constructor(head: CirclePhysics, leftFoot: CirclePhysics, rightFoot: CirclePhysics) {
|
|
||||||
super(id(), head, leftFoot, rightFoot);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boundingBox?: ImmutableBoundingBox;
|
|
||||||
|
|
||||||
public getBoundingBox(): ImmutableBoundingBox {
|
|
||||||
if (!this.boundingBox) {
|
|
||||||
this.boundingBox = (this.head as CirclePhysics).boundingBox;
|
|
||||||
(this.head as CirclePhysics).boundingBox.owner = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.boundingBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
public toJSON(): any {
|
|
||||||
const { type, id, head, leftFoot, rightFoot } = this;
|
|
||||||
return [type, id, head, leftFoot, rightFoot];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
143
backend/src/objects/circle-physical.ts
Normal file
143
backend/src/objects/circle-physical.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
import { vec2 } from 'gl-matrix';
|
||||||
|
import { Circle, clamp, GameObject, settings } from 'shared';
|
||||||
|
import { Physical } from '../physics/physical';
|
||||||
|
|
||||||
|
import { BoundingBox } from '../physics/bounding-boxes/bounding-box';
|
||||||
|
import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base';
|
||||||
|
|
||||||
|
export class CirclePhysical implements Circle, Physical {
|
||||||
|
readonly isInverted = false;
|
||||||
|
readonly canCollide = true;
|
||||||
|
readonly canMove = true;
|
||||||
|
|
||||||
|
private _isAirborne = true;
|
||||||
|
|
||||||
|
private velocity = vec2.create();
|
||||||
|
|
||||||
|
public get isAirborne(): boolean {
|
||||||
|
return this._isAirborne;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _boundingBox: BoundingBox;
|
||||||
|
|
||||||
|
constructor(private _center: vec2, private _radius: number, private owner: GameObject) {
|
||||||
|
this._boundingBox = new BoundingBox(null);
|
||||||
|
this.recalculateBoundingBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get boundingBox(): BoundingBoxBase {
|
||||||
|
return this._boundingBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get center(): vec2 {
|
||||||
|
return this._center;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set center(value: vec2) {
|
||||||
|
this._center = value;
|
||||||
|
this.recalculateBoundingBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get gameObject(): GameObject {
|
||||||
|
return this.owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get radius(): number {
|
||||||
|
return this._radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set radius(value: number) {
|
||||||
|
this._radius = value;
|
||||||
|
this.recalculateBoundingBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
public distance(target: vec2): number {
|
||||||
|
return vec2.distance(target, this.center) - this.radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public distanceBetween(target: CirclePhysical): number {
|
||||||
|
return vec2.distance(target.center, this.center) - this.radius - target.radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public areIntersecting(other: CirclePhysical): boolean {
|
||||||
|
return other.distance(this.center) < this.radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isInside(other: CirclePhysical): boolean {
|
||||||
|
return other.distance(this.center) < -this.radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPerimeterPoints(count: number): Array<vec2> {
|
||||||
|
const result: Array<vec2> = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
result.push(
|
||||||
|
vec2.fromValues(
|
||||||
|
Math.cos((2 * Math.PI * i) / count) * this.radius + this.center.x,
|
||||||
|
Math.sin((2 * Math.PI * i) / count) * this.radius + this.center.y
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private recalculateBoundingBox() {
|
||||||
|
this._boundingBox.xMin = this.center.x - this._radius;
|
||||||
|
this._boundingBox.xMax = this.center.x + this._radius;
|
||||||
|
this._boundingBox.yMin = this.center.y - this._radius;
|
||||||
|
this._boundingBox.yMax = this.center.y + this._radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public applyForce(force: vec2, timeInMilliseconds: number) {
|
||||||
|
vec2.add(
|
||||||
|
this.velocity,
|
||||||
|
this.velocity,
|
||||||
|
vec2.scale(vec2.create(), force, timeInMilliseconds)
|
||||||
|
);
|
||||||
|
|
||||||
|
vec2.set(
|
||||||
|
this.velocity,
|
||||||
|
clamp(this.velocity.x, -settings.maxVelocityX, settings.maxVelocityX),
|
||||||
|
clamp(this.velocity.y, -settings.maxVelocityY, settings.maxVelocityY)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetVelocity() {
|
||||||
|
this.velocity = vec2.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*public step(timeInMilliseconds: number): boolean {
|
||||||
|
vec2.scale(
|
||||||
|
this.velocity,
|
||||||
|
this.velocity,
|
||||||
|
Math.pow(settings.velocityAttenuation, timeInMilliseconds)
|
||||||
|
);
|
||||||
|
const distance = vec2.scale(vec2.create(), this.velocity, timeInMilliseconds);
|
||||||
|
const distanceLength = vec2.length(distance);
|
||||||
|
const stepCount = Math.ceil(distanceLength / settings.physicsMaxStep);
|
||||||
|
vec2.scale(distance, distance, 1 / stepCount);
|
||||||
|
|
||||||
|
let wasHit = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < stepCount; i++) {
|
||||||
|
const { normal, tangent, hitSurface } = this.physics.tryMovingDynamicCircle(
|
||||||
|
this,
|
||||||
|
distance
|
||||||
|
);
|
||||||
|
if (hitSurface) {
|
||||||
|
vec2.scale(this.velocity, tangent, vec2.dot(tangent, this.velocity));
|
||||||
|
wasHit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._isAirborne = !(
|
||||||
|
hitSurface && vec2.dot(normal, settings.gravitationalForce) < 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wasHit;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
public toJSON(): any {
|
||||||
|
const { center, radius } = this;
|
||||||
|
return { center, radius };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
|
||||||
import { Circle } from 'shared';
|
|
||||||
import { BoundingBox } from '../physics/bounds/bounding-box';
|
|
||||||
import { BoundingBoxBase } from '../physics/bounds/bounding-box-base';
|
|
||||||
|
|
||||||
export class CirclePhysics implements Circle {
|
|
||||||
private _boundingBox: BoundingBox;
|
|
||||||
|
|
||||||
constructor(private _center: vec2, private _radius: number) {
|
|
||||||
this._boundingBox = new BoundingBox(null);
|
|
||||||
this.recalculateBoundingBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
public get boundingBox(): BoundingBoxBase {
|
|
||||||
return this._boundingBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get center(): vec2 {
|
|
||||||
return this._center;
|
|
||||||
}
|
|
||||||
|
|
||||||
public set center(value: vec2) {
|
|
||||||
this._center = value;
|
|
||||||
this.recalculateBoundingBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
public get radius(): number {
|
|
||||||
return this._radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
public set radius(value: number) {
|
|
||||||
this._radius = value;
|
|
||||||
this.recalculateBoundingBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
public distance(target: vec2): number {
|
|
||||||
return vec2.distance(target, this.center) - this.radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
public distanceBetween(target: CirclePhysics): number {
|
|
||||||
return vec2.distance(target.center, this.center) - this.radius - target.radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
public areIntersecting(other: CirclePhysics): boolean {
|
|
||||||
return other.distance(this.center) < this.radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
public isInside(other: CirclePhysics): boolean {
|
|
||||||
return other.distance(this.center) < -this.radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getPerimeterPoints(count: number): Array<vec2> {
|
|
||||||
const result: Array<vec2> = [];
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
result.push(
|
|
||||||
vec2.fromValues(
|
|
||||||
Math.cos((2 * Math.PI * i) / count) * this.radius + this.center.x,
|
|
||||||
Math.sin((2 * Math.PI * i) / count) * this.radius + this.center.y
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private recalculateBoundingBox() {
|
|
||||||
this._boundingBox.xMin = this.center.x - this._radius;
|
|
||||||
this._boundingBox.xMax = this.center.x + this._radius;
|
|
||||||
this._boundingBox.yMin = this.center.y - this._radius;
|
|
||||||
this._boundingBox.yMax = this.center.y + this._radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
public toJSON(): any {
|
|
||||||
const { center, radius } = this;
|
|
||||||
return { center, radius };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { vec2, vec3 } from 'gl-matrix';
|
import { vec2, vec3 } from 'gl-matrix';
|
||||||
import { LampBase, settings, id } from 'shared';
|
import { LampBase, settings, id } from 'shared';
|
||||||
|
|
||||||
import { ImmutableBoundingBox } from '../physics/bounds/immutable-bounding-box';
|
import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box';
|
||||||
import { PhysicalGameObject } from '../physics/physical-game-object';
|
|
||||||
|
|
||||||
export class LampPhysics extends LampBase implements PhysicalGameObject {
|
import { Physical } from '../physics/physical';
|
||||||
|
|
||||||
|
export class LampPhysical extends LampBase implements Physical {
|
||||||
public readonly canCollide = false;
|
public readonly canCollide = false;
|
||||||
public readonly isInverted = false;
|
public readonly isInverted = false;
|
||||||
public readonly canMove = false;
|
public readonly canMove = false;
|
||||||
|
|
@ -13,12 +14,11 @@ export class LampPhysics extends LampBase implements PhysicalGameObject {
|
||||||
super(id(), center, color, lightness);
|
super(id(), center, color, lightness);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boundingBox?: ImmutableBoundingBox;
|
private _boundingBox?: ImmutableBoundingBox;
|
||||||
|
|
||||||
public getBoundingBox(): ImmutableBoundingBox {
|
public get boundingBox(): ImmutableBoundingBox {
|
||||||
if (!this.boundingBox) {
|
if (!this._boundingBox) {
|
||||||
this.boundingBox = new ImmutableBoundingBox(
|
this._boundingBox = new ImmutableBoundingBox(
|
||||||
this,
|
|
||||||
this.center.x - settings.lightCutoffDistance,
|
this.center.x - settings.lightCutoffDistance,
|
||||||
this.center.x + settings.lightCutoffDistance,
|
this.center.x + settings.lightCutoffDistance,
|
||||||
this.center.y - settings.lightCutoffDistance,
|
this.center.y - settings.lightCutoffDistance,
|
||||||
|
|
@ -26,7 +26,16 @@ export class LampPhysics extends LampBase implements PhysicalGameObject {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.boundingBox;
|
return this._boundingBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get gameObject(): LampPhysical {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo
|
||||||
|
public distance(_: vec2): number {
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public toJSON(): any {
|
public toJSON(): any {
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
import { vec2 } from 'gl-matrix';
|
||||||
import { clamp01, mix, TunnelBase, id } from 'shared';
|
import { clamp01, mix, TunnelBase, id } from 'shared';
|
||||||
import { PhysicalGameObject } from '../physics/physical-game-object';
|
|
||||||
|
|
||||||
import { ImmutableBoundingBox } from '../physics/bounds/immutable-bounding-box';
|
import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box';
|
||||||
|
import { StaticPhysical } from '../physics/containers/static-physical-object';
|
||||||
|
|
||||||
export class TunnelPhysics extends TunnelBase implements PhysicalGameObject {
|
export class TunnelPhysical extends TunnelBase implements StaticPhysical {
|
||||||
public readonly canCollide = true;
|
public readonly canCollide = true;
|
||||||
public readonly isInverted = true;
|
public readonly isInverted = true;
|
||||||
public readonly canMove = false;
|
public readonly canMove = false;
|
||||||
|
|
||||||
private boundingBox?: ImmutableBoundingBox;
|
private _boundingBox?: ImmutableBoundingBox;
|
||||||
|
|
||||||
constructor(from: vec2, to: vec2, fromRadius: number, toRadius: number) {
|
constructor(from: vec2, to: vec2, fromRadius: number, toRadius: number) {
|
||||||
super(id(), from, to, fromRadius, toRadius);
|
super(id(), from, to, fromRadius, toRadius);
|
||||||
|
|
@ -29,16 +29,20 @@ export class TunnelPhysics extends TunnelBase implements PhysicalGameObject {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getBoundingBox(): ImmutableBoundingBox {
|
public get boundingBox(): ImmutableBoundingBox {
|
||||||
if (!this.boundingBox) {
|
if (!this._boundingBox) {
|
||||||
const xMin = Math.min(this.from.x - this.fromRadius, this.to.x - this.toRadius);
|
const xMin = Math.min(this.from.x - this.fromRadius, this.to.x - this.toRadius);
|
||||||
const yMin = Math.min(this.from.y - this.fromRadius, this.to.y - this.toRadius);
|
const yMin = Math.min(this.from.y - this.fromRadius, this.to.y - this.toRadius);
|
||||||
const xMax = Math.max(this.from.x + this.fromRadius, this.to.x + this.toRadius);
|
const xMax = Math.max(this.from.x + this.fromRadius, this.to.x + this.toRadius);
|
||||||
const yMax = Math.max(this.from.y + this.fromRadius, this.to.y + this.toRadius);
|
const yMax = Math.max(this.from.y + this.fromRadius, this.to.y + this.toRadius);
|
||||||
this.boundingBox = new ImmutableBoundingBox(this, xMin, xMax, yMin, yMax, true);
|
this._boundingBox = new ImmutableBoundingBox(xMin, xMax, yMin, yMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.boundingBox;
|
return this._boundingBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get gameObject(): TunnelPhysical {
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public toJSON(): any {
|
public toJSON(): any {
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
import { vec2 } from 'gl-matrix';
|
||||||
import { PhysicalGameObject } from '../physical-game-object';
|
|
||||||
|
|
||||||
export abstract class BoundingBoxBase {
|
export abstract class BoundingBoxBase {
|
||||||
constructor(
|
constructor(
|
||||||
public owner: PhysicalGameObject,
|
|
||||||
protected _xMin: number = 0,
|
protected _xMin: number = 0,
|
||||||
protected _xMax: number = 0,
|
protected _xMax: number = 0,
|
||||||
protected _yMin: number = 0,
|
protected _yMin: number = 0,
|
||||||
protected _yMax: number = 0,
|
protected _yMax: number = 0
|
||||||
public readonly isInverted = false
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public get 0(): number {
|
public get 0(): number {
|
||||||
|
|
@ -54,12 +54,6 @@ export class BoundingBox extends BoundingBoxBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public cloneAsImmutable(): ImmutableBoundingBox {
|
public cloneAsImmutable(): ImmutableBoundingBox {
|
||||||
return new ImmutableBoundingBox(
|
return new ImmutableBoundingBox(this.xMin, this.xMax, this.yMin, this.yMax);
|
||||||
this.owner,
|
|
||||||
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 {
|
export class BoundingBoxList {
|
||||||
constructor(private boundingBoxes: Array<BoundingBoxBase> = []) {}
|
constructor(private objects: Array<Physical> = []) {}
|
||||||
|
|
||||||
public insert(box: BoundingBoxBase) {
|
public insert(object: Physical) {
|
||||||
this.boundingBoxes.push(box);
|
this.objects.push(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
public remove(box: BoundingBoxBase) {
|
public remove(object: Physical) {
|
||||||
this.boundingBoxes.splice(
|
this.objects.splice(
|
||||||
this.boundingBoxes.findIndex((i) => i === box),
|
this.objects.findIndex((i) => i === object),
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findIntersecting(box: BoundingBoxBase): Array<BoundingBoxBase> {
|
public findIntersecting(boundingBox: BoundingBoxBase): Array<Physical> {
|
||||||
return this.boundingBoxes.filter((b) => b.intersects(box));
|
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
|
// source: https://github.com/ubilabs/kd-tree-javascript/blob/master/kdTree.js
|
||||||
|
|
||||||
import { ImmutableBoundingBox } from '../bounds/immutable-bounding-box';
|
|
||||||
|
|
||||||
class Node {
|
class Node {
|
||||||
public left?: Node = null;
|
public left?: Node = null;
|
||||||
public right?: Node = null;
|
public right?: Node = null;
|
||||||
constructor(public rectangle: ImmutableBoundingBox, public parent: Node) {}
|
constructor(public object: StaticPhysical, public parent: Node) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BoundingBoxTree {
|
export class BoundingBoxTree {
|
||||||
root?: Node;
|
root?: Node;
|
||||||
|
|
||||||
constructor(boxes: Array<ImmutableBoundingBox> = []) {
|
constructor(objects: Array<StaticPhysical> = []) {
|
||||||
this.build(boxes);
|
this.build(objects);
|
||||||
}
|
}
|
||||||
|
|
||||||
public build(boxes: Array<ImmutableBoundingBox>) {
|
public build(objects: Array<StaticPhysical>) {
|
||||||
this.root = this.buildRecursive(boxes, 0, null);
|
this.root = this.buildRecursive(objects, 0, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildRecursive(
|
private buildRecursive(
|
||||||
boxes: Array<ImmutableBoundingBox>,
|
objects: Array<StaticPhysical>,
|
||||||
depth: number,
|
depth: number,
|
||||||
parent: Node
|
parent: Node
|
||||||
): Node {
|
): Node {
|
||||||
if (boxes.length === 0) {
|
if (objects.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (boxes.length === 1) {
|
if (objects.length === 1) {
|
||||||
return new Node(boxes[0], parent);
|
return new Node(objects[0], parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dimension = depth % 4;
|
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);
|
const node = new Node(objects[median], parent);
|
||||||
node.left = this.buildRecursive(boxes.slice(0, median), depth + 1, node);
|
node.left = this.buildRecursive(objects.slice(0, median), depth + 1, node);
|
||||||
node.right = this.buildRecursive(boxes.slice(median + 1), depth + 1, node);
|
node.right = this.buildRecursive(objects.slice(median + 1), depth + 1, node);
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
public insert(box: ImmutableBoundingBox) {
|
public insert(object: StaticPhysical) {
|
||||||
const [insertPosition, depth] = this.findParent(box, this.root, 0, null);
|
const [insertPosition, depth] = this.findParent(object, this.root, 0, null);
|
||||||
|
|
||||||
if (insertPosition === null) {
|
if (insertPosition === null) {
|
||||||
this.root = new Node(box, null);
|
this.root = new Node(object, null);
|
||||||
} else {
|
} else {
|
||||||
const node = new Node(box, insertPosition);
|
const node = new Node(object, insertPosition);
|
||||||
const dimension = depth % 4;
|
const dimension = depth % 4;
|
||||||
|
|
||||||
if (box[dimension] < insertPosition.rectangle[dimension]) {
|
if (object.boundingBox[dimension] < insertPosition.object.boundingBox[dimension]) {
|
||||||
insertPosition.left = node;
|
insertPosition.left = node;
|
||||||
} else {
|
} else {
|
||||||
insertPosition.right = node;
|
insertPosition.right = node;
|
||||||
|
|
@ -62,46 +62,46 @@ export class BoundingBoxTree {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public findIntersecting(box: ImmutableBoundingBox): Array<ImmutableBoundingBox> {
|
public findIntersecting(boundingBox: BoundingBoxBase): Array<StaticPhysical> {
|
||||||
const maybeResults = this.findMaybeIntersecting(box, this.root, 0);
|
const maybeResults = this.findMaybeIntersecting(boundingBox, this.root, 0);
|
||||||
const results = maybeResults.filter((b) => b.intersects(box));
|
const results = maybeResults.filter((b) => b.boundingBox.intersects(boundingBox));
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private findMaybeIntersecting(
|
private findMaybeIntersecting(
|
||||||
box: ImmutableBoundingBox,
|
boundingBox: BoundingBoxBase,
|
||||||
node: Node,
|
node: Node,
|
||||||
depth: number
|
depth: number
|
||||||
): Array<ImmutableBoundingBox> {
|
): Array<StaticPhysical> {
|
||||||
if (node === null) {
|
if (node === null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (depth % 4 == 0 && box.xMax < node.rectangle.xMin) {
|
if (depth % 4 == 0 && boundingBox.xMax < node.object.boundingBox.xMin) {
|
||||||
return this.findMaybeIntersecting(box, node.left, depth + 1);
|
return this.findMaybeIntersecting(boundingBox, node.left, depth + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (depth % 4 == 1 && box.xMin > node.rectangle.xMax) {
|
if (depth % 4 == 1 && boundingBox.xMin > node.object.boundingBox.xMax) {
|
||||||
return this.findMaybeIntersecting(box, node.right, depth + 1);
|
return this.findMaybeIntersecting(boundingBox, node.right, depth + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (depth % 4 == 2 && box.yMax < node.rectangle.yMin) {
|
if (depth % 4 == 2 && boundingBox.yMax < node.object.boundingBox.yMin) {
|
||||||
return this.findMaybeIntersecting(box, node.left, depth + 1);
|
return this.findMaybeIntersecting(boundingBox, node.left, depth + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (depth % 4 == 3 && box.yMin > node.rectangle.yMax) {
|
if (depth % 4 == 3 && boundingBox.yMin > node.object.boundingBox.yMax) {
|
||||||
return this.findMaybeIntersecting(box, node.right, depth + 1);
|
return this.findMaybeIntersecting(boundingBox, node.right, depth + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
node.rectangle,
|
node.object,
|
||||||
...this.findMaybeIntersecting(box, node.left, depth + 1),
|
...this.findMaybeIntersecting(boundingBox, node.left, depth + 1),
|
||||||
...this.findMaybeIntersecting(box, node.right, depth + 1),
|
...this.findMaybeIntersecting(boundingBox, node.right, depth + 1),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private findParent(
|
private findParent(
|
||||||
box: ImmutableBoundingBox,
|
object: StaticPhysical,
|
||||||
node: Node,
|
node: Node,
|
||||||
depth: number,
|
depth: number,
|
||||||
parent: Node
|
parent: Node
|
||||||
|
|
@ -112,10 +112,10 @@ export class BoundingBoxTree {
|
||||||
|
|
||||||
const dimension = depth % 4;
|
const dimension = depth % 4;
|
||||||
|
|
||||||
if (box[dimension] < node.rectangle[dimension]) {
|
if (object.boundingBox[dimension] < node.object.boundingBox[dimension]) {
|
||||||
return this.findParent(box, node.left, depth + 1, node);
|
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 { 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 { BoundingBoxList } from './bounding-box-list';
|
||||||
import { BoundingBoxTree } from './containers/bounding-box-tree';
|
import { BoundingBoxTree } from './bounding-box-tree';
|
||||||
import { Command } from 'shared';
|
import { Command } from 'shared';
|
||||||
import { PhysicalGameObject } from './physical-game-object';
|
import { Physical } from '../physical';
|
||||||
import { ImmutableBoundingBox } from './bounds/immutable-bounding-box';
|
import { StaticPhysical } from './static-physical-object';
|
||||||
|
|
||||||
export class PhysicalGameObjectContainer {
|
export class PhysicalContainer {
|
||||||
private isTreeInitialized = false;
|
private isTreeInitialized = false;
|
||||||
private staticBoundingBoxesWaitList: Array<ImmutableBoundingBox> = [];
|
private staticBoundingBoxesWaitList: Array<StaticPhysical> = [];
|
||||||
private staticBoundingBoxes = new BoundingBoxTree();
|
private staticBoundingBoxes = new BoundingBoxTree();
|
||||||
private dynamicBoundingBoxes = new BoundingBoxList();
|
private dynamicBoundingBoxes = new BoundingBoxList();
|
||||||
|
|
||||||
|
|
@ -21,40 +21,44 @@ export class PhysicalGameObjectContainer {
|
||||||
this.isTreeInitialized = true;
|
this.isTreeInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public addObject(object: PhysicalGameObject, isDynamic) {
|
public addObject(object: Physical) {
|
||||||
this.objects.set(object.id, object);
|
this.objects.set(object.gameObject.id, object.gameObject);
|
||||||
|
|
||||||
for (const command of this.objectsGroupedByAbilities.keys()) {
|
for (const command of this.objectsGroupedByAbilities.keys()) {
|
||||||
if (object.reactsToCommand(command)) {
|
if (object.gameObject.reactsToCommand(command)) {
|
||||||
this.objectsGroupedByAbilities.get(command).push(object);
|
this.objectsGroupedByAbilities.get(command).push(object.gameObject);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDynamic) {
|
this.addPhysical(object);
|
||||||
this.dynamicBoundingBoxes.insert(object.getBoundingBox());
|
}
|
||||||
|
|
||||||
|
public addPhysical(physical: Physical) {
|
||||||
|
if (physical.canMove) {
|
||||||
|
this.dynamicBoundingBoxes.insert(physical);
|
||||||
} else {
|
} else {
|
||||||
if (!this.isTreeInitialized) {
|
if (!this.isTreeInitialized) {
|
||||||
this.staticBoundingBoxesWaitList.push(object.getBoundingBox());
|
this.staticBoundingBoxesWaitList.push(physical);
|
||||||
} else {
|
} else {
|
||||||
this.staticBoundingBoxes.insert(object.getBoundingBox());
|
this.staticBoundingBoxes.insert(physical);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeObject(object: PhysicalGameObject) {
|
public removeObject(object: Physical) {
|
||||||
this.objects.delete(object.id);
|
this.objects.delete(object.gameObject.id);
|
||||||
|
|
||||||
for (const command of this.objectsGroupedByAbilities.keys()) {
|
for (const command of this.objectsGroupedByAbilities.keys()) {
|
||||||
if (object.reactsToCommand(command)) {
|
if (object.gameObject.reactsToCommand(command)) {
|
||||||
const array = this.objectsGroupedByAbilities.get(command);
|
const array = this.objectsGroupedByAbilities.get(command);
|
||||||
array.splice(
|
array.splice(
|
||||||
array.findIndex((i) => i.id == object.id),
|
array.findIndex((i) => i.id == object.gameObject.id),
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dynamicBoundingBoxes.remove(object.getBoundingBox());
|
this.dynamicBoundingBoxes.remove(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sendCommand(e: Command) {
|
public sendCommand(e: Command) {
|
||||||
|
|
@ -77,10 +81,10 @@ export class PhysicalGameObjectContainer {
|
||||||
this.objectsGroupedByAbilities.set(commandType, objectsReactingToCommand);
|
this.objectsGroupedByAbilities.set(commandType, objectsReactingToCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findIntersecting(box: BoundingBoxBase): Array<PhysicalGameObject> {
|
public findIntersecting(box: BoundingBoxBase): Array<Physical> {
|
||||||
return [
|
return [
|
||||||
...this.staticBoundingBoxes.findIntersecting(box),
|
...this.staticBoundingBoxes.findIntersecting(box),
|
||||||
...this.dynamicBoundingBoxes.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;
|
||||||
|
}
|
||||||
|
|
@ -11,25 +11,20 @@ import {
|
||||||
TransportEvents,
|
TransportEvents,
|
||||||
UpdateObjectsCommand,
|
UpdateObjectsCommand,
|
||||||
} from 'shared';
|
} from 'shared';
|
||||||
import { CharacterPhysics } from '../objects/character-physics';
|
import { CharacterPhysical } from '../objects/character-physical';
|
||||||
import { CirclePhysics } from '../objects/circle-physics';
|
|
||||||
|
|
||||||
import { BoundingBox } from '../physics/bounds/bounding-box';
|
import { BoundingBox } from '../physics/bounding-boxes/bounding-box';
|
||||||
import { PhysicalGameObject } from '../physics/physical-game-object';
|
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||||
import { PhysicalGameObjectContainer } from '../physics/physical-game-object-container';
|
import { Physical } from '../physics/physical';
|
||||||
import { jsonSerialize } from '../serialize';
|
import { jsonSerialize } from '../serialize';
|
||||||
|
|
||||||
export class Player extends CommandReceiver {
|
export class Player extends CommandReceiver {
|
||||||
public isActive = true;
|
public isActive = true;
|
||||||
|
|
||||||
private character: CharacterPhysics = new CharacterPhysics(
|
private character: CharacterPhysical = new CharacterPhysical();
|
||||||
new CirclePhysics(vec2.fromValues(50, 50), 50),
|
|
||||||
new CirclePhysics(vec2.fromValues(50, 50), 50),
|
|
||||||
new CirclePhysics(vec2.fromValues(50, 50), 50)
|
|
||||||
);
|
|
||||||
|
|
||||||
private objectsPreviouslyInViewArea: Array<PhysicalGameObject> = [];
|
private objectsPreviouslyInViewArea: Array<Physical> = [];
|
||||||
private objectsInViewArea: Array<PhysicalGameObject> = [];
|
private objectsInViewArea: Array<Physical> = [];
|
||||||
|
|
||||||
protected commandExecutors: CommandExecutors = {
|
protected commandExecutors: CommandExecutors = {
|
||||||
[SetViewAreaActionCommand.type]: this.setViewArea.bind(this),
|
[SetViewAreaActionCommand.type]: this.setViewArea.bind(this),
|
||||||
|
|
@ -47,14 +42,14 @@ export class Player extends CommandReceiver {
|
||||||
protected defaultCommandExecutor(command: Command) {}
|
protected defaultCommandExecutor(command: Command) {}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly objects: PhysicalGameObjectContainer,
|
private readonly objects: PhysicalContainer,
|
||||||
private readonly socket: SocketIO.Socket
|
private readonly socket: SocketIO.Socket
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.objectsPreviouslyInViewArea.push(this.character);
|
this.objectsPreviouslyInViewArea.push(this.character);
|
||||||
this.objectsInViewArea.push(this.character);
|
this.objectsInViewArea.push(this.character);
|
||||||
|
|
||||||
this.objects.addObject(this.character, true);
|
this.objects.addObject(this.character);
|
||||||
|
|
||||||
socket.emit(
|
socket.emit(
|
||||||
TransportEvents.ServerToPlayer,
|
TransportEvents.ServerToPlayer,
|
||||||
|
|
@ -86,14 +81,20 @@ export class Player extends CommandReceiver {
|
||||||
if (noLongerIntersecting.length > 0) {
|
if (noLongerIntersecting.length > 0) {
|
||||||
this.socket.emit(
|
this.socket.emit(
|
||||||
TransportEvents.ServerToPlayer,
|
TransportEvents.ServerToPlayer,
|
||||||
jsonSerialize(new DeleteObjectsCommand(noLongerIntersecting.map((o) => o.id)))
|
jsonSerialize(
|
||||||
|
new DeleteObjectsCommand(noLongerIntersecting.map((p) => p.gameObject.id))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newlyIntersecting.length > 0) {
|
if (newlyIntersecting.length > 0) {
|
||||||
this.socket.emit(
|
this.socket.emit(
|
||||||
TransportEvents.ServerToPlayer,
|
TransportEvents.ServerToPlayer,
|
||||||
jsonSerialize(new CreateObjectsCommand(jsonSerialize(newlyIntersecting)))
|
jsonSerialize(
|
||||||
|
new CreateObjectsCommand(
|
||||||
|
jsonSerialize(newlyIntersecting.map((p) => p.gameObject))
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import { glMatrix } from 'gl-matrix';
|
import { glMatrix } from 'gl-matrix';
|
||||||
import { Game } from './scripts/game';
|
import { Game } from './scripts/game';
|
||||||
import { Random } from './scripts/helper/random';
|
|
||||||
import './styles/main.scss';
|
import './styles/main.scss';
|
||||||
|
|
||||||
glMatrix.setMatrixArrayType(Array);
|
glMatrix.setMatrixArrayType(Array);
|
||||||
Random.seed = 42;
|
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,13 @@ import {
|
||||||
renderNoise,
|
renderNoise,
|
||||||
WrapOptions,
|
WrapOptions,
|
||||||
} from 'sdf-2d';
|
} from 'sdf-2d';
|
||||||
import { broadcastCommands, SetViewAreaActionCommand, TransportEvents } from 'shared';
|
import {
|
||||||
|
broadcastCommands,
|
||||||
|
prettyPrint,
|
||||||
|
settings,
|
||||||
|
SetViewAreaActionCommand,
|
||||||
|
TransportEvents,
|
||||||
|
} from 'shared';
|
||||||
import io from 'socket.io-client';
|
import io from 'socket.io-client';
|
||||||
import { KeyboardListener } from './commands/generators/keyboard-listener';
|
import { KeyboardListener } from './commands/generators/keyboard-listener';
|
||||||
import { MouseListener } from './commands/generators/mouse-listener';
|
import { MouseListener } from './commands/generators/mouse-listener';
|
||||||
|
|
@ -20,10 +26,8 @@ import { RenderCommand } from './commands/types/render';
|
||||||
import { StepCommand } from './commands/types/step';
|
import { StepCommand } from './commands/types/step';
|
||||||
import { Configuration } from './config/configuration';
|
import { Configuration } from './config/configuration';
|
||||||
import { DeltaTimeCalculator } from './helper/delta-time-calculator';
|
import { DeltaTimeCalculator } from './helper/delta-time-calculator';
|
||||||
import { prettyPrint } from './helper/pretty-print';
|
|
||||||
import { rgb } from './helper/rgb';
|
import { rgb } from './helper/rgb';
|
||||||
import { GameObjectContainer } from './objects/game-object-container';
|
import { GameObjectContainer } from './objects/game-object-container';
|
||||||
import { settings } from './settings';
|
|
||||||
import { BlobShape } from './shapes/blob-shape';
|
import { BlobShape } from './shapes/blob-shape';
|
||||||
import { deserialize } from './transport/deserialize';
|
import { deserialize } from './transport/deserialize';
|
||||||
|
|
||||||
|
|
@ -69,7 +73,7 @@ export class Game {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setupRenderer(): Promise<void> {
|
private async setupRenderer(): Promise<void> {
|
||||||
const noiseTexture = await renderNoise([1024, 1], 60, 1 / 8);
|
const noiseTexture = await renderNoise([512, 1], 50, 1 / 10);
|
||||||
|
|
||||||
this.renderer = await compile(
|
this.renderer = await compile(
|
||||||
this.canvas,
|
this.canvas,
|
||||||
|
|
@ -96,7 +100,7 @@ export class Game {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
shadowTraceCount: 12,
|
shadowTraceCount: 16,
|
||||||
paletteSize: 10,
|
paletteSize: 10,
|
||||||
enableStopwatch: true,
|
enableStopwatch: true,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export const clamp = (value: number, min: number, max: number): number =>
|
|
||||||
Math.min(max, Math.max(min, value));
|
|
||||||
|
|
||||||
export const clamp01 = (value: number): number => Math.min(1, Math.max(0, value));
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export function last<T>(a: Array<T>): T | null {
|
|
||||||
return a.length > 0 ? a[a.length - 1] : null;
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export const mix = (from: number, to: number, q: number) => from + (to - from) * q;
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export const prettyPrint = (o: any): string =>
|
|
||||||
JSON.stringify(o, (_, v) => (v.toFixed ? Number(v.toFixed(3)) : v), ' ')
|
|
||||||
.replace(/("|,|{|^\n)/g, '')
|
|
||||||
.replace(/(\W*}\n?)+/g, '\n\n');
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
// src
|
|
||||||
// https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript
|
|
||||||
// Mulberry32
|
|
||||||
|
|
||||||
export abstract class Random {
|
|
||||||
private static _seed = Math.random();
|
|
||||||
|
|
||||||
public static set seed(value: number) {
|
|
||||||
Random._seed = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getRandom(): number {
|
|
||||||
let t = (Random._seed += 0x6d2b79f5);
|
|
||||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
||||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
||||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
|
||||||
|
|
||||||
export const rotate90Deg = (vec: vec2): vec2 => vec2.fromValues(-vec.y, vec.x);
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export const toPercent = (value: number) => `${Math.round(value * 100)}%`;
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export const wait = (ms: number): Promise<void> => {
|
|
||||||
return new Promise<void>((resolve, _) => setTimeout(resolve, ms));
|
|
||||||
};
|
|
||||||
|
|
@ -1,181 +0,0 @@
|
||||||
/*import { vec2 } from 'gl-matrix';
|
|
||||||
import { Flashlight } from 'sdf-2d';
|
|
||||||
import { rgb } from '../../helper/rgb';
|
|
||||||
import { IGame } from '../../i-game';
|
|
||||||
import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base';
|
|
||||||
import { BoundingCircle } from '../../physics/bounds/bounding-circle';
|
|
||||||
import { PhysicsCircle } from '../../physics/bounds/physics-circle';
|
|
||||||
import { PhysicalGameObject } from '../../physics/physical-object';
|
|
||||||
import { Physics } from '../../physics/physics';
|
|
||||||
import { settings } from '../../settings';
|
|
||||||
import { BlobShape } from '../../shapes/blob-shape';
|
|
||||||
import { Projectile } from './projectile';
|
|
||||||
|
|
||||||
export class Character extends PhysicalGameObject {
|
|
||||||
protected head: PhysicsCircle;
|
|
||||||
protected leftFoot: PhysicsCircle;
|
|
||||||
protected rightFoot: PhysicsCircle;
|
|
||||||
|
|
||||||
private keysDown: Set<string> = new Set();
|
|
||||||
|
|
||||||
private flashlight = new Flashlight(
|
|
||||||
vec2.create(),
|
|
||||||
rgb(1, 0.6, 0.45),
|
|
||||||
0.15,
|
|
||||||
vec2.fromValues(1, 0),
|
|
||||||
50
|
|
||||||
);
|
|
||||||
|
|
||||||
private static walkForce = 0.005;
|
|
||||||
private static jumpForce = 10;
|
|
||||||
|
|
||||||
private shape = new BlobShape();
|
|
||||||
private boundingCircle = new BoundingCircle(this, vec2.create(), 50);
|
|
||||||
|
|
||||||
private readonly headOffset = vec2.fromValues(0, 40);
|
|
||||||
private readonly leftFootOffset = vec2.fromValues(-20, -10);
|
|
||||||
private readonly rightFootOffset = vec2.fromValues(20, -10);
|
|
||||||
|
|
||||||
constructor(physics: Physics, private game: IGame) {
|
|
||||||
super(physics, true);
|
|
||||||
|
|
||||||
this.addCommandExecutor(RenderCommand, this.draw.bind(this));
|
|
||||||
this.addCommandExecutor(TeleportToCommand, (c) => this.setPosition(c.position));
|
|
||||||
this.addCommandExecutor(KeyDownCommand, (c) => this.keysDown.add(c.key));
|
|
||||||
this.addCommandExecutor(KeyUpCommand, (c) => this.keysDown.delete(c.key));
|
|
||||||
this.addCommandExecutor(StepCommand, this.stepHandler.bind(this));
|
|
||||||
this.addCommandExecutor(CursorMoveCommand, this.directLight.bind(this));
|
|
||||||
this.addCommandExecutor(PrimaryActionCommand, this.spawnProjectile.bind(this));
|
|
||||||
this.addCommandExecutor(SwipeCommand, (c) => {
|
|
||||||
//this.tryMoving(vec2.multiply(vec2.create(), c.delta, this.game.viewArea.size));
|
|
||||||
});
|
|
||||||
|
|
||||||
this.head = new PhysicsCircle(physics, this, vec2.clone(this.headOffset), 50);
|
|
||||||
this.leftFoot = new PhysicsCircle(physics, this, vec2.clone(this.leftFootOffset), 20);
|
|
||||||
this.rightFoot = new PhysicsCircle(
|
|
||||||
physics,
|
|
||||||
this,
|
|
||||||
vec2.clone(this.rightFootOffset),
|
|
||||||
20
|
|
||||||
);
|
|
||||||
|
|
||||||
this.shape.setCircles([this.head, this.leftFoot, this.rightFoot]);
|
|
||||||
|
|
||||||
this.addToPhysics(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public distance(target: vec2): number {
|
|
||||||
return this.shape.minDistance(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
private draw(c: RenderCommand) {
|
|
||||||
c.renderer.addDrawable(this.shape);
|
|
||||||
c.renderer.addDrawable(this.flashlight);
|
|
||||||
}
|
|
||||||
|
|
||||||
private directLight(e: CursorMoveCommand) {
|
|
||||||
const pos = this.game.displayToWorldCoordinates(e.position);
|
|
||||||
vec2.sub(pos, pos, this.head.center);
|
|
||||||
this.flashlight.direction = pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
private spawnProjectile(e: PrimaryActionCommand) {
|
|
||||||
const pos = this.game.displayToWorldCoordinates(e.position);
|
|
||||||
const direction = vec2.sub(vec2.create(), pos, this.head.center);
|
|
||||||
vec2.normalize(direction, direction);
|
|
||||||
const start = vec2.add(
|
|
||||||
vec2.create(),
|
|
||||||
this.head.center,
|
|
||||||
vec2.scale(vec2.create(), direction, this.head.radius)
|
|
||||||
);
|
|
||||||
|
|
||||||
vec2.scale(direction, direction, 5);
|
|
||||||
|
|
||||||
const projectile = new Projectile(this.game, this.physics, start, direction);
|
|
||||||
this.game.addObject(projectile);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get position(): vec2 {
|
|
||||||
return this.head.center;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getBoundingCircles(): Array<BoundingCircle> {
|
|
||||||
return [this.boundingCircle];
|
|
||||||
}
|
|
||||||
|
|
||||||
public getBoundingBox(): BoundingBoxBase {
|
|
||||||
return this.head.boundingBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
private setPosition(value: vec2) {
|
|
||||||
this.head.center = vec2.clone(value);
|
|
||||||
this.leftFoot.center = vec2.clone(value);
|
|
||||||
this.rightFoot.center = vec2.clone(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public stepHandler(c: StepCommand) {
|
|
||||||
const deltaTime = c.deltaTimeInMiliseconds;
|
|
||||||
|
|
||||||
const isAirborne = this.leftFoot.isAirborne && this.rightFoot.isAirborne;
|
|
||||||
const up =
|
|
||||||
~~(
|
|
||||||
!isAirborne &&
|
|
||||||
(this.keysDown.has('w') || this.keysDown.has('arrowup') || this.keysDown.has(' '))
|
|
||||||
) * Character.jumpForce;
|
|
||||||
const down = ~~(
|
|
||||||
!isAirborne &&
|
|
||||||
(this.keysDown.has('s') || this.keysDown.has('arrowdown'))
|
|
||||||
);
|
|
||||||
const left =
|
|
||||||
~~(this.keysDown.has('a') || this.keysDown.has('arrowleft')) *
|
|
||||||
Character.walkForce *
|
|
||||||
deltaTime;
|
|
||||||
const right =
|
|
||||||
~~(this.keysDown.has('d') || this.keysDown.has('arrowright')) *
|
|
||||||
Character.walkForce *
|
|
||||||
deltaTime;
|
|
||||||
|
|
||||||
const movementForce = vec2.fromValues(right - left, up - down);
|
|
||||||
this.head.applyForce(movementForce, deltaTime);
|
|
||||||
this.head.applyForce(settings.gravitationalForce, deltaTime);
|
|
||||||
|
|
||||||
const bodyCenter = vec2.sub(vec2.create(), this.head.center, this.headOffset);
|
|
||||||
|
|
||||||
const leftFootPositon = vec2.add(vec2.create(), bodyCenter, this.leftFootOffset);
|
|
||||||
const rightFootPositon = vec2.add(vec2.create(), bodyCenter, this.rightFootOffset);
|
|
||||||
|
|
||||||
const leftFootDelta = vec2.sub(vec2.create(), this.leftFoot.center, leftFootPositon);
|
|
||||||
const rightFootDelta = vec2.sub(
|
|
||||||
vec2.create(),
|
|
||||||
this.rightFoot.center,
|
|
||||||
rightFootPositon
|
|
||||||
);
|
|
||||||
|
|
||||||
vec2.scale(leftFootDelta, leftFootDelta, 0.0006);
|
|
||||||
vec2.scale(rightFootDelta, rightFootDelta, 0.0006);
|
|
||||||
|
|
||||||
this.head.applyForce(leftFootDelta, deltaTime);
|
|
||||||
this.head.applyForce(rightFootDelta, deltaTime);
|
|
||||||
|
|
||||||
vec2.scale(leftFootDelta, leftFootDelta, -0.5);
|
|
||||||
vec2.scale(rightFootDelta, rightFootDelta, -0.5);
|
|
||||||
|
|
||||||
this.leftFoot.applyForce(movementForce, deltaTime);
|
|
||||||
this.rightFoot.applyForce(movementForce, deltaTime);
|
|
||||||
this.leftFoot.applyForce(leftFootDelta, deltaTime);
|
|
||||||
this.rightFoot.applyForce(rightFootDelta, deltaTime);
|
|
||||||
this.leftFoot.applyForce(settings.gravitationalForce, deltaTime);
|
|
||||||
this.rightFoot.applyForce(settings.gravitationalForce, deltaTime);
|
|
||||||
|
|
||||||
this.head.step(deltaTime);
|
|
||||||
this.leftFoot.step(deltaTime);
|
|
||||||
this.rightFoot.step(deltaTime);
|
|
||||||
|
|
||||||
this.flashlight.center = vec2.add(
|
|
||||||
vec2.create(),
|
|
||||||
this.head.center,
|
|
||||||
vec2.fromValues(0, 0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
/*import { vec2 } from 'gl-matrix';
|
|
||||||
import { Circle } from 'sdf-2d';
|
|
||||||
import { RenderCommand } from '../../commands/render';
|
|
||||||
import { StepCommand } from '../../commands/step';
|
|
||||||
import { IGame } from '../../i-game';
|
|
||||||
import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base';
|
|
||||||
import { BoundingCircle } from '../../physics/bounds/bounding-circle';
|
|
||||||
import { PhysicsCircle } from '../../physics/bounds/physics-circle';
|
|
||||||
import { PhysicalGameObject } from '../../physics/physical-object';
|
|
||||||
import { Physics } from '../../physics/physics';
|
|
||||||
import { settings } from '../../settings';
|
|
||||||
|
|
||||||
export class Projectile extends PhysicalGameObject {
|
|
||||||
private shape: Circle;
|
|
||||||
private bounding: PhysicsCircle;
|
|
||||||
|
|
||||||
constructor(private game: IGame, physics: Physics, position: vec2, velocity: vec2) {
|
|
||||||
super(physics, true);
|
|
||||||
|
|
||||||
this.shape = new Circle(position, 20);
|
|
||||||
this.bounding = new PhysicsCircle(physics, this, position, 20);
|
|
||||||
|
|
||||||
this.bounding.applyForce(velocity, 100);
|
|
||||||
|
|
||||||
this.addCommandExecutor(RenderCommand, this.draw.bind(this));
|
|
||||||
this.addCommandExecutor(StepCommand, this.step.bind(this));
|
|
||||||
|
|
||||||
this.addToPhysics(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private draw(c: RenderCommand) {
|
|
||||||
c.renderer.addDrawable(this.shape);
|
|
||||||
}
|
|
||||||
|
|
||||||
private step(c: StepCommand) {
|
|
||||||
this.bounding.applyForce(settings.gravitationalForce, c.deltaTimeInMiliseconds);
|
|
||||||
if (this.bounding.step(c.deltaTimeInMiliseconds)) {
|
|
||||||
this.game.removeObject(this);
|
|
||||||
this.physics.removeDynamicBoundingBox(this.getBoundingBox());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getBoundingCircles(): BoundingCircle[] {
|
|
||||||
return [this.bounding];
|
|
||||||
}
|
|
||||||
|
|
||||||
public getBoundingBox(): BoundingBoxBase {
|
|
||||||
return this.bounding.boundingBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
public distance(target: vec2): number {
|
|
||||||
return this.bounding.distance(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
/*import { vec2 } from 'gl-matrix';
|
|
||||||
import { clamp } from '../../helper/clamp';
|
|
||||||
import { settings } from '../../settings';
|
|
||||||
import { PhysicalGameObject } from '../physical-object';
|
|
||||||
import { Physics } from '../physics';
|
|
||||||
import { BoundingCircle } from './bounding-circle';
|
|
||||||
|
|
||||||
export class PhysicsCircle extends BoundingCircle {
|
|
||||||
private velocity = vec2.create();
|
|
||||||
private _isAirborne = true;
|
|
||||||
|
|
||||||
public get isAirborne(): boolean {
|
|
||||||
return this._isAirborne;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly physics: Physics,
|
|
||||||
owner: PhysicalGameObject,
|
|
||||||
center: vec2,
|
|
||||||
radius: number
|
|
||||||
) {
|
|
||||||
super(owner, center, radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
public resetVelocity() {
|
|
||||||
this.velocity = vec2.create();
|
|
||||||
}
|
|
||||||
|
|
||||||
public applyForce(force: vec2, timeInMilliseconds: number) {
|
|
||||||
vec2.add(
|
|
||||||
this.velocity,
|
|
||||||
this.velocity,
|
|
||||||
vec2.scale(vec2.create(), force, timeInMilliseconds)
|
|
||||||
);
|
|
||||||
|
|
||||||
vec2.set(
|
|
||||||
this.velocity,
|
|
||||||
clamp(this.velocity.x, -settings.maxVelocityX, settings.maxVelocityX),
|
|
||||||
clamp(this.velocity.y, -settings.maxVelocityY, settings.maxVelocityY)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public step(timeInMilliseconds: number): boolean {
|
|
||||||
vec2.scale(
|
|
||||||
this.velocity,
|
|
||||||
this.velocity,
|
|
||||||
Math.pow(settings.velocityAttenuation, timeInMilliseconds)
|
|
||||||
);
|
|
||||||
const distance = vec2.scale(vec2.create(), this.velocity, timeInMilliseconds);
|
|
||||||
const distanceLength = vec2.length(distance);
|
|
||||||
const stepCount = Math.ceil(distanceLength / settings.physicsMaxStep);
|
|
||||||
vec2.scale(distance, distance, 1 / stepCount);
|
|
||||||
|
|
||||||
let wasHit = false;
|
|
||||||
|
|
||||||
for (let i = 0; i < stepCount; i++) {
|
|
||||||
const { normal, tangent, hitSurface } = this.physics.tryMovingDynamicCircle(
|
|
||||||
this,
|
|
||||||
distance
|
|
||||||
);
|
|
||||||
if (hitSurface) {
|
|
||||||
vec2.scale(this.velocity, tangent, vec2.dot(tangent, this.velocity));
|
|
||||||
wasHit = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._isAirborne = !(
|
|
||||||
hitSurface && vec2.dot(normal, settings.gravitationalForce) < 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return wasHit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
/*import { vec2 } from 'gl-matrix';
|
|
||||||
import { GameObject } from '../objects/game-object';
|
|
||||||
import { BoundingBoxBase } from './bounds/bounding-box-base';
|
|
||||||
import { Physics } from './physics';
|
|
||||||
|
|
||||||
export abstract class PhysicalGameObject extends GameObject {
|
|
||||||
public abstract getBoundingBox(): BoundingBoxBase;
|
|
||||||
public abstract distance(target: vec2): number;
|
|
||||||
public readonly isInverted: boolean = false;
|
|
||||||
|
|
||||||
constructor(protected physics: Physics, public readonly canCollide: boolean) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected addToPhysics(isDynamic: boolean) {
|
|
||||||
if (isDynamic) {
|
|
||||||
this.physics.addDynamicBoundingBox(this.getBoundingBox());
|
|
||||||
} else {
|
|
||||||
this.physics.addStaticBoundingBox(this.getBoundingBox());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
/*import { vec2 } from 'gl-matrix';
|
|
||||||
import { rotate90Deg } from '../helper/rotate-90-deg';
|
|
||||||
import { settings } from '../settings';
|
|
||||||
import { BoundingBoxBase } from './bounds/bounding-box-base';
|
|
||||||
import { BoundingCircle } from './bounds/bounding-circle';
|
|
||||||
import { ImmutableBoundingBox } from './bounds/immutable-bounding-box';
|
|
||||||
import { BoundingBoxList } from './containers/bounding-box-list';
|
|
||||||
import { BoundingBoxTree } from './containers/bounding-box-tree';
|
|
||||||
|
|
||||||
export class Physics {
|
|
||||||
private isTreeInitialized = false;
|
|
||||||
private staticBoundingBoxesWaitList = [];
|
|
||||||
private staticBoundingBoxes = new BoundingBoxTree();
|
|
||||||
private dynamicBoundingBoxes = new BoundingBoxList();
|
|
||||||
|
|
||||||
public addStaticBoundingBox(boundingBox: ImmutableBoundingBox) {
|
|
||||||
if (!this.isTreeInitialized) {
|
|
||||||
this.staticBoundingBoxesWaitList.push(boundingBox);
|
|
||||||
} else {
|
|
||||||
this.staticBoundingBoxes.insert(boundingBox);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public addDynamicBoundingBox(boundingBox: BoundingBoxBase) {
|
|
||||||
this.dynamicBoundingBoxes.insert(boundingBox);
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeDynamicBoundingBox(boundingBox: BoundingBoxBase) {
|
|
||||||
this.dynamicBoundingBoxes.remove(boundingBox);
|
|
||||||
}
|
|
||||||
|
|
||||||
public tryMovingDynamicCircle(
|
|
||||||
circle: BoundingCircle,
|
|
||||||
delta: vec2
|
|
||||||
): {
|
|
||||||
realDelta: vec2;
|
|
||||||
hitSurface: boolean;
|
|
||||||
normal?: vec2;
|
|
||||||
tangent?: vec2;
|
|
||||||
} {
|
|
||||||
circle.center = vec2.add(circle.center, circle.center, delta);
|
|
||||||
|
|
||||||
const intersecting = this.findIntersecting(circle.boundingBox).filter(
|
|
||||||
(b) =>
|
|
||||||
b.owner !== circle.owner && circle.areIntersecting(b.owner) && b.owner.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),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public findIntersecting(box: BoundingBoxBase): Array<BoundingBoxBase> {
|
|
||||||
return [
|
|
||||||
...this.staticBoundingBoxes.findIntersecting(box),
|
|
||||||
...this.dynamicBoundingBoxes.findIntersecting(box),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public start() {
|
|
||||||
this.staticBoundingBoxes.build(this.staticBoundingBoxesWaitList);
|
|
||||||
this.isTreeInitialized = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
|
||||||
|
|
||||||
export const settings = {
|
|
||||||
lightCutoffDistance: 600,
|
|
||||||
hitDetectionCirclePointCount: 32,
|
|
||||||
hitDetectionMaxOverlap: 0.01,
|
|
||||||
physicsMaxStep: 5,
|
|
||||||
gravitationalForce: vec2.fromValues(0, -0.015),
|
|
||||||
maxVelocityX: 1.5,
|
|
||||||
maxVelocityY: 8,
|
|
||||||
velocityAttenuation: 0.99,
|
|
||||||
};
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
export abstract class Typed {
|
|
||||||
public abstract get type(): string;
|
|
||||||
|
|
||||||
public toJSON() {
|
|
||||||
return { type: this.type, ...this };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
export class DeltaTimeCalculator {
|
|
||||||
private previousTime: DOMHighResTimeStamp | null = null;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
public getNextDeltaTime(currentTime: DOMHighResTimeStamp): DOMHighResTimeStamp {
|
|
||||||
if (this.previousTime === null) {
|
|
||||||
this.previousTime = currentTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
const delta = currentTime - this.previousTime;
|
|
||||||
this.previousTime = currentTime;
|
|
||||||
return delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleVisibilityChange() {
|
|
||||||
if (!document.hidden) {
|
|
||||||
this.previousTime = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import { vec3 } from 'gl-matrix';
|
|
||||||
|
|
||||||
export const rgb = (r: number, g: number, b: number): vec3 => vec3.fromValues(r, g, b);
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
import { vec3 } from 'gl-matrix';
|
|
||||||
|
|
||||||
export const rgb255 = (r: number, g: number, b: number): vec3 =>
|
|
||||||
vec3.fromValues(r / 255, g / 255, b / 255);
|
|
||||||
|
|
@ -13,6 +13,7 @@ export * from './commands/command-generator';
|
||||||
export * from './commands/broadcast-commands';
|
export * from './commands/broadcast-commands';
|
||||||
export * from './helper/array';
|
export * from './helper/array';
|
||||||
export * from './helper/clamp';
|
export * from './helper/clamp';
|
||||||
|
export * from './helper/pretty-print';
|
||||||
export * from './helper/circle';
|
export * from './helper/circle';
|
||||||
export * from './helper/rectangle';
|
export * from './helper/rectangle';
|
||||||
export * from './helper/mix';
|
export * from './helper/mix';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue