From efb01476b2d9af1322cf07d9b760d3d2b01c3a7d Mon Sep 17 00:00:00 2001 From: schmelczerandras Date: Sun, 19 Jul 2020 13:50:03 +0200 Subject: [PATCH] Add basic gameobject system --- frontend/package.json | 2 + frontend/src/index.html | 5 +- frontend/src/index.ts | 4 +- frontend/src/scripts/commands/command.ts | 5 +- frontend/src/scripts/commands/types/draw.ts | 8 +++ .../src/scripts/commands/types/key-down.ts | 2 +- frontend/src/scripts/commands/types/key-up.ts | 2 +- .../src/scripts/commands/types/move-to.ts | 9 +++ .../scripts/commands/types/primary-action.ts | 2 +- .../commands/types/secondary-action.ts | 2 +- frontend/src/scripts/commands/types/step.ts | 9 +++ frontend/src/scripts/commands/types/swipe.ts | 2 +- frontend/src/scripts/drawing/drawer.ts | 9 +++ .../src/scripts/drawing/graphics-library.ts | 1 + frontend/src/scripts/drawing/renderer.ts | 34 ++++++++--- frontend/src/scripts/game-logic/game-logic.ts | 54 ----------------- frontend/src/scripts/game.ts | 60 +++++++++++++++++++ frontend/src/scripts/helper/timing.ts | 49 +++++++++------ .../src/scripts/identity/identity-manager.ts | 8 ++- frontend/src/scripts/identity/identity.ts | 1 + .../src/scripts/input/keyboard-listener.ts | 2 +- frontend/src/scripts/input/mouse-listener.ts | 10 ++-- frontend/src/scripts/input/touch-listener.ts | 14 +++-- frontend/src/scripts/main.ts | 48 --------------- frontend/src/scripts/math/vec2.ts | 19 ++++++ frontend/src/scripts/objects/game-object.ts | 37 +++++++++++- .../src/scripts/objects/object-container.ts | 24 ++++++-- frontend/src/scripts/objects/types/camera.ts | 22 ++++++- .../src/scripts/objects/types/character.ts | 53 ++++++++++++++++ .../src/scripts/objects/types/info-text.ts | 22 +++++++ frontend/src/styles/main.scss | 44 ++++++++++---- frontend/tsconfig.json | 3 +- frontend/webpack.config.js | 4 -- 33 files changed, 399 insertions(+), 171 deletions(-) create mode 100644 frontend/src/scripts/commands/types/draw.ts create mode 100644 frontend/src/scripts/commands/types/move-to.ts create mode 100644 frontend/src/scripts/commands/types/step.ts create mode 100644 frontend/src/scripts/drawing/drawer.ts create mode 100644 frontend/src/scripts/drawing/graphics-library.ts delete mode 100644 frontend/src/scripts/game-logic/game-logic.ts create mode 100644 frontend/src/scripts/game.ts create mode 100644 frontend/src/scripts/identity/identity.ts delete mode 100644 frontend/src/scripts/main.ts create mode 100644 frontend/src/scripts/objects/types/character.ts create mode 100644 frontend/src/scripts/objects/types/info-text.ts diff --git a/frontend/package.json b/frontend/package.json index 5a4e037..3b07fb7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,6 +6,7 @@ "main": "index.js", "scripts": { "start": "webpack-dev-server --mode development", + "start-unsafe": "webpack-dev-server --mode development --useLocalIp", "build": "webpack && find dist -type f -not -name '*.html' | xargs rm" }, "keywords": [], @@ -25,6 +26,7 @@ "usedExports": true }, "devDependencies": { + "@types/uuid": "^8.0.0", "autoprefixer": "^9.8.5", "clean-webpack-plugin": "^3.0.0", "css-loader": "^3.5.2", diff --git a/frontend/src/index.html b/frontend/src/index.html index 1774dc2..ab49e95 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -17,7 +17,10 @@
- +
+
+ +
diff --git a/frontend/src/index.ts b/frontend/src/index.ts index aadeadf..5687ef5 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -1,4 +1,4 @@ import './styles/main.scss'; -import { main } from './scripts/main'; +import { Game } from './scripts/game'; -main(); +new Game(); diff --git a/frontend/src/scripts/commands/command.ts b/frontend/src/scripts/commands/command.ts index f53a31a..ed5a9ff 100644 --- a/frontend/src/scripts/commands/command.ts +++ b/frontend/src/scripts/commands/command.ts @@ -1,3 +1,6 @@ import { Typed } from '../transport/serializable'; +import { Id } from '../identity/identity'; -export abstract class Command extends Typed {} +export abstract class Command extends Typed { + target?: Id; +} diff --git a/frontend/src/scripts/commands/types/draw.ts b/frontend/src/scripts/commands/types/draw.ts new file mode 100644 index 0000000..d453aa9 --- /dev/null +++ b/frontend/src/scripts/commands/types/draw.ts @@ -0,0 +1,8 @@ +import { Drawer } from '../../drawing/drawer'; +import { Command } from '../command'; + +export class DrawCommand extends Command { + public constructor(public readonly drawer?: Drawer) { + super(); + } +} diff --git a/frontend/src/scripts/commands/types/key-down.ts b/frontend/src/scripts/commands/types/key-down.ts index 6adcc0b..ff48a0b 100644 --- a/frontend/src/scripts/commands/types/key-down.ts +++ b/frontend/src/scripts/commands/types/key-down.ts @@ -1,7 +1,7 @@ import { Command } from '../command'; export class KeyDownCommand extends Command { - public constructor(public readonly key: string) { + public constructor(public readonly key?: string) { super(); } } diff --git a/frontend/src/scripts/commands/types/key-up.ts b/frontend/src/scripts/commands/types/key-up.ts index b70f7f7..f9c72cd 100644 --- a/frontend/src/scripts/commands/types/key-up.ts +++ b/frontend/src/scripts/commands/types/key-up.ts @@ -1,7 +1,7 @@ import { Command } from '../command'; export class KeyUpCommand extends Command { - public constructor(public readonly key: string) { + public constructor(public readonly key?: string) { super(); } } diff --git a/frontend/src/scripts/commands/types/move-to.ts b/frontend/src/scripts/commands/types/move-to.ts new file mode 100644 index 0000000..da0f29b --- /dev/null +++ b/frontend/src/scripts/commands/types/move-to.ts @@ -0,0 +1,9 @@ +import { Drawer } from '../../drawing/drawer'; +import { Command } from '../command'; +import { Vec2 } from '../../math/vec2'; + +export class MoveToCommand extends Command { + public constructor(public readonly position?: Vec2) { + super(); + } +} diff --git a/frontend/src/scripts/commands/types/primary-action.ts b/frontend/src/scripts/commands/types/primary-action.ts index 6a9d443..8797939 100644 --- a/frontend/src/scripts/commands/types/primary-action.ts +++ b/frontend/src/scripts/commands/types/primary-action.ts @@ -2,7 +2,7 @@ import { Command } from '../command'; import { Vec2 } from '../../math/vec2'; export class PrimaryActionCommand extends Command { - public constructor(public readonly position: Vec2) { + public constructor(public readonly position?: Vec2) { super(); } } diff --git a/frontend/src/scripts/commands/types/secondary-action.ts b/frontend/src/scripts/commands/types/secondary-action.ts index 369701c..e3e946e 100644 --- a/frontend/src/scripts/commands/types/secondary-action.ts +++ b/frontend/src/scripts/commands/types/secondary-action.ts @@ -2,7 +2,7 @@ import { Command } from '../command'; import { Vec2 } from '../../math/vec2'; export class SecondaryActionCommand extends Command { - public constructor(public readonly position: Vec2) { + public constructor(public readonly position?: Vec2) { super(); } } diff --git a/frontend/src/scripts/commands/types/step.ts b/frontend/src/scripts/commands/types/step.ts new file mode 100644 index 0000000..c54f398 --- /dev/null +++ b/frontend/src/scripts/commands/types/step.ts @@ -0,0 +1,9 @@ +import { Command } from '../command'; + +export class StepCommand extends Command { + public constructor( + public readonly deltaTimeInMiliseconds?: DOMHighResTimeStamp + ) { + super(); + } +} diff --git a/frontend/src/scripts/commands/types/swipe.ts b/frontend/src/scripts/commands/types/swipe.ts index 929129e..f74e391 100644 --- a/frontend/src/scripts/commands/types/swipe.ts +++ b/frontend/src/scripts/commands/types/swipe.ts @@ -2,7 +2,7 @@ import { Command } from '../command'; import { Vec2 } from '../../math/vec2'; export class SwipeCommand extends Command { - public constructor(public readonly delta: Vec2) { + public constructor(public readonly delta?: Vec2) { super(); } } diff --git a/frontend/src/scripts/drawing/drawer.ts b/frontend/src/scripts/drawing/drawer.ts new file mode 100644 index 0000000..25c8ede --- /dev/null +++ b/frontend/src/scripts/drawing/drawer.ts @@ -0,0 +1,9 @@ +import { Vec2 } from '../math/vec2'; + +export interface Drawer { + startWaitingForInstructions(); + finishWaitingForInstructions(); + setCameraPosition(position: Vec2); + setViewBoxSize(size: Vec2); + drawCornerText(text: string); +} diff --git a/frontend/src/scripts/drawing/graphics-library.ts b/frontend/src/scripts/drawing/graphics-library.ts new file mode 100644 index 0000000..9c73922 --- /dev/null +++ b/frontend/src/scripts/drawing/graphics-library.ts @@ -0,0 +1 @@ +//export function diff --git a/frontend/src/scripts/drawing/renderer.ts b/frontend/src/scripts/drawing/renderer.ts index 11b27f7..8ffed11 100644 --- a/frontend/src/scripts/drawing/renderer.ts +++ b/frontend/src/scripts/drawing/renderer.ts @@ -1,10 +1,9 @@ const twgl = require('twgl.js'); -import { TimeIt } from '../helper/timing'; +import { Drawer } from './drawer'; import { Vec2 } from '../math/vec2'; -import { ObjectContainer } from '../objects/object-container'; -export class Renderer { +export class WebGl2Renderer implements Drawer { private gl: WebGL2RenderingContext; private programInfo: any; private bufferInfo: any; @@ -12,7 +11,7 @@ export class Renderer { constructor( private canvas: HTMLCanvasElement, - private objects: ObjectContainer, + private overlay: HTMLElement, shaderSources: Array ) { twgl.setDefaults({ attribPrefix: 'a_' }); @@ -42,7 +41,14 @@ export class Renderer { ); } - public render(time: number) { + startWaitingForInstructions() { + //throw new Error("Method not implemented."); + } + + private cameraPosition: Vec2; + private viewBoxSize: Vec2; + + finishWaitingForInstructions() { const gl = this.gl; twgl.resizeCanvasToDisplaySize(this.canvas); @@ -51,8 +57,8 @@ export class Renderer { gl.clear(gl.COLOR_BUFFER_BIT); const uniforms = { - cameraPosition: this.objects.camera.position.list, - viewBoxSize: [this.gl.canvas.width, this.gl.canvas.height], + cameraPosition: this.cameraPosition.list, + viewBoxSize: this.viewBoxSize.list, resolution: [this.gl.canvas.width, this.gl.canvas.height], }; @@ -63,4 +69,18 @@ export class Renderer { twgl.setUniforms(this.programInfo, uniforms); twgl.drawBufferInfo(this.gl, this.bufferInfo); } + + setCameraPosition(position: Vec2) { + this.cameraPosition = position; + } + + setViewBoxSize(size: Vec2) { + this.viewBoxSize = size; + } + + drawCornerText(text: string) { + if (this.overlay.innerText != text) { + this.overlay.innerText = text; + } + } } diff --git a/frontend/src/scripts/game-logic/game-logic.ts b/frontend/src/scripts/game-logic/game-logic.ts deleted file mode 100644 index 60cc378..0000000 --- a/frontend/src/scripts/game-logic/game-logic.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Command } from '../commands/command'; -import { CommandReceiver } from '../commands/command-receiver'; -import { ObjectContainer } from '../objects/object-container'; -import { KeyDownCommand } from '../commands/types/key-down'; -import { KeyUpCommand } from '../commands/types/key-up'; -import { SwipeCommand } from '../commands/types/swipe'; - -export class GameLogic implements CommandReceiver { - private commandBuffer: Array = []; - private keysDown: Set = new Set(); - - constructor(private objects: ObjectContainer) {} - - public step(time: DOMHighResTimeStamp) { - while (this.commandBuffer.length > 0) { - const command = this.commandBuffer.pop(); - console.log(command); - - if (command instanceof KeyDownCommand) { - this.keysDown.add(command.key); - } - - if (command instanceof KeyUpCommand) { - this.keysDown.delete(command.key); - } - - if (command instanceof SwipeCommand) { - this.objects.camera.position = this.objects.camera.position.subtract( - command.delta.scale(200) - ); - } - } - - if (this.keysDown.has('w')) { - this.objects.camera.position.y += 10; - } - - if (this.keysDown.has('s')) { - this.objects.camera.position.y -= 10; - } - - if (this.keysDown.has('a')) { - this.objects.camera.position.x -= 10; - } - - if (this.keysDown.has('d')) { - this.objects.camera.position.x += 10; - } - } - - public sendCommand(command: Command) { - this.commandBuffer.push(command); - } -} diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts new file mode 100644 index 0000000..621b4a6 --- /dev/null +++ b/frontend/src/scripts/game.ts @@ -0,0 +1,60 @@ +import { WebGl2Renderer } from './drawing/renderer'; +import { KeyboardListener } from './input/keyboard-listener'; +import { MouseListener } from './input/mouse-listener'; +import { TouchListener } from './input/touch-listener'; +import { CommandBroadcaster } from './commands/command-broadcaster'; +import { ObjectContainer } from './objects/object-container'; +import { DrawCommand } from './commands/types/draw'; +import passthroughVertexShader from '../shaders/passthrough.vert'; +import distanceFragmentShader from '../shaders/dist.frag'; +import { StepCommand } from './commands/types/step'; +import { Character } from './objects/types/character'; +import { InfoText } from './objects/types/info-text'; +import { timeIt } from './helper/timing'; + +export class Game { + private previousTime: DOMHighResTimeStamp = 0; + private objects: ObjectContainer = new ObjectContainer(); + private renderer: WebGl2Renderer; + + constructor() { + const canvas: HTMLCanvasElement = document.querySelector('canvas#main'); + const overlay: HTMLElement = document.querySelector('#overlay'); + + new CommandBroadcaster( + [ + new KeyboardListener(document.body), + new MouseListener(canvas), + new TouchListener(canvas), + ], + [this.objects] + ); + + this.renderer = new WebGl2Renderer(canvas, overlay, [ + passthroughVertexShader, + distanceFragmentShader, + ]); + + this.initializeScene(); + requestAnimationFrame(this.gameLoop.bind(this)); + } + + private initializeScene() { + this.objects.addObject(new Character(this.objects)); + this.objects.addObject(new InfoText()); + } + + @timeIt() + private gameLoop(time: DOMHighResTimeStamp) { + const deltaTime = time - this.previousTime; + this.previousTime = time; + + this.objects.sendCommand(new StepCommand(deltaTime)); + + this.renderer.startWaitingForInstructions(); + this.objects.sendCommand(new DrawCommand(this.renderer)); + this.renderer.finishWaitingForInstructions(); + + requestAnimationFrame(this.gameLoop.bind(this)); + } +} diff --git a/frontend/src/scripts/helper/timing.ts b/frontend/src/scripts/helper/timing.ts index 70e9eab..6da804a 100644 --- a/frontend/src/scripts/helper/timing.ts +++ b/frontend/src/scripts/helper/timing.ts @@ -1,22 +1,37 @@ -export const TimeIt = (func, interval = 60) => { - let i = 0; - let values = []; +import { InfoText } from '../objects/types/info-text'; - return () => { - const start = performance.now(); - func(); - const end = performance.now(); +export function timeIt(interval = 60) { + return function ( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor + ) { + let i = 0; + let previousTimes: Array = []; - values.push(end - start); - if (++i % interval == 0) { - values.sort(); - console.log( - `${func.name}\n\tMax ${values[values.length - 1].toFixed( + const targetFunction = descriptor.value; + + descriptor.value = function (...values) { + const start = performance.now(); + targetFunction.bind(this)(...values); + const end = performance.now(); + + previousTimes.push(end - start); + + if (i++ % interval == 0) { + previousTimes.sort(); + const text = `Max: ${previousTimes[previousTimes.length - 1].toFixed( 2 - )} ms\n\tMedian ${values[Math.floor(values.length / 2)].toFixed(2)} ms` - ); + )} ms\n\tMedian: ${previousTimes[ + Math.floor(previousTimes.length / 2) + ].toFixed(2)} ms`; - values = []; - } + InfoText.modifyRecord(propertyKey, text); + + previousTimes = []; + } + }; + + return descriptor; }; -}; +} diff --git a/frontend/src/scripts/identity/identity-manager.ts b/frontend/src/scripts/identity/identity-manager.ts index 49b5ad8..3fe5e6a 100644 --- a/frontend/src/scripts/identity/identity-manager.ts +++ b/frontend/src/scripts/identity/identity-manager.ts @@ -1,4 +1,8 @@ +import { Id } from './identity'; +import { v4 } from 'uuid'; + export class IdentityManager { - public static sourceId: string; - public static targetId: string; + public static generateId(): Id { + return v4(); + } } diff --git a/frontend/src/scripts/identity/identity.ts b/frontend/src/scripts/identity/identity.ts new file mode 100644 index 0000000..5dfbae8 --- /dev/null +++ b/frontend/src/scripts/identity/identity.ts @@ -0,0 +1 @@ +export type Id = string; diff --git a/frontend/src/scripts/input/keyboard-listener.ts b/frontend/src/scripts/input/keyboard-listener.ts index a94ed88..98add50 100644 --- a/frontend/src/scripts/input/keyboard-listener.ts +++ b/frontend/src/scripts/input/keyboard-listener.ts @@ -3,7 +3,7 @@ import { KeyDownCommand } from '../commands/types/key-down'; import { KeyUpCommand } from '../commands/types/key-up'; export class KeyboardListener extends CommandGenerator { - constructor(target: Element = document.body) { + constructor(target: Element) { super(); target.addEventListener('keydown', (event: KeyboardEvent) => { diff --git a/frontend/src/scripts/input/mouse-listener.ts b/frontend/src/scripts/input/mouse-listener.ts index e14ff81..9eee4b7 100644 --- a/frontend/src/scripts/input/mouse-listener.ts +++ b/frontend/src/scripts/input/mouse-listener.ts @@ -8,7 +8,7 @@ export class MouseListener extends CommandGenerator { private previousPosition: Vec2 = null; private isMouseDown = false; - constructor(private target: Element = document.body) { + constructor(private target: Element) { super(); target.addEventListener('mousedown', (event: MouseEvent) => { @@ -48,9 +48,11 @@ export class MouseListener extends CommandGenerator { } private positionFromEvent(event: MouseEvent): Vec2 { + const bb = this.target.getBoundingClientRect(); + return new Vec2( - -event.clientX / this.target.clientWidth, - event.clientY / this.target.clientHeight - ); + (event.clientX - bb.x) / bb.width, + 1 - (event.clientY - bb.y) / bb.height + ).clamped_0_1; } } diff --git a/frontend/src/scripts/input/touch-listener.ts b/frontend/src/scripts/input/touch-listener.ts index b441a7c..83e7564 100644 --- a/frontend/src/scripts/input/touch-listener.ts +++ b/frontend/src/scripts/input/touch-listener.ts @@ -7,10 +7,12 @@ import { SwipeCommand } from '../commands/types/swipe'; export class TouchListener extends CommandGenerator { private previousPosition: Vec2 = null; - constructor(private target: Element = document.body) { + constructor(private target: HTMLElement) { super(); target.addEventListener('touchstart', (event: TouchEvent) => { + event.preventDefault(); + const touchCount = event.touches.length; const position = this.positionFromEvent(event); this.previousPosition = position; @@ -23,6 +25,8 @@ export class TouchListener extends CommandGenerator { }); target.addEventListener('touchmove', (event: TouchEvent) => { + event.preventDefault(); + const position = this.positionFromEvent(event); this.sendCommand( @@ -34,9 +38,11 @@ export class TouchListener extends CommandGenerator { } private positionFromEvent(event: TouchEvent): Vec2 { + const bb = this.target.getBoundingClientRect(); + return new Vec2( - event.touches[0].clientX / this.target.clientWidth, - -event.touches[0].clientY / this.target.clientHeight - ); + 1 - (event.touches[0].clientX - bb.x) / bb.width, + (event.touches[0].clientY - bb.y) / bb.height + ).clamped_0_1; } } diff --git a/frontend/src/scripts/main.ts b/frontend/src/scripts/main.ts deleted file mode 100644 index 5458188..0000000 --- a/frontend/src/scripts/main.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Renderer } from './drawing/renderer'; - -import passthroughVertexShader from '../shaders/passthrough.vert'; -import distanceFragmentShader from '../shaders/dist.frag'; -import { KeyboardListener } from './input/keyboard-listener'; -import { MouseListener } from './input/mouse-listener'; -import { TouchListener } from './input/touch-listener'; -import { CommandBroadcaster } from './commands/command-broadcaster'; -import { GameLogic } from './game-logic/game-logic'; -import { GameObject } from './objects/game-object'; -import { ObjectContainer } from './objects/object-container'; -import { Camera } from './objects/types/camera'; - -const startGameLoop = (gameLogic: GameLogic, renderer: Renderer) => { - const loop = (time: DOMHighResTimeStamp) => { - gameLogic.step(time); - renderer.render(time); - requestAnimationFrame(loop); - }; - requestAnimationFrame(loop); -}; - -export const main = async () => { - try { - const canvas: HTMLCanvasElement = document.querySelector('canvas#main'); - - const objects = new ObjectContainer(new Camera()); - - const gameLogic = new GameLogic(objects); - new CommandBroadcaster( - [ - new KeyboardListener(document.body), - new MouseListener(canvas), - new TouchListener(canvas), - ], - [gameLogic] - ); - - const renderer = new Renderer(canvas, objects, [ - passthroughVertexShader, - distanceFragmentShader, - ]); - - startGameLoop(gameLogic, renderer); - } catch (e) { - console.error(e); - } -}; diff --git a/frontend/src/scripts/math/vec2.ts b/frontend/src/scripts/math/vec2.ts index f47bd39..a71b2a0 100644 --- a/frontend/src/scripts/math/vec2.ts +++ b/frontend/src/scripts/math/vec2.ts @@ -13,6 +13,10 @@ export class Vec2 extends Typed { return new Vec2(this.x * scalar, this.y * scalar); } + public times(other: Vec2): Vec2 { + return new Vec2(this.x * other.x, this.y * other.y); + } + public add(other: Vec2): Vec2 { return new Vec2(this.x + other.x, this.y + other.y); } @@ -21,6 +25,21 @@ export class Vec2 extends Typed { return new Vec2(this.x - other.x, this.y - other.y); } + public get length(): number { + return Math.sqrt(this.x * this.x + this.y * this.y); + } + + public get normalized(): Vec2 { + return this.scale(1 / this.length); + } + + public get clamped_0_1(): Vec2 { + return new Vec2( + Math.min(1, Math.max(0, this.x)), + Math.min(1, Math.max(0, this.y)) + ); + } + public get list(): [number, number] { return [this.x, this.y]; } diff --git a/frontend/src/scripts/objects/game-object.ts b/frontend/src/scripts/objects/game-object.ts index 512e39f..7628bec 100644 --- a/frontend/src/scripts/objects/game-object.ts +++ b/frontend/src/scripts/objects/game-object.ts @@ -1,7 +1,38 @@ import { Typed } from '../transport/serializable'; import { Vec2 } from '../math/vec2'; +import { IdentityManager } from '../identity/identity-manager'; +import { Command } from '../commands/command'; +import { CommandReceiver } from '../commands/command-receiver'; -export class GameObject extends Typed { - public position = new Vec2(); - public boundingBoxSize = new Vec2(); +export abstract class GameObject extends Typed implements CommandReceiver { + public readonly id = IdentityManager.generateId(); + + protected _position = new Vec2(); + public get position(): Vec2 { + return this._position; + } + + protected _boundingBoxSize = new Vec2(); + public get boundingBoxSize(): Vec2 { + return this._boundingBoxSize; + } + + private commandExecutors: { + [commandName: string]: (e: Command) => void; + } = {}; + + protected addCommandExecutor( + commandType: new () => T, + handler: (command: T) => void + ) { + this.commandExecutors[commandType.name] = handler; + } + + public sendCommand(command: Command) { + const commandType = command.constructor.name; + + if (this.commandExecutors.hasOwnProperty(commandType)) { + this.commandExecutors[commandType](command); + } + } } diff --git a/frontend/src/scripts/objects/object-container.ts b/frontend/src/scripts/objects/object-container.ts index 31a3bc9..42ef3f1 100644 --- a/frontend/src/scripts/objects/object-container.ts +++ b/frontend/src/scripts/objects/object-container.ts @@ -1,8 +1,24 @@ import { GameObject } from './game-object'; -import { Camera } from './types/camera'; +import { Id } from '../identity/identity'; +import { Command } from '../commands/command'; +import { CommandReceiver } from '../commands/command-receiver'; -export class ObjectContainer { - private objects: Array = []; +export class ObjectContainer implements CommandReceiver { + private objects: Map = new Map(); - constructor(public camera: Camera) {} + public addObject(o: GameObject) { + this.objects.set(o.id, o); + } + + public removeObject(o: GameObject) { + this.objects.delete(o.id); + } + + public sendCommandToSingleObject(id: Id, e: Command) { + this.objects.get(id)?.sendCommand(e); + } + + public sendCommand(e: Command) { + this.objects.forEach((o, _) => o.sendCommand(e)); + } } diff --git a/frontend/src/scripts/objects/types/camera.ts b/frontend/src/scripts/objects/types/camera.ts index bf612b2..1fb2234 100644 --- a/frontend/src/scripts/objects/types/camera.ts +++ b/frontend/src/scripts/objects/types/camera.ts @@ -1,3 +1,23 @@ import { GameObject } from '../game-object'; +import { DrawCommand } from '../../commands/types/draw'; +import { Vec2 } from '../../math/vec2'; +import { MoveToCommand } from '../../commands/types/move-to'; -export class Camera extends GameObject {} +export class Camera extends GameObject { + constructor() { + super(); + this._boundingBoxSize = new Vec2(1200, 800); + + this.addCommandExecutor(DrawCommand, this.draw.bind(this)); + this.addCommandExecutor(MoveToCommand, this.moveTo.bind(this)); + } + + private draw(e: DrawCommand) { + e.drawer.setCameraPosition(this.position); + e.drawer.setViewBoxSize(this.boundingBoxSize); + } + + private moveTo(e: MoveToCommand) { + this._position = e.position; + } +} diff --git a/frontend/src/scripts/objects/types/character.ts b/frontend/src/scripts/objects/types/character.ts new file mode 100644 index 0000000..b850f10 --- /dev/null +++ b/frontend/src/scripts/objects/types/character.ts @@ -0,0 +1,53 @@ +import { GameObject } from '../game-object'; +import { Vec2 } from '../../math/vec2'; +import { ObjectContainer } from '../object-container'; +import { Camera } from './camera'; +import { MoveToCommand } from '../../commands/types/move-to'; +import { StepCommand } from '../../commands/types/step'; +import { KeyDownCommand } from '../../commands/types/key-down'; +import { KeyUpCommand } from '../../commands/types/key-up'; +import { SwipeCommand } from '../../commands/types/swipe'; + +export class Character extends GameObject { + private keysDown: Set = new Set(); + private camera = new Camera(); + + private static speed = 0.5; + + constructor(objects: ObjectContainer) { + super(); + + objects.addObject(this.camera); + this.addCommandExecutor(StepCommand, this.stepHandler.bind(this)); + 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.position.add(c.delta.times(this.camera.boundingBoxSize)) + ) + ); + } + + private setPosition(value: Vec2) { + this._position = value; + this.camera.sendCommand(new MoveToCommand(this.position)); + } + + public stepHandler(c: StepCommand) { + const deltaTime = c.deltaTimeInMiliseconds; + + const up = ~~this.keysDown.has('w'); + const down = ~~this.keysDown.has('s'); + const left = ~~this.keysDown.has('a'); + const right = ~~this.keysDown.has('d'); + + const movementVector = new Vec2(right - left, up - down); + if (movementVector.length > 0) { + this.setPosition( + this.position.add( + movementVector.normalized.scale(Character.speed * deltaTime) + ) + ); + } + } +} diff --git a/frontend/src/scripts/objects/types/info-text.ts b/frontend/src/scripts/objects/types/info-text.ts new file mode 100644 index 0000000..c71527e --- /dev/null +++ b/frontend/src/scripts/objects/types/info-text.ts @@ -0,0 +1,22 @@ +import { GameObject } from '../game-object'; +import { DrawCommand } from '../../commands/types/draw'; + +export class InfoText extends GameObject { + constructor() { + super(); + + this.addCommandExecutor(DrawCommand, this.draw.bind(this)); + } + + private static records: Map = new Map(); + + public static modifyRecord(key: string, value: string) { + InfoText.records.set(key, value); + } + + private draw(e: DrawCommand) { + let text = ''; + InfoText.records.forEach((v, k) => (text += `${k}\n\t${v}\n`)); + e.drawer.drawCornerText(text); + } +} diff --git a/frontend/src/styles/main.scss b/frontend/src/styles/main.scss index b82cc94..aad4d1b 100644 --- a/frontend/src/styles/main.scss +++ b/frontend/src/styles/main.scss @@ -1,19 +1,39 @@ -html, body, main { - height: 100%; +html, +body, +main { + height: 100%; } body { - margin: 0; + margin: 0; + background-color: #333; - background-color: #333; - main { - display: flex; - justify-content: center; - align-items: center; + main { + position: relative; - canvas { - width: 100%; - background-color: hotpink; - } + .canvas-container { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 100%; + + #overlay { + font-family: Helvetica, Arial, sans-serif; + font-size: 1em; + padding: 0.5em 1em; + user-select: none; + pointer-events: none; + position: absolute; + right: 0; + -webkit-text-stroke: 1px black; + -webkit-text-fill-color: white; + white-space: pre; + } + + canvas#main { + width: 100%; + background-color: hotpink; + } } + } } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 32cb314..3c93363 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -6,6 +6,7 @@ "module": "es6", "target": "es5", "downlevelIteration": true, - "allowJs": true + "allowJs": true, + "experimentalDecorators": true } } diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index 1724428..343f0c0 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -7,7 +7,6 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin'); const Sharp = require('responsive-loader/sharp'); const Sass = require('sass'); -const tsNameof = require('ts-nameof'); const isProduction = process.env.NODE_ENV === 'production'; @@ -123,9 +122,6 @@ module.exports = { test: /\.ts$/, use: { loader: 'ts-loader', - options: { - getCustomTransformers: () => ({ before: [tsNameof] }), - }, }, exclude: /node_modules/, },