Add basic multiplayer
This commit is contained in:
parent
0f0a1eaf67
commit
46a48e7c15
113 changed files with 1361 additions and 753 deletions
|
|
@ -15,7 +15,6 @@
|
||||||
"@types/express": "^4.17.8",
|
"@types/express": "^4.17.8",
|
||||||
"@types/gl-matrix": "^2.4.5",
|
"@types/gl-matrix": "^2.4.5",
|
||||||
"@types/node": "^14.11.2",
|
"@types/node": "^14.11.2",
|
||||||
"@types/uuid": "^8.0.0",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^3.9.1",
|
"@typescript-eslint/eslint-plugin": "^3.9.1",
|
||||||
"@typescript-eslint/parser": "^3.9.1",
|
"@typescript-eslint/parser": "^3.9.1",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
|
|
@ -32,20 +31,22 @@
|
||||||
"terser-webpack-plugin": "^2.3.5",
|
"terser-webpack-plugin": "^2.3.5",
|
||||||
"ts-loader": "^8.0.1",
|
"ts-loader": "^8.0.1",
|
||||||
"typescript": "^3.8.3",
|
"typescript": "^3.8.3",
|
||||||
|
"@types/uuid": "^8.0.0",
|
||||||
"uuid": "^8.2.0",
|
"uuid": "^8.2.0",
|
||||||
"webpack": "^4.43.0",
|
"webpack": "^4.43.0",
|
||||||
"webpack-cli": "^3.3.11",
|
"webpack-cli": "^3.3.11",
|
||||||
"webpack-dev-server": "^3.10.3",
|
"webpack-dev-server": "^3.10.3",
|
||||||
"file-loader": "^6.1.0"
|
"file-loader": "^6.1.0",
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@types/cors": "^2.8.7",
|
"@types/cors": "^2.8.7",
|
||||||
"@types/socket.io": "^2.1.11",
|
"@types/socket.io": "^2.1.11",
|
||||||
|
"webpack-node-externals": "^2.5.2",
|
||||||
|
"shared": "file:../shared"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"http": "0.0.1-security",
|
"http": "0.0.1-security",
|
||||||
"socket.io": "^2.3.0",
|
"socket.io": "^2.3.0",
|
||||||
"uws": "^10.148.1",
|
"uws": "^10.148.1"
|
||||||
"webpack-node-externals": "^2.5.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,27 @@ import ioserver, { Socket } from 'socket.io';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { Server } from 'http';
|
import { Server } from 'http';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
|
import { applyArrayPlugins, Random, TransportEvents, deserializeCommand } from 'shared';
|
||||||
import { PlayerContainer } from './players/player-container';
|
|
||||||
import './index.html';
|
import './index.html';
|
||||||
import { TransportEvents } from '../../shared/src/transport/transport-events';
|
|
||||||
import { Player } from './players/player';
|
import { Player } from './players/player';
|
||||||
|
import { PhysicalGameObjectContainer } from './physics/physical-game-object-container';
|
||||||
|
import { createDungeon } from './map/create-dungeon';
|
||||||
|
import { glMatrix } from 'gl-matrix';
|
||||||
|
|
||||||
|
glMatrix.setMatrixArrayType(Array);
|
||||||
|
|
||||||
|
applyArrayPlugins();
|
||||||
|
|
||||||
|
Random.seed = 42;
|
||||||
|
|
||||||
|
const objects = new PhysicalGameObjectContainer();
|
||||||
|
|
||||||
|
createDungeon(objects);
|
||||||
|
createDungeon(objects);
|
||||||
|
createDungeon(objects);
|
||||||
|
createDungeon(objects);
|
||||||
|
|
||||||
|
objects.initialize();
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
|
@ -23,8 +39,6 @@ const port = 3000;
|
||||||
const server = new Server(app);
|
const server = new Server(app);
|
||||||
const io = ioserver(server);
|
const io = ioserver(server);
|
||||||
|
|
||||||
const players = new PlayerContainer();
|
|
||||||
|
|
||||||
const log = (text: string) => {
|
const log = (text: string) => {
|
||||||
io.to('insights').emit('insights', text + '\n');
|
io.to('insights').emit('insights', text + '\n');
|
||||||
};
|
};
|
||||||
|
|
@ -35,17 +49,15 @@ app.get('/', function (req, res) {
|
||||||
|
|
||||||
io.on('connection', (socket: SocketIO.Socket) => {
|
io.on('connection', (socket: SocketIO.Socket) => {
|
||||||
socket.on(TransportEvents.PlayerJoining, () => {
|
socket.on(TransportEvents.PlayerJoining, () => {
|
||||||
log('player joined');
|
const player = new Player(objects, socket);
|
||||||
|
|
||||||
const player = new Player(socket);
|
socket.on(TransportEvents.PlayerToServer, (text: string) => {
|
||||||
players.addPlayer(player);
|
const command = deserializeCommand(JSON.parse(text));
|
||||||
|
player.sendCommand(command);
|
||||||
socket.on(TransportEvents.PlayerSendingInfo, () => {});
|
});
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
log('player disconnected');
|
player.destroy();
|
||||||
|
|
||||||
players.removePlayerBySocketId(player.socketId);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
46
backend/src/map/create-dungeon.ts
Normal file
46
backend/src/map/create-dungeon.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { vec2, vec3 } from 'gl-matrix';
|
||||||
|
import { Random } from 'shared';
|
||||||
|
import { LampPhysics } from '../objects/lamp-physics';
|
||||||
|
import { TunnelPhysics } from '../objects/tunnel-physics';
|
||||||
|
import { PhysicalGameObjectContainer } from '../physics/physical-game-object-container';
|
||||||
|
|
||||||
|
export const createDungeon = (objects: PhysicalGameObjectContainer) => {
|
||||||
|
let previousRadius = 350;
|
||||||
|
let previousEnd = vec2.create();
|
||||||
|
|
||||||
|
let tunnelsCountSinceLastLight = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < 500000; i += 500) {
|
||||||
|
const deltaHeight = (Random.getRandom() - 0.5) * 2000;
|
||||||
|
const height = previousEnd.y + deltaHeight;
|
||||||
|
const currentEnd = vec2.fromValues(i, height);
|
||||||
|
const currentToRadius = Random.getRandom() * 300 + 150;
|
||||||
|
|
||||||
|
const tunnel = new TunnelPhysics(
|
||||||
|
previousEnd,
|
||||||
|
currentEnd,
|
||||||
|
previousRadius,
|
||||||
|
currentToRadius
|
||||||
|
);
|
||||||
|
|
||||||
|
objects.addObject(tunnel, false);
|
||||||
|
|
||||||
|
if (++tunnelsCountSinceLastLight > 3 && Random.getRandom() > 0.7) {
|
||||||
|
objects.addObject(
|
||||||
|
new LampPhysics(
|
||||||
|
currentEnd,
|
||||||
|
vec3.normalize(
|
||||||
|
vec3.create(),
|
||||||
|
vec3.fromValues(Random.getRandom(), 0, Random.getRandom())
|
||||||
|
),
|
||||||
|
0.5
|
||||||
|
),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
tunnelsCountSinceLastLight = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousEnd = currentEnd;
|
||||||
|
previousRadius = currentToRadius;
|
||||||
|
}
|
||||||
|
};
|
||||||
31
backend/src/objects/character-physics.ts
Normal file
31
backend/src/objects/character-physics.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { id, CharacterBase } from 'shared';
|
||||||
|
|
||||||
|
import { ImmutableBoundingBox } from '../physics/bounds/immutable-bounding-box';
|
||||||
|
import { PhysicalGameObject } from '../physics/physical-game-object';
|
||||||
|
import { CirclePhysics } from './circle-physics';
|
||||||
|
|
||||||
|
export class CharacterPhysics extends CharacterBase implements PhysicalGameObject {
|
||||||
|
public readonly canCollide = true;
|
||||||
|
public readonly isInverted = false;
|
||||||
|
public readonly canMove = true;
|
||||||
|
|
||||||
|
constructor(head: CirclePhysics, leftFoot: CirclePhysics, rightFoot: CirclePhysics) {
|
||||||
|
super(id(), head, leftFoot, rightFoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boundingBox?: ImmutableBoundingBox;
|
||||||
|
|
||||||
|
public getBoundingBox(): ImmutableBoundingBox {
|
||||||
|
if (!this.boundingBox) {
|
||||||
|
this.boundingBox = (this.head as CirclePhysics).boundingBox;
|
||||||
|
(this.head as CirclePhysics).boundingBox.owner = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.boundingBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toJSON(): any {
|
||||||
|
const { type, id, head, leftFoot, rightFoot } = this;
|
||||||
|
return [type, id, head, leftFoot, rightFoot];
|
||||||
|
}
|
||||||
|
}
|
||||||
76
backend/src/objects/circle-physics.ts
Normal file
76
backend/src/objects/circle-physics.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { vec2 } from 'gl-matrix';
|
||||||
|
import { Circle } from 'shared';
|
||||||
|
import { BoundingBox } from '../physics/bounds/bounding-box';
|
||||||
|
import { BoundingBoxBase } from '../physics/bounds/bounding-box-base';
|
||||||
|
|
||||||
|
export class CirclePhysics implements Circle {
|
||||||
|
private _boundingBox: BoundingBox;
|
||||||
|
|
||||||
|
constructor(private _center: vec2, private _radius: number) {
|
||||||
|
this._boundingBox = new BoundingBox(null);
|
||||||
|
this.recalculateBoundingBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get boundingBox(): BoundingBoxBase {
|
||||||
|
return this._boundingBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get center(): vec2 {
|
||||||
|
return this._center;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set center(value: vec2) {
|
||||||
|
this._center = value;
|
||||||
|
this.recalculateBoundingBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get radius(): number {
|
||||||
|
return this._radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set radius(value: number) {
|
||||||
|
this._radius = value;
|
||||||
|
this.recalculateBoundingBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
public distance(target: vec2): number {
|
||||||
|
return vec2.distance(target, this.center) - this.radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public distanceBetween(target: CirclePhysics): number {
|
||||||
|
return vec2.distance(target.center, this.center) - this.radius - target.radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public areIntersecting(other: CirclePhysics): boolean {
|
||||||
|
return other.distance(this.center) < this.radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isInside(other: CirclePhysics): boolean {
|
||||||
|
return other.distance(this.center) < -this.radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPerimeterPoints(count: number): Array<vec2> {
|
||||||
|
const result: Array<vec2> = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
result.push(
|
||||||
|
vec2.fromValues(
|
||||||
|
Math.cos((2 * Math.PI * i) / count) * this.radius + this.center.x,
|
||||||
|
Math.sin((2 * Math.PI * i) / count) * this.radius + this.center.y
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private recalculateBoundingBox() {
|
||||||
|
this._boundingBox.xMin = this.center.x - this._radius;
|
||||||
|
this._boundingBox.xMax = this.center.x + this._radius;
|
||||||
|
this._boundingBox.yMin = this.center.y - this._radius;
|
||||||
|
this._boundingBox.yMax = this.center.y + this._radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toJSON(): any {
|
||||||
|
const { center, radius } = this;
|
||||||
|
return { center, radius };
|
||||||
|
}
|
||||||
|
}
|
||||||
36
backend/src/objects/lamp-physics.ts
Normal file
36
backend/src/objects/lamp-physics.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { vec2, vec3 } from 'gl-matrix';
|
||||||
|
import { LampBase, settings, id } from 'shared';
|
||||||
|
|
||||||
|
import { ImmutableBoundingBox } from '../physics/bounds/immutable-bounding-box';
|
||||||
|
import { PhysicalGameObject } from '../physics/physical-game-object';
|
||||||
|
|
||||||
|
export class LampPhysics extends LampBase implements PhysicalGameObject {
|
||||||
|
public readonly canCollide = false;
|
||||||
|
public readonly isInverted = false;
|
||||||
|
public readonly canMove = false;
|
||||||
|
|
||||||
|
constructor(center: vec2, color: vec3, lightness: number) {
|
||||||
|
super(id(), center, color, lightness);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boundingBox?: ImmutableBoundingBox;
|
||||||
|
|
||||||
|
public getBoundingBox(): ImmutableBoundingBox {
|
||||||
|
if (!this.boundingBox) {
|
||||||
|
this.boundingBox = new ImmutableBoundingBox(
|
||||||
|
this,
|
||||||
|
this.center.x - settings.lightCutoffDistance,
|
||||||
|
this.center.x + settings.lightCutoffDistance,
|
||||||
|
this.center.y - settings.lightCutoffDistance,
|
||||||
|
this.center.y + settings.lightCutoffDistance
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.boundingBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toJSON(): any {
|
||||||
|
const { type, id, center, color, lightness } = this;
|
||||||
|
return [type, id, center, color, lightness];
|
||||||
|
}
|
||||||
|
}
|
||||||
48
backend/src/objects/tunnel-physics.ts
Normal file
48
backend/src/objects/tunnel-physics.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { vec2 } from 'gl-matrix';
|
||||||
|
import { clamp01, mix, TunnelBase, id } from 'shared';
|
||||||
|
import { PhysicalGameObject } from '../physics/physical-game-object';
|
||||||
|
|
||||||
|
import { ImmutableBoundingBox } from '../physics/bounds/immutable-bounding-box';
|
||||||
|
|
||||||
|
export class TunnelPhysics extends TunnelBase implements PhysicalGameObject {
|
||||||
|
public readonly canCollide = true;
|
||||||
|
public readonly isInverted = true;
|
||||||
|
public readonly canMove = false;
|
||||||
|
|
||||||
|
private boundingBox?: ImmutableBoundingBox;
|
||||||
|
|
||||||
|
constructor(from: vec2, to: vec2, fromRadius: number, toRadius: number) {
|
||||||
|
super(id(), from, to, fromRadius, toRadius);
|
||||||
|
}
|
||||||
|
|
||||||
|
public distance(target: vec2): number {
|
||||||
|
const toFromDelta = vec2.subtract(vec2.create(), this.to, this.from);
|
||||||
|
const targetFromDelta = vec2.subtract(vec2.create(), target, this.from);
|
||||||
|
|
||||||
|
const h = clamp01(
|
||||||
|
vec2.dot(targetFromDelta, toFromDelta) / vec2.dot(toFromDelta, toFromDelta)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
vec2.distance(targetFromDelta, vec2.scale(vec2.create(), toFromDelta, h)) -
|
||||||
|
mix(this.fromRadius, this.toRadius, h)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getBoundingBox(): ImmutableBoundingBox {
|
||||||
|
if (!this.boundingBox) {
|
||||||
|
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);
|
||||||
|
this.boundingBox = new ImmutableBoundingBox(this, xMin, xMax, yMin, yMax, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.boundingBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toJSON(): any {
|
||||||
|
const { type, id, from, to, fromRadius, toRadius } = this;
|
||||||
|
return [type, id, from, to, fromRadius, toRadius];
|
||||||
|
}
|
||||||
|
}
|
||||||
62
backend/src/physics/bounds/bounding-box-base.ts
Normal file
62
backend/src/physics/bounds/bounding-box-base.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { vec2 } from 'gl-matrix';
|
||||||
|
import { PhysicalGameObject } from '../physical-game-object';
|
||||||
|
|
||||||
|
export abstract class BoundingBoxBase {
|
||||||
|
constructor(
|
||||||
|
public owner: PhysicalGameObject,
|
||||||
|
protected _xMin: number = 0,
|
||||||
|
protected _xMax: number = 0,
|
||||||
|
protected _yMin: number = 0,
|
||||||
|
protected _yMax: number = 0,
|
||||||
|
public readonly isInverted = false
|
||||||
|
) {}
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
backend/src/physics/bounds/bounding-box.ts
Normal file
65
backend/src/physics/bounds/bounding-box.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { vec2 } from 'gl-matrix';
|
||||||
|
import { BoundingBoxBase } from './bounding-box-base';
|
||||||
|
import { ImmutableBoundingBox } from './immutable-bounding-box';
|
||||||
|
|
||||||
|
export class BoundingBox extends BoundingBoxBase {
|
||||||
|
public get xMin(): number {
|
||||||
|
return this._xMin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set xMin(value: number) {
|
||||||
|
this._xMin = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set xMax(value: number) {
|
||||||
|
this._xMax = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get xMax(): number {
|
||||||
|
return this._xMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set yMin(value: number) {
|
||||||
|
this._yMin = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get yMin(): number {
|
||||||
|
return this._yMin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set yMax(value: number) {
|
||||||
|
this._yMax = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get yMax(): number {
|
||||||
|
return this._yMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get topLeft(): vec2 {
|
||||||
|
return vec2.fromValues(this._xMin, this._yMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get size(): vec2 {
|
||||||
|
return vec2.fromValues(this._xMax - this._xMin, this._yMax - this._yMin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public cloneAsImmutable(): ImmutableBoundingBox {
|
||||||
|
return new ImmutableBoundingBox(
|
||||||
|
this.owner,
|
||||||
|
this.xMin,
|
||||||
|
this.xMax,
|
||||||
|
this.yMin,
|
||||||
|
this.yMax
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
backend/src/physics/bounds/immutable-bounding-box.ts
Normal file
3
backend/src/physics/bounds/immutable-bounding-box.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { BoundingBoxBase } from './bounding-box-base';
|
||||||
|
|
||||||
|
export class ImmutableBoundingBox extends BoundingBoxBase {}
|
||||||
20
backend/src/physics/containers/bounding-box-list.ts
Normal file
20
backend/src/physics/containers/bounding-box-list.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { BoundingBoxBase } from '../bounds/bounding-box-base';
|
||||||
|
|
||||||
|
export class BoundingBoxList {
|
||||||
|
constructor(private boundingBoxes: Array<BoundingBoxBase> = []) {}
|
||||||
|
|
||||||
|
public insert(box: BoundingBoxBase) {
|
||||||
|
this.boundingBoxes.push(box);
|
||||||
|
}
|
||||||
|
|
||||||
|
public remove(box: BoundingBoxBase) {
|
||||||
|
this.boundingBoxes.splice(
|
||||||
|
this.boundingBoxes.findIndex((i) => i === box),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public findIntersecting(box: BoundingBoxBase): Array<BoundingBoxBase> {
|
||||||
|
return this.boundingBoxes.filter((b) => b.intersects(box));
|
||||||
|
}
|
||||||
|
}
|
||||||
121
backend/src/physics/containers/bounding-box-tree.ts
Normal file
121
backend/src/physics/containers/bounding-box-tree.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
// source: https://github.com/ubilabs/kd-tree-javascript/blob/master/kdTree.js
|
||||||
|
|
||||||
|
import { ImmutableBoundingBox } from '../bounds/immutable-bounding-box';
|
||||||
|
|
||||||
|
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<ImmutableBoundingBox> = []) {
|
||||||
|
this.build(boxes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(boxes: Array<ImmutableBoundingBox>) {
|
||||||
|
this.root = this.buildRecursive(boxes, 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildRecursive(
|
||||||
|
boxes: Array<ImmutableBoundingBox>,
|
||||||
|
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 findIntersecting(box: ImmutableBoundingBox): Array<ImmutableBoundingBox> {
|
||||||
|
const maybeResults = this.findMaybeIntersecting(box, this.root, 0);
|
||||||
|
const results = maybeResults.filter((b) => b.intersects(box));
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private findMaybeIntersecting(
|
||||||
|
box: ImmutableBoundingBox,
|
||||||
|
node: Node,
|
||||||
|
depth: number
|
||||||
|
): Array<ImmutableBoundingBox> {
|
||||||
|
if (node === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth % 4 == 0 && box.xMax < node.rectangle.xMin) {
|
||||||
|
return this.findMaybeIntersecting(box, node.left, depth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth % 4 == 1 && box.xMin > node.rectangle.xMax) {
|
||||||
|
return this.findMaybeIntersecting(box, node.right, depth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth % 4 == 2 && box.yMax < node.rectangle.yMin) {
|
||||||
|
return this.findMaybeIntersecting(box, node.left, depth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth % 4 == 3 && box.yMin > node.rectangle.yMax) {
|
||||||
|
return this.findMaybeIntersecting(box, node.right, depth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
node.rectangle,
|
||||||
|
...this.findMaybeIntersecting(box, node.left, depth + 1),
|
||||||
|
...this.findMaybeIntersecting(box, node.right, depth + 1),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private findParent(
|
||||||
|
box: ImmutableBoundingBox,
|
||||||
|
node: Node,
|
||||||
|
depth: number,
|
||||||
|
parent: Node
|
||||||
|
): [Node, number] {
|
||||||
|
if (node === null) {
|
||||||
|
return [parent, depth - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
86
backend/src/physics/physical-game-object-container.ts
Normal file
86
backend/src/physics/physical-game-object-container.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { GameObject, Id } from 'shared';
|
||||||
|
import { BoundingBoxBase } from './bounds/bounding-box-base';
|
||||||
|
|
||||||
|
import { BoundingBoxList } from './containers/bounding-box-list';
|
||||||
|
import { BoundingBoxTree } from './containers/bounding-box-tree';
|
||||||
|
import { Command } from 'shared';
|
||||||
|
import { PhysicalGameObject } from './physical-game-object';
|
||||||
|
import { ImmutableBoundingBox } from './bounds/immutable-bounding-box';
|
||||||
|
|
||||||
|
export class PhysicalGameObjectContainer {
|
||||||
|
private isTreeInitialized = false;
|
||||||
|
private staticBoundingBoxesWaitList: Array<ImmutableBoundingBox> = [];
|
||||||
|
private staticBoundingBoxes = new BoundingBoxTree();
|
||||||
|
private dynamicBoundingBoxes = new BoundingBoxList();
|
||||||
|
|
||||||
|
protected objects: Map<Id, GameObject> = new Map();
|
||||||
|
private objectsGroupedByAbilities: Map<string, Array<GameObject>> = new Map();
|
||||||
|
|
||||||
|
public initialize() {
|
||||||
|
this.staticBoundingBoxes.build(this.staticBoundingBoxesWaitList);
|
||||||
|
this.isTreeInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addObject(object: PhysicalGameObject, isDynamic) {
|
||||||
|
this.objects.set(object.id, object);
|
||||||
|
|
||||||
|
for (const command of this.objectsGroupedByAbilities.keys()) {
|
||||||
|
if (object.reactsToCommand(command)) {
|
||||||
|
this.objectsGroupedByAbilities.get(command).push(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDynamic) {
|
||||||
|
this.dynamicBoundingBoxes.insert(object.getBoundingBox());
|
||||||
|
} else {
|
||||||
|
if (!this.isTreeInitialized) {
|
||||||
|
this.staticBoundingBoxesWaitList.push(object.getBoundingBox());
|
||||||
|
} else {
|
||||||
|
this.staticBoundingBoxes.insert(object.getBoundingBox());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeObject(object: PhysicalGameObject) {
|
||||||
|
this.objects.delete(object.id);
|
||||||
|
|
||||||
|
for (const command of this.objectsGroupedByAbilities.keys()) {
|
||||||
|
if (object.reactsToCommand(command)) {
|
||||||
|
const array = this.objectsGroupedByAbilities.get(command);
|
||||||
|
array.splice(
|
||||||
|
array.findIndex((i) => i.id == object.id),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dynamicBoundingBoxes.remove(object.getBoundingBox());
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendCommand(e: Command) {
|
||||||
|
if (!this.objectsGroupedByAbilities.has(e.type)) {
|
||||||
|
this.createGroupForCommand(e.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.objectsGroupedByAbilities.get(e.type).forEach((o, _) => o.sendCommand(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
private createGroupForCommand(commandType: string) {
|
||||||
|
const objectsReactingToCommand = [];
|
||||||
|
|
||||||
|
this.objects.forEach((o, _) => {
|
||||||
|
if (o.reactsToCommand(commandType)) {
|
||||||
|
objectsReactingToCommand.push(o);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.objectsGroupedByAbilities.set(commandType, objectsReactingToCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
public findIntersecting(box: BoundingBoxBase): Array<PhysicalGameObject> {
|
||||||
|
return [
|
||||||
|
...this.staticBoundingBoxes.findIntersecting(box),
|
||||||
|
...this.dynamicBoundingBoxes.findIntersecting(box),
|
||||||
|
].map((b) => b.owner);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
backend/src/physics/physical-game-object.ts
Normal file
10
backend/src/physics/physical-game-object.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { GameObject } from 'shared';
|
||||||
|
import { BoundingBoxBase } from './bounds/bounding-box-base';
|
||||||
|
|
||||||
|
export interface PhysicalGameObject extends GameObject {
|
||||||
|
getBoundingBox(): BoundingBoxBase;
|
||||||
|
//distance(target: vec2): number;
|
||||||
|
readonly isInverted: boolean;
|
||||||
|
readonly canCollide: boolean;
|
||||||
|
readonly canMove: boolean;
|
||||||
|
}
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { Player } from './player';
|
|
||||||
|
|
||||||
export class PlayerContainer {
|
|
||||||
private socketIdToPlayer = new Map<string, Player>();
|
|
||||||
|
|
||||||
public addPlayer(player: Player) {
|
|
||||||
this.socketIdToPlayer.set(player.socketId, player);
|
|
||||||
}
|
|
||||||
|
|
||||||
public removePlayerBySocketId(socketId: string) {
|
|
||||||
this.socketIdToPlayer.delete(socketId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +1,119 @@
|
||||||
import { Socket } from 'dgram';
|
import { vec2 } from 'gl-matrix';
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandExecutors,
|
||||||
|
CommandReceiver,
|
||||||
|
CreateObjectsCommand,
|
||||||
|
CreatePlayerCommand,
|
||||||
|
DeleteObjectsCommand,
|
||||||
|
MoveActionCommand,
|
||||||
|
SetViewAreaActionCommand,
|
||||||
|
TransportEvents,
|
||||||
|
UpdateObjectsCommand,
|
||||||
|
} from 'shared';
|
||||||
|
import { CharacterPhysics } from '../objects/character-physics';
|
||||||
|
import { CirclePhysics } from '../objects/circle-physics';
|
||||||
|
|
||||||
export class Player {
|
import { BoundingBox } from '../physics/bounds/bounding-box';
|
||||||
constructor(private readonly socket: SocketIO.Socket) {}
|
import { PhysicalGameObject } from '../physics/physical-game-object';
|
||||||
|
import { PhysicalGameObjectContainer } from '../physics/physical-game-object-container';
|
||||||
|
import { jsonSerialize } from '../serialize';
|
||||||
|
|
||||||
public get socketId(): string {
|
export class Player extends CommandReceiver {
|
||||||
return this.socket.id;
|
public isActive = true;
|
||||||
|
|
||||||
|
private character: CharacterPhysics = new CharacterPhysics(
|
||||||
|
new CirclePhysics(vec2.fromValues(50, 50), 50),
|
||||||
|
new CirclePhysics(vec2.fromValues(50, 50), 50),
|
||||||
|
new CirclePhysics(vec2.fromValues(50, 50), 50)
|
||||||
|
);
|
||||||
|
|
||||||
|
private objectsPreviouslyInViewArea: Array<PhysicalGameObject> = [];
|
||||||
|
private objectsInViewArea: Array<PhysicalGameObject> = [];
|
||||||
|
|
||||||
|
protected commandExecutors: CommandExecutors = {
|
||||||
|
[SetViewAreaActionCommand.type]: this.setViewArea.bind(this),
|
||||||
|
[MoveActionCommand.type]: (c: MoveActionCommand) => {
|
||||||
|
vec2.normalize(c.delta, c.delta);
|
||||||
|
vec2.scale(c.delta, c.delta, 40);
|
||||||
|
this.character.head.center = vec2.add(
|
||||||
|
this.character.head.center,
|
||||||
|
this.character.head.center,
|
||||||
|
c.delta
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
protected defaultCommandExecutor(command: Command) {}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly objects: PhysicalGameObjectContainer,
|
||||||
|
private readonly socket: SocketIO.Socket
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.objectsPreviouslyInViewArea.push(this.character);
|
||||||
|
this.objectsInViewArea.push(this.character);
|
||||||
|
|
||||||
|
this.objects.addObject(this.character, true);
|
||||||
|
|
||||||
|
socket.emit(
|
||||||
|
TransportEvents.ServerToPlayer,
|
||||||
|
jsonSerialize(new CreatePlayerCommand(jsonSerialize(this.character)))
|
||||||
|
);
|
||||||
|
|
||||||
|
this.sendObjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
public setViewArea(c: SetViewAreaActionCommand) {
|
||||||
|
const viewArea = new BoundingBox(null);
|
||||||
|
viewArea.topLeft = c.viewArea.topLeft;
|
||||||
|
viewArea.size = c.viewArea.size;
|
||||||
|
|
||||||
|
this.objectsInViewArea = this.objects.findIntersecting(viewArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendObjects() {
|
||||||
|
const newlyIntersecting = this.objectsInViewArea.filter(
|
||||||
|
(o) => !this.objectsPreviouslyInViewArea.includes(o)
|
||||||
|
);
|
||||||
|
|
||||||
|
const noLongerIntersecting = this.objectsPreviouslyInViewArea.filter(
|
||||||
|
(o) => !this.objectsInViewArea.includes(o)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.objectsPreviouslyInViewArea = this.objectsInViewArea;
|
||||||
|
|
||||||
|
if (noLongerIntersecting.length > 0) {
|
||||||
|
this.socket.emit(
|
||||||
|
TransportEvents.ServerToPlayer,
|
||||||
|
jsonSerialize(new DeleteObjectsCommand(noLongerIntersecting.map((o) => o.id)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newlyIntersecting.length > 0) {
|
||||||
|
this.socket.emit(
|
||||||
|
TransportEvents.ServerToPlayer,
|
||||||
|
jsonSerialize(new CreateObjectsCommand(jsonSerialize(newlyIntersecting)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.socket.emit(
|
||||||
|
TransportEvents.ServerToPlayer,
|
||||||
|
jsonSerialize(
|
||||||
|
new UpdateObjectsCommand(
|
||||||
|
jsonSerialize(this.objectsInViewArea.filter((o) => o.canMove))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.isActive) {
|
||||||
|
//setImmediate(this.sendObjects.bind(this));
|
||||||
|
setTimeout(this.sendObjects.bind(this), 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
this.isActive = false;
|
||||||
|
this.objects.removeObject(this.character);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
backend/src/serialize.ts
Normal file
2
backend/src/serialize.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const jsonSerialize = (o: any): string =>
|
||||||
|
JSON.stringify(o, (key, value) => (value?.toFixed ? Number(value.toFixed(3)) : value));
|
||||||
|
|
@ -3,17 +3,12 @@
|
||||||
"outDir": "./dist/",
|
"outDir": "./dist/",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"target": "es6",
|
"target": "es5",
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true
|
||||||
"baseUrl": ".",
|
}
|
||||||
"paths": {
|
|
||||||
"*": ["node_modules/*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,11 @@ module.exports = {
|
||||||
entry: {
|
entry: {
|
||||||
main: [PATHS.entryPoint],
|
main: [PATHS.entryPoint],
|
||||||
},
|
},
|
||||||
externals: [nodeExternals()],
|
externals: [
|
||||||
|
nodeExternals({
|
||||||
|
allowlist: [/(^shared)/],
|
||||||
|
}),
|
||||||
|
],
|
||||||
target: 'node',
|
target: 'node',
|
||||||
output: {
|
output: {
|
||||||
filename: '[name].js',
|
filename: '[name].js',
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@
|
||||||
"webpack-dev-server": "^3.10.3",
|
"webpack-dev-server": "^3.10.3",
|
||||||
"firebase": "^7.22.0",
|
"firebase": "^7.22.0",
|
||||||
"@types/socket.io-client": "^1.4.34",
|
"@types/socket.io-client": "^1.4.34",
|
||||||
"socket.io-client": "^2.3.1"
|
"socket.io-client": "^2.3.1",
|
||||||
|
"shared": "file:../shared"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue