Add basic multiplayer
This commit is contained in:
parent
0f0a1eaf67
commit
46a48e7c15
113 changed files with 1362 additions and 754 deletions
|
|
@ -15,7 +15,6 @@
|
|||
"@types/express": "^4.17.8",
|
||||
"@types/gl-matrix": "^2.4.5",
|
||||
"@types/node": "^14.11.2",
|
||||
"@types/uuid": "^8.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^3.9.1",
|
||||
"@typescript-eslint/parser": "^3.9.1",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
|
|
@ -32,20 +31,22 @@
|
|||
"terser-webpack-plugin": "^2.3.5",
|
||||
"ts-loader": "^8.0.1",
|
||||
"typescript": "^3.8.3",
|
||||
"@types/uuid": "^8.0.0",
|
||||
"uuid": "^8.2.0",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-server": "^3.10.3",
|
||||
"file-loader": "^6.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"file-loader": "^6.1.0",
|
||||
"@types/cors": "^2.8.7",
|
||||
"@types/socket.io": "^2.1.11",
|
||||
"webpack-node-externals": "^2.5.2",
|
||||
"shared": "file:../shared"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"http": "0.0.1-security",
|
||||
"socket.io": "^2.3.0",
|
||||
"uws": "^10.148.1",
|
||||
"webpack-node-externals": "^2.5.2"
|
||||
"uws": "^10.148.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,27 @@ import ioserver, { Socket } from 'socket.io';
|
|||
import express from 'express';
|
||||
import { Server } from 'http';
|
||||
import cors from 'cors';
|
||||
|
||||
import { PlayerContainer } from './players/player-container';
|
||||
import { applyArrayPlugins, Random, TransportEvents, deserializeCommand } from 'shared';
|
||||
import './index.html';
|
||||
import { TransportEvents } from '../../shared/src/transport/transport-events';
|
||||
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();
|
||||
|
||||
|
|
@ -23,8 +39,6 @@ const port = 3000;
|
|||
const server = new Server(app);
|
||||
const io = ioserver(server);
|
||||
|
||||
const players = new PlayerContainer();
|
||||
|
||||
const log = (text: string) => {
|
||||
io.to('insights').emit('insights', text + '\n');
|
||||
};
|
||||
|
|
@ -35,17 +49,15 @@ app.get('/', function (req, res) {
|
|||
|
||||
io.on('connection', (socket: SocketIO.Socket) => {
|
||||
socket.on(TransportEvents.PlayerJoining, () => {
|
||||
log('player joined');
|
||||
const player = new Player(objects, socket);
|
||||
|
||||
const player = new Player(socket);
|
||||
players.addPlayer(player);
|
||||
|
||||
socket.on(TransportEvents.PlayerSendingInfo, () => {});
|
||||
socket.on(TransportEvents.PlayerToServer, (text: string) => {
|
||||
const command = deserializeCommand(JSON.parse(text));
|
||||
player.sendCommand(command);
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
log('player disconnected');
|
||||
|
||||
players.removePlayerBySocketId(player.socketId);
|
||||
player.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
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 {
|
||||
constructor(private readonly socket: SocketIO.Socket) {}
|
||||
import { BoundingBox } from '../physics/bounds/bounding-box';
|
||||
import { PhysicalGameObject } from '../physics/physical-game-object';
|
||||
import { PhysicalGameObjectContainer } from '../physics/physical-game-object-container';
|
||||
import { jsonSerialize } from '../serialize';
|
||||
|
||||
public get socketId(): string {
|
||||
return this.socket.id;
|
||||
export class Player extends CommandReceiver {
|
||||
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/",
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": false,
|
||||
"target": "es6",
|
||||
"target": "es5",
|
||||
"downlevelIteration": true,
|
||||
"allowJs": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": ["node_modules/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ module.exports = {
|
|||
entry: {
|
||||
main: [PATHS.entryPoint],
|
||||
},
|
||||
externals: [nodeExternals()],
|
||||
externals: [
|
||||
nodeExternals({
|
||||
allowlist: [/(^shared)/],
|
||||
}),
|
||||
],
|
||||
target: 'node',
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue