Add basic multiplayer
This commit is contained in:
parent
0f0a1eaf67
commit
46a48e7c15
57 changed files with 1361 additions and 753 deletions
|
|
@ -63,6 +63,7 @@
|
|||
"webpack-dev-server": "^3.10.3",
|
||||
"firebase": "^7.22.0",
|
||||
"@types/socket.io-client": "^1.4.34",
|
||||
"socket.io-client": "^2.3.1"
|
||||
"socket.io-client": "^2.3.1",
|
||||
"shared": "file:../shared"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,13 @@
|
|||
import { glMatrix } from 'gl-matrix';
|
||||
import io from 'socket.io-client';
|
||||
import { TransportEvents } from '../../shared/src/transport/transport-events';
|
||||
import { Configuration } from './scripts/config/configuration';
|
||||
import { Game } from './scripts/game';
|
||||
import { Random } from './scripts/helper/random';
|
||||
import './styles/main.scss';
|
||||
|
||||
glMatrix.setMatrixArrayType(Array);
|
||||
Random.seed = 42;
|
||||
|
||||
const main = async () => {
|
||||
await Configuration.initialize();
|
||||
|
||||
const socket = io(Configuration.servers[0], {
|
||||
reconnectionDelayMax: 10000,
|
||||
transports: ['websocket'],
|
||||
});
|
||||
|
||||
socket.on('reconnect_attempt', () => {
|
||||
socket.io.opts.transports = ['polling', 'websocket'];
|
||||
});
|
||||
|
||||
socket.emit(TransportEvents.PlayerJoining, null);
|
||||
|
||||
try {
|
||||
Random.seed = 42;
|
||||
await new Game().start();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
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));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { Command } from './command';
|
||||
|
||||
export interface CommandReceiver {
|
||||
sendCommand(command: Command): void;
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { Typed } from '../transport/serializable';
|
||||
import { Id } from '../identity/identity';
|
||||
|
||||
export abstract class Command extends Typed {
|
||||
target?: Id;
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { CommandGenerator, MoveActionCommand } from 'shared';
|
||||
|
||||
export class KeyboardListener extends CommandGenerator {
|
||||
private keysDown: Set<string> = new Set();
|
||||
|
||||
constructor(target: Element, private moveScale: number) {
|
||||
super();
|
||||
|
||||
target.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
const key = this.normalize(event.key);
|
||||
this.keysDown.add(key);
|
||||
});
|
||||
|
||||
target.addEventListener('keyup', (event: KeyboardEvent) => {
|
||||
const key = this.normalize(event.key);
|
||||
this.keysDown.delete(key);
|
||||
});
|
||||
}
|
||||
|
||||
public generateCommands() {
|
||||
const up = ~~(
|
||||
this.keysDown.has('w') ||
|
||||
this.keysDown.has('arrowup') ||
|
||||
this.keysDown.has(' ')
|
||||
);
|
||||
const down = ~~(this.keysDown.has('s') || this.keysDown.has('arrowdown'));
|
||||
const left = ~~(this.keysDown.has('a') || this.keysDown.has('arrowleft'));
|
||||
const right = ~~(this.keysDown.has('d') || this.keysDown.has('arrowright'));
|
||||
|
||||
const movement = vec2.fromValues(right - left, up - down);
|
||||
if (vec2.squaredLength(movement) > 0) {
|
||||
vec2.normalize(movement, movement);
|
||||
vec2.scale(movement, movement, this.moveScale);
|
||||
this.sendCommandToSubcribers(new MoveActionCommand(movement));
|
||||
}
|
||||
}
|
||||
|
||||
private normalize(key: string): string {
|
||||
return key.toLowerCase();
|
||||
}
|
||||
}
|
||||
36
frontend/src/scripts/commands/generators/mouse-listener.ts
Normal file
36
frontend/src/scripts/commands/generators/mouse-listener.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import {
|
||||
CommandGenerator,
|
||||
PrimaryActionCommand,
|
||||
SecondaryActionCommand,
|
||||
TernaryActionCommand,
|
||||
} from 'shared';
|
||||
|
||||
export class MouseListener extends CommandGenerator {
|
||||
constructor(target: Element) {
|
||||
super();
|
||||
|
||||
target.addEventListener('mousemove', (event: MouseEvent) => {
|
||||
const position = this.positionFromEvent(event);
|
||||
this.sendCommandToSubcribers(new PrimaryActionCommand(position));
|
||||
});
|
||||
|
||||
target.addEventListener('mousedown', (event: MouseEvent) => {
|
||||
const position = this.positionFromEvent(event);
|
||||
|
||||
if (event.button == 0) {
|
||||
this.sendCommandToSubcribers(new SecondaryActionCommand(position));
|
||||
}
|
||||
});
|
||||
|
||||
target.addEventListener('contextmenu', (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
const position = this.positionFromEvent(event);
|
||||
this.sendCommandToSubcribers(new TernaryActionCommand(position));
|
||||
});
|
||||
}
|
||||
|
||||
private positionFromEvent(event: MouseEvent): vec2 {
|
||||
return vec2.fromValues(event.clientX, event.clientY);
|
||||
}
|
||||
}
|
||||
57
frontend/src/scripts/commands/generators/touch-listener.ts
Normal file
57
frontend/src/scripts/commands/generators/touch-listener.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import {
|
||||
CommandGenerator,
|
||||
MoveActionCommand,
|
||||
PrimaryActionCommand,
|
||||
SecondaryActionCommand,
|
||||
TernaryActionCommand,
|
||||
} from 'shared';
|
||||
|
||||
export class TouchListener extends CommandGenerator {
|
||||
private previousPosition = vec2.create();
|
||||
|
||||
constructor(target: HTMLElement) {
|
||||
super();
|
||||
|
||||
target.addEventListener('touchstart', (event: TouchEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const touchCount = event.touches.length;
|
||||
const position = this.positionFromEvent(event);
|
||||
this.previousPosition = position;
|
||||
|
||||
if (touchCount == 1) {
|
||||
this.sendCommandToSubcribers(new PrimaryActionCommand(position));
|
||||
} else if (touchCount == 2) {
|
||||
this.sendCommandToSubcribers(new SecondaryActionCommand(position));
|
||||
} else {
|
||||
this.sendCommandToSubcribers(new TernaryActionCommand(position));
|
||||
}
|
||||
});
|
||||
|
||||
target.addEventListener('touchmove', (event: TouchEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const position = this.positionFromEvent(event);
|
||||
|
||||
this.sendCommandToSubcribers(
|
||||
new MoveActionCommand(
|
||||
vec2.subtract(vec2.create(), position, this.previousPosition)
|
||||
)
|
||||
);
|
||||
|
||||
this.previousPosition = position;
|
||||
});
|
||||
}
|
||||
|
||||
private positionFromEvent(event: TouchEvent): vec2 {
|
||||
const center = Array.prototype.reduce.call(
|
||||
event.touches,
|
||||
(center: vec2, touch: Touch) =>
|
||||
vec2.add(center, center, vec2.fromValues(-touch.clientX, touch.clientY)),
|
||||
vec2.create()
|
||||
);
|
||||
|
||||
return vec2.scale(center, center, 1 / event.touches.length);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { Command } from './command';
|
||||
|
||||
export class MoveToCommand extends Command {
|
||||
public constructor(public readonly position?: vec2) {
|
||||
super();
|
||||
}
|
||||
|
||||
public get type(): string {
|
||||
return 'MoveToCommand';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Command, CommandReceiver, TransportEvents } from 'shared';
|
||||
|
||||
export class CommandReceiverSocket extends CommandReceiver {
|
||||
constructor(private readonly socket: SocketIOClient.Socket) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected defaultCommandExecutor(command: Command) {
|
||||
this.socket.emit(TransportEvents.PlayerToServer, JSON.stringify(command));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { Renderer } from 'sdf-2d';
|
||||
import { Command } from './command';
|
||||
|
||||
export class RenderCommand extends Command {
|
||||
public constructor(public readonly renderer?: Renderer) {
|
||||
super();
|
||||
}
|
||||
|
||||
public get type(): string {
|
||||
return 'RenderCommand';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { Command } from './command';
|
||||
|
||||
export class StepCommand extends Command {
|
||||
public constructor(public readonly deltaTimeInMiliseconds?: DOMHighResTimeStamp) {
|
||||
super();
|
||||
}
|
||||
|
||||
public get type(): string {
|
||||
return 'StepCommand';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { Command } from './command';
|
||||
|
||||
export class TeleportToCommand extends Command {
|
||||
public constructor(public readonly position?: vec2) {
|
||||
super();
|
||||
}
|
||||
|
||||
public get type(): string {
|
||||
return 'TeleportToCommand';
|
||||
}
|
||||
}
|
||||
10
frontend/src/scripts/commands/types/move-to.ts
Normal file
10
frontend/src/scripts/commands/types/move-to.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { Command } from 'shared';
|
||||
|
||||
export class MoveToCommand extends Command {
|
||||
public static readonly type = 'MoveToCommand';
|
||||
|
||||
public constructor(public readonly position: vec2) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
10
frontend/src/scripts/commands/types/render.ts
Normal file
10
frontend/src/scripts/commands/types/render.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Renderer } from 'sdf-2d';
|
||||
import { Command } from 'shared';
|
||||
|
||||
export class RenderCommand extends Command {
|
||||
public static readonly type = 'RenderCommand';
|
||||
|
||||
public constructor(public readonly renderer: Renderer) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
9
frontend/src/scripts/commands/types/step.ts
Normal file
9
frontend/src/scripts/commands/types/step.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Command } from 'shared';
|
||||
|
||||
export class StepCommand extends Command {
|
||||
public static readonly type = 'StepCommand';
|
||||
|
||||
public constructor(public readonly deltaTimeInMiliseconds: DOMHighResTimeStamp) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
|
@ -10,50 +10,69 @@ import {
|
|||
renderNoise,
|
||||
WrapOptions,
|
||||
} from 'sdf-2d';
|
||||
import { CommandBroadcaster } from './commands/command-broadcaster';
|
||||
import { MoveToCommand } from './commands/move-to';
|
||||
import { RenderCommand } from './commands/render';
|
||||
import { StepCommand } from './commands/step';
|
||||
import { broadcastCommands, SetViewAreaActionCommand, TransportEvents } from 'shared';
|
||||
import io from 'socket.io-client';
|
||||
import { KeyboardListener } from './commands/generators/keyboard-listener';
|
||||
import { MouseListener } from './commands/generators/mouse-listener';
|
||||
import { TouchListener } from './commands/generators/touch-listener';
|
||||
import { CommandReceiverSocket } from './commands/receivers/command-receiver-socket';
|
||||
import { RenderCommand } from './commands/types/render';
|
||||
import { StepCommand } from './commands/types/step';
|
||||
import { Configuration } from './config/configuration';
|
||||
import { DeltaTimeCalculator } from './helper/delta-time-calculator';
|
||||
import { prettyPrint } from './helper/pretty-print';
|
||||
import { rgb } from './helper/rgb';
|
||||
import { IGame } from './i-game';
|
||||
import { KeyboardListener } from './input/keyboard-listener';
|
||||
import { MouseListener } from './input/mouse-listener';
|
||||
import { TouchListener } from './input/touch-listener';
|
||||
import { GameObject } from './objects/game-object';
|
||||
import { Objects } from './objects/objects';
|
||||
import { Camera } from './objects/types/camera';
|
||||
import { Character } from './objects/types/character';
|
||||
import { createDungeon } from './objects/world/create-dungeon';
|
||||
import { BoundingBoxBase } from './physics/bounds/bounding-box-base';
|
||||
import { Physics } from './physics/physics';
|
||||
import { GameObjectContainer } from './objects/game-object-container';
|
||||
import { settings } from './settings';
|
||||
import { BlobShape } from './shapes/blob-shape';
|
||||
import { deserialize } from './transport/deserialize';
|
||||
|
||||
export class Game implements IGame {
|
||||
public readonly objects = new Objects();
|
||||
public readonly physics = new Physics();
|
||||
public readonly camera = new Camera();
|
||||
private character: Character;
|
||||
export class Game {
|
||||
public readonly gameObjects = new GameObjectContainer();
|
||||
private readonly canvas: HTMLCanvasElement = document.querySelector('canvas#main');
|
||||
private renderer: Renderer;
|
||||
private rendererPromise: Promise<Renderer>;
|
||||
private socket: SocketIOClient.Socket;
|
||||
private deltaTimeCalculator = new DeltaTimeCalculator();
|
||||
private overlay: HTMLElement = document.querySelector('#overlay');
|
||||
private keyboardListener: KeyboardListener;
|
||||
|
||||
constructor() {
|
||||
const canvas: HTMLCanvasElement = document.querySelector('canvas#main');
|
||||
private async setupCommunication(): Promise<void> {
|
||||
await Configuration.initialize();
|
||||
|
||||
new CommandBroadcaster(
|
||||
this.socket = io(Configuration.servers[0], {
|
||||
reconnectionDelayMax: 10000,
|
||||
transports: ['websocket'],
|
||||
});
|
||||
|
||||
this.socket.on('reconnect_attempt', () => {
|
||||
this.socket.io.opts.transports = ['polling', 'websocket'];
|
||||
});
|
||||
|
||||
this.socket.on(TransportEvents.ServerToPlayer, (serialized: string) => {
|
||||
const command = deserialize(serialized);
|
||||
console.log(command);
|
||||
this.gameObjects.sendCommand(command);
|
||||
});
|
||||
|
||||
this.socket.emit(TransportEvents.PlayerJoining, null);
|
||||
|
||||
this.keyboardListener = new KeyboardListener(document.body, 100);
|
||||
|
||||
broadcastCommands(
|
||||
[
|
||||
new KeyboardListener(document.body),
|
||||
new MouseListener(canvas),
|
||||
new TouchListener(canvas),
|
||||
this.keyboardListener,
|
||||
new MouseListener(this.canvas),
|
||||
new TouchListener(this.canvas),
|
||||
],
|
||||
[this.objects]
|
||||
[this.gameObjects, new CommandReceiverSocket(this.socket)]
|
||||
);
|
||||
}
|
||||
|
||||
this.rendererPromise = compile(
|
||||
canvas,
|
||||
private async setupRenderer(): Promise<void> {
|
||||
const noiseTexture = await renderNoise([1024, 1], 60, 1 / 8);
|
||||
|
||||
this.renderer = await compile(
|
||||
this.canvas,
|
||||
[
|
||||
{
|
||||
...InvertedTunnel.descriptor,
|
||||
|
|
@ -61,7 +80,7 @@ export class Game implements IGame {
|
|||
},
|
||||
{
|
||||
...BlobShape.descriptor,
|
||||
shaderCombinationSteps: [0, 1],
|
||||
shaderCombinationSteps: [0, 1, 2, 3, 4, 5, 8],
|
||||
},
|
||||
{
|
||||
...Circle.descriptor,
|
||||
|
|
@ -82,12 +101,7 @@ export class Game implements IGame {
|
|||
enableStopwatch: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
const noiseTexture = await renderNoise([1024, 1], 60, 1 / 8);
|
||||
|
||||
this.renderer = await this.rendererPromise;
|
||||
this.renderer.setRuntimeSettings({
|
||||
isWorldInverted: true,
|
||||
ambientLight: rgb(0.35, 0.1, 0.45),
|
||||
|
|
@ -112,62 +126,46 @@ export class Game implements IGame {
|
|||
},
|
||||
},
|
||||
});
|
||||
this.initializeScene();
|
||||
this.physics.start();
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
await Promise.all([this.setupCommunication(), this.setupRenderer()]);
|
||||
requestAnimationFrame(this.gameLoop.bind(this));
|
||||
}
|
||||
|
||||
public get viewArea(): BoundingBoxBase {
|
||||
return this.camera.viewArea;
|
||||
}
|
||||
|
||||
public findIntersecting(box: BoundingBoxBase): Array<BoundingBoxBase> {
|
||||
return this.physics.findIntersecting(box);
|
||||
}
|
||||
|
||||
public displayToWorldCoordinates(p: vec2): vec2 {
|
||||
return this.renderer.displayToWorldCoordinates(p);
|
||||
}
|
||||
|
||||
private initializeScene() {
|
||||
createDungeon(this.objects, this.physics);
|
||||
createDungeon(this.objects, this.physics);
|
||||
createDungeon(this.objects, this.physics);
|
||||
createDungeon(this.objects, this.physics);
|
||||
|
||||
this.character = new Character(this.physics, this);
|
||||
this.objects.addObject(this.character);
|
||||
this.objects.addObject(this.camera);
|
||||
}
|
||||
|
||||
private deltaTimeCalculator = new DeltaTimeCalculator();
|
||||
|
||||
private gameLoop(time: DOMHighResTimeStamp) {
|
||||
const deltaTime = this.deltaTimeCalculator.getNextDeltaTime(time);
|
||||
this.keyboardListener.generateCommands();
|
||||
|
||||
this.objects.sendCommand(new StepCommand(deltaTime));
|
||||
this.camera.sendCommand(new MoveToCommand(this.character.position));
|
||||
this.camera.sendCommand(new RenderCommand(this.renderer));
|
||||
if (this.gameObjects.camera) {
|
||||
// todo: Should only send aspect ratio
|
||||
this.socket.emit(
|
||||
TransportEvents.PlayerToServer,
|
||||
JSON.stringify(new SetViewAreaActionCommand(this.gameObjects.camera.viewArea))
|
||||
);
|
||||
}
|
||||
|
||||
const shouldBeDrawn = this.physics
|
||||
this.gameObjects.sendCommand(new StepCommand(deltaTime));
|
||||
/*this.camera.sendCommand(new MoveToCommand(this.character.position));
|
||||
this.camera.sendCommand(new RenderCommand(this.renderer));*/
|
||||
|
||||
/*const shouldBeDrawn = this.physics
|
||||
.findIntersecting(this.camera.viewArea)
|
||||
.map((b) => b.owner);
|
||||
|
||||
for (const object of shouldBeDrawn) {
|
||||
object?.sendCommand(new RenderCommand(this.renderer));
|
||||
}
|
||||
}*/
|
||||
|
||||
this.gameObjects.sendCommand(new RenderCommand(this.renderer) as any);
|
||||
|
||||
this.renderer.renderDrawables();
|
||||
|
||||
this.overlay.innerText = prettyPrint(this.renderer.insights);
|
||||
requestAnimationFrame(this.gameLoop.bind(this));
|
||||
}
|
||||
|
||||
public addObject(o: GameObject) {
|
||||
this.objects.addObject(o);
|
||||
}
|
||||
|
||||
public removeObject(o: GameObject) {
|
||||
this.objects.removeObject(o);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { GameObject } from './objects/game-object';
|
||||
import { BoundingBoxBase } from './physics/bounds/bounding-box-base';
|
||||
|
||||
export interface IGame {
|
||||
readonly viewArea: BoundingBoxBase;
|
||||
findIntersecting(box: BoundingBoxBase): Array<BoundingBoxBase>;
|
||||
displayToWorldCoordinates(p: vec2): vec2;
|
||||
addObject(o: GameObject): void;
|
||||
removeObject(o: GameObject): void;
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { v4 } from 'uuid';
|
||||
import { Id } from './identity';
|
||||
|
||||
export class IdentityManager {
|
||||
public static generateId(): Id {
|
||||
return v4();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue