Update physics

This commit is contained in:
schmelczerandras 2020-10-02 15:12:30 +02:00
parent 1b7dee4be0
commit d9d8d03c36
30 changed files with 447 additions and 552 deletions

View file

@ -23,6 +23,7 @@
"unused-imports/no-unused-imports-ts": "error",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-module-boundary-types": "off"
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-non-null-assertion": "off"
}
}

View file

@ -12,10 +12,10 @@
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
<title>Test</title>
<title>Decla.red</title>
</head>
<body>
<noscript><h1>Javascript is required for this website.</h1></noscript>
<noscript>Javascript is required for this website.</noscript>
<div id="overlay"></div>
<canvas id="main"></canvas>
</body>

View file

@ -3,123 +3,9 @@ import { Game } from './scripts/game';
import { Random } from './scripts/helper/random';
import './styles/main.scss';
glMatrix.setMatrixArrayType(
Array
); /*
}
}
glMatrix.setMatrixArrayType(Array);
zeroes.forEach((z) => {
const blueIndex = z.y * (width * 4) + z.x * 4 + 2;
imageData.data[blueIndex] = 255;
imageData.data[blueIndex - 1] = 255;
imageData.data[blueIndex - 2] = 255;
});
console.log(errors);
ctx.putImageData(imageData, 0, 0);
};*/ //testSDF(); // extract as new image (data-uri)
/*
const testSDF = () => {
Random.seed = 44;
const objects: Array<TunnelShape> = [
new TunnelShape(vec2.fromValues(20, 20), vec2.fromValues(40, 80), 10, 9),
new TunnelShape(vec2.fromValues(40, 80), vec2.fromValues(60, 20), 9, 15),
];
const getPixelValue = (position: vec2) => {
/*const k = 0.15;
let res = Math.pow(2, -k * (vec2.distance(position, vec2.fromValues(20, 20)) - 20));
res += Math.pow(2, -k * (vec2.distance(position, vec2.fromValues(40, 80)) - 30));
res += Math.pow(2, -k * (vec2.distance(position, vec2.fromValues(60, 20)) - 20));
res = -Math.log2(res) / k;
return -res;
*/ /*let min = 1000;
for (const t of objects) {
min = Math.min(min, t.distance(position));
}
if (min < 0) {
// min = Math.min(min, vec2.distance(position, vec2.fromValues(40, 80 - 31.62)) - 31.62);
}
return -min;*/ /*
const tree = new BoundingBoxTree([
new BoundingBox(300, 550, 150, 550, 'A'),
new BoundingBox(400, 800, 50, 200, 'B'),
new BoundingBox(450, 500, 175, 185, 'C'),
new BoundingBox(100, 200, 100, 500, 'D'),
new BoundingBox(750, 950, 450, 600, 'E'),
new BoundingBox(940, 1000, -2, 180, 'F'),
new BoundingBox(960, 1050, 50, 190, 'G'),
new BoundingBox(150, 900, 0, 575, 'H'),
new BoundingBox(-10000, 10000, -10000, 10000, 'I'),
]);
tree.print();
console.log(tree.findIntersecting(new BoundingBox(960, 1050, 50, 190, 'G')));
*/ /*
return -vec2.distance(position, vec2.fromValues(40, 40)) + 30;
};
/*
const width = 80;
const height = 100;
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
// set desired size of transparent image
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(width, height);
const zeroes = [];
for (let x = 0; x < width; x++) {
console.log(x);
for (let y = 0; y < height; y++) {
const position = vec2.fromValues(x, y);
const redIndex = y * (width * 4) + x * 4;
const dist = getPixelValue(position);
if (Math.abs(dist) < 1.5) {
zeroes.push(position);
}
const blueIndex = y * (width * 4) + x * 4 + 2;
//imageData.data[blueIndex] = dist * 10;
imageData.data[redIndex + 3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
console.log(zeroes);
const errors = [];
for (let x = 0; x < width; x++) {
console.log('verify ', x);
for (let y = 0; y < height; y++) {
const position = vec2.fromValues(x, y);
const dist = getPixelValue(position);
const realDistance = zeroes.map((z) => vec2.distance(z, position)).sort()[0];
if (Math.abs(realDistance - dist) < 3) {
const greenIndex = y * (width * 4) + x * 4 + 1;
imageData.data[greenIndex] = 255;
} else if (dist >= 0) {
const redIndex = y * (width * 4) + x * 4;
errors.push(realDistance - dist);
imageData.data[redIndex] = 255;
}
/*if (zeroes.find((z) => vec2.distance(z, position) < dist) !== undefined) {
const blueIndex = y * (width * 4) + x * 4 + 2;
imageData.data[blueIndex] = 255;
}*/ const main = async () => {
const main = async () => {
try {
Random.seed = 42;
await new Game().start();

View file

@ -1,31 +1,30 @@
import { vec3 } from 'gl-matrix';
import { CircleLight, compile, Flashlight, Renderer } from 'sdf-2d';
import { vec2 } from 'gl-matrix';
import { CircleLight, compile, Flashlight, InvertedTunnel, Renderer } from 'sdf-2d';
import { CommandBroadcaster } from './commands/command-broadcaster';
import { RenderCommand } from './graphics/commands/render';
import { DeltaTimeCalculator } from './helper/delta-time-calculator';
import { prettyPrint } from './helper/pretty-print';
import { rgb } from './helper/rgb';
import { IGame } from './i-game';
import { KeyboardListener } from './input/keyboard-listener';
import { MouseListener } from './input/mouse-listener';
import { TouchListener } from './input/touch-listener';
import { GameObject } from './objects/game-object';
import { Objects } from './objects/objects';
import { Camera } from './objects/types/camera';
import { Character } from './objects/types/character';
import { createDungeon } from './objects/world/create-dungeon';
import { BoundingBoxBase } from './physics/bounds/bounding-box-base';
import { MoveToCommand } from './physics/commands/move-to';
import { StepCommand } from './physics/commands/step';
import { TeleportToCommand } from './physics/commands/teleport-to';
import { Physics } from './physics/physics';
import { BoundingBoxBase } from './shapes/bounding-box-base';
import { CircleShape } from './shapes/types/circle-shape';
import { TunnelShape } from './shapes/types/tunnel-shape';
import { settings } from './settings';
import { BlobShape } from './shapes/types/blob-shape';
export class Game implements IGame {
public readonly objects = new Objects();
public readonly physics = new Physics();
public readonly camera = new Camera();
private previousTime?: DOMHighResTimeStamp = null;
private previousFpsValues: Array<number> = [];
private character: Character;
private renderer: Renderer;
private rendererPromise: Promise<Renderer>;
@ -34,8 +33,6 @@ export class Game implements IGame {
constructor() {
const canvas: HTMLCanvasElement = document.querySelector('canvas#main');
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
new CommandBroadcaster(
[
new KeyboardListener(document.body),
@ -48,16 +45,16 @@ export class Game implements IGame {
this.rendererPromise = compile(
canvas,
[
CircleShape.descriptor,
TunnelShape.descriptor,
InvertedTunnel.descriptor,
Flashlight.descriptor,
BlobShape.descriptor,
CircleLight.descriptor,
],
[
vec3.fromValues(0, 0, 0),
vec3.fromValues(224 / 255, 96 / 255, 126 / 255),
vec3.fromValues(119 / 255, 173 / 255, 120 / 255),
]
{
shadowTraceCount: 12,
paletteSize: 10,
enableStopwatch: true,
}
);
this.initializeScene();
this.physics.start();
@ -67,16 +64,22 @@ export class Game implements IGame {
this.renderer = await this.rendererPromise;
this.renderer.setRuntimeSettings({
isWorldInverted: true,
ambientLight: vec3.fromValues(0.35, 0.1, 0.45),
shadowLength: 300,
ambientLight: rgb(0.35, 0.1, 0.45),
colorPalette: [
rgb(0.4, 1, 0.6),
rgb(1, 1, 0),
rgb(0.3, 1, 1),
rgb(0.3, 1, 1),
rgb(0.3, 1, 1),
rgb(0.3, 1, 1),
rgb(0.3, 1, 1),
],
enableHighDpiRendering: false,
lightCutoffDistance: settings.lightCutoffDistance,
});
requestAnimationFrame(this.gameLoop.bind(this));
}
public addObject(o: GameObject) {
this.objects.addObject(o);
}
public get viewArea(): BoundingBoxBase {
return this.camera.viewArea;
}
@ -86,51 +89,38 @@ export class Game implements IGame {
}
private initializeScene() {
const start = createDungeon(this.objects, this.physics);
createDungeon(this.objects, this.physics);
//createDungeon(this.objects, this.physics);
this.character = new Character(this);
// this.physics.addDynamicBoundingBox(this.character.boundingBox);
this.addObject(this.character);
this.addObject(this.camera);
let pos: any = localStorage.getItem('character-position');
pos = pos ? JSON.parse(pos) : start.from;
this.character = new Character(this.physics, this);
this.objects.addObject(this.character);
this.objects.addObject(this.camera);
let pos: any = localStorage.getItem('characterPosition');
pos = pos ? JSON.parse(pos) : vec2.fromValues(0, 0);
this.character.sendCommand(new TeleportToCommand(pos));
}
private handleVisibilityChange() {
if (!document.hidden) {
this.previousTime = null;
}
}
private deltaTimeCalculator = new DeltaTimeCalculator();
private gameLoop(time: DOMHighResTimeStamp) {
if (this.previousTime === null) {
this.previousTime = time;
}
const deltaTime = time - this.previousTime;
this.previousTime = time;
const deltaTime = this.deltaTimeCalculator.getNextDeltaTime(time);
this.objects.sendCommand(new StepCommand(deltaTime));
this.camera.sendCommand(new MoveToCommand(this.character.position));
this.camera.sendCommand(new RenderCommand(this.renderer));
const shouldBeDrawn = this.physics
.findIntersecting(this.camera.viewArea)
.map((b) => b.shape?.gameObject);
.map((b) => b.owner);
for (const object of shouldBeDrawn) {
object?.sendCommand(new RenderCommand(this.renderer));
}
this.character.sendCommand(new RenderCommand(this.renderer));
this.renderer.renderDrawables();
this.overlay.innerText = prettyPrint(this.renderer.insights);
localStorage.setItem('character-position', JSON.stringify(this.character.position));
localStorage.setItem('characterPosition', JSON.stringify(this.character.position));
requestAnimationFrame(this.gameLoop.bind(this));
}
}

View file

@ -1,5 +0,0 @@
export const exponentialDecay = (
accumulator: number,
nextValue: number,
biasOfNextValue: number
) => accumulator * (1 - biasOfNextValue) + nextValue * biasOfNextValue;

View file

@ -1,29 +0,0 @@
export const getCombinations = (values: Array<Array<number>>): Array<Array<number>> => {
if (!values.every((a) => a.length > 0)) {
return [];
}
const result: Array<Array<number>> = [];
const counters = values.map((_) => 0);
const increaseCounter = (i: number) => {
if (i >= counters.length) {
return false;
}
counters[i]++;
if (counters[i] >= values[i].length) {
counters[i] = 0;
return increaseCounter(i + 1);
}
return true;
};
do {
result.push(values.map((v, i) => v[counters[i]]));
} while (increaseCounter(0));
return result;
};

View file

@ -0,0 +1,3 @@
import { vec3 } from 'gl-matrix';
export const rgb = (r: number, g: number, b: number): vec3 => vec3.fromValues(r, g, b);

View file

@ -0,0 +1,4 @@
import { vec3 } from 'gl-matrix';
export const rgb255 = (r: number, g: number, b: number): vec3 =>
vec3.fromValues(r / 255, g / 255, b / 255);

View file

@ -1,29 +0,0 @@
export const waitWhileFalse = (body: () => boolean): Promise<void> => {
let resolveOnDone: () => void;
let rejectOnDone: (e: Error) => void;
const onDone = new Promise<void>((resolve, reject) => {
resolveOnDone = resolve;
rejectOnDone = reject;
});
const waiter = () => {
let success: boolean;
try {
success = body();
} catch (e) {
rejectOnDone(e);
return;
}
if (success) {
resolveOnDone();
} else {
requestAnimationFrame(waiter);
}
};
waiter();
return onDone;
};

View file

@ -1,8 +1,6 @@
import { GameObject } from './objects/game-object';
import { BoundingBoxBase } from './shapes/bounding-box-base';
import { BoundingBoxBase } from './physics/bounds/bounding-box-base';
export interface IGame {
addObject(o: GameObject): void;
readonly viewArea: BoundingBoxBase;
findIntersecting(box: BoundingBoxBase): Array<BoundingBoxBase>;
}

View file

@ -2,8 +2,8 @@ import { vec2 } from 'gl-matrix';
import { RenderCommand } from '../../graphics/commands/render';
import { CursorMoveCommand } from '../../input/commands/cursor-move-command';
import { ZoomCommand } from '../../input/commands/zoom';
import { BoundingBox } from '../../physics/bounds/bounding-box';
import { MoveToCommand } from '../../physics/commands/move-to';
import { BoundingBox } from '../../shapes/bounding-box';
import { GameObject } from '../game-object';
export class Camera extends GameObject {

View file

@ -1,41 +1,61 @@
import { vec2, vec3 } from 'gl-matrix';
import { vec2 } from 'gl-matrix';
import { Flashlight } from 'sdf-2d';
import { RenderCommand } from '../../graphics/commands/render';
import { rgb } from '../../helper/rgb';
import { IGame } from '../../i-game';
import { KeyDownCommand } from '../../input/commands/key-down';
import { KeyUpCommand } from '../../input/commands/key-up';
import { SwipeCommand } from '../../input/commands/swipe';
import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base';
import { BoundingCircle } from '../../physics/bounds/bounding-circle';
import { StepCommand } from '../../physics/commands/step';
import { TeleportToCommand } from '../../physics/commands/teleport-to';
import { IShape } from '../../shapes/i-shape';
import { DynamicPhysicalObject } from '../../physics/dynamic-physical-object';
import { Physics } from '../../physics/physics';
import { BlobShape } from '../../shapes/types/blob-shape';
import { GameObject } from '../game-object';
export class Character extends GameObject {
export class Character extends DynamicPhysicalObject {
protected head: BoundingCircle;
protected leftFoot: BoundingCircle;
protected rightFoot: BoundingCircle;
private keysDown: Set<string> = new Set();
private light = new Flashlight(
vec2.create(),
vec3.fromValues(1, 0.6, 0.45),
1.5,
vec2.fromValues(0, 0),
rgb(1, 0.6, 0.45),
0.5,
vec2.fromValues(-1, 0)
);
private shape = new BlobShape(vec2.create());
private shape = new BlobShape();
private boundingCircle = new BoundingCircle(this, vec2.create(), 50);
private center = vec2.create();
private static speed = 1.5;
constructor(private game: IGame) {
super();
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(SwipeCommand, (c) => {
this.tryMoving(vec2.multiply(vec2.create(), c.delta, this.game.viewArea.size));
});
this.head = new BoundingCircle(this, this.center, 50);
this.leftFoot = new BoundingCircle(this, this.center, 50);
this.rightFoot = new BoundingCircle(this, this.center, 50);
this.shape.setCircles([this.head, this.leftFoot, this.rightFoot]);
this.addToPhysics();
}
public get position(): vec2 {
return this.shape.center;
public distance(target: vec2): number {
return this.shape.minDistance(target);
}
private draw(c: RenderCommand) {
@ -43,73 +63,24 @@ export class Character extends GameObject {
c.renderer.addDrawable(this.light);
}
private tryMoving(delta: vec2, isFirstIteration = true) {
const maxStep = 2;
if (vec2.length(delta) > maxStep) {
let steppedDelta = vec2.normalize(vec2.create(), delta);
vec2.scale(steppedDelta, steppedDelta, maxStep - 0.001);
for (let i = 0; i <= vec2.length(delta) / maxStep - 1; i++) {
this.tryMoving(vec2.clone(steppedDelta), isFirstIteration);
public get position(): vec2 {
return this.center;
}
steppedDelta = vec2.normalize(vec2.create(), delta);
vec2.scale(steppedDelta, steppedDelta, vec2.length(delta) % maxStep);
this.tryMoving(vec2.clone(steppedDelta), isFirstIteration);
return;
public getBoundingCircles(): Array<BoundingCircle> {
return [this.boundingCircle];
}
const nextShape = this.shape.clone();
vec2.add(nextShape.center, nextShape.center, delta);
const nextNearShapes = this.getNearShapesTo(nextShape);
if (nextNearShapes.length && nextNearShapes[0].distance < 0) {
this.setPosition(nextShape.center);
} else {
if (!isFirstIteration) {
return;
public getBoundingBox(): BoundingBoxBase {
return this.head.boundingBox;
}
const currentNearShapes = this.getNearShapesTo(this.shape);
const intersecting = nextNearShapes
.filter(
(n) =>
currentNearShapes.find((c) => c.shape === n.shape && c.distance <= 0) !==
undefined
)
.sort((e) => Math.abs(e.distance));
if (intersecting.length < 1) {
return;
}
const normal = intersecting[0].shape.normal(this.shape.center);
const maxDistance = intersecting.reduce((p, c) => (p.distance > c.distance ? p : c))
.distance;
vec2.add(delta, delta, vec2.scale(vec2.create(), normal, -maxDistance - 2));
this.tryMoving(delta, false);
}
}
private getNearShapesTo(shape: BlobShape): Array<{ shape: IShape; distance: number }> {
return this.game
.findIntersecting(shape.boundingBox)
.filter((b) => b.shape)
.map((b) => ({
shape: b.shape,
// TODO: fix this
distance: b.shape.minDistance(shape.center) + shape.radius - 20,
}))
.sort((e) => e.distance);
private tryMoving(delta: vec2) {
this.physics.tryMovingDynamicCircle(this.head, delta);
}
private setPosition(value: vec2) {
this.shape.position = value;
//this.head.center = value;
this.light.center = vec2.add(vec2.create(), value, vec2.fromValues(50, -40));
}

View file

@ -1,21 +1,40 @@
import { vec2, vec3 } from 'gl-matrix';
import { CircleLight } from 'sdf-2d';
import { RenderCommand } from '../../graphics/commands/render';
import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base';
import { ImmutableBoundingBox } from '../../physics/bounds/immutable-bounding-box';
import { MoveToCommand } from '../../physics/commands/move-to';
import { GameObject } from '../game-object';
import { PhysicalObject } from '../../physics/physical-object';
import { Physics } from '../../physics/physics';
import { settings } from '../../settings';
export class Lamp extends GameObject {
export class Lamp extends PhysicalObject {
private light: CircleLight;
constructor(center: vec2, color: vec3, lightness: number) {
super();
constructor(center: vec2, color: vec3, lightness: number, physics: Physics) {
super(physics, false);
this.light = new CircleLight(center, color, lightness);
this.addToPhysics();
this.addCommandExecutor(RenderCommand, this.draw.bind(this));
this.addCommandExecutor(MoveToCommand, this.moveTo.bind(this));
}
public distance(target: vec2): number {
return this.light.minDistance(target);
}
public getBoundingBox(): BoundingBoxBase {
return new ImmutableBoundingBox(
this,
this.light.center.x - settings.lightCutoffDistance,
this.light.center.x + settings.lightCutoffDistance,
this.light.center.y - settings.lightCutoffDistance,
this.light.center.y + settings.lightCutoffDistance
);
}
private draw(c: RenderCommand) {
c.renderer.addDrawable(this.light);
}

View file

@ -1,26 +1,52 @@
import { vec2 } from 'gl-matrix';
import { RenderCommand } from '../../graphics/commands/render';
import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base';
import { ImmutableBoundingBox } from '../../physics/bounds/immutable-bounding-box';
import { Physics } from '../../physics/physics';
import { StaticPhysicalObject } from '../../physics/static-physical-object';
import { TunnelShape } from '../../shapes/types/tunnel-shape';
import { GameObject } from '../game-object';
export class Tunnel extends GameObject {
export class Tunnel extends StaticPhysicalObject {
private shape: TunnelShape;
constructor(
physics: Physics,
public readonly from: vec2,
from: vec2,
to: vec2,
fromRadius: number,
toRadius: number
) {
super();
super(physics, true);
this.shape = new TunnelShape(from, to, fromRadius, toRadius);
this.addToPhysics();
this.shape = new TunnelShape(from, to, fromRadius, toRadius, this);
physics.addStaticBoundingBox(this.shape.boundingBox);
this.addCommandExecutor(RenderCommand, this.draw.bind(this));
}
public distance(target: vec2): number {
return this.shape.minDistance(target);
}
public getBoundingBox(): BoundingBoxBase {
const xMin = Math.min(
this.shape.from.x - this.shape.fromRadius,
this.shape.to.x - this.shape.toRadius
);
const yMin = Math.min(
this.shape.from.y - this.shape.fromRadius,
this.shape.to.y - this.shape.toRadius
);
const xMax = Math.max(
this.shape.from.x + this.shape.fromRadius,
this.shape.to.x + this.shape.toRadius
);
const yMax = Math.max(
this.shape.from.y + this.shape.fromRadius,
this.shape.to.y + this.shape.toRadius
);
return new ImmutableBoundingBox(this, xMin, xMax, yMin, yMax, true);
}
private draw(c: RenderCommand) {
c.renderer.addDrawable(this.shape);
}

View file

@ -1,14 +1,15 @@
import { vec2 } from 'gl-matrix';
import { vec2, vec3 } from 'gl-matrix';
import { Random } from '../../helper/random';
import { Physics } from '../../physics/physics';
import { Objects } from '../objects';
import { Lamp } from '../types/lamp';
import { Tunnel } from '../types/tunnel';
export const createDungeon = (objects: Objects, physics: Physics): Tunnel => {
export const createDungeon = (objects: Objects, physics: Physics) => {
let previousRadius = 350;
let previousEnd = vec2.create();
let first: Tunnel;
let tunnelsCountSinceLastLight = 0;
for (let i = 0; i < 500000; i += 500) {
const deltaHeight = (Random.getRandom() - 0.5) * 2000;
@ -24,30 +25,24 @@ export const createDungeon = (objects: Objects, physics: Physics): Tunnel => {
currentToRadius
);
if (!first) {
first = tunnel;
}
objects.addObject(tunnel);
/* if (deltaHeight > 0 && Random.getRandom() > 0.8) {
if (++tunnelsCountSinceLastLight > 3 && Random.getRandom() > 0.6) {
objects.addObject(
new Lamp(
currentEnd,
Random.getRandom() * 20 + 30,
vec3.scale(
vec3.normalize(
vec3.create(),
vec3.normalize(vec3.create(), vec3.fromValues(0.5, 0.1, 0.8)),
Random.getRandom() * 0.5 + 0.5
vec3.fromValues(Random.getRandom(), 0, Random.getRandom())
),
1
0.5,
physics
)
);
} */
tunnelsCountSinceLastLight = 0;
}
previousEnd = currentEnd;
previousRadius = currentToRadius;
}
return first;
};

View file

@ -1,13 +1,14 @@
import { vec2 } from 'gl-matrix';
import { IShape } from './i-shape';
import { PhysicalObject } from '../physical-object';
export abstract class BoundingBoxBase {
constructor(
public readonly shape: IShape,
public readonly owner: PhysicalObject,
protected _xMin: number = 0,
protected _xMax: number = 0,
protected _yMin: number = 0,
protected _yMax: number = 0
protected _yMax: number = 0,
public readonly isInverted = false
) {}
public get 0(): number {

View file

@ -3,6 +3,38 @@ 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);
}
@ -12,18 +44,18 @@ export class BoundingBox extends BoundingBoxBase {
this._yMax = value.y;
}
public get size(): vec2 {
return vec2.fromValues(this._xMax - this._xMin, this._yMax - this._yMin);
}
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.shape,
this.owner,
this.xMin,
this.xMax,
this.yMin,

View file

@ -0,0 +1,67 @@
import { vec2 } from 'gl-matrix';
import { PhysicalObject } from '../physical-object';
import { BoundingBox } from './bounding-box';
import { BoundingBoxBase } from './bounding-box-base';
export class BoundingCircle {
private _boundingBox: BoundingBox;
constructor(public readonly owner, private _center: vec2, private _radius: number) {
this._boundingBox = new BoundingBox(owner);
this.recalculateBoundingBox();
}
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 areIntersecting(other: PhysicalObject): boolean {
return other.distance(this.center) < this.radius;
}
public isInside(other: PhysicalObject): 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;
}
public get boundingBox(): BoundingBoxBase {
return this._boundingBox;
}
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;
}
}

View file

@ -1,4 +1,4 @@
import { BoundingBoxBase } from '../../shapes/bounding-box-base';
import { BoundingBoxBase } from '../bounds/bounding-box-base';
export class BoundingBoxList {
constructor(private boundingBoxes: Array<BoundingBoxBase> = []) {}

View file

@ -1,12 +1,10 @@
// source: https://github.com/ubilabs/kd-tree-javascript/blob/master/kdTree.js
import { ImmutableBoundingBox } from '../../shapes/immutable-bounding-box';
import { ImmutableBoundingBox } from '../bounds/immutable-bounding-box';
class Node {
public left?: Node = null;
public right?: Node = null;
constructor(public rectangle: ImmutableBoundingBox, public parent: Node) {}
}

View file

@ -0,0 +1,10 @@
import { BoundingCircle } from './bounds/bounding-circle';
import { PhysicalObject } from './physical-object';
export abstract class DynamicPhysicalObject extends PhysicalObject {
public abstract getBoundingCircles(): Array<BoundingCircle>;
protected addToPhysics() {
super.addToPhysics(true);
}
}

View file

@ -0,0 +1,21 @@
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 PhysicalObject extends GameObject {
public abstract getBoundingBox(): BoundingBoxBase;
public abstract distance(target: vec2): number;
constructor(protected physics: Physics, public readonly canCollide: boolean) {
super();
}
protected addToPhysics(isDynamic = false) {
if (isDynamic) {
this.physics.addDynamicBoundingBox(this.getBoundingBox());
} else {
this.physics.addStaticBoundingBox(this.getBoundingBox());
}
}
}

View file

@ -1,15 +1,14 @@
import { BoundingBoxTree } from './containers/bounding-box-tree';
import { vec2 } from 'gl-matrix';
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 { ImmutableBoundingBox } from '../shapes/immutable-bounding-box';
import { BoundingBoxBase } from '../shapes/bounding-box-base';
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) {
@ -24,6 +23,62 @@ export class Physics {
this.dynamicBoundingBoxes.insert(boundingBox);
}
public tryMovingDynamicCircle(
circle: BoundingCircle,
delta: vec2,
invocationCount = 0
) {
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
);
const pointCount = 16;
const points = circle.getPerimeterPoints(pointCount);
const distancesOfPoints = points.map((point) => ({
point,
distances: intersecting.map((i) => i.owner.distance(point)).sort((a, b) => a - b),
}));
const distancesOfIntersectingPoints = distancesOfPoints.filter(
(d) => d.distances[0] > 0
);
if (distancesOfIntersectingPoints.length === 0) {
return;
}
const distanceToLeastIntersectingForEachPoint = distancesOfIntersectingPoints.map(
(pointDistances) => ({
point: pointDistances.point,
distance: pointDistances.distances[0],
})
);
const deltas = distanceToLeastIntersectingForEachPoint.map((pointDistance) => {
vec2.subtract(pointDistance.point, circle.center, pointDistance.point);
vec2.normalize(pointDistance.point, pointDistance.point);
vec2.scale(pointDistance.point, pointDistance.point, pointDistance.distance);
return pointDistance.point;
});
const sumDelta = vec2.fromValues(0, 0);
deltas.forEach((d) => vec2.add(sumDelta, sumDelta, d));
vec2.scale(sumDelta, sumDelta, 1 / deltas.length);
if (invocationCount > 10) {
return;
}
this.tryMovingDynamicCircle(circle, sumDelta, ++invocationCount);
}
public findIntersecting(box: BoundingBoxBase): Array<BoundingBoxBase> {
return [
...this.staticBoundingBoxes.findIntersecting(box),

View file

@ -0,0 +1,7 @@
import { PhysicalObject } from './physical-object';
export abstract class StaticPhysicalObject extends PhysicalObject {
protected addToPhysics() {
super.addToPhysics(false);
}
}

View file

@ -0,0 +1,3 @@
export const settings = {
lightCutoffDistance: 600,
};

View file

@ -1,12 +0,0 @@
import { vec2 } from 'gl-matrix';
import { GameObject } from '../objects/game-object';
import { BoundingBox } from './bounding-box';
export interface IShape {
readonly boundingBox: BoundingBox;
readonly gameObject?: GameObject;
minDistance(target: vec2): number;
normal(from: vec2): vec2;
clone(): IShape;
}

View file

@ -1,22 +1,17 @@
import { mat2d, vec2 } from 'gl-matrix';
import { Circle, Drawable, DrawableDescriptor } from 'sdf-2d';
import { GameObject } from '../../objects/game-object';
import { BoundingBox } from '../bounding-box';
import { IShape } from '../i-shape';
import { CircleShape } from './circle-shape';
import { Drawable, DrawableDescriptor } from 'sdf-2d';
import { BoundingCircle } from '../../physics/bounds/bounding-circle';
export class BlobShape extends Drawable implements IShape {
export class BlobShape extends Drawable {
public static descriptor: DrawableDescriptor = {
sdf: {
shader: `
uniform struct {
vec2 headCenter;
vec2 leftFootCenter;
vec2 rightFootCenter;
float headRadius;
float footRadius;
float k;
}[BLOB_COUNT] blobs;
uniform vec2 headCenters[BLOB_COUNT];
uniform vec2 leftFootCenters[BLOB_COUNT];
uniform vec2 rightFootCenters[BLOB_COUNT];
uniform float headRadii[BLOB_COUNT];
uniform float footRadii[BLOB_COUNT];
//uniform float ks[BLOB_COUNT];
float smoothMin(float a, float b)
{
@ -25,15 +20,18 @@ export class BlobShape extends Drawable implements IShape {
return -log2( res )/k;
}
float circleDistance(vec2 circleCenter, float radius) {
return distance(position, circleCenter) - radius;
float circleDistance(vec2 circleCenter, float radius, vec2 target) {
return distance(target, circleCenter) - radius;
}
void blobMinDistance(inout float minDistance, inout float color) {
float blobMinDistance(vec2 target, out float colorIndex) {
float minDistance = 1000.0;
colorIndex = 3.0;
for (int i = 0; i < BLOB_COUNT; i++) {
float headDistance = circleDistance(blobs[i].headCenter, blobs[i].headRadius);
float leftFootDistance = circleDistance(blobs[i].leftFootCenter, blobs[i].footRadius);
float rightFootDistance = circleDistance(blobs[i].rightFootCenter, blobs[i].footRadius);
float headDistance = circleDistance(headCenters[i], headRadii[i], target);
float leftFootDistance = circleDistance(leftFootCenters[i], footRadii[i], target);
float rightFootDistance = circleDistance(rightFootCenters[i], footRadii[i], target);
float res = min(
smoothMin(headDistance, leftFootDistance),
@ -41,70 +39,51 @@ export class BlobShape extends Drawable implements IShape {
);
minDistance = min(minDistance, res);
color = mix(1.0, color, step(distanceNdcPixelSize + SURFACE_OFFSET, res));
}
return minDistance;
}
`,
distanceFunctionName: 'blobMinDistance',
},
uniformName: 'blobs',
propertyUniformMapping: {
footRadius: 'footRadii',
headRadius: 'headRadii',
rightFootCenter: 'rightFootCenters',
leftFootCenter: 'leftFootCenters',
headCenter: 'headCenters',
},
uniformCountMacroName: 'BLOB_COUNT',
shaderCombinationSteps: [1],
empty: new BlobShape(vec2.fromValues(0, 0)),
shaderCombinationSteps: [0, 1, 10],
empty: new BlobShape(),
};
protected head: BoundingCircle;
protected leftFoot: BoundingCircle;
protected rightFoot: BoundingCircle;
public readonly boundingCircleRadius = 100;
protected readonly headRadius = 40;
protected readonly footRadius = 15;
private readonly headOffset = vec2.fromValues(0, -15);
private readonly leftFootOffset = vec2.fromValues(-12, -60);
private readonly rightFootOffset = vec2.fromValues(12, -60);
public readonly isInverted = false;
protected boundingCircle = new CircleShape(vec2.create(), this.boundingCircleRadius);
protected head = new Circle(vec2.create(), this.headRadius);
protected leftFoot = new Circle(vec2.create(), this.footRadius);
protected rightFoot = new Circle(vec2.create(), this.footRadius);
public constructor(center: vec2, public readonly gameObject: GameObject = null) {
public constructor() {
super();
this.position = center;
const circle = new BoundingCircle(null, vec2.create(), 200);
this.setCircles([circle, circle, circle]);
}
public set position(value: vec2) {
vec2.copy(this.boundingCircle.center, value);
vec2.add(this.head.center, value, this.headOffset);
vec2.add(this.leftFoot.center, value, this.leftFootOffset);
vec2.add(this.rightFoot.center, value, this.rightFootOffset);
}
public get center(): vec2 {
return this.boundingCircle.center;
}
public get radius(): number {
return this.boundingCircle.radius;
public setCircles([head, leftFoot, rightFoot]: [
BoundingCircle,
BoundingCircle,
BoundingCircle
]) {
this.head = head;
this.leftFoot = leftFoot;
this.rightFoot = rightFoot;
}
public minDistance(target: vec2): number {
return this.boundingCircle.minDistance(target);
}
public normal(from: vec2): vec2 {
return this.boundingCircle.normal(from);
}
public get boundingBox(): BoundingBox {
return this.boundingCircle.boundingBox;
}
public isInside(target: vec2): boolean {
return this.minDistance(target) < 0;
}
public clone(): BlobShape {
return new BlobShape(this.boundingCircle.center, this.gameObject);
return Math.min(
this.head.distance(target),
this.leftFoot.distance(target),
this.rightFoot.distance(target)
);
}
protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any {
@ -120,8 +99,8 @@ export class BlobShape extends Drawable implements IShape {
this.rightFoot.center,
transform2d
),
headRadius: this.headRadius * transform1d,
footRadius: this.footRadius * transform1d,
headRadius: this.head.radius * transform1d,
footRadius: this.leftFoot.radius * transform1d,
};
}
}

View file

@ -1,12 +1,8 @@
import { vec2 } from 'gl-matrix';
import { Circle } from 'sdf-2d';
import { GameObject } from '../../objects/game-object';
import { BoundingBox } from '../bounding-box';
import { IShape } from '../i-shape';
export class CircleShape extends Circle implements IShape {
public readonly isInverted = false;
export class CircleShape extends Circle {
public constructor(
center = vec2.create(),
radius = 0,
@ -15,34 +11,6 @@ export class CircleShape extends Circle implements IShape {
super(center, radius);
}
public distance(target: vec2): number {
return vec2.distance(this.center, target) - this.radius;
}
public normal(from: vec2): vec2 {
const diff = vec2.subtract(vec2.create(), from, this.center);
return vec2.normalize(diff, diff);
}
public get boundingBox(): BoundingBox {
return new BoundingBox(
this,
this.center.x - this.radius,
this.center.x + this.radius,
this.center.y - this.radius,
this.center.y + this.radius
);
}
public isInside(target: vec2): boolean {
return this.distance(target) < 0;
}
public areIntersecting(other: CircleShape): boolean {
const distance = vec2.distance(this.center, other.center);
return distance < this.radius + other.radius;
}
public clone(): CircleShape {
return new CircleShape(vec2.clone(this.center), this.radius, this.gameObject);
}

View file

@ -1,81 +1,17 @@
import { vec2 } from 'gl-matrix';
import { InvertedTunnel } from 'sdf-2d';
import { clamp01 } from '../../helper/clamp';
import { rotate90Deg } from '../../helper/rotate-90-deg';
import { GameObject } from '../../objects/game-object';
import { BoundingBox } from '../bounding-box';
import { IShape } from '../i-shape';
export class TunnelShape extends InvertedTunnel implements IShape {
constructor(
readonly from: vec2,
readonly to: vec2,
readonly fromRadius: number,
readonly toRadius: number,
public readonly gameObject: GameObject = null
) {
export class TunnelShape extends InvertedTunnel {
constructor(from: vec2, to: vec2, fromRadius: number, toRadius: number) {
super(from, to, fromRadius, toRadius);
}
public get boundingBox(): BoundingBox {
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 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);
return new BoundingBox(this, xMin, xMax, yMin, yMax);
}
public normal(target: vec2): vec2 {
const targetFromDelta = vec2.subtract(vec2.create(), target, this.from);
const toFromDelta = vec2.subtract(vec2.create(), this.to, this.from);
const h = clamp01(
vec2.dot(targetFromDelta, toFromDelta) / vec2.dot(toFromDelta, toFromDelta)
);
let diff = vec2.create();
if (h == 1) {
vec2.subtract(diff, target, this.to);
} else if (h == 0) {
vec2.subtract(diff, target, this.from);
} else {
const side = Math.sign(
toFromDelta.x * targetFromDelta.y - toFromDelta.y * targetFromDelta.x
);
const normal = rotate90Deg(toFromDelta);
vec2.normalize(normal, normal);
const translatedFrom = vec2.add(
vec2.create(),
this.from,
vec2.scale(vec2.create(), normal, side * this.fromRadius)
);
const translatedTo = vec2.add(
vec2.create(),
this.to,
vec2.scale(vec2.create(), normal, side * this.toRadius)
);
diff = rotate90Deg(vec2.subtract(vec2.create(), translatedTo, translatedFrom));
vec2.scale(diff, diff, side);
}
return vec2.normalize(diff, diff);
}
public clone(): TunnelShape {
return new TunnelShape(
vec2.clone(this.from),
vec2.clone(this.to),
this.fromRadius,
this.toRadius,
this.gameObject
this.toRadius
);
}
}