add entity updates over network!
This commit is contained in:
parent
594921352c
commit
6ce6946a44
@ -1,5 +1,5 @@
|
|||||||
import { Game } from '@engine/Game';
|
import { Game } from '@engine/Game';
|
||||||
import { Entity, Floor } from '@engine/entities';
|
import { Entity } from '@engine/entities';
|
||||||
import { Grid } from '@engine/structures';
|
import { Grid } from '@engine/structures';
|
||||||
import {
|
import {
|
||||||
WallBounds,
|
WallBounds,
|
||||||
@ -16,11 +16,10 @@ import {
|
|||||||
type MessageProcessor,
|
type MessageProcessor,
|
||||||
type Message,
|
type Message,
|
||||||
type EntityAddBody,
|
type EntityAddBody,
|
||||||
MessageType
|
MessageType,
|
||||||
|
type EntityUpdateBody
|
||||||
} from '@engine/network';
|
} from '@engine/network';
|
||||||
import { stringify, parse } from '@engine/utils';
|
import { stringify, parse } from '@engine/utils';
|
||||||
import { BoundingBox, Sprite } from '@engine/components';
|
|
||||||
import { Miscellaneous } from '@engine/config';
|
|
||||||
|
|
||||||
class ClientMessageProcessor implements MessageProcessor {
|
class ClientMessageProcessor implements MessageProcessor {
|
||||||
private game: Game;
|
private game: Game;
|
||||||
@ -34,17 +33,24 @@ class ClientMessageProcessor implements MessageProcessor {
|
|||||||
case MessageType.NEW_ENTITIES:
|
case MessageType.NEW_ENTITIES:
|
||||||
const entityAdditions = message.body as unknown as EntityAddBody[];
|
const entityAdditions = message.body as unknown as EntityAddBody[];
|
||||||
entityAdditions.forEach((addBody) =>
|
entityAdditions.forEach((addBody) =>
|
||||||
this.game.addEntity(Entity.from(addBody.entityName, addBody.args))
|
this.game.addEntity(
|
||||||
|
Entity.from(addBody.entityName, addBody.id, addBody.args)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case MessageType.REMOVE_ENTITIES:
|
case MessageType.REMOVE_ENTITIES:
|
||||||
const ids = message.body as unknown as string[];
|
const ids = message.body as unknown as string[];
|
||||||
ids.forEach((id) => this.game.removeEntity(id));
|
ids.forEach((id) => this.game.removeEntity(id));
|
||||||
break;
|
break;
|
||||||
|
case MessageType.UPDATE_ENTITIES:
|
||||||
|
const entityUpdates = message.body as unknown as EntityUpdateBody[];
|
||||||
|
entityUpdates.forEach(
|
||||||
|
({ id, args }) => this.game.getEntity(id)?.setFrom(args)
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
console.log(message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,9 +91,12 @@ class ClientSocketMessagePublisher implements MessagePublisher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public publish() {
|
public publish() {
|
||||||
this.messages.forEach((message: Message) =>
|
if (this.socket.readyState == WebSocket.OPEN) {
|
||||||
this.socket.send(stringify(message))
|
this.messages.forEach((message: Message) =>
|
||||||
);
|
this.socket.send(stringify(message))
|
||||||
|
);
|
||||||
|
this.messages = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,19 +114,9 @@ export class JumpStorm {
|
|||||||
wsMethod: string,
|
wsMethod: string,
|
||||||
host: string
|
host: string
|
||||||
) {
|
) {
|
||||||
await fetch(`${httpMethod}://${host}/assign`)
|
this.clientId = await this.getAssignedCookie(
|
||||||
.then((resp) => {
|
`${httpMethod}://${host}/assign`
|
||||||
if (resp.ok) {
|
);
|
||||||
return resp.text();
|
|
||||||
}
|
|
||||||
throw resp;
|
|
||||||
})
|
|
||||||
.then((cookie) => {
|
|
||||||
this.clientId = cookie;
|
|
||||||
});
|
|
||||||
|
|
||||||
const grid = new Grid();
|
|
||||||
|
|
||||||
const socket = new WebSocket(`${wsMethod}://${host}/game`);
|
const socket = new WebSocket(`${wsMethod}://${host}/game`);
|
||||||
const clientSocketMessageQueueProvider =
|
const clientSocketMessageQueueProvider =
|
||||||
new ClientSocketMessageQueueProvider(socket);
|
new ClientSocketMessageQueueProvider(socket);
|
||||||
@ -125,33 +124,25 @@ export class JumpStorm {
|
|||||||
socket
|
socket
|
||||||
);
|
);
|
||||||
const clientMessageProcessor = new ClientMessageProcessor(this.game);
|
const clientMessageProcessor = new ClientMessageProcessor(this.game);
|
||||||
|
|
||||||
|
const inputSystem = new Input(this.clientId, clientSocketMessagePublisher);
|
||||||
|
this.addWindowEventListenersToInputSystem(inputSystem);
|
||||||
|
|
||||||
|
const grid = new Grid();
|
||||||
|
|
||||||
[
|
[
|
||||||
this.createInputSystem(),
|
|
||||||
new FacingDirection(),
|
|
||||||
new Physics(),
|
|
||||||
new Collision(grid),
|
|
||||||
new WallBounds(),
|
|
||||||
new NetworkUpdate(
|
new NetworkUpdate(
|
||||||
clientSocketMessageQueueProvider,
|
clientSocketMessageQueueProvider,
|
||||||
clientSocketMessagePublisher,
|
clientSocketMessagePublisher,
|
||||||
clientMessageProcessor
|
clientMessageProcessor
|
||||||
),
|
),
|
||||||
|
inputSystem,
|
||||||
|
new FacingDirection(),
|
||||||
|
new Physics(),
|
||||||
|
new Collision(grid),
|
||||||
|
new WallBounds(),
|
||||||
new Render(ctx)
|
new Render(ctx)
|
||||||
].forEach((system) => this.game.addSystem(system));
|
].forEach((system) => this.game.addSystem(system));
|
||||||
|
|
||||||
const floor = new Floor(160);
|
|
||||||
const floorHeight = 40;
|
|
||||||
|
|
||||||
floor.addComponent(
|
|
||||||
new BoundingBox(
|
|
||||||
{
|
|
||||||
x: Miscellaneous.WIDTH / 2,
|
|
||||||
y: Miscellaneous.HEIGHT - floorHeight / 2
|
|
||||||
},
|
|
||||||
{ width: Miscellaneous.WIDTH, height: floorHeight }
|
|
||||||
)
|
|
||||||
);
|
|
||||||
this.game.addEntity(floor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public play() {
|
public play() {
|
||||||
@ -164,17 +155,26 @@ export class JumpStorm {
|
|||||||
requestAnimationFrame(loop);
|
requestAnimationFrame(loop);
|
||||||
}
|
}
|
||||||
|
|
||||||
private createInputSystem(): Input {
|
private addWindowEventListenersToInputSystem(input: Input) {
|
||||||
const inputSystem = new Input(this.clientId);
|
|
||||||
|
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
if (!e.repeat) {
|
if (!e.repeat) {
|
||||||
inputSystem.keyPressed(e.key);
|
input.keyPressed(e.key.toLowerCase());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('keyup', (e) => inputSystem.keyReleased(e.key));
|
window.addEventListener('keyup', (e) =>
|
||||||
|
input.keyReleased(e.key.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return inputSystem;
|
private async getAssignedCookie(endpoint: string): Promise<string> {
|
||||||
|
return fetch(endpoint)
|
||||||
|
.then((resp) => {
|
||||||
|
if (resp.ok) {
|
||||||
|
return resp.text();
|
||||||
|
}
|
||||||
|
throw resp;
|
||||||
|
})
|
||||||
|
.then((cookie) => cookie);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,10 @@ import { fileURLToPath, URL } from 'node:url';
|
|||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
server: {
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:8080',
|
target: 'http://10.0.0.237:8080',
|
||||||
ws: true,
|
ws: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, '')
|
rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@ export class BoundingBox extends Component {
|
|||||||
this.rotation = rotation ?? 0;
|
this.rotation = rotation ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://en.wikipedia.org/wiki/Hyperplane_separation_theorem
|
|
||||||
public isCollidingWith(box: BoundingBox): boolean {
|
public isCollidingWith(box: BoundingBox): boolean {
|
||||||
if (this.rotation == 0 && box.rotation == 0) {
|
if (this.rotation == 0 && box.rotation == 0) {
|
||||||
const thisTopLeft = this.getTopLeft();
|
const thisTopLeft = this.getTopLeft();
|
||||||
@ -36,6 +35,7 @@ export class BoundingBox extends Component {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://en.wikipedia.org/wiki/Hyperplane_separation_theorem
|
||||||
const boxes = [this.getVertices(), box.getVertices()];
|
const boxes = [this.getVertices(), box.getVertices()];
|
||||||
for (const poly of boxes) {
|
for (const poly of boxes) {
|
||||||
for (let i = 0; i < poly.length; i++) {
|
for (let i = 0; i < poly.length; i++) {
|
||||||
@ -89,6 +89,8 @@ export class BoundingBox extends Component {
|
|||||||
let rads = this.getRotationInPiOfUnitCircle();
|
let rads = this.getRotationInPiOfUnitCircle();
|
||||||
const { width, height } = this.dimension;
|
const { width, height } = this.dimension;
|
||||||
|
|
||||||
|
if (rads == 0) return this.dimension;
|
||||||
|
|
||||||
if (rads <= Math.PI / 2) {
|
if (rads <= Math.PI / 2) {
|
||||||
return {
|
return {
|
||||||
width: Math.abs(height * Math.sin(rads) + width * Math.cos(rads)),
|
width: Math.abs(height * Math.sin(rads) + width * Math.cos(rads)),
|
||||||
|
@ -3,6 +3,7 @@ import { Component, ComponentNames, Velocity } from '.';
|
|||||||
export class Control extends Component {
|
export class Control extends Component {
|
||||||
public controlVelocityComponent: Velocity;
|
public controlVelocityComponent: Velocity;
|
||||||
public controllableBy: string;
|
public controllableBy: string;
|
||||||
|
public isControllable: boolean; // computed each update in the input system
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
controllableBy: string,
|
controllableBy: string,
|
||||||
@ -12,5 +13,6 @@ export class Control extends Component {
|
|||||||
|
|
||||||
this.controllableBy = controllableBy;
|
this.controllableBy = controllableBy;
|
||||||
this.controlVelocityComponent = controlVelocityComponent;
|
this.controlVelocityComponent = controlVelocityComponent;
|
||||||
|
this.isControllable = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,7 @@
|
|||||||
import { Component, ComponentNames } from '.';
|
import { Component, ComponentNames } from '.';
|
||||||
|
|
||||||
export class NetworkUpdateable extends Component {
|
export class NetworkUpdateable extends Component {
|
||||||
public isPublish: boolean;
|
constructor() {
|
||||||
public isSubscribe: boolean;
|
|
||||||
|
|
||||||
constructor(isPublish: boolean, isSubscribe: boolean) {
|
|
||||||
super(ComponentNames.NetworkUpdateable);
|
super(ComponentNames.NetworkUpdateable);
|
||||||
|
|
||||||
this.isPublish = isPublish;
|
|
||||||
this.isSubscribe = isSubscribe;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,13 @@ import { Action } from '../interfaces';
|
|||||||
export namespace KeyConstants {
|
export namespace KeyConstants {
|
||||||
export const KeyActions: Record<string, Action> = {
|
export const KeyActions: Record<string, Action> = {
|
||||||
a: Action.MOVE_LEFT,
|
a: Action.MOVE_LEFT,
|
||||||
ArrowLeft: Action.MOVE_LEFT,
|
arrowleft: Action.MOVE_LEFT,
|
||||||
|
|
||||||
d: Action.MOVE_RIGHT,
|
d: Action.MOVE_RIGHT,
|
||||||
ArrowRight: Action.MOVE_RIGHT,
|
arrowright: Action.MOVE_RIGHT,
|
||||||
|
|
||||||
w: Action.JUMP,
|
w: Action.JUMP,
|
||||||
ArrowUp: Action.JUMP,
|
arrowup: Action.JUMP,
|
||||||
|
|
||||||
' ': Action.JUMP
|
' ': Action.JUMP
|
||||||
};
|
};
|
||||||
@ -18,7 +18,7 @@ export namespace KeyConstants {
|
|||||||
export const ActionKeys: Map<Action, string[]> = Object.keys(
|
export const ActionKeys: Map<Action, string[]> = Object.keys(
|
||||||
KeyActions
|
KeyActions
|
||||||
).reduce((acc: Map<Action, string[]>, key) => {
|
).reduce((acc: Map<Action, string[]>, key) => {
|
||||||
const action = KeyActions[key];
|
const action = KeyActions[key.toLowerCase()];
|
||||||
|
|
||||||
if (acc.has(action)) {
|
if (acc.has(action)) {
|
||||||
acc.get(action)!.push(key);
|
acc.get(action)!.push(key);
|
||||||
@ -33,7 +33,7 @@ export namespace KeyConstants {
|
|||||||
export namespace PhysicsConstants {
|
export namespace PhysicsConstants {
|
||||||
export const MAX_JUMP_TIME_MS = 150;
|
export const MAX_JUMP_TIME_MS = 150;
|
||||||
export const GRAVITY = 0.0075;
|
export const GRAVITY = 0.0075;
|
||||||
export const PLAYER_MOVE_VEL = 1;
|
export const PLAYER_MOVE_VEL = 0.8;
|
||||||
export const PLAYER_JUMP_ACC = -0.008;
|
export const PLAYER_JUMP_ACC = -0.008;
|
||||||
export const PLAYER_JUMP_INITIAL_VEL = -1;
|
export const PLAYER_JUMP_INITIAL_VEL = -1;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import { EntityNames, Player } from '.';
|
import { EntityNames, Floor, Player } from '.';
|
||||||
import type { Component } from '../components';
|
import { type Component } from '../components';
|
||||||
|
|
||||||
|
const randomId = () =>
|
||||||
|
(performance.now() + Math.random() * 10_000_000).toString();
|
||||||
|
|
||||||
export abstract class Entity {
|
export abstract class Entity {
|
||||||
public id: string;
|
public id: string;
|
||||||
public components: Map<string, Component>;
|
public components: Map<string, Component>;
|
||||||
public name: string;
|
public name: string;
|
||||||
|
|
||||||
constructor(name: string, id: string = crypto.randomUUID()) {
|
constructor(name: string, id: string = randomId()) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.components = new Map();
|
this.components = new Map();
|
||||||
@ -31,14 +34,29 @@ export abstract class Entity {
|
|||||||
return this.components.has(name);
|
return this.components.has(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
static from(entityName: string, args: any): Entity {
|
public static from(entityName: string, id: string, args: any): Entity {
|
||||||
|
let entity: Entity;
|
||||||
|
|
||||||
switch (entityName) {
|
switch (entityName) {
|
||||||
case EntityNames.Player:
|
case EntityNames.Player:
|
||||||
const player = new Player(args.playerId);
|
const player = new Player();
|
||||||
player.id = args.id;
|
player.setFrom(args);
|
||||||
return player;
|
entity = player;
|
||||||
|
break;
|
||||||
|
case EntityNames.Floor:
|
||||||
|
const floor = new Floor(args.floorWidth);
|
||||||
|
floor.setFrom(args);
|
||||||
|
entity = floor;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error('.from() Entity type not implemented: ' + entityName);
|
throw new Error('.from() Entity type not implemented: ' + entityName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity.id = id;
|
||||||
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public abstract setFrom(args: Record<string, any>): void;
|
||||||
|
|
||||||
|
public abstract serialize(): Record<string, any>;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from '../config';
|
import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from '../config';
|
||||||
import { BoundingBox, Sprite } from '../components';
|
import { BoundingBox, ComponentNames, Sprite } from '../components';
|
||||||
import { TopCollidable } from '../components/TopCollidable';
|
import { TopCollidable } from '../components/TopCollidable';
|
||||||
import { Entity, EntityNames } from '../entities';
|
import { Entity, EntityNames } from '../entities';
|
||||||
|
|
||||||
@ -8,9 +8,13 @@ export class Floor extends Entity {
|
|||||||
Sprites.FLOOR
|
Sprites.FLOOR
|
||||||
) as SpriteSpec;
|
) as SpriteSpec;
|
||||||
|
|
||||||
|
private width: number;
|
||||||
|
|
||||||
constructor(width: number) {
|
constructor(width: number) {
|
||||||
super(EntityNames.Floor);
|
super(EntityNames.Floor);
|
||||||
|
|
||||||
|
this.width = width;
|
||||||
|
|
||||||
this.addComponent(
|
this.addComponent(
|
||||||
new Sprite(
|
new Sprite(
|
||||||
IMAGES.get((Floor.spriteSpec?.states?.get(width) as SpriteSpec).sheet),
|
IMAGES.get((Floor.spriteSpec?.states?.get(width) as SpriteSpec).sheet),
|
||||||
@ -23,4 +27,22 @@ export class Floor extends Entity {
|
|||||||
|
|
||||||
this.addComponent(new TopCollidable());
|
this.addComponent(new TopCollidable());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public serialize() {
|
||||||
|
return {
|
||||||
|
floorWidth: this.width,
|
||||||
|
boundingBox: this.getComponent<BoundingBox>(ComponentNames.BoundingBox)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public setFrom(args: any) {
|
||||||
|
const { boundingBox } = args;
|
||||||
|
this.addComponent(
|
||||||
|
new BoundingBox(
|
||||||
|
boundingBox.center,
|
||||||
|
boundingBox.dimension,
|
||||||
|
boundingBox.rotation
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,10 @@ import {
|
|||||||
WallBounded,
|
WallBounded,
|
||||||
Forces,
|
Forces,
|
||||||
Collide,
|
Collide,
|
||||||
Control,
|
|
||||||
Mass,
|
Mass,
|
||||||
Moment
|
Moment,
|
||||||
|
ComponentNames,
|
||||||
|
Control
|
||||||
} from '../components';
|
} from '../components';
|
||||||
import { Direction } from '../interfaces';
|
import { Direction } from '../interfaces';
|
||||||
|
|
||||||
@ -24,14 +25,14 @@ export class Player extends Entity {
|
|||||||
Sprites.COFFEE
|
Sprites.COFFEE
|
||||||
) as SpriteSpec;
|
) as SpriteSpec;
|
||||||
|
|
||||||
constructor(playerId: string) {
|
constructor() {
|
||||||
super(EntityNames.Player);
|
super(EntityNames.Player);
|
||||||
|
|
||||||
this.addComponent(
|
this.addComponent(
|
||||||
new BoundingBox(
|
new BoundingBox(
|
||||||
{
|
{
|
||||||
x: 300,
|
x: 0,
|
||||||
y: 100
|
y: 0
|
||||||
},
|
},
|
||||||
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height },
|
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height },
|
||||||
0
|
0
|
||||||
@ -48,7 +49,6 @@ export class Player extends Entity {
|
|||||||
this.addComponent(new Gravity());
|
this.addComponent(new Gravity());
|
||||||
|
|
||||||
this.addComponent(new Jump());
|
this.addComponent(new Jump());
|
||||||
this.addComponent(new Control(playerId));
|
|
||||||
|
|
||||||
this.addComponent(new Collide());
|
this.addComponent(new Collide());
|
||||||
this.addComponent(new WallBounded());
|
this.addComponent(new WallBounded());
|
||||||
@ -69,6 +69,36 @@ export class Player extends Entity {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.addComponent(new FacingDirection(leftSprite, rightSprite));
|
this.addComponent(new FacingDirection(leftSprite, rightSprite));
|
||||||
this.addComponent(leftSprite); // face Left by default
|
this.addComponent(leftSprite); // face left by default
|
||||||
|
}
|
||||||
|
|
||||||
|
public serialize(): Record<string, any> {
|
||||||
|
return {
|
||||||
|
control: this.getComponent<Control>(ComponentNames.Control),
|
||||||
|
boundingBox: this.getComponent<BoundingBox>(ComponentNames.BoundingBox),
|
||||||
|
velocity: this.getComponent<Velocity>(ComponentNames.Velocity),
|
||||||
|
forces: this.getComponent<Forces>(ComponentNames.Forces)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public setFrom(args: Record<string, any>) {
|
||||||
|
const { control, velocity, forces, boundingBox } = args;
|
||||||
|
|
||||||
|
let center = boundingBox.center;
|
||||||
|
|
||||||
|
const myCenter = this.getComponent<BoundingBox>(
|
||||||
|
ComponentNames.BoundingBox
|
||||||
|
).center;
|
||||||
|
const distance = Math.sqrt(
|
||||||
|
Math.pow(center.y - myCenter.y, 2) + Math.pow(center.x - myCenter.x, 2)
|
||||||
|
);
|
||||||
|
if (distance < 30) center = myCenter;
|
||||||
|
|
||||||
|
[
|
||||||
|
Object.assign(new Control(control.controllableBy), control),
|
||||||
|
new Velocity(velocity.velocity),
|
||||||
|
new Forces(forces.forces),
|
||||||
|
new BoundingBox(center, boundingBox.dimension, boundingBox.rotation)
|
||||||
|
].forEach((component) => this.addComponent(component));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
export enum MessageType {
|
export enum MessageType {
|
||||||
NEW_ENTITIES = 'NEW_ENTITIES',
|
NEW_ENTITIES = 'NEW_ENTITIES',
|
||||||
REMOVE_ENTITIES = 'REMOVE_ENTITIES',
|
REMOVE_ENTITIES = 'REMOVE_ENTITIES',
|
||||||
UPDATE_ENTITY = 'UPDATE_ENTITY'
|
UPDATE_ENTITIES = 'UPDATE_ENTITIES',
|
||||||
|
NEW_INPUT = 'NEW_INPUT',
|
||||||
|
REMOVE_INPUT = 'REMOVE_INPUT'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EntityAddBody = {
|
export type EntityAddBody = {
|
||||||
entityName: string;
|
entityName: string;
|
||||||
args: any;
|
id: string;
|
||||||
|
args: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EntityUpdateBody = {
|
||||||
|
id: string;
|
||||||
|
args: Record<string, any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Message = {
|
export type Message = {
|
||||||
|
@ -10,26 +10,111 @@ import { Game } from '../Game';
|
|||||||
import { KeyConstants, PhysicsConstants } from '../config';
|
import { KeyConstants, PhysicsConstants } from '../config';
|
||||||
import { Action } from '../interfaces';
|
import { Action } from '../interfaces';
|
||||||
import { System, SystemNames } from '.';
|
import { System, SystemNames } from '.';
|
||||||
|
import { MessagePublisher, MessageType } from '../network';
|
||||||
|
import { Entity } from '../entities';
|
||||||
|
|
||||||
export class Input extends System {
|
export class Input extends System {
|
||||||
public clientId: string;
|
public clientId: string;
|
||||||
|
|
||||||
private keys: Set<string>;
|
private keys: Set<string>;
|
||||||
private actionTimeStamps: Map<Action, number>;
|
private actionTimeStamps: Map<Action, number>;
|
||||||
|
private messagePublisher?: MessagePublisher;
|
||||||
|
|
||||||
constructor(clientId: string) {
|
constructor(clientId: string, messagePublisher?: MessagePublisher) {
|
||||||
super(SystemNames.Input);
|
super(SystemNames.Input);
|
||||||
|
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
this.keys = new Set<string>();
|
this.keys = new Set();
|
||||||
this.actionTimeStamps = new Map<Action, number>();
|
this.actionTimeStamps = new Map();
|
||||||
|
|
||||||
|
this.messagePublisher = messagePublisher;
|
||||||
}
|
}
|
||||||
|
|
||||||
public keyPressed(key: string) {
|
public keyPressed(key: string) {
|
||||||
this.keys.add(key);
|
this.keys.add(key);
|
||||||
|
|
||||||
|
if (this.messagePublisher) {
|
||||||
|
this.messagePublisher.addMessage({
|
||||||
|
type: MessageType.NEW_INPUT,
|
||||||
|
body: key
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public keyReleased(key: string) {
|
public keyReleased(key: string) {
|
||||||
this.keys.delete(key);
|
this.keys.delete(key);
|
||||||
|
|
||||||
|
if (this.messagePublisher) {
|
||||||
|
this.messagePublisher.addMessage({
|
||||||
|
type: MessageType.REMOVE_INPUT,
|
||||||
|
body: key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(_dt: number, game: Game) {
|
||||||
|
game.forEachEntityWithComponent(ComponentNames.Control, (entity) =>
|
||||||
|
this.handleInput(entity)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleInput(entity: Entity) {
|
||||||
|
const controlComponent = entity.getComponent<Control>(
|
||||||
|
ComponentNames.Control
|
||||||
|
);
|
||||||
|
controlComponent.isControllable =
|
||||||
|
controlComponent.controllableBy === this.clientId;
|
||||||
|
|
||||||
|
if (!controlComponent.isControllable) return;
|
||||||
|
|
||||||
|
if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_RIGHT))) {
|
||||||
|
controlComponent.controlVelocityComponent.velocity.dCartesian.dx +=
|
||||||
|
PhysicsConstants.PLAYER_MOVE_VEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_LEFT))) {
|
||||||
|
controlComponent.controlVelocityComponent.velocity.dCartesian.dx +=
|
||||||
|
-PhysicsConstants.PLAYER_MOVE_VEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
entity.hasComponent(ComponentNames.Jump) &&
|
||||||
|
this.hasSomeKey(KeyConstants.ActionKeys.get(Action.JUMP))
|
||||||
|
) {
|
||||||
|
this.performJump(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private performJump(entity: Entity) {
|
||||||
|
const velocity = entity.getComponent<Velocity>(
|
||||||
|
ComponentNames.Velocity
|
||||||
|
).velocity;
|
||||||
|
const jump = entity.getComponent<Jump>(ComponentNames.Jump);
|
||||||
|
|
||||||
|
if (jump.canJump) {
|
||||||
|
this.actionTimeStamps.set(Action.JUMP, performance.now());
|
||||||
|
|
||||||
|
velocity.dCartesian.dy += PhysicsConstants.PLAYER_JUMP_INITIAL_VEL;
|
||||||
|
jump.canJump = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
performance.now() - (this.actionTimeStamps.get(Action.JUMP) || 0) <
|
||||||
|
PhysicsConstants.MAX_JUMP_TIME_MS
|
||||||
|
) {
|
||||||
|
const mass = entity.getComponent<Mass>(ComponentNames.Mass).mass;
|
||||||
|
|
||||||
|
const jumpForce = {
|
||||||
|
fCartesian: {
|
||||||
|
fy: mass * PhysicsConstants.PLAYER_JUMP_ACC,
|
||||||
|
fx: 0
|
||||||
|
},
|
||||||
|
torque: 0
|
||||||
|
};
|
||||||
|
entity
|
||||||
|
.getComponent<Forces>(ComponentNames.Forces)
|
||||||
|
?.forces.push(jumpForce);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private hasSomeKey(keys?: string[]): boolean {
|
private hasSomeKey(keys?: string[]): boolean {
|
||||||
@ -38,57 +123,4 @@ export class Input extends System {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(_dt: number, game: Game) {
|
|
||||||
game.forEachEntityWithComponent(ComponentNames.Control, (entity) => {
|
|
||||||
const controlComponent = entity.getComponent<Control>(
|
|
||||||
ComponentNames.Control
|
|
||||||
);
|
|
||||||
if (controlComponent.controllableBy != this.clientId) return;
|
|
||||||
|
|
||||||
if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_RIGHT))) {
|
|
||||||
controlComponent.controlVelocityComponent.velocity.dCartesian.dx +=
|
|
||||||
PhysicsConstants.PLAYER_MOVE_VEL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_LEFT))) {
|
|
||||||
controlComponent.controlVelocityComponent.velocity.dCartesian.dx +=
|
|
||||||
-PhysicsConstants.PLAYER_MOVE_VEL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entity.hasComponent(ComponentNames.Jump)) {
|
|
||||||
const velocity = entity.getComponent<Velocity>(
|
|
||||||
ComponentNames.Velocity
|
|
||||||
).velocity;
|
|
||||||
const jump = entity.getComponent<Jump>(ComponentNames.Jump);
|
|
||||||
|
|
||||||
if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.JUMP))) {
|
|
||||||
if (jump.canJump) {
|
|
||||||
this.actionTimeStamps.set(Action.JUMP, performance.now());
|
|
||||||
|
|
||||||
velocity.dCartesian.dy += PhysicsConstants.PLAYER_JUMP_INITIAL_VEL;
|
|
||||||
jump.canJump = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
performance.now() - (this.actionTimeStamps.get(Action.JUMP) || 0) <
|
|
||||||
PhysicsConstants.MAX_JUMP_TIME_MS
|
|
||||||
) {
|
|
||||||
const mass = entity.getComponent<Mass>(ComponentNames.Mass).mass;
|
|
||||||
|
|
||||||
const jumpForce = {
|
|
||||||
fCartesian: {
|
|
||||||
fy: mass * PhysicsConstants.PLAYER_JUMP_ACC,
|
|
||||||
fx: 0
|
|
||||||
},
|
|
||||||
torque: 0
|
|
||||||
};
|
|
||||||
entity
|
|
||||||
.getComponent<Forces>(ComponentNames.Forces)
|
|
||||||
?.forces.push(jumpForce);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { System, SystemNames } from '.';
|
import { System, SystemNames } from '.';
|
||||||
import { Game } from '../Game';
|
import { Game } from '../Game';
|
||||||
import { ComponentNames, NetworkUpdateable } from '../components';
|
import { ComponentNames } from '../components';
|
||||||
import {
|
import {
|
||||||
type MessageQueueProvider,
|
type MessageQueueProvider,
|
||||||
type MessagePublisher,
|
type MessagePublisher,
|
||||||
type MessageProcessor
|
type MessageProcessor,
|
||||||
|
MessageType,
|
||||||
|
EntityUpdateBody
|
||||||
} from '../network';
|
} from '../network';
|
||||||
|
|
||||||
export class NetworkUpdate extends System {
|
export class NetworkUpdate extends System {
|
||||||
@ -12,6 +14,8 @@ export class NetworkUpdate extends System {
|
|||||||
private publisher: MessagePublisher;
|
private publisher: MessagePublisher;
|
||||||
private messageProcessor: MessageProcessor;
|
private messageProcessor: MessageProcessor;
|
||||||
|
|
||||||
|
private entityUpdateTimers: Map<string, number>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
queueProvider: MessageQueueProvider,
|
queueProvider: MessageQueueProvider,
|
||||||
publisher: MessagePublisher,
|
publisher: MessagePublisher,
|
||||||
@ -22,23 +26,47 @@ export class NetworkUpdate extends System {
|
|||||||
this.queueProvider = queueProvider;
|
this.queueProvider = queueProvider;
|
||||||
this.publisher = publisher;
|
this.publisher = publisher;
|
||||||
this.messageProcessor = messageProcessor;
|
this.messageProcessor = messageProcessor;
|
||||||
|
|
||||||
|
this.entityUpdateTimers = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(_dt: number, game: Game) {
|
public update(dt: number, game: Game) {
|
||||||
|
// 1. process new messages
|
||||||
this.queueProvider
|
this.queueProvider
|
||||||
.getNewMessages()
|
.getNewMessages()
|
||||||
.forEach((message) => this.messageProcessor.process(message));
|
.forEach((message) => this.messageProcessor.process(message));
|
||||||
this.queueProvider.clearMessages();
|
this.queueProvider.clearMessages();
|
||||||
|
|
||||||
|
// 2. send entity updates
|
||||||
|
const updateMessages: EntityUpdateBody[] = [];
|
||||||
game.forEachEntityWithComponent(
|
game.forEachEntityWithComponent(
|
||||||
ComponentNames.NetworkUpdateable,
|
ComponentNames.NetworkUpdateable,
|
||||||
(entity) => {
|
(entity) => {
|
||||||
const networkUpdateComponent = entity.getComponent<NetworkUpdateable>(
|
let timer = this.entityUpdateTimers.get(entity.id) ?? dt;
|
||||||
ComponentNames.NetworkUpdateable
|
timer -= dt;
|
||||||
);
|
this.entityUpdateTimers.set(entity.id, timer);
|
||||||
|
|
||||||
|
if (timer > 0) return;
|
||||||
|
this.entityUpdateTimers.set(entity.id, this.getNextUpdateTimeMs());
|
||||||
|
|
||||||
|
if (entity.hasComponent(ComponentNames.NetworkUpdateable)) {
|
||||||
|
updateMessages.push({
|
||||||
|
id: entity.id,
|
||||||
|
args: entity.serialize()
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
this.publisher.addMessage({
|
||||||
|
type: MessageType.UPDATE_ENTITIES,
|
||||||
|
body: updateMessages
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. publish changes
|
||||||
this.publisher.publish();
|
this.publisher.publish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getNextUpdateTimeMs() {
|
||||||
|
return Math.random() * 70 + 50;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,7 +99,7 @@ export class Physics extends System {
|
|||||||
: boundingBox.rotation) % 360;
|
: boundingBox.rotation) % 360;
|
||||||
|
|
||||||
// clear the control velocity
|
// clear the control velocity
|
||||||
if (control) {
|
if (control && control.isControllable) {
|
||||||
control.controlVelocityComponent = new Velocity();
|
control.controlVelocityComponent = new Velocity();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -2,28 +2,55 @@ import { Grid } from '@engine/structures';
|
|||||||
import {
|
import {
|
||||||
ServerMessageProcessor,
|
ServerMessageProcessor,
|
||||||
ServerSocketMessagePublisher,
|
ServerSocketMessagePublisher,
|
||||||
ServerSocketMessageReceiver
|
ServerSocketMessageReceiver,
|
||||||
|
MemorySessionManager,
|
||||||
|
SessionInputSystem
|
||||||
} from './network';
|
} from './network';
|
||||||
import { Collision, NetworkUpdate, Physics, WallBounds } from '@engine/systems';
|
import { Collision, NetworkUpdate, Physics, WallBounds } from '@engine/systems';
|
||||||
import { Game } from '@engine/Game';
|
import { Game } from '@engine/Game';
|
||||||
import { Constants } from './constants';
|
import { Constants } from './constants';
|
||||||
import { GameServer } from './server';
|
import { GameServer } from './server';
|
||||||
|
import { Floor } from '@engine/entities';
|
||||||
const messageReceiver = new ServerSocketMessageReceiver();
|
import { BoundingBox } from '@engine/components';
|
||||||
const messagePublisher = new ServerSocketMessagePublisher();
|
import { Miscellaneous } from '@engine/config';
|
||||||
const messageProcessor = new ServerMessageProcessor();
|
|
||||||
|
|
||||||
const game = new Game();
|
const game = new Game();
|
||||||
|
|
||||||
const server = new GameServer(game, messageReceiver, messagePublisher);
|
const sessionManager = new MemorySessionManager();
|
||||||
|
|
||||||
|
const messageReceiver = new ServerSocketMessageReceiver();
|
||||||
|
const messagePublisher = new ServerSocketMessagePublisher();
|
||||||
|
const messageProcessor = new ServerMessageProcessor(game, sessionManager);
|
||||||
|
|
||||||
|
const server = new GameServer(
|
||||||
|
game,
|
||||||
|
messageReceiver,
|
||||||
|
messagePublisher,
|
||||||
|
sessionManager
|
||||||
|
);
|
||||||
|
|
||||||
[
|
[
|
||||||
|
new SessionInputSystem(sessionManager),
|
||||||
|
new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor),
|
||||||
new Physics(),
|
new Physics(),
|
||||||
new Collision(new Grid()),
|
new Collision(new Grid()),
|
||||||
new WallBounds(),
|
new WallBounds()
|
||||||
new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor)
|
|
||||||
].forEach((system) => game.addSystem(system));
|
].forEach((system) => game.addSystem(system));
|
||||||
|
|
||||||
|
const floor = new Floor(160);
|
||||||
|
const floorHeight = 200;
|
||||||
|
|
||||||
|
floor.addComponent(
|
||||||
|
new BoundingBox(
|
||||||
|
{
|
||||||
|
x: Miscellaneous.WIDTH / 2,
|
||||||
|
y: Miscellaneous.HEIGHT + floorHeight / 2
|
||||||
|
},
|
||||||
|
{ width: Miscellaneous.WIDTH, height: floorHeight }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
game.addEntity(floor);
|
||||||
|
|
||||||
game.start();
|
game.start();
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
game.doGameLoop(performance.now());
|
game.doGameLoop(performance.now());
|
||||||
|
@ -1,8 +1,36 @@
|
|||||||
import { MessageProcessor } from '@engine/network';
|
import {
|
||||||
import { ServerMessage } from '.';
|
EntityUpdateBody,
|
||||||
|
MessageProcessor,
|
||||||
|
MessageType
|
||||||
|
} from '@engine/network';
|
||||||
|
import { ServerMessage, SessionManager } from '.';
|
||||||
|
import { Game } from '@engine/Game';
|
||||||
|
|
||||||
export class ServerMessageProcessor implements MessageProcessor {
|
export class ServerMessageProcessor implements MessageProcessor {
|
||||||
constructor() {}
|
private game: Game;
|
||||||
|
private sessionManager: SessionManager;
|
||||||
|
|
||||||
public process(_message: ServerMessage) {}
|
constructor(game: Game, sessionManager: SessionManager) {
|
||||||
|
this.game = game;
|
||||||
|
this.sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public process(message: ServerMessage) {
|
||||||
|
switch (message.type) {
|
||||||
|
case MessageType.NEW_INPUT: {
|
||||||
|
const { sessionId } = message.sessionData;
|
||||||
|
const session = this.sessionManager.getSession(sessionId);
|
||||||
|
session?.inputSystem.keyPressed(message.body as string);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MessageType.REMOVE_INPUT: {
|
||||||
|
const { sessionId } = message.sessionData;
|
||||||
|
const session = this.sessionManager.getSession(sessionId);
|
||||||
|
session?.inputSystem.keyReleased(message.body as string);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
32
server/src/network/SessionInputSystem.ts
Normal file
32
server/src/network/SessionInputSystem.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Game } from '@engine/Game';
|
||||||
|
import { SessionManager } from '.';
|
||||||
|
import { System } from '@engine/systems';
|
||||||
|
import { BoundingBox, ComponentNames, Control } from '@engine/components';
|
||||||
|
|
||||||
|
export class SessionInputSystem extends System {
|
||||||
|
private sessionManager: SessionManager;
|
||||||
|
|
||||||
|
constructor(sessionManager: SessionManager) {
|
||||||
|
super('SessionInputSystem');
|
||||||
|
|
||||||
|
this.sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(_dt: number, game: Game) {
|
||||||
|
this.sessionManager.getSessions().forEach((sessionId) => {
|
||||||
|
const session = this.sessionManager.getSession(sessionId);
|
||||||
|
|
||||||
|
if (!session) return;
|
||||||
|
|
||||||
|
const { inputSystem } = session;
|
||||||
|
session.controllableEntities.forEach((entityId) => {
|
||||||
|
const entity = game.getEntity(entityId);
|
||||||
|
if (!entity) return;
|
||||||
|
|
||||||
|
if (entity.hasComponent(ComponentNames.Control)) {
|
||||||
|
inputSystem.handleInput(entity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
33
server/src/network/SessionManager.ts
Normal file
33
server/src/network/SessionManager.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Session, SessionManager } from '.';
|
||||||
|
|
||||||
|
export class MemorySessionManager implements SessionManager {
|
||||||
|
private sessions: Map<string, Session>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.sessions = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSessions() {
|
||||||
|
return Array.from(this.sessions.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
public uniqueSessionId() {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSession(id: string) {
|
||||||
|
return this.sessions.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public putSession(id: string, session: Session) {
|
||||||
|
return this.sessions.set(id, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
public numSessions() {
|
||||||
|
return this.sessions.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeSession(id: string) {
|
||||||
|
this.sessions.delete(id);
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,29 @@
|
|||||||
import { Message } from '@engine/network';
|
import { Message } from '@engine/network';
|
||||||
|
import { Input } from '@engine/systems';
|
||||||
|
|
||||||
export * from './MessageProcessor';
|
export * from './MessageProcessor';
|
||||||
export * from './MessagePublisher';
|
export * from './MessagePublisher';
|
||||||
export * from './MessageReceiver';
|
export * from './MessageReceiver';
|
||||||
|
export * from './SessionManager';
|
||||||
|
export * from './SessionInputSystem';
|
||||||
|
|
||||||
export type SessionData = { sessionId: string };
|
export type SessionData = { sessionId: string };
|
||||||
|
|
||||||
export type Session = {
|
export type Session = {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
controllableEntities: Set<string>;
|
controllableEntities: Set<string>;
|
||||||
|
inputSystem: Input;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ServerMessage extends Message {
|
export interface ServerMessage extends Message {
|
||||||
sessionData: SessionData;
|
sessionData: SessionData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SessionManager {
|
||||||
|
uniqueSessionId(): string;
|
||||||
|
getSession(id: string): Session | undefined;
|
||||||
|
getSessions(): string[];
|
||||||
|
putSession(id: string, session: Session): void;
|
||||||
|
removeSession(id: string): void;
|
||||||
|
numSessions(): number;
|
||||||
|
}
|
||||||
|
@ -1,35 +1,38 @@
|
|||||||
import { Game } from '@engine/Game';
|
import { Game } from '@engine/Game';
|
||||||
import { EntityNames, Player } from '@engine/entities';
|
import { Player } from '@engine/entities';
|
||||||
import { MessageType } from '@engine/network';
|
import { Message, MessageType } from '@engine/network';
|
||||||
import { Constants } from './constants';
|
import { Constants } from './constants';
|
||||||
import {
|
import {
|
||||||
ServerSocketMessageReceiver,
|
ServerSocketMessageReceiver,
|
||||||
ServerSocketMessagePublisher,
|
ServerSocketMessagePublisher,
|
||||||
SessionData,
|
SessionData,
|
||||||
ServerMessage,
|
ServerMessage,
|
||||||
Session
|
Session,
|
||||||
|
SessionManager
|
||||||
} from './network';
|
} from './network';
|
||||||
import { parse } from '@engine/utils';
|
import { parse } from '@engine/utils';
|
||||||
import { Server, ServerWebSocket } from 'bun';
|
import { Server, ServerWebSocket } from 'bun';
|
||||||
|
import { Input } from '@engine/systems';
|
||||||
|
import { Control, NetworkUpdateable } from '@engine/components';
|
||||||
|
import { stringify } from '@engine/utils';
|
||||||
|
|
||||||
export class GameServer {
|
export class GameServer {
|
||||||
private sessions: Map<string, Session>;
|
|
||||||
|
|
||||||
private server?: Server;
|
private server?: Server;
|
||||||
private game: Game;
|
private game: Game;
|
||||||
private messageReceiver: ServerSocketMessageReceiver;
|
private messageReceiver: ServerSocketMessageReceiver;
|
||||||
private messagePublisher: ServerSocketMessagePublisher;
|
private messagePublisher: ServerSocketMessagePublisher;
|
||||||
|
private sessionManager: SessionManager;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
game: Game,
|
game: Game,
|
||||||
messageReceiver: ServerSocketMessageReceiver,
|
messageReceiver: ServerSocketMessageReceiver,
|
||||||
messagePublisher: ServerSocketMessagePublisher
|
messagePublisher: ServerSocketMessagePublisher,
|
||||||
|
sessionManager: SessionManager
|
||||||
) {
|
) {
|
||||||
this.sessions = new Map();
|
|
||||||
|
|
||||||
this.game = game;
|
this.game = game;
|
||||||
this.messageReceiver = messageReceiver;
|
this.messageReceiver = messageReceiver;
|
||||||
this.messagePublisher = messagePublisher;
|
this.messagePublisher = messagePublisher;
|
||||||
|
this.sessionManager = sessionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public serve() {
|
public serve() {
|
||||||
@ -64,10 +67,12 @@ export class GameServer {
|
|||||||
private closeWebsocket(websocket: ServerWebSocket<SessionData>) {
|
private closeWebsocket(websocket: ServerWebSocket<SessionData>) {
|
||||||
const { sessionId } = websocket.data;
|
const { sessionId } = websocket.data;
|
||||||
|
|
||||||
const sessionEntities = this.sessions.get(sessionId)!.controllableEntities;
|
const sessionEntities =
|
||||||
this.sessions.delete(sessionId);
|
this.sessionManager.getSession(sessionId)!.controllableEntities;
|
||||||
|
this.sessionManager.removeSession(sessionId);
|
||||||
|
|
||||||
if (!sessionEntities) return;
|
if (!sessionEntities) return;
|
||||||
|
sessionEntities.forEach((id) => this.game.removeEntity(id));
|
||||||
|
|
||||||
this.messagePublisher.addMessage({
|
this.messagePublisher.addMessage({
|
||||||
type: MessageType.REMOVE_ENTITIES,
|
type: MessageType.REMOVE_ENTITIES,
|
||||||
@ -79,28 +84,51 @@ export class GameServer {
|
|||||||
websocket.subscribe(Constants.GAME_TOPIC);
|
websocket.subscribe(Constants.GAME_TOPIC);
|
||||||
|
|
||||||
const { sessionId } = websocket.data;
|
const { sessionId } = websocket.data;
|
||||||
if (this.sessions.has(sessionId)) {
|
if (this.sessionManager.getSession(sessionId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sessions.set(sessionId, {
|
const newSession: Session = {
|
||||||
sessionId,
|
sessionId,
|
||||||
controllableEntities: new Set()
|
controllableEntities: new Set(),
|
||||||
});
|
inputSystem: new Input(sessionId)
|
||||||
|
};
|
||||||
|
|
||||||
const player = new Player(sessionId);
|
const player = new Player();
|
||||||
|
player.addComponent(new Control(sessionId));
|
||||||
|
player.addComponent(new NetworkUpdateable());
|
||||||
this.game.addEntity(player);
|
this.game.addEntity(player);
|
||||||
this.messagePublisher.addMessage({
|
|
||||||
|
newSession.controllableEntities.add(player.id);
|
||||||
|
this.sessionManager.putSession(sessionId, newSession);
|
||||||
|
|
||||||
|
const addCurrentEntities: Message[] = [
|
||||||
|
{
|
||||||
|
type: MessageType.NEW_ENTITIES,
|
||||||
|
body: Array.from(this.game.entities.values())
|
||||||
|
.filter((entity) => entity.id != player.id)
|
||||||
|
.map((entity) => {
|
||||||
|
return {
|
||||||
|
id: entity.id,
|
||||||
|
entityName: entity.name,
|
||||||
|
args: entity.serialize()
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
];
|
||||||
|
websocket.sendText(stringify(addCurrentEntities));
|
||||||
|
|
||||||
|
const addNewPlayer: Message = {
|
||||||
type: MessageType.NEW_ENTITIES,
|
type: MessageType.NEW_ENTITIES,
|
||||||
body: [
|
body: [
|
||||||
{
|
{
|
||||||
entityName: EntityNames.Player,
|
id: player.id,
|
||||||
args: { playerId: sessionId, id: player.id }
|
entityName: player.name,
|
||||||
|
args: player.serialize()
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
};
|
||||||
|
this.messagePublisher.addMessage(addNewPlayer);
|
||||||
this.sessions.get(sessionId)!.controllableEntities.add(player.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchHandler(req: Request, server: Server): Response {
|
private fetchHandler(req: Request, server: Server): Response {
|
||||||
@ -110,7 +138,7 @@ export class GameServer {
|
|||||||
headers.set('Access-Control-Allow-Origin', '*');
|
headers.set('Access-Control-Allow-Origin', '*');
|
||||||
|
|
||||||
if (url.pathname == '/assign') {
|
if (url.pathname == '/assign') {
|
||||||
if (this.sessions.size > Constants.MAX_PLAYERS)
|
if (this.sessionManager.numSessions() > Constants.MAX_PLAYERS)
|
||||||
return new Response('too many players', { headers, status: 400 });
|
return new Response('too many players', { headers, status: 400 });
|
||||||
|
|
||||||
const sessionId = crypto.randomUUID();
|
const sessionId = crypto.randomUUID();
|
||||||
@ -127,10 +155,6 @@ export class GameServer {
|
|||||||
const sessionId = cookie.split(';').at(0)!.split('SessionId=').at(1);
|
const sessionId = cookie.split(';').at(0)!.split('SessionId=').at(1);
|
||||||
|
|
||||||
if (url.pathname == '/game') {
|
if (url.pathname == '/game') {
|
||||||
headers.set(
|
|
||||||
'Set-Cookie',
|
|
||||||
`SessionId=${sessionId}; HttpOnly; SameSite=Strict;`
|
|
||||||
);
|
|
||||||
server.upgrade(req, {
|
server.upgrade(req, {
|
||||||
headers,
|
headers,
|
||||||
data: {
|
data: {
|
||||||
|
Loading…
Reference in New Issue
Block a user