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

View file

@ -0,0 +1,36 @@
import { vec2 } from 'gl-matrix';
import { CommandExecutors, GameObject, Rectangle, settings } from 'shared';
import { RenderCommand } from '../commands/types/render';
export class Camera extends GameObject {
private static readonly inViewAreaSize = settings.inViewAreaSize;
private _viewArea = new Rectangle();
protected commandExecutors: CommandExecutors = {
[RenderCommand.type]: this.draw.bind(this),
};
constructor(public center: vec2 = vec2.create()) {
super(null);
}
public get viewArea(): Rectangle {
return this._viewArea;
}
private draw(c: RenderCommand) {
const canvasAspectRatio = c.renderer.canvasSize.x / c.renderer.canvasSize.y;
this._viewArea.topLeft = vec2.fromValues(
this.center.x - this._viewArea.size.x / 2,
this.center.y + this._viewArea.size.y / 2
);
this._viewArea.size = vec2.fromValues(
Math.sqrt(Camera.inViewAreaSize * canvasAspectRatio),
Math.sqrt(Camera.inViewAreaSize / canvasAspectRatio)
);
c.renderer.setViewArea(this._viewArea.topLeft, this._viewArea.size);
}
}

View file

@ -0,0 +1,19 @@
import { vec2 } from 'gl-matrix';
import { CharacterBase, CommandExecutors } from 'shared';
import { RenderCommand } from '../commands/types/render';
import { BlobShape } from '../shapes/blob-shape';
export class CharacterView extends CharacterBase {
private shape = new BlobShape();
protected commandExecutors: CommandExecutors = {
[RenderCommand.type]: (c: RenderCommand) => {
this.shape.setCircles([this.head, this.leftFoot, this.rightFoot]);
c.renderer.addDrawable(this.shape);
},
};
public get position(): vec2 {
return this.head.center;
}
}

View file

@ -0,0 +1,63 @@
import {
Command,
CommandExecutors,
CommandReceiver,
CreateObjectsCommand,
CreatePlayerCommand,
DeleteObjectsCommand,
GameObject,
Id,
UpdateObjectsCommand,
} from 'shared';
import { StepCommand } from '../commands/types/step';
import { deserialize, deserializeJsonArray } from '../transport/deserialize';
import { Camera } from './camera';
import { CharacterView } from './character-view';
export class GameObjectContainer extends CommandReceiver {
protected objects: Map<Id, GameObject> = new Map();
public player: CharacterView;
public camera: Camera;
protected commandExecutors: CommandExecutors = {
[CreatePlayerCommand.type]: (c: CreatePlayerCommand) => {
this.player = deserialize(c.serializedPlayer) as CharacterView;
this.camera = new Camera();
this.addObject(this.player);
this.addObject(this.camera);
},
[StepCommand.type]: (c: StepCommand) => {
if (this.player) {
this.camera.center = this.player.position;
}
},
[CreateObjectsCommand.type]: (c: CreateObjectsCommand) =>
deserializeJsonArray(c.serializedObjects).forEach((o) => this.addObject(o)),
[DeleteObjectsCommand.type]: (c: DeleteObjectsCommand) =>
c.ids.forEach((id: Id) => this.objects.delete(id)),
[UpdateObjectsCommand.type]: (c: UpdateObjectsCommand) =>
deserializeJsonArray(c.serializedObjects).forEach((o) => {
this.objects.delete(o.id);
this.addObject(o);
if (o.id === this.player.id) {
this.player = o as CharacterView;
}
}),
};
protected defaultCommandExecutor(c: Command) {
this.objects.forEach((o) => o.sendCommand(c));
}
public sendCommandToSingleObject(id: Id, e: Command) {
this.objects.get(id)!.sendCommand(e);
}
private addObject(object: GameObject) {
this.objects.set(object.id, object);
}
}

View file

@ -1,31 +0,0 @@
import { Command } from '../commands/command';
import { CommandReceiver } from '../commands/command-receiver';
import { IdentityManager } from '../identity/identity-manager';
export abstract class GameObject implements CommandReceiver {
public readonly id = IdentityManager.generateId();
private commandExecutors: {
[commandType: string]: (e: Command) => void;
} = {};
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);
}
}
// can only be called inside the constructor
protected addCommandExecutor<T extends Command>(
commandType: new () => T,
handler: (command: T) => void
) {
this.commandExecutors[new commandType().type] = handler;
}
}

View file

@ -0,0 +1,17 @@
import { vec2, vec3 } from 'gl-matrix';
import { CircleLight } from 'sdf-2d';
import { CommandExecutors, Id, LampBase } from 'shared';
import { RenderCommand } from '../commands/types/render';
export class LampView extends LampBase {
private light: CircleLight;
protected commandExecutors: CommandExecutors = {
[RenderCommand.type]: (c: RenderCommand) => c.renderer.addDrawable(this.light),
} as any;
constructor(id: Id, center: vec2, color: vec3, lightness: number) {
super(id, center, color, lightness);
this.light = new CircleLight(center, color, lightness);
}
}

View file

@ -1,58 +0,0 @@
import { Command } from '../commands/command';
import { CommandReceiver } from '../commands/command-receiver';
import { Id } from '../identity/identity';
import { GameObject } from './game-object';
export class Objects implements CommandReceiver {
private objects: Map<Id, GameObject> = new Map();
private objectsGroupedByAbilities: Map<string, Array<GameObject>> = new Map();
public addObject(o: GameObject) {
this.objects.set(o.id, o);
for (const command of this.objectsGroupedByAbilities.keys()) {
if (o.reactsToCommand(command)) {
this.objectsGroupedByAbilities.get(command).push(o);
}
}
}
public removeObject(o: GameObject) {
this.objects.delete(o.id);
for (const command of this.objectsGroupedByAbilities.keys()) {
if (o.reactsToCommand(command)) {
const array = this.objectsGroupedByAbilities.get(command);
array.splice(
array.findIndex((i) => i.id == o.id),
1
);
}
}
}
public sendCommandToSingleObject(id: Id, e: Command) {
this.objects.get(id)?.sendCommand(e);
}
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);
}
}

View file

@ -0,0 +1,17 @@
import { vec2 } from 'gl-matrix';
import { InvertedTunnel } from 'sdf-2d';
import { CommandExecutors, Id, TunnelBase } from 'shared';
import { RenderCommand } from '../commands/types/render';
export class TunnelView extends TunnelBase {
private shape: InvertedTunnel;
protected commandExecutors: CommandExecutors = {
[RenderCommand.type]: (c: RenderCommand) => c.renderer.addDrawable(this.shape),
} as any;
constructor(id: Id, from: vec2, to: vec2, fromRadius: number, toRadius: number) {
super(id, from, to, fromRadius, toRadius);
this.shape = new InvertedTunnel(from, to, fromRadius, toRadius);
}
}

View file

@ -1,56 +0,0 @@
import { vec2 } from 'gl-matrix';
import { MoveToCommand } from '../../commands/move-to';
import { RenderCommand } from '../../commands/render';
import { CursorMoveCommand } from '../../input/commands/cursor-move-command';
import { ZoomCommand } from '../../input/commands/zoom';
import { BoundingBox } from '../../physics/bounds/bounding-box';
import { GameObject } from '../game-object';
export class Camera extends GameObject {
private inViewAreaSize = 1920 * 1080 * 2;
private cursorPosition = vec2.create();
private _viewArea: BoundingBox;
constructor() {
super();
this._viewArea = new BoundingBox(null);
this.addCommandExecutor(RenderCommand, this.draw.bind(this));
this.addCommandExecutor(MoveToCommand, this.moveTo.bind(this));
this.addCommandExecutor(CursorMoveCommand, this.setCursorPosition.bind(this));
this.addCommandExecutor(ZoomCommand, this.zoom.bind(this));
}
public get viewArea(): BoundingBox {
return this._viewArea;
}
private draw(c: RenderCommand) {
const canvasAspectRatio = c.renderer.canvasSize.x / c.renderer.canvasSize.y;
this.viewArea.size = vec2.fromValues(
Math.sqrt(this.inViewAreaSize * canvasAspectRatio),
Math.sqrt(this.inViewAreaSize / canvasAspectRatio)
);
c.renderer.setViewArea(this._viewArea.topLeft, this.viewArea.size);
//c.renderer.setCursorPosition(this.cursorPosition);
}
private moveTo(c: MoveToCommand) {
this._viewArea.topLeft = vec2.fromValues(
c.position.x - this._viewArea.size.x / 2,
c.position.y + this._viewArea.size.y / 2
);
}
private zoom(c: ZoomCommand) {
this.inViewAreaSize *= c.factor;
}
private setCursorPosition(c: CursorMoveCommand) {
this.cursorPosition = c.position;
}
}

View file

@ -1,25 +1,17 @@
import { vec2 } from 'gl-matrix';
/*import { vec2 } from 'gl-matrix';
import { Flashlight } from 'sdf-2d';
import { RenderCommand } from '../../commands/render';
import { StepCommand } from '../../commands/step';
import { TeleportToCommand } from '../../commands/teleport-to';
import { rgb } from '../../helper/rgb';
import { IGame } from '../../i-game';
import { CursorMoveCommand } from '../../input/commands/cursor-move-command';
import { KeyDownCommand } from '../../input/commands/key-down';
import { KeyUpCommand } from '../../input/commands/key-up';
import { PrimaryActionCommand } from '../../input/commands/primary-action';
import { SwipeCommand } from '../../input/commands/swipe';
import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base';
import { BoundingCircle } from '../../physics/bounds/bounding-circle';
import { PhysicsCircle } from '../../physics/bounds/physics-circle';
import { DynamicPhysicalObject } from '../../physics/dynamic-physical-object';
import { PhysicalGameObject } from '../../physics/physical-object';
import { Physics } from '../../physics/physics';
import { settings } from '../../settings';
import { BlobShape } from '../../shapes/blob-shape';
import { Projectile } from './projectile';
export class Character extends DynamicPhysicalObject {
export class Character extends PhysicalGameObject {
protected head: PhysicsCircle;
protected leftFoot: PhysicsCircle;
protected rightFoot: PhysicsCircle;
@ -69,7 +61,7 @@ export class Character extends DynamicPhysicalObject {
this.shape.setCircles([this.head, this.leftFoot, this.rightFoot]);
this.addToPhysics();
this.addToPhysics(true);
}
public distance(target: vec2): number {
@ -186,3 +178,4 @@ export class Character extends DynamicPhysicalObject {
);
}
}
*/

View file

@ -1,45 +0,0 @@
import { vec2, vec3 } from 'gl-matrix';
import { CircleLight } from 'sdf-2d';
import { MoveToCommand } from '../../commands/move-to';
import { RenderCommand } from '../../commands/render';
import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base';
import { ImmutableBoundingBox } from '../../physics/bounds/immutable-bounding-box';
import { PhysicalObject } from '../../physics/physical-object';
import { Physics } from '../../physics/physics';
import { settings } from '../../settings';
export class Lamp extends PhysicalObject {
private light: CircleLight;
constructor(center: vec2, color: vec3, lightness: number, physics: Physics) {
super(physics, false);
this.light = new CircleLight(center, color, lightness);
this.addToPhysics();
this.addCommandExecutor(RenderCommand, this.draw.bind(this));
this.addCommandExecutor(MoveToCommand, this.moveTo.bind(this));
}
public distance(target: vec2): number {
return this.light.minDistance(target);
}
public getBoundingBox(): BoundingBoxBase {
return new ImmutableBoundingBox(
this,
this.light.center.x - settings.lightCutoffDistance,
this.light.center.x + settings.lightCutoffDistance,
this.light.center.y - settings.lightCutoffDistance,
this.light.center.y + settings.lightCutoffDistance
);
}
private draw(c: RenderCommand) {
c.renderer.addDrawable(this.light);
}
private moveTo(c: MoveToCommand) {
this.light.center = c.position;
}
}

View file

@ -1,4 +1,4 @@
import { vec2 } from 'gl-matrix';
/*import { vec2 } from 'gl-matrix';
import { Circle } from 'sdf-2d';
import { RenderCommand } from '../../commands/render';
import { StepCommand } from '../../commands/step';
@ -6,11 +6,11 @@ import { IGame } from '../../i-game';
import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base';
import { BoundingCircle } from '../../physics/bounds/bounding-circle';
import { PhysicsCircle } from '../../physics/bounds/physics-circle';
import { DynamicPhysicalObject } from '../../physics/dynamic-physical-object';
import { PhysicalGameObject } from '../../physics/physical-object';
import { Physics } from '../../physics/physics';
import { settings } from '../../settings';
export class Projectile extends DynamicPhysicalObject {
export class Projectile extends PhysicalGameObject {
private shape: Circle;
private bounding: PhysicsCircle;
@ -25,7 +25,7 @@ export class Projectile extends DynamicPhysicalObject {
this.addCommandExecutor(RenderCommand, this.draw.bind(this));
this.addCommandExecutor(StepCommand, this.step.bind(this));
this.addToPhysics();
this.addToPhysics(true);
}
private draw(c: RenderCommand) {
@ -52,3 +52,4 @@ export class Projectile extends DynamicPhysicalObject {
return this.bounding.distance(target);
}
}
*/

View file

@ -1,54 +0,0 @@
import { vec2 } from 'gl-matrix';
import { InvertedTunnel } from 'sdf-2d';
import { RenderCommand } from '../../commands/render';
import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base';
import { ImmutableBoundingBox } from '../../physics/bounds/immutable-bounding-box';
import { Physics } from '../../physics/physics';
import { StaticPhysicalObject } from '../../physics/static-physical-object';
export class Tunnel extends StaticPhysicalObject {
public readonly isInverted = true;
private shape: InvertedTunnel;
constructor(
physics: Physics,
from: vec2,
to: vec2,
fromRadius: number,
toRadius: number
) {
super(physics, true);
this.shape = new InvertedTunnel(from, to, fromRadius, toRadius);
this.addToPhysics();
this.addCommandExecutor(RenderCommand, this.draw.bind(this));
}
public distance(target: vec2): number {
return this.shape.minDistance(target);
}
public getBoundingBox(): BoundingBoxBase {
const xMin = Math.min(
this.shape.from.x - this.shape.fromRadius,
this.shape.to.x - this.shape.toRadius
);
const yMin = Math.min(
this.shape.from.y - this.shape.fromRadius,
this.shape.to.y - this.shape.toRadius
);
const xMax = Math.max(
this.shape.from.x + this.shape.fromRadius,
this.shape.to.x + this.shape.toRadius
);
const yMax = Math.max(
this.shape.from.y + this.shape.fromRadius,
this.shape.to.y + this.shape.toRadius
);
return new ImmutableBoundingBox(this, xMin, xMax, yMin, yMax, true);
}
private draw(c: RenderCommand) {
c.renderer.addDrawable(this.shape);
}
}

View file

@ -1,48 +0,0 @@
import { vec2, vec3 } from 'gl-matrix';
import { Random } from '../../helper/random';
import { Physics } from '../../physics/physics';
import { Objects } from '../objects';
import { Lamp } from '../types/lamp';
import { Tunnel } from '../types/tunnel';
export const createDungeon = (objects: Objects, physics: Physics) => {
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 Tunnel(
physics,
previousEnd,
currentEnd,
previousRadius,
currentToRadius
);
objects.addObject(tunnel);
if (++tunnelsCountSinceLastLight > 3 && Random.getRandom() > 0.7) {
objects.addObject(
new Lamp(
currentEnd,
vec3.normalize(
vec3.create(),
vec3.fromValues(Random.getRandom(), 0, Random.getRandom())
),
0.5,
physics
)
);
tunnelsCountSinceLastLight = 0;
}
previousEnd = currentEnd;
previousRadius = currentToRadius;
}
};