Add input handling
This commit is contained in:
parent
29c6b14440
commit
d5be727040
36 changed files with 417 additions and 113 deletions
13
frontend/src/scripts/commands/command-broadcaster.ts
Normal file
13
frontend/src/scripts/commands/command-broadcaster.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { CommandReceiver } from './command-receiver';
|
||||
import { CommandGenerator } from './command-generator';
|
||||
|
||||
export class CommandBroadcaster {
|
||||
constructor(
|
||||
commandGenerators: Array<CommandGenerator>,
|
||||
commandReceivers: Array<CommandReceiver>
|
||||
) {
|
||||
commandReceivers.forEach((r) =>
|
||||
commandGenerators.forEach((g) => g.subscribe(r))
|
||||
);
|
||||
}
|
||||
}
|
||||
14
frontend/src/scripts/commands/command-generator.ts
Normal file
14
frontend/src/scripts/commands/command-generator.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { CommandReceiver } from './command-receiver';
|
||||
import { Command } from './command';
|
||||
|
||||
export class CommandGenerator {
|
||||
private subscribers: Array<CommandReceiver> = [];
|
||||
|
||||
public subscribe(subscriber: CommandReceiver): void {
|
||||
this.subscribers.push(subscriber);
|
||||
}
|
||||
|
||||
protected sendCommand(command: Command): void {
|
||||
this.subscribers.forEach((s) => s.sendCommand(command));
|
||||
}
|
||||
}
|
||||
5
frontend/src/scripts/commands/command-receiver.ts
Normal file
5
frontend/src/scripts/commands/command-receiver.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Command } from './command';
|
||||
|
||||
export interface CommandReceiver {
|
||||
sendCommand(command: Command): void;
|
||||
}
|
||||
3
frontend/src/scripts/commands/command.ts
Normal file
3
frontend/src/scripts/commands/command.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { Typed } from '../transport/serializable';
|
||||
|
||||
export abstract class Command extends Typed {}
|
||||
7
frontend/src/scripts/commands/types/key-down.ts
Normal file
7
frontend/src/scripts/commands/types/key-down.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { Command } from '../command';
|
||||
|
||||
export class KeyDownCommand extends Command {
|
||||
public constructor(public readonly key: string) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
7
frontend/src/scripts/commands/types/key-up.ts
Normal file
7
frontend/src/scripts/commands/types/key-up.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { Command } from '../command';
|
||||
|
||||
export class KeyUpCommand extends Command {
|
||||
public constructor(public readonly key: string) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
8
frontend/src/scripts/commands/types/primary-action.ts
Normal file
8
frontend/src/scripts/commands/types/primary-action.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Command } from '../command';
|
||||
import { Vec2 } from '../../math/vec2';
|
||||
|
||||
export class PrimaryActionCommand extends Command {
|
||||
public constructor(public readonly position: Vec2) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
8
frontend/src/scripts/commands/types/secondary-action.ts
Normal file
8
frontend/src/scripts/commands/types/secondary-action.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Command } from '../command';
|
||||
import { Vec2 } from '../../math/vec2';
|
||||
|
||||
export class SecondaryActionCommand extends Command {
|
||||
public constructor(public readonly position: Vec2) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
8
frontend/src/scripts/commands/types/swipe.ts
Normal file
8
frontend/src/scripts/commands/types/swipe.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Command } from '../command';
|
||||
import { Vec2 } from '../../math/vec2';
|
||||
|
||||
export class SwipeCommand extends Command {
|
||||
public constructor(public readonly delta: Vec2) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
0
frontend/src/scripts/drawing/pipeline.ts
Normal file
0
frontend/src/scripts/drawing/pipeline.ts
Normal file
66
frontend/src/scripts/drawing/renderer.ts
Normal file
66
frontend/src/scripts/drawing/renderer.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
const twgl = require('twgl.js');
|
||||
|
||||
import { TimeIt } from '../helper/timing';
|
||||
import { Vec2 } from '../math/vec2';
|
||||
import { ObjectContainer } from '../objects/object-container';
|
||||
|
||||
export class Renderer {
|
||||
private gl: WebGL2RenderingContext;
|
||||
private programInfo: any;
|
||||
private bufferInfo: any;
|
||||
private vao: any;
|
||||
|
||||
constructor(
|
||||
private canvas: HTMLCanvasElement,
|
||||
private objects: ObjectContainer,
|
||||
shaderSources: Array<string>
|
||||
) {
|
||||
twgl.setDefaults({ attribPrefix: 'a_' });
|
||||
|
||||
this.gl = this.canvas.getContext('webgl2');
|
||||
if (!this.gl) {
|
||||
throw new Error('WebGl2 not supported');
|
||||
}
|
||||
|
||||
this.programInfo = twgl.createProgramInfo(this.gl, shaderSources);
|
||||
|
||||
const arrays = {
|
||||
position: {
|
||||
numComponents: 3,
|
||||
data: [-1.0, -1.0, 0.0, -1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, -1.0, 0.0],
|
||||
},
|
||||
indices: {
|
||||
numComponents: 3,
|
||||
data: [0, 1, 2, 0, 2, 3],
|
||||
},
|
||||
};
|
||||
this.bufferInfo = twgl.createBufferInfoFromArrays(this.gl, arrays);
|
||||
this.vao = twgl.createVAOFromBufferInfo(
|
||||
this.gl,
|
||||
this.programInfo,
|
||||
this.bufferInfo
|
||||
);
|
||||
}
|
||||
|
||||
public render(time: number) {
|
||||
const gl = this.gl;
|
||||
|
||||
twgl.resizeCanvasToDisplaySize(this.canvas);
|
||||
this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
|
||||
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
const uniforms = {
|
||||
cameraPosition: this.objects.camera.position.list,
|
||||
viewBoxSize: [this.gl.canvas.width, this.gl.canvas.height],
|
||||
resolution: [this.gl.canvas.width, this.gl.canvas.height],
|
||||
};
|
||||
|
||||
this.gl.useProgram(this.programInfo.program);
|
||||
this.gl.bindVertexArray(this.vao);
|
||||
//twgl.setBuffersAndAttributes(this.gl, this.programInfo, this.bufferInfo);
|
||||
|
||||
twgl.setUniforms(this.programInfo, uniforms);
|
||||
twgl.drawBufferInfo(this.gl, this.bufferInfo);
|
||||
}
|
||||
}
|
||||
54
frontend/src/scripts/game-logic/game-logic.ts
Normal file
54
frontend/src/scripts/game-logic/game-logic.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
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<Command> = [];
|
||||
private keysDown: Set<string> = 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);
|
||||
}
|
||||
}
|
||||
22
frontend/src/scripts/helper/timing.ts
Normal file
22
frontend/src/scripts/helper/timing.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
export const TimeIt = (func, interval = 60) => {
|
||||
let i = 0;
|
||||
let values = [];
|
||||
|
||||
return () => {
|
||||
const start = performance.now();
|
||||
func();
|
||||
const end = performance.now();
|
||||
|
||||
values.push(end - start);
|
||||
if (++i % interval == 0) {
|
||||
values.sort();
|
||||
console.log(
|
||||
`${func.name}\n\tMax ${values[values.length - 1].toFixed(
|
||||
2
|
||||
)} ms\n\tMedian ${values[Math.floor(values.length / 2)].toFixed(2)} ms`
|
||||
);
|
||||
|
||||
values = [];
|
||||
}
|
||||
};
|
||||
};
|
||||
4
frontend/src/scripts/identity/identity-manager.ts
Normal file
4
frontend/src/scripts/identity/identity-manager.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export class IdentityManager {
|
||||
public static sourceId: string;
|
||||
public static targetId: string;
|
||||
}
|
||||
19
frontend/src/scripts/input/keyboard-listener.ts
Normal file
19
frontend/src/scripts/input/keyboard-listener.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { CommandGenerator } from '../commands/command-generator';
|
||||
import { KeyDownCommand } from '../commands/types/key-down';
|
||||
import { KeyUpCommand } from '../commands/types/key-up';
|
||||
|
||||
export class KeyboardListener extends CommandGenerator {
|
||||
constructor(target: Element = document.body) {
|
||||
super();
|
||||
|
||||
target.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
const key = event.key.toLowerCase();
|
||||
this.sendCommand(new KeyDownCommand(key));
|
||||
});
|
||||
|
||||
target.addEventListener('keyup', (event: KeyboardEvent) => {
|
||||
const key = event.key.toLowerCase();
|
||||
this.sendCommand(new KeyUpCommand(key));
|
||||
});
|
||||
}
|
||||
}
|
||||
56
frontend/src/scripts/input/mouse-listener.ts
Normal file
56
frontend/src/scripts/input/mouse-listener.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { CommandGenerator } from '../commands/command-generator';
|
||||
import { PrimaryActionCommand } from '../commands/types/primary-action';
|
||||
import { SecondaryActionCommand } from '../commands/types/secondary-action';
|
||||
import { Vec2 } from '../math/vec2';
|
||||
import { SwipeCommand } from '../commands/types/swipe';
|
||||
|
||||
export class MouseListener extends CommandGenerator {
|
||||
private previousPosition: Vec2 = null;
|
||||
private isMouseDown = false;
|
||||
|
||||
constructor(private target: Element = document.body) {
|
||||
super();
|
||||
|
||||
target.addEventListener('mousedown', (event: MouseEvent) => {
|
||||
const position = this.positionFromEvent(event);
|
||||
|
||||
this.previousPosition = position;
|
||||
this.isMouseDown = true;
|
||||
|
||||
if (event.button == 0) {
|
||||
this.sendCommand(new PrimaryActionCommand(position));
|
||||
}
|
||||
});
|
||||
|
||||
target.addEventListener('mousemove', (event: MouseEvent) => {
|
||||
if (this.isMouseDown) {
|
||||
const position = this.positionFromEvent(event);
|
||||
this.sendCommand(
|
||||
new SwipeCommand(this.previousPosition.subtract(position))
|
||||
);
|
||||
this.previousPosition = position;
|
||||
}
|
||||
});
|
||||
|
||||
target.addEventListener('mouseup', (event: MouseEvent) => {
|
||||
this.isMouseDown = false;
|
||||
});
|
||||
|
||||
target.addEventListener('mouseleave', (event: MouseEvent) => {
|
||||
this.isMouseDown = false;
|
||||
});
|
||||
|
||||
target.addEventListener('contextmenu', (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
const position = this.positionFromEvent(event);
|
||||
this.sendCommand(new SecondaryActionCommand(position));
|
||||
});
|
||||
}
|
||||
|
||||
private positionFromEvent(event: MouseEvent): Vec2 {
|
||||
return new Vec2(
|
||||
-event.clientX / this.target.clientWidth,
|
||||
event.clientY / this.target.clientHeight
|
||||
);
|
||||
}
|
||||
}
|
||||
42
frontend/src/scripts/input/touch-listener.ts
Normal file
42
frontend/src/scripts/input/touch-listener.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { CommandGenerator } from '../commands/command-generator';
|
||||
import { PrimaryActionCommand } from '../commands/types/primary-action';
|
||||
import { SecondaryActionCommand } from '../commands/types/secondary-action';
|
||||
import { Vec2 } from '../math/vec2';
|
||||
import { SwipeCommand } from '../commands/types/swipe';
|
||||
|
||||
export class TouchListener extends CommandGenerator {
|
||||
private previousPosition: Vec2 = null;
|
||||
|
||||
constructor(private target: Element = document.body) {
|
||||
super();
|
||||
|
||||
target.addEventListener('touchstart', (event: TouchEvent) => {
|
||||
const touchCount = event.touches.length;
|
||||
const position = this.positionFromEvent(event);
|
||||
this.previousPosition = position;
|
||||
|
||||
if (touchCount == 1) {
|
||||
this.sendCommand(new PrimaryActionCommand(position));
|
||||
} else {
|
||||
this.sendCommand(new SecondaryActionCommand(position));
|
||||
}
|
||||
});
|
||||
|
||||
target.addEventListener('touchmove', (event: TouchEvent) => {
|
||||
const position = this.positionFromEvent(event);
|
||||
|
||||
this.sendCommand(
|
||||
new SwipeCommand(position.subtract(this.previousPosition))
|
||||
);
|
||||
|
||||
this.previousPosition = position;
|
||||
});
|
||||
}
|
||||
|
||||
private positionFromEvent(event: TouchEvent): Vec2 {
|
||||
return new Vec2(
|
||||
event.touches[0].clientX / this.target.clientWidth,
|
||||
-event.touches[0].clientY / this.target.clientHeight
|
||||
);
|
||||
}
|
||||
}
|
||||
48
frontend/src/scripts/main.ts
Normal file
48
frontend/src/scripts/main.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
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);
|
||||
}
|
||||
};
|
||||
7
frontend/src/scripts/math/line.ts
Normal file
7
frontend/src/scripts/math/line.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { Vec2 } from './vec2';
|
||||
|
||||
export interface Line {
|
||||
a: Vec2;
|
||||
b: Vec2;
|
||||
normal: Vec2;
|
||||
}
|
||||
27
frontend/src/scripts/math/vec2.ts
Normal file
27
frontend/src/scripts/math/vec2.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { Typed } from '../transport/serializable';
|
||||
|
||||
export class Vec2 extends Typed {
|
||||
public constructor(public x: number = 0.0, public y: number = null) {
|
||||
super();
|
||||
|
||||
if (this.y === null) {
|
||||
this.y = this.x;
|
||||
}
|
||||
}
|
||||
|
||||
public scale(scalar: number): Vec2 {
|
||||
return new Vec2(this.x * scalar, this.y * scalar);
|
||||
}
|
||||
|
||||
public add(other: Vec2): Vec2 {
|
||||
return new Vec2(this.x + other.x, this.y + other.y);
|
||||
}
|
||||
|
||||
public subtract(other: Vec2): Vec2 {
|
||||
return new Vec2(this.x - other.x, this.y - other.y);
|
||||
}
|
||||
|
||||
public get list(): [number, number] {
|
||||
return [this.x, this.y];
|
||||
}
|
||||
}
|
||||
7
frontend/src/scripts/objects/game-object.ts
Normal file
7
frontend/src/scripts/objects/game-object.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { Typed } from '../transport/serializable';
|
||||
import { Vec2 } from '../math/vec2';
|
||||
|
||||
export class GameObject extends Typed {
|
||||
public position = new Vec2();
|
||||
public boundingBoxSize = new Vec2();
|
||||
}
|
||||
8
frontend/src/scripts/objects/object-container.ts
Normal file
8
frontend/src/scripts/objects/object-container.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { GameObject } from './game-object';
|
||||
import { Camera } from './types/camera';
|
||||
|
||||
export class ObjectContainer {
|
||||
private objects: Array<GameObject> = [];
|
||||
|
||||
constructor(public camera: Camera) {}
|
||||
}
|
||||
3
frontend/src/scripts/objects/types/camera.ts
Normal file
3
frontend/src/scripts/objects/types/camera.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { GameObject } from '../game-object';
|
||||
|
||||
export class Camera extends GameObject {}
|
||||
9
frontend/src/scripts/transport/serializable.ts
Normal file
9
frontend/src/scripts/transport/serializable.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export class Typed {
|
||||
public get type(): string {
|
||||
return this.constructor.name;
|
||||
}
|
||||
|
||||
public toJSON() {
|
||||
return { type: this.type, ...this };
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue