Add basic multiplayer

This commit is contained in:
schmelczerandras 2020-10-05 19:07:17 +02:00
parent 0f0a1eaf67
commit 46a48e7c15
113 changed files with 1362 additions and 754 deletions

1
shared/.eslintignore Normal file
View file

@ -0,0 +1 @@
**/*.js

29
shared/.eslintrc.json Normal file
View file

@ -0,0 +1,29 @@
{
"root": true,
"env": {
"browser": true,
"es2020": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"prettier/@typescript-eslint"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 11,
"sourceType": "module"
},
"plugins": ["unused-imports", "@typescript-eslint", "prettier"],
"rules": {
"prettier/prettier": "error",
"no-unused-vars": "off",
"unused-imports/no-unused-imports-ts": "error",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-non-null-assertion": "off"
}
}

4
shared/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
dist
node_modules
package-lock.json
.firebase

7
shared/.prettierrc Normal file
View file

@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"printWidth": 90,
"tabWidth": 2,
"singleQuote": true,
"endOfLine": "lf"
}

16
shared/firebase.json Normal file
View file

@ -0,0 +1,16 @@
{
"hosting": {
"public": "dist",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}

17
shared/package.json Normal file
View file

@ -0,0 +1,17 @@
{
"name": "shared",
"version": "0.0.0",
"description": "Shared library between backend and frontend",
"main": "src/main.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@types/gl-matrix": "^3.2.0",
"@types/uuid": "^8.0.0",
"gl-matrix": "^3.3.0",
"uuid": "^8.2.0"
}
}

View file

@ -0,0 +1,7 @@
import { CommandReceiver } from './command-receiver';
import { CommandGenerator } from './command-generator';
export const broadcastCommands = (
commandGenerators: Array<CommandGenerator>,
commandReceivers: Array<CommandReceiver>
) => commandReceivers.forEach((r) => commandGenerators.forEach((g) => g.subscribe(r)));

View 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 sendCommandToSubcribers(command: Command): void {
this.subscribers.forEach((s) => s.sendCommand(command));
}
}

View file

@ -0,0 +1,25 @@
import { Command } from './command';
export type CommandExecutors = {
[type: string]: (command: Command) => void;
};
export abstract class CommandReceiver {
protected commandExecutors: CommandExecutors = {};
protected defaultCommandExecutor(command: Command) {}
public reactsToCommand(commandType: string): boolean {
return Object.prototype.hasOwnProperty.call(this.commandExecutors, commandType);
}
public sendCommand(command: Command) {
const commandType = command.type;
if (Object.prototype.hasOwnProperty.call(this.commandExecutors, commandType)) {
this.commandExecutors[commandType](command);
} else {
this.defaultCommandExecutor(command);
}
}
}

View file

@ -0,0 +1,7 @@
import { Id } from '../transport/identity';
export abstract class Command {
public get type(): string {
return (this as any).constructor.type;
}
}

View file

@ -0,0 +1,13 @@
import { Command } from '../command';
export class CreateObjectsCommand extends Command {
public static readonly type = 'CreateObjectsCommand';
public constructor(public readonly serializedObjects: string) {
super();
}
public toJSON(): any {
return [this.type, this.serializedObjects];
}
}

View file

@ -0,0 +1,13 @@
import { Command } from '../command';
export class CreatePlayerCommand extends Command {
public static readonly type = 'CreatePlayerCommand';
public constructor(public readonly serializedPlayer: string) {
super();
}
public toJSON(): any {
return [this.type, this.serializedPlayer];
}
}

View file

@ -0,0 +1,14 @@
import { Id } from '../../transport/identity';
import { Command } from '../command';
export class DeleteObjectsCommand extends Command {
public static readonly type = 'DeleteObjectsCommand';
public constructor(public readonly ids: Array<Id>) {
super();
}
public toJSON(): any {
return [this.type, this.ids];
}
}

View file

@ -0,0 +1,14 @@
import { vec2 } from 'gl-matrix';
import { Command } from '../command';
export class MoveActionCommand extends Command {
public static readonly type = 'MoveActionCommand';
public constructor(public readonly delta: vec2) {
super();
}
public toJSON(): any {
return [this.type, this.delta];
}
}

View file

@ -0,0 +1,14 @@
import { vec2 } from 'gl-matrix';
import { Command } from '../command';
export class PrimaryActionCommand extends Command {
public static readonly type = 'PrimaryActionCommand';
public constructor(public readonly position: vec2) {
super();
}
public toJSON(): any {
return [this.type, this.position];
}
}

View file

@ -0,0 +1,14 @@
import { vec2 } from 'gl-matrix';
import { Command } from '../command';
export class SecondaryActionCommand extends Command {
public static readonly type = 'SecondaryActionCommand';
public constructor(public readonly position: vec2) {
super();
}
public toJSON(): any {
return [this.type, this.position];
}
}

View file

@ -0,0 +1,14 @@
import { Rectangle } from '../../helper/rectangle';
import { Command } from '../command';
export class SetViewAreaActionCommand extends Command {
public static readonly type = 'SetViewAreaAction';
public constructor(public readonly viewArea: Rectangle) {
super();
}
public toJSON(): any {
return [this.type, this.viewArea];
}
}

View file

@ -0,0 +1,14 @@
import { vec2 } from 'gl-matrix';
import { Command } from '../command';
export class TernaryActionCommand extends Command {
public static readonly type = 'TernaryActionCommand';
public constructor(public readonly position: vec2) {
super();
}
public toJSON(): any {
return [this.type, this.position];
}
}

View file

@ -0,0 +1,13 @@
import { Command } from '../command';
export class UpdateObjectsCommand extends Command {
public static readonly type = 'UpdateObjectsCommand';
public constructor(public readonly serializedObjects: string) {
super();
}
public toJSON(): any {
return [this.type, this.serializedObjects];
}
}

View file

@ -0,0 +1,31 @@
declare global {
interface Array<T> {
x: number;
y: number;
}
interface Float32Array {
x: number;
y: number;
}
}
const setIndexAlias = (name: string, index: number, type: any) => {
if (!Object.prototype.hasOwnProperty.call(type.prototype, name)) {
Object.defineProperty(type.prototype, name, {
get() {
return this[index];
},
set(value) {
this[index] = value;
},
});
}
};
export const applyArrayPlugins = () => {
setIndexAlias('x', 0, Array);
setIndexAlias('y', 1, Array);
setIndexAlias('x', 0, Float32Array);
setIndexAlias('y', 1, Float32Array);
};

View file

@ -0,0 +1,5 @@
import { vec2 } from 'gl-matrix';
export class Circle {
constructor(public center: vec2, public radius: number) {}
}

View file

@ -0,0 +1,4 @@
export const clamp = (value: number, min: number, max: number): number =>
Math.min(max, Math.max(min, value));
export const clamp01 = (value: number): number => Math.min(1, Math.max(0, value));

View file

@ -0,0 +1,23 @@
export class DeltaTimeCalculator {
private previousTime: DOMHighResTimeStamp | null = null;
constructor() {
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
}
public getNextDeltaTime(currentTime: DOMHighResTimeStamp): DOMHighResTimeStamp {
if (this.previousTime === null) {
this.previousTime = currentTime;
}
const delta = currentTime - this.previousTime;
this.previousTime = currentTime;
return delta;
}
private handleVisibilityChange() {
if (!document.hidden) {
this.previousTime = null;
}
}
}

View file

@ -0,0 +1,3 @@
export function last<T>(a: Array<T>): T | null {
return a.length > 0 ? a[a.length - 1] : null;
}

1
shared/src/helper/mix.ts Normal file
View file

@ -0,0 +1 @@
export const mix = (from: number, to: number, q: number) => from + (to - from) * q;

View file

@ -0,0 +1,4 @@
export const prettyPrint = (o: any): string =>
JSON.stringify(o, (_, v) => (v.toFixed ? Number(v.toFixed(3)) : v), ' ')
.replace(/("|,|{|^\n)/g, '')
.replace(/(\W*}\n?)+/g, '\n\n');

View file

@ -0,0 +1,18 @@
// src
// https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript
// Mulberry32
export abstract class Random {
private static _seed = Math.random();
public static set seed(value: number) {
Random._seed = value;
}
public static getRandom(): number {
let t = (Random._seed += 0x6d2b79f5);
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
}

View file

@ -0,0 +1,5 @@
import { vec2 } from 'gl-matrix';
export class Rectangle {
constructor(public topLeft = vec2.create(), public size = vec2.create()) {}
}

3
shared/src/helper/rgb.ts Normal file
View file

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

View file

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

View file

@ -0,0 +1,3 @@
import { vec2 } from 'gl-matrix';
export const rotate90Deg = (vec: vec2): vec2 => vec2.fromValues(-vec.y, vec.x);

View file

@ -0,0 +1 @@
export const toPercent = (value: number) => `${Math.round(value * 100)}%`;

View file

@ -0,0 +1,5 @@
let currentId = 0;
export const id = (): number => {
return currentId++;
};

View file

@ -0,0 +1,3 @@
export const wait = (ms: number): Promise<void> => {
return new Promise<void>((resolve, _) => setTimeout(resolve, ms));
};

29
shared/src/main.ts Normal file
View file

@ -0,0 +1,29 @@
export * from './commands/command';
export * from './commands/types/create-objects';
export * from './commands/types/create-player';
export * from './commands/types/delete-objects';
export * from './commands/types/update-objects';
export * from './commands/types/ternary-action';
export * from './commands/types/move-action';
export * from './commands/types/set-view-area-action';
export * from './commands/types/primary-action';
export * from './commands/types/secondary-action';
export * from './commands/command-receiver';
export * from './commands/command-generator';
export * from './commands/broadcast-commands';
export * from './helper/array';
export * from './helper/clamp';
export * from './helper/circle';
export * from './helper/rectangle';
export * from './helper/mix';
export * from './helper/random';
export * from './helper/unique';
export * from './helper/rotate-90-deg';
export * from './objects/game-object';
export * from './objects/deserialize';
export * from './objects/types/character-base';
export * from './objects/types/lamp-base';
export * from './objects/types/tunnel-base';
export * from './settings';
export * from './transport/transport-events';
export * from './transport/identity';

View file

@ -0,0 +1,26 @@
import { CreatePlayerCommand } from '../commands/types/create-player';
import { MoveActionCommand } from '../commands/types/move-action';
import { PrimaryActionCommand } from '../commands/types/primary-action';
import { SecondaryActionCommand } from '../commands/types/secondary-action';
import { SetViewAreaActionCommand } from '../commands/types/set-view-area-action';
import { TernaryActionCommand } from '../commands/types/ternary-action';
import { UpdateObjectsCommand } from '../commands/types/update-objects';
import { Command, CreateObjectsCommand, DeleteObjectsCommand } from '../main';
export const commandConstructors: {
[type: string]: new (...values: Array<any>) => any;
} = {
[CreateObjectsCommand.type]: CreateObjectsCommand,
[DeleteObjectsCommand.type]: DeleteObjectsCommand,
[CreatePlayerCommand.type]: CreatePlayerCommand,
[MoveActionCommand.type]: MoveActionCommand,
[PrimaryActionCommand.type]: PrimaryActionCommand,
[SecondaryActionCommand.type]: SecondaryActionCommand,
[TernaryActionCommand.type]: TernaryActionCommand,
[SetViewAreaActionCommand.type]: SetViewAreaActionCommand,
[UpdateObjectsCommand.type]: UpdateObjectsCommand,
};
export const deserializeCommand = ([type, ...values]: [string, Array<any>]): Command => {
return new commandConstructors[type](...values);
};

View file

@ -0,0 +1,12 @@
import { CommandReceiver } from '../commands/command-receiver';
import { Id } from '../transport/identity';
export abstract class GameObject extends CommandReceiver {
public get type(): string {
return (this as any).constructor.type;
}
constructor(public readonly id: Id) {
super();
}
}

View file

@ -0,0 +1,17 @@
import { vec2 } from 'gl-matrix';
import { Circle } from '../../helper/circle';
import { Id } from '../../main';
import { GameObject } from '../game-object';
export abstract class CharacterBase extends GameObject {
public static readonly type = 'Character';
constructor(
id: Id,
public head: Circle,
public leftFoot: Circle,
public rightFoot: Circle
) {
super(id);
}
}

View file

@ -0,0 +1,11 @@
import { vec2, vec3 } from 'gl-matrix';
import { Id } from '../../main';
import { GameObject } from '../game-object';
export abstract class LampBase extends GameObject {
public static readonly type = 'Lamp';
constructor(id: Id, public center: vec2, public color: vec3, public lightness: number) {
super(id);
}
}

View file

@ -0,0 +1,17 @@
import { vec2 } from 'gl-matrix';
import { GameObject, Id } from '../../main';
import { typed } from '../../transport/typed';
export abstract class TunnelBase extends GameObject {
public static readonly type = 'Tunnel';
constructor(
id: Id,
public readonly from: vec2,
public readonly to: vec2,
public readonly fromRadius: number,
public readonly toRadius: number
) {
super(id);
}
}

13
shared/src/settings.ts Normal file
View file

@ -0,0 +1,13 @@
import { vec2 } from 'gl-matrix';
export const settings = {
lightCutoffDistance: 600,
hitDetectionCirclePointCount: 32,
hitDetectionMaxOverlap: 0.01,
physicsMaxStep: 5,
gravitationalForce: vec2.fromValues(0, -0.015),
maxVelocityX: 1.5,
maxVelocityY: 8,
velocityAttenuation: 0.99,
inViewAreaSize: 1920 * 1080 * 3,
};

View file

@ -0,0 +1,8 @@
import { v4 } from 'uuid';
import { Id } from './identity';
export class IdentityManager {
public static generateId(): Id {
return v4();
}
}

View file

@ -0,0 +1 @@
export type Id = number;

View file

@ -1,4 +1,5 @@
export enum TransportEvents {
PlayerJoining = "PlayerJoining",
PlayerSendingInfo = "PlayerSendingInfo",
PlayerJoining = 'PlayerJoining',
PlayerToServer = 'PlayerToServer',
ServerToPlayer = 'ServerToPlayer',
}

View file

@ -0,0 +1,11 @@
/**
* static type: string must be implemented
* @param constructor
*/
export const typed = <T extends { new (...args: any[]): {} }>(constructor: T) => {
return class extends constructor {
public get type(): string {
return (this as any).constructor.type;
}
};
};

15
shared/tsconfig.json Normal file
View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": false,
"target": "es6",
"downlevelIteration": true,
"allowJs": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "Node",
"module": "es6",
"lib": ["es2015", "dom"]
}
}