diff --git a/frontend/src/index.ts b/frontend/src/index.ts index bebec70..ff2abea 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -6,8 +6,26 @@ import './styles/main.scss'; glMatrix.setMatrixArrayType(Array); applyArrayPlugins(); +/* +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'))); +*/ + try { new Game(); } catch (e) { + console.error(e); alert(e); } diff --git a/frontend/src/scripts/drawing/drawables/i-drawable.ts b/frontend/src/scripts/drawing/drawables/i-drawable.ts index 2e7d4d9..70d2618 100644 --- a/frontend/src/scripts/drawing/drawables/i-drawable.ts +++ b/frontend/src/scripts/drawing/drawables/i-drawable.ts @@ -1,7 +1,11 @@ import { vec2 } from 'gl-matrix'; +import { ImmutableBoundingBox } from '../../physics/containers/immutable-bounding-box'; +import { GameObject } from '../../objects/game-object'; export interface IDrawable { serializeToUniforms(uniforms: any): void; distance(target: vec2): number; minimumDistance(target: vec2): number; + readonly owner: GameObject; + readonly boundingBox: ImmutableBoundingBox; } diff --git a/frontend/src/scripts/drawing/drawables/lights/circle-light.ts b/frontend/src/scripts/drawing/drawables/lights/circle-light.ts index 3c678da..1bc54e7 100644 --- a/frontend/src/scripts/drawing/drawables/lights/circle-light.ts +++ b/frontend/src/scripts/drawing/drawables/lights/circle-light.ts @@ -2,6 +2,8 @@ import { ILight } from './i-light'; import { vec2, vec3 } from 'gl-matrix'; import { settings } from '../../settings'; import { IDrawableDescriptor } from '../i-drawable-descriptor'; +import { ImmutableBoundingBox } from '../../../physics/containers/immutable-bounding-box'; +import { GameObject } from '../../../objects/game-object'; export class CircleLight implements ILight { public static descriptor: IDrawableDescriptor = { @@ -11,12 +13,15 @@ export class CircleLight implements ILight { }; constructor( + public readonly owner: GameObject, public center: vec2, public radius: number, public color: vec3, public lightness: number ) {} + boundingBox: ImmutableBoundingBox; + public distance(target: vec2): number { return 0; } diff --git a/frontend/src/scripts/drawing/drawables/lights/point-light.ts b/frontend/src/scripts/drawing/drawables/lights/point-light.ts index bf5f2c0..0832db9 100644 --- a/frontend/src/scripts/drawing/drawables/lights/point-light.ts +++ b/frontend/src/scripts/drawing/drawables/lights/point-light.ts @@ -2,6 +2,8 @@ import { ILight } from './i-light'; import { vec2, vec3 } from 'gl-matrix'; import { IDrawableDescriptor } from '../i-drawable-descriptor'; import { settings } from '../../settings'; +import { ImmutableBoundingBox } from '../../../physics/containers/immutable-bounding-box'; +import { GameObject } from '../../../objects/game-object'; export class PointLight implements ILight { public static descriptor: IDrawableDescriptor = { @@ -10,13 +12,16 @@ export class PointLight implements ILight { shaderCombinationSteps: settings.shaderCombinations.pointLightSteps, }; - constructor( + public constructor( + public readonly owner: GameObject, public center: vec2, public radius: number, public color: vec3, public lightness: number ) {} + boundingBox: ImmutableBoundingBox; + public distance(target: vec2): number { return vec2.distance(this.center, target) - this.radius; } diff --git a/frontend/src/scripts/drawing/drawables/primitives/circle.ts b/frontend/src/scripts/drawing/drawables/primitives/circle.ts index 55c353e..5872ae3 100644 --- a/frontend/src/scripts/drawing/drawables/primitives/circle.ts +++ b/frontend/src/scripts/drawing/drawables/primitives/circle.ts @@ -1,8 +1,14 @@ import { vec2 } from 'gl-matrix'; import { IPrimitive } from './i-primitive'; +import { ImmutableBoundingBox } from '../../../physics/containers/immutable-bounding-box'; +import { GameObject } from '../../../objects/game-object'; export class Circle implements IPrimitive { - public constructor(public center = vec2.create(), public radius = 0) {} + public constructor( + public readonly owner: GameObject, + public center = vec2.create(), + public radius = 0 + ) {} public serializeToUniforms(uniforms: any): void { throw new Error('Method not implemented.'); @@ -16,6 +22,16 @@ export class Circle implements IPrimitive { return vec2.distance(this.center, target) - this.radius; } + public get boundingBox(): ImmutableBoundingBox { + return new ImmutableBoundingBox( + 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; } diff --git a/frontend/src/scripts/drawing/drawables/primitives/rectangle.ts b/frontend/src/scripts/drawing/drawables/primitives/rectangle.ts deleted file mode 100644 index e2d39e6..0000000 --- a/frontend/src/scripts/drawing/drawables/primitives/rectangle.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { vec2 } from 'gl-matrix'; - -export class Rectangle { - public constructor( - public topLeft: vec2 = vec2.create(), - public size: vec2 = vec2.create() - ) {} - - public isInside(position: vec2): boolean { - const translated = vec2.subtract(vec2.create(), position, this.topLeft); - return ( - 0 <= translated.x && - translated.x < this.size.x && - 0 <= translated.y && - translated.y < this.size.y - ); - } -} diff --git a/frontend/src/scripts/drawing/drawables/primitives/tunnel-shape.ts b/frontend/src/scripts/drawing/drawables/primitives/tunnel-shape.ts index 5db2e4a..b07077e 100644 --- a/frontend/src/scripts/drawing/drawables/primitives/tunnel-shape.ts +++ b/frontend/src/scripts/drawing/drawables/primitives/tunnel-shape.ts @@ -1,10 +1,12 @@ import { vec2 } from 'gl-matrix'; -import { clamp01 } from '../../../helper/clamp'; -import { mix } from '../../../helper/mix'; -import { Circle } from './circle'; -import { IPrimitive } from './i-primitive'; import { IDrawableDescriptor } from '../i-drawable-descriptor'; +import { IPrimitive } from './i-primitive'; import { settings } from '../../settings'; +import { Circle } from './circle'; +import { mix } from '../../../helper/mix'; +import { clamp01 } from '../../../helper/clamp'; +import { ImmutableBoundingBox } from '../../../physics/containers/immutable-bounding-box'; +import { GameObject } from '../../../objects/game-object'; export class TunnelShape implements IPrimitive { public static descriptor: IDrawableDescriptor = { @@ -14,11 +16,11 @@ export class TunnelShape implements IPrimitive { }; public readonly toFromDelta: vec2; - private toFromDeltaLength: number; private boundingCircle: Circle; constructor( + public readonly owner: GameObject, public readonly from: vec2, public readonly to: vec2, public readonly fromRadius: number, @@ -27,12 +29,13 @@ export class TunnelShape implements IPrimitive { this.toFromDelta = vec2.subtract(vec2.create(), to, from); this.boundingCircle = new Circle( + this.owner, vec2.fromValues(from.x / 2 + to.x / 2, from.y / 2 + to.y / 2), Math.max(fromRadius, toRadius) + vec2.distance(from, to) ); } - serializeToUniforms(uniforms: any): void { + public serializeToUniforms(uniforms: any): void { if (!uniforms.hasOwnProperty(TunnelShape.descriptor.uniformName)) { uniforms[TunnelShape.descriptor.uniformName] = []; } @@ -45,6 +48,27 @@ export class TunnelShape implements IPrimitive { }); } + public get boundingBox(): ImmutableBoundingBox { + 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 ImmutableBoundingBox(this, xMin, xMax, yMin, yMax); + } + public distance(target: vec2): number { const targetFromDelta = vec2.subtract(vec2.create(), target, this.from); const h = clamp01( diff --git a/frontend/src/scripts/drawing/graphics-library/program/i-program.ts b/frontend/src/scripts/drawing/graphics-library/program/i-program.ts index 27052f5..086026a 100644 --- a/frontend/src/scripts/drawing/graphics-library/program/i-program.ts +++ b/frontend/src/scripts/drawing/graphics-library/program/i-program.ts @@ -1,7 +1,7 @@ import { vec2 } from 'gl-matrix'; export interface IProgram { - setDrawingRectangle(topLeft: vec2, size: vec2): void; + setDrawingRectangle(bottomLeft: vec2, size: vec2): void; bindAndSetUniforms(values: { [name: string]: any }): void; draw(): void; delete(): void; diff --git a/frontend/src/scripts/drawing/graphics-library/program/program.ts b/frontend/src/scripts/drawing/graphics-library/program/program.ts index 0440f5d..2038e17 100644 --- a/frontend/src/scripts/drawing/graphics-library/program/program.ts +++ b/frontend/src/scripts/drawing/graphics-library/program/program.ts @@ -32,10 +32,10 @@ export abstract class Program implements IProgram { this.setUniforms({ modelTransform: this.modelTransform, ...values }); } - public setDrawingRectangle(topLeft: vec2, size: vec2) { + public setDrawingRectangle(bottomLeft: vec2, size: vec2) { mat2d.invert(this.modelTransform, this.ndcToUv); - mat2d.translate(this.modelTransform, this.modelTransform, topLeft); + mat2d.translate(this.modelTransform, this.modelTransform, bottomLeft); mat2d.scale(this.modelTransform, this.modelTransform, size); mat2d.multiply(this.modelTransform, this.modelTransform, this.ndcToUv); } diff --git a/frontend/src/scripts/drawing/i-renderer.ts b/frontend/src/scripts/drawing/i-renderer.ts index 61ba749..76bc606 100644 --- a/frontend/src/scripts/drawing/i-renderer.ts +++ b/frontend/src/scripts/drawing/i-renderer.ts @@ -1,6 +1,6 @@ import { vec2 } from 'gl-matrix'; -import { IPrimitive } from './drawables/primitives/i-primitive'; import { ILight } from './drawables/lights/i-light'; +import { IPrimitive } from './drawables/primitives/i-primitive'; export interface IRenderer { startFrame(deltaTime: DOMHighResTimeStamp): void; diff --git a/frontend/src/scripts/drawing/rendering/rendering-pass.ts b/frontend/src/scripts/drawing/rendering/rendering-pass.ts index e20d8bb..0d647d9 100644 --- a/frontend/src/scripts/drawing/rendering/rendering-pass.ts +++ b/frontend/src/scripts/drawing/rendering/rendering-pass.ts @@ -1,11 +1,10 @@ -import { FrameBuffer } from '../graphics-library/frame-buffer/frame-buffer'; -import { IDrawable } from '../drawables/i-drawable'; -import { settings } from '../settings'; import { vec2 } from 'gl-matrix'; -import { Circle } from '../drawables/primitives/circle'; import { InfoText } from '../../objects/types/info-text'; -import { UniformArrayAutoScalingProgram } from '../graphics-library/program/uniform-array-autoscaling-program'; +import { IDrawable } from '../drawables/i-drawable'; import { IDrawableDescriptor } from '../drawables/i-drawable-descriptor'; +import { FrameBuffer } from '../graphics-library/frame-buffer/frame-buffer'; +import { UniformArrayAutoScalingProgram } from '../graphics-library/program/uniform-array-autoscaling-program'; +import { settings } from '../settings'; export class RenderingPass { private drawables: Array = []; @@ -30,7 +29,8 @@ export class RenderingPass { public render( commonUniforms: any, - viewCircle: Circle, + viewBoxCenter: vec2, + viewBoxRadius: number, inputTexture?: WebGLTexture ) { this.frame.bindAndClear(inputTexture); @@ -38,7 +38,7 @@ export class RenderingPass { const tileUvSize = vec2.fromValues(q, q); const possiblyOnScreenDrawables = this.drawables.filter( - (p) => p.minimumDistance(viewCircle.center) < viewCircle.radius + (p) => p.minimumDistance(viewBoxCenter) < viewBoxRadius ); const origin = vec2.transformMat2d( diff --git a/frontend/src/scripts/drawing/rendering/webgl2-renderer.ts b/frontend/src/scripts/drawing/rendering/webgl2-renderer.ts index 191a70f..8de4339 100644 --- a/frontend/src/scripts/drawing/rendering/webgl2-renderer.ts +++ b/frontend/src/scripts/drawing/rendering/webgl2-renderer.ts @@ -1,34 +1,29 @@ import { mat2d, vec2, vec3 } from 'gl-matrix'; +import { CircleLight } from '../drawables/lights/circle-light'; +import { ILight } from '../drawables/lights/i-light'; +import { PointLight } from '../drawables/lights/point-light'; +import { IPrimitive } from '../drawables/primitives/i-primitive'; +import { TunnelShape } from '../drawables/primitives/tunnel-shape'; +// import lightsShader from '../shaders/rainbow-shading-fs.glsl'; +import { DefaultFrameBuffer } from '../graphics-library/frame-buffer/default-frame-buffer'; +import { IntermediateFrameBuffer } from '../graphics-library/frame-buffer/intermediate-frame-buffer'; +import { getWebGl2Context } from '../graphics-library/helper/get-webgl2-context'; +import { WebGlStopwatch } from '../graphics-library/helper/stopwatch'; +import { IRenderer } from '../i-renderer'; import caveFragmentShader from '../shaders/cave-distance-fs.glsl'; import lightsFragmentShader from '../shaders/lights-shading-fs.glsl'; import caveVertexShader from '../shaders/passthrough-distance-vs.glsl'; import lightsVertexShader from '../shaders/passthrough-shading-vs.glsl'; -// import lightsShader from '../shaders/rainbow-shading-fs.glsl'; -import { DefaultFrameBuffer } from '../graphics-library/frame-buffer/default-frame-buffer'; -import { IntermediateFrameBuffer } from '../graphics-library/frame-buffer/intermediate-frame-buffer'; -import { WebGlStopwatch } from '../graphics-library/helper/stopwatch'; -import { IProgram } from '../graphics-library/program/i-program'; -import { UniformArrayAutoScalingProgram } from '../graphics-library/program/uniform-array-autoscaling-program'; -import { IRenderer } from '../i-renderer'; -import { Circle } from '../drawables/primitives/circle'; -import { IPrimitive } from '../drawables/primitives/i-primitive'; -import { Rectangle } from '../drawables/primitives/rectangle'; -import { ILight } from '../drawables/lights/i-light'; -import { settings } from '../settings'; import { FpsAutoscaler } from './fps-autoscaler'; -import { getWebGl2Context } from '../graphics-library/helper/get-webgl2-context'; import { RenderingPass } from './rendering-pass'; -import { TunnelShape } from '../drawables/primitives/tunnel-shape'; -import { CircleLight } from '../drawables/lights/circle-light'; -import { PointLight } from '../drawables/lights/point-light'; export class WebGl2Renderer implements IRenderer { private gl: WebGL2RenderingContext; private stopwatch?: WebGlStopwatch; - private viewBox: Rectangle = new Rectangle(); - private viewCircle: Circle = new Circle(); - + private viewBoxBottomLeft = vec2.create(); + private cameraPosition = vec2.create(); + private viewBoxSize = vec2.create(); private cursorPosition = vec2.create(); private distanceFieldFrameBuffer: IntermediateFrameBuffer; @@ -38,6 +33,15 @@ export class WebGl2Renderer implements IRenderer { private autoscaler: FpsAutoscaler; + private matrices: { + distanceScreenToWorld?: mat2d; + worldToDistanceUV?: mat2d; + cursorPosition?: mat2d; + ndcToUv?: mat2d; + uvToWorld?: mat2d; + viewBoxSize?: mat2d; + } = { ndcToUv: mat2d.fromValues(0.5, 0, 0, 0.5, 0.5, 0.5) }; + constructor(private canvas: HTMLCanvasElement, private overlay: HTMLElement) { this.gl = getWebGl2Context(canvas); @@ -85,64 +89,69 @@ export class WebGl2Renderer implements IRenderer { } public finishFrame() { - const uniforms = this.calculateOwnUniforms(); - this.lightingPass.addDrawable( - new PointLight(uniforms.cursorPosition, 200, vec3.fromValues(1, 1, 0), 1) - ); - this.distancePass.render(uniforms, this.viewCircle); - this.lightingPass.render( - uniforms, - this.viewCircle, - this.distanceFieldFrameBuffer.colorTexture - ); - this.stopwatch?.stop(); - } - - private calculateOwnUniforms(): any { - const distanceScreenToWorld = this.getScreenToWorldTransform( - this.distanceFieldFrameBuffer.getSize() - ); - - const uvToWorld = mat2d.fromTranslation( - mat2d.create(), - this.viewBox.topLeft - ); - mat2d.scale(uvToWorld, uvToWorld, this.viewBox.size); - - const worldToDistanceUV = mat2d.scale( - mat2d.create(), - distanceScreenToWorld, - this.distanceFieldFrameBuffer.getSize() - ); - mat2d.invert(worldToDistanceUV, worldToDistanceUV); - - const ndcToUv = mat2d.fromScaling( - mat2d.create(), - vec2.fromValues(0.5, 0.5) - ); - mat2d.translate(ndcToUv, ndcToUv, vec2.fromValues(1, 1)); + this.calculateMatrices(); const cursorPosition = this.screenUvToWorldCoordinate(this.cursorPosition); - return { - distanceScreenToWorld, - worldToDistanceUV, - cursorPosition, - ndcToUv, - uvToWorld, - viewBoxSize: this.viewBox.size, - }; + this.lightingPass.addDrawable( + new PointLight(null, cursorPosition, 200, vec3.fromValues(1, 1, 0), 1) + ); + + const viewBoxRadius = vec2.length( + vec2.scale(vec2.create(), this.viewBoxSize, 0.5) + ); + + this.distancePass.render( + { ...this.matrices, cursorPosition }, + this.cameraPosition, + viewBoxRadius + ); + + this.lightingPass.render( + { ...this.matrices, cursorPosition }, + this.cameraPosition, + viewBoxRadius, + this.distanceFieldFrameBuffer.colorTexture + ); + + this.stopwatch?.stop(); + } + + private calculateMatrices() { + this.matrices.uvToWorld = mat2d.fromTranslation( + mat2d.create(), + this.viewBoxBottomLeft + ); + mat2d.scale( + this.matrices.uvToWorld, + this.matrices.uvToWorld, + this.viewBoxSize + ); + + this.matrices.distanceScreenToWorld = this.getScreenToWorldTransform( + this.distanceFieldFrameBuffer.getSize() + ); + + this.matrices.worldToDistanceUV = mat2d.scale( + mat2d.create(), + this.matrices.distanceScreenToWorld, + this.distanceFieldFrameBuffer.getSize() + ); + mat2d.invert( + this.matrices.worldToDistanceUV, + this.matrices.worldToDistanceUV + ); } private getScreenToWorldTransform(screenSize: vec2) { const transform = mat2d.fromTranslation( mat2d.create(), - this.viewBox.topLeft + this.viewBoxBottomLeft ); mat2d.scale( transform, transform, - vec2.divide(vec2.create(), this.viewBox.size, screenSize) + vec2.divide(vec2.create(), this.viewBoxSize, screenSize) ); mat2d.translate(transform, transform, vec2.fromValues(0.5, 0.5)); @@ -160,12 +169,10 @@ export class WebGl2Renderer implements IRenderer { } public setCameraPosition(position: vec2) { - this.viewBox.topLeft = position; - const halfDiagonal = vec2.scale(vec2.create(), this.viewBox.size, 0.5); - this.viewCircle.center = vec2.add( - vec2.create(), - this.viewBox.topLeft, - halfDiagonal + this.cameraPosition = position; + this.viewBoxBottomLeft = vec2.fromValues( + this.cameraPosition.x - this.viewBoxSize.x / 2, + this.cameraPosition.y - this.viewBoxSize.y / 2 ); } @@ -177,21 +184,10 @@ export class WebGl2Renderer implements IRenderer { const canvasAspectRatio = this.canvas.clientWidth / this.canvas.clientHeight; - this.viewBox.size = vec2.fromValues( + return (this.viewBoxSize = vec2.fromValues( Math.sqrt(size * canvasAspectRatio), Math.sqrt(size / canvasAspectRatio) - ); - - const halfDiagonal = vec2.scale(vec2.create(), this.viewBox.size, 0.5); - - this.viewCircle.center = vec2.add( - vec2.create(), - this.viewBox.topLeft, - halfDiagonal - ); - this.viewCircle.radius = vec2.length(halfDiagonal); - - return this.viewBox.size; + )); } public drawInfoText(text: string) { diff --git a/frontend/src/scripts/drawing/shaders/passthrough-shading-vs.glsl b/frontend/src/scripts/drawing/shaders/passthrough-shading-vs.glsl index 801fdcd..dcb4baa 100644 --- a/frontend/src/scripts/drawing/shaders/passthrough-shading-vs.glsl +++ b/frontend/src/scripts/drawing/shaders/passthrough-shading-vs.glsl @@ -6,6 +6,7 @@ precision mediump float; #define POINT_LIGHT_COUNT {pointLightCount} uniform mat3 modelTransform; +uniform mat3 cameraTransform; uniform mat3 ndcToUv; uniform mat3 uvToWorld; diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts index 81b48f5..07f55fe 100644 --- a/frontend/src/scripts/game.ts +++ b/frontend/src/scripts/game.ts @@ -6,16 +6,22 @@ import { timeIt } from './helper/timing'; import { KeyboardListener } from './input/keyboard-listener'; import { MouseListener } from './input/mouse-listener'; import { TouchListener } from './input/touch-listener'; -import { ObjectContainer } from './objects/object-container'; +import { Objects } from './objects/objects'; import { InfoText } from './objects/types/info-text'; import { createCharacter } from './objects/world/create-character'; import { createDungeon } from './objects/world/create-dungeon'; import { RenderCommand } from './drawing/commands/render'; +import { Physics } from './physics/physics'; +import { MoveToCommand } from './physics/commands/move-to'; +import { TeleportToCommand } from './physics/commands/teleport-to'; +import { IRenderer } from './drawing/i-renderer'; export class Game { private previousTime?: DOMHighResTimeStamp = null; - private objects: ObjectContainer = new ObjectContainer(); - private renderer: WebGl2Renderer; + private objects = new Objects(); + private physics = new Physics(); + + private renderer: IRenderer; private previousFpsValues: Array = []; constructor() { @@ -39,14 +45,18 @@ export class Game { this.renderer = new WebGl2Renderer(canvas, overlay); this.initializeScene(); + this.physics.start(); + requestAnimationFrame(this.gameLoop.bind(this)); } private initializeScene() { this.objects.addObject(new InfoText()); - createCharacter(this.objects); - createDungeon(this.objects); - createDungeon(this.objects); + const start = createDungeon(this.objects, this.physics); + createDungeon(this.objects, this.physics); + const character = createCharacter(this.objects, this.physics); + console.log('start', start.from); + character.sendCommand(new TeleportToCommand(start.from)); } private handleVisibilityChange() { diff --git a/frontend/src/scripts/objects/game-object.ts b/frontend/src/scripts/objects/game-object.ts index bf18ff4..3662f15 100644 --- a/frontend/src/scripts/objects/game-object.ts +++ b/frontend/src/scripts/objects/game-object.ts @@ -1,33 +1,16 @@ -import { vec2 } from 'gl-matrix'; import { Command } from '../commands/command'; import { CommandReceiver } from '../commands/command-receiver'; import { IdentityManager } from '../identity/identity-manager'; +import { Objects } from './objects'; +import { Physics } from '../physics/physics'; export abstract class GameObject implements CommandReceiver { public readonly id = IdentityManager.generateId(); - protected _position = vec2.create(); - public get position(): vec2 { - return this._position; - } - - protected _boundingBoxSize = vec2.create(); - public get boundingBoxSize(): vec2 { - return this._boundingBoxSize; - } - private commandExecutors: { [commandType: string]: (e: Command) => void; } = {}; - // can only be called inside the constructor - protected addCommandExecutor( - commandType: new () => T, - handler: (command: T) => void - ) { - this.commandExecutors[new commandType().type] = handler; - } - public reactsToCommand(commandType: string): boolean { return this.commandExecutors.hasOwnProperty(commandType); } @@ -39,4 +22,12 @@ export abstract class GameObject implements CommandReceiver { this.commandExecutors[commandType](command); } } + + // can only be called inside the constructor + protected addCommandExecutor( + commandType: new () => T, + handler: (command: T) => void + ) { + this.commandExecutors[new commandType().type] = handler; + } } diff --git a/frontend/src/scripts/objects/object-container.ts b/frontend/src/scripts/objects/objects.ts similarity index 95% rename from frontend/src/scripts/objects/object-container.ts rename to frontend/src/scripts/objects/objects.ts index ac44edf..cc46933 100644 --- a/frontend/src/scripts/objects/object-container.ts +++ b/frontend/src/scripts/objects/objects.ts @@ -3,7 +3,7 @@ import { CommandReceiver } from '../commands/command-receiver'; import { Id } from '../identity/identity'; import { GameObject } from './game-object'; -export class ObjectContainer implements CommandReceiver { +export class Objects implements CommandReceiver { private objects: Map = new Map(); private objectsGroupedByAbilities: Map> = new Map(); diff --git a/frontend/src/scripts/objects/types/camera.ts b/frontend/src/scripts/objects/types/camera.ts index 3b67929..19d2f12 100644 --- a/frontend/src/scripts/objects/types/camera.ts +++ b/frontend/src/scripts/objects/types/camera.ts @@ -5,14 +5,20 @@ import { GameObject } from '../game-object'; import { Lamp } from './lamp'; import { ZoomCommand } from '../../input/commands/zoom'; import { MoveToCommand } from '../../physics/commands/move-to'; +import { BoundingBox } from '../../physics/containers/bounding-box'; +import { Physics } from '../../physics/physics'; export class Camera extends GameObject { private inViewArea = 1920 * 1080 * 5; private cursorPosition = vec2.create(); + private boundingBox: BoundingBox; - constructor(private light: Lamp) { + constructor(physics: Physics, private light: Lamp) { super(); + this.boundingBox = new BoundingBox(null); + physics.addDynamicBoundingBox(this.boundingBox); + this.addCommandExecutor(BeforeRenderCommand, this.draw.bind(this)); this.addCommandExecutor(MoveToCommand, this.moveTo.bind(this)); this.addCommandExecutor( @@ -22,20 +28,27 @@ export class Camera extends GameObject { this.addCommandExecutor(ZoomCommand, this.zoom.bind(this)); } + public get viewAreaSize(): vec2 { + return this.boundingBox.size; + } + private draw(c: BeforeRenderCommand) { - c.renderer.setCameraPosition(this.position); + console.log('camera', this.boundingBox.topLeft); + + c.renderer.setCameraPosition(this.boundingBox.topLeft); c.renderer.setCursorPosition(this.cursorPosition); - this._boundingBoxSize = c.renderer.setInViewArea(this.inViewArea); + this.boundingBox.size = c.renderer.setInViewArea(this.inViewArea); } private moveTo(c: MoveToCommand) { - this._position = c.position; + console.log('camera', c.position); + this.boundingBox.topLeft = c.position; this.light.sendCommand( new MoveToCommand( vec2.add( vec2.create(), c.position, - vec2.scale(vec2.create(), this.boundingBoxSize, 0.5) + vec2.scale(vec2.create(), this.boundingBox.size, 0.5) ) ) ); diff --git a/frontend/src/scripts/objects/types/character.ts b/frontend/src/scripts/objects/types/character.ts index 23ea155..a094638 100644 --- a/frontend/src/scripts/objects/types/character.ts +++ b/frontend/src/scripts/objects/types/character.ts @@ -1,37 +1,80 @@ import { vec2 } from 'gl-matrix'; -import { GameObject } from '../game-object'; -import { Camera } from './camera'; -import { StepCommand } from '../../physics/commands/step'; +import { Circle } from '../../drawing/drawables/primitives/circle'; import { KeyDownCommand } from '../../input/commands/key-down'; import { KeyUpCommand } from '../../input/commands/key-up'; import { SwipeCommand } from '../../input/commands/swipe'; import { MoveToCommand } from '../../physics/commands/move-to'; +import { StepCommand } from '../../physics/commands/step'; +import { TeleportToCommand } from '../../physics/commands/teleport-to'; +import { BoundingBox } from '../../physics/containers/bounding-box'; +import { Physics } from '../../physics/physics'; +import { GameObject } from '../game-object'; +import { Camera } from './camera'; export class Character extends GameObject { private keysDown: Set = new Set(); - private static speed = 0.5; + private primitive: Circle; + private static speed = 1.5; - constructor(private camera: Camera) { + constructor(private physics: Physics, private camera: Camera) { super(); + this.primitive = new Circle(this); + this.primitive.radius = 20; + this.addCommandExecutor(StepCommand, this.stepHandler.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(SwipeCommand, (c) => - this.setPosition( + this.checkAndSetPosition( vec2.add( vec2.create(), - this.position, - vec2.multiply(vec2.create(), c.delta, this.camera.boundingBoxSize) + this.primitive.center, + vec2.multiply(vec2.create(), c.delta, this.camera.viewAreaSize) ) ) ); } + private checkAndSetPosition(value: vec2) { + const size = this.camera.viewAreaSize; + const realCenter = vec2.fromValues( + value.x + size.x / 2, + value.y + size.y / 2 + ); + + const targetBoundingBox = new BoundingBox( + null, + value.x + size.x / 2, + value.x + size.x / 2 + 10, + value.y + size.y / 2, + value.y + size.y / 2 + 10 + ); + + console.log( + this.physics + .findIntersecting(targetBoundingBox) + .map((b) => b.value.distance(realCenter)) + ); + if ( + this.physics + .findIntersecting(targetBoundingBox) + .map((b) => b.value.distance(realCenter)) + .find((d) => d < 0) !== undefined + ) { + this.setPosition(value); + } + } + private setPosition(value: vec2) { - this._position = value; - this.camera.sendCommand(new MoveToCommand(this.position)); + console.log('character', value); + + this.primitive.center = value; + this.camera.sendCommand(new MoveToCommand(this.primitive.center)); } public stepHandler(c: StepCommand) { @@ -44,16 +87,11 @@ export class Character extends GameObject { const movementVector = vec2.fromValues(right - left, up - down); if (movementVector.length > 0) { - this.setPosition( - vec2.add( - vec2.create(), - this.position, - vec2.scale( - vec2.create(), - vec2.normalize(movementVector, movementVector), - Character.speed * deltaTime - ) - ) + vec2.normalize(movementVector, movementVector); + vec2.scale(movementVector, movementVector, Character.speed * deltaTime); + + this.checkAndSetPosition( + vec2.add(vec2.create(), this.primitive.center, movementVector) ); } } diff --git a/frontend/src/scripts/objects/types/lamp.ts b/frontend/src/scripts/objects/types/lamp.ts index 1e31d7d..fbb19af 100644 --- a/frontend/src/scripts/objects/types/lamp.ts +++ b/frontend/src/scripts/objects/types/lamp.ts @@ -10,7 +10,7 @@ export class Lamp extends GameObject { constructor(center: vec2, radius: number, color: vec3, lightness: number) { super(); - this.light = new CircleLight(center, radius, color, lightness); + this.light = new CircleLight(this, center, radius, color, lightness); this.addCommandExecutor(RenderCommand, this.draw.bind(this)); this.addCommandExecutor(MoveToCommand, this.moveTo.bind(this)); diff --git a/frontend/src/scripts/objects/types/tunnel.ts b/frontend/src/scripts/objects/types/tunnel.ts index 4dd510b..7e8f972 100644 --- a/frontend/src/scripts/objects/types/tunnel.ts +++ b/frontend/src/scripts/objects/types/tunnel.ts @@ -1,16 +1,25 @@ import { vec2 } from 'gl-matrix'; -import { TunnelShape } from '../../drawing/drawables/primitives/tunnel-shape'; import { GameObject } from '../game-object'; import { RenderCommand } from '../../drawing/commands/render'; +import { Physics } from '../../physics/physics'; +import { TunnelShape } from '../../drawing/drawables/primitives/tunnel-shape'; export interface Line {} export class Tunnel extends GameObject { private primitive: TunnelShape; - constructor(from: vec2, to: vec2, fromRadius: number, toRadius: number) { + constructor( + physics: Physics, + public readonly from: vec2, + to: vec2, + fromRadius: number, + toRadius: number + ) { super(); - this.primitive = new TunnelShape(from, to, fromRadius, toRadius); + + this.primitive = new TunnelShape(this, from, to, fromRadius, toRadius); + physics.addStaticBoundingBox(this.primitive.boundingBox); this.addCommandExecutor(RenderCommand, this.draw.bind(this)); } diff --git a/frontend/src/scripts/objects/world/create-character.ts b/frontend/src/scripts/objects/world/create-character.ts index fa5582a..8f0e1e0 100644 --- a/frontend/src/scripts/objects/world/create-character.ts +++ b/frontend/src/scripts/objects/world/create-character.ts @@ -1,10 +1,15 @@ import { vec2, vec3 } from 'gl-matrix'; -import { ObjectContainer } from '../object-container'; +import { Objects } from '../objects'; import { Camera } from '../types/camera'; import { Character } from '../types/character'; import { Lamp } from '../types/lamp'; +import { Physics } from '../../physics/physics'; +import { GameObject } from '../game-object'; -export const createCharacter = (objects: ObjectContainer) => { +export const createCharacter = ( + objects: Objects, + physics: Physics +): GameObject => { const light = new Lamp( vec2.create(), 40, @@ -12,8 +17,11 @@ export const createCharacter = (objects: ObjectContainer) => { 2 ); - const camera = new Camera(light); + const camera = new Camera(physics, light); + const character = new Character(physics, camera); objects.addObject(light); objects.addObject(camera); - objects.addObject(new Character(camera)); + objects.addObject(character); + + return character; }; diff --git a/frontend/src/scripts/objects/world/create-dungeon.ts b/frontend/src/scripts/objects/world/create-dungeon.ts index 91541d7..948e187 100644 --- a/frontend/src/scripts/objects/world/create-dungeon.ts +++ b/frontend/src/scripts/objects/world/create-dungeon.ts @@ -1,22 +1,35 @@ import { vec2, vec3 } from 'gl-matrix'; -import { ObjectContainer } from '../object-container'; -import { Lamp } from '../types/lamp'; +import { Objects } from '../objects'; import { Tunnel } from '../types/tunnel'; +import { Physics } from '../../physics/physics'; +import { GameObject } from '../game-object'; -export const createDungeon = (objects: ObjectContainer) => { +export const createDungeon = (objects: Objects, physics: Physics): Tunnel => { let previousRadius = 350; let previousEnd = vec2.create(); - for (let i = 0; i < 500000; i += 500) { + let first: Tunnel; + + for (let i = 0; i < 1; i += 500) { const deltaHeight = (Math.random() - 0.5) * 2000; const height = previousEnd.y + deltaHeight; const currentEnd = vec2.fromValues(i, height); const currentToRadius = Math.random() * 300 + 150; - objects.addObject( - new Tunnel(previousEnd, currentEnd, previousRadius, currentToRadius) + const tunnel = new Tunnel( + physics, + previousEnd, + currentEnd, + previousRadius, + currentToRadius ); + if (!first) { + first = tunnel; + } + + objects.addObject(tunnel); + /*if (deltaHeight > 0 && Math.random() > 0.8) { objects.addObject( new Lamp( @@ -35,4 +48,6 @@ export const createDungeon = (objects: ObjectContainer) => { previousEnd = currentEnd; previousRadius = currentToRadius; } + + return first; }; diff --git a/frontend/src/scripts/physics/commands/teleport-to.ts b/frontend/src/scripts/physics/commands/teleport-to.ts new file mode 100644 index 0000000..46d1758 --- /dev/null +++ b/frontend/src/scripts/physics/commands/teleport-to.ts @@ -0,0 +1,12 @@ +import { vec2 } from 'gl-matrix'; +import { Command } from '../../commands/command'; + +export class TeleportToCommand extends Command { + public constructor(public readonly position?: vec2) { + super(); + } + + public get type(): string { + return 'TeleportToCommand'; + } +} diff --git a/frontend/src/scripts/physics/containers/bounding-box-base.ts b/frontend/src/scripts/physics/containers/bounding-box-base.ts new file mode 100644 index 0000000..b674561 --- /dev/null +++ b/frontend/src/scripts/physics/containers/bounding-box-base.ts @@ -0,0 +1,61 @@ +import { vec2 } from 'gl-matrix'; +import { IPrimitive } from '../../drawing/drawables/primitives/i-primitive'; + +export abstract class BoundingBoxBase { + constructor( + public readonly value: IPrimitive, + protected _xMin: number, + protected _xMax: number, + protected _yMin: number, + protected _yMax: number + ) {} + + public get 0(): number { + return this._xMin; + } + + public get 1(): number { + return this._xMax; + } + + public get 2(): number { + return this._yMin; + } + + public get 3(): number { + return this._yMax; + } + + public get xMin(): number { + return this._xMin; + } + + public get xMax(): number { + return this._xMax; + } + + public get yMin(): number { + return this._yMin; + } + + public get yMax(): number { + return this._yMax; + } + + public get topLeft(): vec2 { + return vec2.fromValues(this._xMin, this._yMax); + } + + public get size(): vec2 { + return vec2.fromValues(this._xMax - this._xMin, this._yMax - this._yMin); + } + + public intersects(other: BoundingBoxBase): boolean { + return ( + this._xMin < other._xMax && + this._xMax > other._xMin && + this._yMin < other._yMax && + this._yMax > other._yMin + ); + } +} diff --git a/frontend/src/scripts/physics/containers/bounding-box-list.ts b/frontend/src/scripts/physics/containers/bounding-box-list.ts new file mode 100644 index 0000000..a16f70e --- /dev/null +++ b/frontend/src/scripts/physics/containers/bounding-box-list.ts @@ -0,0 +1,13 @@ +import { BoundingBoxBase } from './bounding-box-base'; + +export class BoundingBoxList { + constructor(private boundingBoxes: Array = []) {} + + public insert(box: BoundingBoxBase) { + this.boundingBoxes.push(box); + } + + public findIntersecting(box: BoundingBoxBase): Array { + return this.boundingBoxes.filter((b) => b.intersects(box)); + } +} diff --git a/frontend/src/scripts/physics/containers/bounding-box-tree.ts b/frontend/src/scripts/physics/containers/bounding-box-tree.ts new file mode 100644 index 0000000..058dbf9 --- /dev/null +++ b/frontend/src/scripts/physics/containers/bounding-box-tree.ts @@ -0,0 +1,138 @@ +import { ImmutableBoundingBox } from './immutable-bounding-box'; + +// source: https://github.com/ubilabs/kd-tree-javascript/blob/master/kdTree.js + +class Node { + public left?: Node = null; + public right?: Node = null; + + constructor(public rectangle: ImmutableBoundingBox, public parent: Node) {} +} + +export class BoundingBoxTree { + root?: Node; + + constructor(boxes?: Array) { + if (boxes) { + this.build(boxes); + } + } + + public build(boxes: Array) { + this.root = this.buildRecursive(boxes, 0, null); + } + + private buildRecursive( + boxes: Array, + depth: number, + parent: Node + ): Node { + if (boxes.length === 0) { + return null; + } + + if (boxes.length === 1) { + return new Node(boxes[0], parent); + } + + const dimension = depth % 4; + + boxes.sort((a, b) => a[dimension] - b[dimension]); + + const median = Math.floor(boxes.length / 2); + + const node = new Node(boxes[median], parent); + node.left = this.buildRecursive(boxes.slice(0, median), depth + 1, node); + node.right = this.buildRecursive(boxes.slice(median + 1), depth + 1, node); + + return node; + } + + public insert(box: ImmutableBoundingBox) { + const [insertPosition, depth] = this.findParent(box, this.root, 0, null); + + if (insertPosition === null) { + this.root = new Node(box, null); + } else { + const node = new Node(box, insertPosition); + const dimension = depth % 4; + + if (box[dimension] < insertPosition.rectangle[dimension]) { + insertPosition.left = node; + } else { + insertPosition.right = node; + } + } + } + + public print() { + this.printRecursive(this.root, 0); + } + + private printRecursive(node: Node, tabCount: number) { + if (node === null) { + return; + } + + console.log(' '.repeat(tabCount) + '- ' + node.rectangle.value); + this.printRecursive(node.left, tabCount + 2); + this.printRecursive(node.right, tabCount + 2); + } + + public findIntersecting( + box: ImmutableBoundingBox + ): Array { + const maybeResults = this.findMaybeIntersecting(box, this.root, 0); + const results = maybeResults.filter(box.intersects.bind(box)); + return results; + } + + private findMaybeIntersecting( + box: ImmutableBoundingBox, + node: Node, + depth: number + ): Array { + if (node === null) { + return []; + } + + const comparisons: Array<( + a: ImmutableBoundingBox, + b: ImmutableBoundingBox + ) => boolean> = [ + (a, b) => a.xMin < b.xMax, + (a, b) => a.xMax > b.xMin, + (a, b) => a.yMin < b.yMax, + (a, b) => a.xMax > b.xMin, + ]; + + if (comparisons[depth % 4](node.rectangle, box)) { + return [ + node.rectangle, + ...this.findMaybeIntersecting(box, node.left, depth + 1), + ...this.findMaybeIntersecting(box, node.right, depth + 1), + ]; + } + + return this.findMaybeIntersecting(box, node.left, depth + 1); + } + + private findParent( + box: ImmutableBoundingBox, + node: Node, + depth: number, + parent: Node + ): [Node, number] { + if (node === null) { + return [parent, depth]; + } + + const dimension = depth % 4; + + if (box[dimension] < node.rectangle[dimension]) { + return this.findParent(box, node.left, depth + 1, node); + } + + return this.findParent(box, node.right, depth + 1, node); + } +} diff --git a/frontend/src/scripts/physics/containers/bounding-box.ts b/frontend/src/scripts/physics/containers/bounding-box.ts new file mode 100644 index 0000000..5fda1ab --- /dev/null +++ b/frontend/src/scripts/physics/containers/bounding-box.ts @@ -0,0 +1,32 @@ +import { vec2 } from 'gl-matrix'; +import { BoundingBoxBase } from './bounding-box-base'; +import { IPrimitive } from '../../drawing/drawables/primitives/i-primitive'; + +export class BoundingBox extends BoundingBoxBase { + constructor( + value: IPrimitive, + xMin: number = 0, + xMax: number = 0, + yMin: number = 0, + yMax: number = 0 + ) { + super(value, xMin, xMax, yMin, yMax); + } + + public get topLeft(): vec2 { + return vec2.fromValues(this._xMin, this._yMax); + } + + public get size(): vec2 { + return vec2.fromValues(this._xMax - this._xMin, this._yMax - this._yMin); + } + public set topLeft(value: vec2) { + this._xMin = value.x; + this._yMax = value.y; + } + + public set size(value: vec2) { + this._xMax = this.xMin + value.x; + this._yMin = this.yMax - value.y; + } +} diff --git a/frontend/src/scripts/physics/containers/immutable-bounding-box.ts b/frontend/src/scripts/physics/containers/immutable-bounding-box.ts new file mode 100644 index 0000000..1aabc35 --- /dev/null +++ b/frontend/src/scripts/physics/containers/immutable-bounding-box.ts @@ -0,0 +1,14 @@ +import { BoundingBoxBase } from './bounding-box-base'; +import { IPrimitive } from '../../drawing/drawables/primitives/i-primitive'; + +export class ImmutableBoundingBox extends BoundingBoxBase { + constructor( + value: IPrimitive, + xMin: number = 0, + xMax: number = 0, + yMin: number = 0, + yMax: number = 0 + ) { + super(value, xMin, xMax, yMin, yMax); + } +} diff --git a/frontend/src/scripts/physics/physics.ts b/frontend/src/scripts/physics/physics.ts new file mode 100644 index 0000000..77dcd57 --- /dev/null +++ b/frontend/src/scripts/physics/physics.ts @@ -0,0 +1,36 @@ +import { BoundingBoxTree } from './containers/bounding-box-tree'; +import { BoundingBoxList } from './containers/bounding-box-list'; +import { ImmutableBoundingBox } from './containers/immutable-bounding-box'; +import { BoundingBoxBase } from './containers/bounding-box-base'; + +export class Physics { + private staticBoundingBoxesWaitList = []; + private isTreeInitialized = false; + private staticBoundingBoxes = new BoundingBoxTree(); + + private dynamicBoundingBoxes = new BoundingBoxList(); + + public addStaticBoundingBox(boundingBox: ImmutableBoundingBox) { + this.staticBoundingBoxesWaitList.push(boundingBox); + } + + public addDynamicBoundingBox(boundingBox: BoundingBoxBase) { + if (this.isTreeInitialized) { + this.staticBoundingBoxes.insert(boundingBox); + } else { + this.dynamicBoundingBoxes.insert(boundingBox); + } + } + + public findIntersecting(box: BoundingBoxBase): Array { + return [ + ...this.staticBoundingBoxes.findIntersecting(box), + ...this.dynamicBoundingBoxes.findIntersecting(box), + ]; + } + + public start() { + this.staticBoundingBoxes.build(this.staticBoundingBoxesWaitList); + this.isTreeInitialized = true; + } +} diff --git a/infrastructure/init-letsencrypt.sh b/infrastructure/init-letsencrypt.sh index e81379c..8720454 100644 --- a/infrastructure/init-letsencrypt.sh +++ b/infrastructure/init-letsencrypt.sh @@ -8,7 +8,7 @@ certbot certonly \ --dns-digitalocean-propagation-seconds 120 \ -m "schmelczerandras@gmail.com" \ -d "decla.red" -d "*.decla.red" \ - --agree-tos + --agree-tos --non-interactive curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "/etc/letsencrypt/options-ssl-nginx.conf" curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "/etc/letsencrypt/ssl-dhparams.pem" \ No newline at end of file