add entity updates over network!

This commit is contained in:
Elizabeth Hunt 2023-08-26 17:55:27 -06:00
parent 594921352c
commit 6ce6946a44
Signed by: simponic
GPG Key ID: 52B3774857EB24B1
19 changed files with 473 additions and 179 deletions

View File

@ -1,5 +1,5 @@
import { Game } from '@engine/Game';
import { Entity, Floor } from '@engine/entities';
import { Entity } from '@engine/entities';
import { Grid } from '@engine/structures';
import {
WallBounds,
@ -16,11 +16,10 @@ import {
type MessageProcessor,
type Message,
type EntityAddBody,
MessageType
MessageType,
type EntityUpdateBody
} from '@engine/network';
import { stringify, parse } from '@engine/utils';
import { BoundingBox, Sprite } from '@engine/components';
import { Miscellaneous } from '@engine/config';
class ClientMessageProcessor implements MessageProcessor {
private game: Game;
@ -34,17 +33,24 @@ class ClientMessageProcessor implements MessageProcessor {
case MessageType.NEW_ENTITIES:
const entityAdditions = message.body as unknown as EntityAddBody[];
entityAdditions.forEach((addBody) =>
this.game.addEntity(Entity.from(addBody.entityName, addBody.args))
this.game.addEntity(
Entity.from(addBody.entityName, addBody.id, addBody.args)
)
);
break;
case MessageType.REMOVE_ENTITIES:
const ids = message.body as unknown as string[];
ids.forEach((id) => this.game.removeEntity(id));
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:
break;
}
console.log(message);
}
}
@ -85,9 +91,12 @@ class ClientSocketMessagePublisher implements MessagePublisher {
}
public publish() {
this.messages.forEach((message: Message) =>
this.socket.send(stringify(message))
);
if (this.socket.readyState == WebSocket.OPEN) {
this.messages.forEach((message: Message) =>
this.socket.send(stringify(message))
);
this.messages = [];
}
}
}
@ -105,19 +114,9 @@ export class JumpStorm {
wsMethod: string,
host: string
) {
await fetch(`${httpMethod}://${host}/assign`)
.then((resp) => {
if (resp.ok) {
return resp.text();
}
throw resp;
})
.then((cookie) => {
this.clientId = cookie;
});
const grid = new Grid();
this.clientId = await this.getAssignedCookie(
`${httpMethod}://${host}/assign`
);
const socket = new WebSocket(`${wsMethod}://${host}/game`);
const clientSocketMessageQueueProvider =
new ClientSocketMessageQueueProvider(socket);
@ -125,33 +124,25 @@ export class JumpStorm {
socket
);
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(
clientSocketMessageQueueProvider,
clientSocketMessagePublisher,
clientMessageProcessor
),
inputSystem,
new FacingDirection(),
new Physics(),
new Collision(grid),
new WallBounds(),
new Render(ctx)
].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() {
@ -164,17 +155,26 @@ export class JumpStorm {
requestAnimationFrame(loop);
}
private createInputSystem(): Input {
const inputSystem = new Input(this.clientId);
private addWindowEventListenersToInputSystem(input: Input) {
window.addEventListener('keydown', (e) => {
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);
}
}

View File

@ -5,9 +5,10 @@ import { fileURLToPath, URL } from 'node:url';
// https://vitejs.dev/config/
export default defineConfig({
server: {
host: '0.0.0.0',
proxy: {
'/api': {
target: 'http://localhost:8080',
target: 'http://10.0.0.237:8080',
ws: true,
rewrite: (path) => path.replace(/^\/api/, '')
}

View File

@ -15,7 +15,6 @@ export class BoundingBox extends Component {
this.rotation = rotation ?? 0;
}
// https://en.wikipedia.org/wiki/Hyperplane_separation_theorem
public isCollidingWith(box: BoundingBox): boolean {
if (this.rotation == 0 && box.rotation == 0) {
const thisTopLeft = this.getTopLeft();
@ -36,6 +35,7 @@ export class BoundingBox extends Component {
return true;
}
// https://en.wikipedia.org/wiki/Hyperplane_separation_theorem
const boxes = [this.getVertices(), box.getVertices()];
for (const poly of boxes) {
for (let i = 0; i < poly.length; i++) {
@ -89,6 +89,8 @@ export class BoundingBox extends Component {
let rads = this.getRotationInPiOfUnitCircle();
const { width, height } = this.dimension;
if (rads == 0) return this.dimension;
if (rads <= Math.PI / 2) {
return {
width: Math.abs(height * Math.sin(rads) + width * Math.cos(rads)),

View File

@ -3,6 +3,7 @@ import { Component, ComponentNames, Velocity } from '.';
export class Control extends Component {
public controlVelocityComponent: Velocity;
public controllableBy: string;
public isControllable: boolean; // computed each update in the input system
constructor(
controllableBy: string,
@ -12,5 +13,6 @@ export class Control extends Component {
this.controllableBy = controllableBy;
this.controlVelocityComponent = controlVelocityComponent;
this.isControllable = false;
}
}

View File

@ -1,13 +1,7 @@
import { Component, ComponentNames } from '.';
export class NetworkUpdateable extends Component {
public isPublish: boolean;
public isSubscribe: boolean;
constructor(isPublish: boolean, isSubscribe: boolean) {
constructor() {
super(ComponentNames.NetworkUpdateable);
this.isPublish = isPublish;
this.isSubscribe = isSubscribe;
}
}

View File

@ -3,13 +3,13 @@ import { Action } from '../interfaces';
export namespace KeyConstants {
export const KeyActions: Record<string, Action> = {
a: Action.MOVE_LEFT,
ArrowLeft: Action.MOVE_LEFT,
arrowleft: Action.MOVE_LEFT,
d: Action.MOVE_RIGHT,
ArrowRight: Action.MOVE_RIGHT,
arrowright: Action.MOVE_RIGHT,
w: Action.JUMP,
ArrowUp: Action.JUMP,
arrowup: Action.JUMP,
' ': Action.JUMP
};
@ -18,7 +18,7 @@ export namespace KeyConstants {
export const ActionKeys: Map<Action, string[]> = Object.keys(
KeyActions
).reduce((acc: Map<Action, string[]>, key) => {
const action = KeyActions[key];
const action = KeyActions[key.toLowerCase()];
if (acc.has(action)) {
acc.get(action)!.push(key);
@ -33,7 +33,7 @@ export namespace KeyConstants {
export namespace PhysicsConstants {
export const MAX_JUMP_TIME_MS = 150;
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_INITIAL_VEL = -1;
}

View File

@ -1,12 +1,15 @@
import { EntityNames, Player } from '.';
import type { Component } from '../components';
import { EntityNames, Floor, Player } from '.';
import { type Component } from '../components';
const randomId = () =>
(performance.now() + Math.random() * 10_000_000).toString();
export abstract class Entity {
public id: string;
public components: Map<string, Component>;
public name: string;
constructor(name: string, id: string = crypto.randomUUID()) {
constructor(name: string, id: string = randomId()) {
this.name = name;
this.id = id;
this.components = new Map();
@ -31,14 +34,29 @@ export abstract class Entity {
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) {
case EntityNames.Player:
const player = new Player(args.playerId);
player.id = args.id;
return player;
const player = new Player();
player.setFrom(args);
entity = player;
break;
case EntityNames.Floor:
const floor = new Floor(args.floorWidth);
floor.setFrom(args);
entity = floor;
break;
default:
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>;
}

View File

@ -1,5 +1,5 @@
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 { Entity, EntityNames } from '../entities';
@ -8,9 +8,13 @@ export class Floor extends Entity {
Sprites.FLOOR
) as SpriteSpec;
private width: number;
constructor(width: number) {
super(EntityNames.Floor);
this.width = width;
this.addComponent(
new Sprite(
IMAGES.get((Floor.spriteSpec?.states?.get(width) as SpriteSpec).sheet),
@ -23,4 +27,22 @@ export class Floor extends Entity {
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
)
);
}
}

View File

@ -10,9 +10,10 @@ import {
WallBounded,
Forces,
Collide,
Control,
Mass,
Moment
Moment,
ComponentNames,
Control
} from '../components';
import { Direction } from '../interfaces';
@ -24,14 +25,14 @@ export class Player extends Entity {
Sprites.COFFEE
) as SpriteSpec;
constructor(playerId: string) {
constructor() {
super(EntityNames.Player);
this.addComponent(
new BoundingBox(
{
x: 300,
y: 100
x: 0,
y: 0
},
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height },
0
@ -48,7 +49,6 @@ export class Player extends Entity {
this.addComponent(new Gravity());
this.addComponent(new Jump());
this.addComponent(new Control(playerId));
this.addComponent(new Collide());
this.addComponent(new WallBounded());
@ -69,6 +69,36 @@ export class Player extends Entity {
);
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));
}
}

View File

@ -1,12 +1,20 @@
export enum MessageType {
NEW_ENTITIES = 'NEW_ENTITIES',
REMOVE_ENTITIES = 'REMOVE_ENTITIES',
UPDATE_ENTITY = 'UPDATE_ENTITY'
UPDATE_ENTITIES = 'UPDATE_ENTITIES',
NEW_INPUT = 'NEW_INPUT',
REMOVE_INPUT = 'REMOVE_INPUT'
}
export type EntityAddBody = {
entityName: string;
args: any;
id: string;
args: Record<string, any>;
};
export type EntityUpdateBody = {
id: string;
args: Record<string, any>;
};
export type Message = {

View File

@ -10,26 +10,111 @@ import { Game } from '../Game';
import { KeyConstants, PhysicsConstants } from '../config';
import { Action } from '../interfaces';
import { System, SystemNames } from '.';
import { MessagePublisher, MessageType } from '../network';
import { Entity } from '../entities';
export class Input extends System {
public clientId: string;
private keys: Set<string>;
private actionTimeStamps: Map<Action, number>;
private messagePublisher?: MessagePublisher;
constructor(clientId: string) {
constructor(clientId: string, messagePublisher?: MessagePublisher) {
super(SystemNames.Input);
this.clientId = clientId;
this.keys = new Set<string>();
this.actionTimeStamps = new Map<Action, number>();
this.keys = new Set();
this.actionTimeStamps = new Map();
this.messagePublisher = messagePublisher;
}
public keyPressed(key: string) {
this.keys.add(key);
if (this.messagePublisher) {
this.messagePublisher.addMessage({
type: MessageType.NEW_INPUT,
body: key
});
}
}
public keyReleased(key: string) {
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 {
@ -38,57 +123,4 @@ export class Input extends System {
}
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);
}
}
}
});
}
}

View File

@ -1,10 +1,12 @@
import { System, SystemNames } from '.';
import { Game } from '../Game';
import { ComponentNames, NetworkUpdateable } from '../components';
import { ComponentNames } from '../components';
import {
type MessageQueueProvider,
type MessagePublisher,
type MessageProcessor
type MessageProcessor,
MessageType,
EntityUpdateBody
} from '../network';
export class NetworkUpdate extends System {
@ -12,6 +14,8 @@ export class NetworkUpdate extends System {
private publisher: MessagePublisher;
private messageProcessor: MessageProcessor;
private entityUpdateTimers: Map<string, number>;
constructor(
queueProvider: MessageQueueProvider,
publisher: MessagePublisher,
@ -22,23 +26,47 @@ export class NetworkUpdate extends System {
this.queueProvider = queueProvider;
this.publisher = publisher;
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
.getNewMessages()
.forEach((message) => this.messageProcessor.process(message));
this.queueProvider.clearMessages();
// 2. send entity updates
const updateMessages: EntityUpdateBody[] = [];
game.forEachEntityWithComponent(
ComponentNames.NetworkUpdateable,
(entity) => {
const networkUpdateComponent = entity.getComponent<NetworkUpdateable>(
ComponentNames.NetworkUpdateable
);
let timer = this.entityUpdateTimers.get(entity.id) ?? dt;
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();
}
private getNextUpdateTimeMs() {
return Math.random() * 70 + 50;
}
}

View File

@ -99,7 +99,7 @@ export class Physics extends System {
: boundingBox.rotation) % 360;
// clear the control velocity
if (control) {
if (control && control.isControllable) {
control.controlVelocityComponent = new Velocity();
}
});

View File

@ -2,28 +2,55 @@ import { Grid } from '@engine/structures';
import {
ServerMessageProcessor,
ServerSocketMessagePublisher,
ServerSocketMessageReceiver
ServerSocketMessageReceiver,
MemorySessionManager,
SessionInputSystem
} from './network';
import { Collision, NetworkUpdate, Physics, WallBounds } from '@engine/systems';
import { Game } from '@engine/Game';
import { Constants } from './constants';
import { GameServer } from './server';
const messageReceiver = new ServerSocketMessageReceiver();
const messagePublisher = new ServerSocketMessagePublisher();
const messageProcessor = new ServerMessageProcessor();
import { Floor } from '@engine/entities';
import { BoundingBox } from '@engine/components';
import { Miscellaneous } from '@engine/config';
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 Collision(new Grid()),
new WallBounds(),
new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor)
new WallBounds()
].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();
setInterval(() => {
game.doGameLoop(performance.now());

View File

@ -1,8 +1,36 @@
import { MessageProcessor } from '@engine/network';
import { ServerMessage } from '.';
import {
EntityUpdateBody,
MessageProcessor,
MessageType
} from '@engine/network';
import { ServerMessage, SessionManager } from '.';
import { Game } from '@engine/Game';
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;
}
}
}

View 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);
}
});
});
}
}

View 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);
}
}

View File

@ -1,16 +1,29 @@
import { Message } from '@engine/network';
import { Input } from '@engine/systems';
export * from './MessageProcessor';
export * from './MessagePublisher';
export * from './MessageReceiver';
export * from './SessionManager';
export * from './SessionInputSystem';
export type SessionData = { sessionId: string };
export type Session = {
sessionId: string;
controllableEntities: Set<string>;
inputSystem: Input;
};
export interface ServerMessage extends Message {
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;
}

View File

@ -1,35 +1,38 @@
import { Game } from '@engine/Game';
import { EntityNames, Player } from '@engine/entities';
import { MessageType } from '@engine/network';
import { Player } from '@engine/entities';
import { Message, MessageType } from '@engine/network';
import { Constants } from './constants';
import {
ServerSocketMessageReceiver,
ServerSocketMessagePublisher,
SessionData,
ServerMessage,
Session
Session,
SessionManager
} from './network';
import { parse } from '@engine/utils';
import { Server, ServerWebSocket } from 'bun';
import { Input } from '@engine/systems';
import { Control, NetworkUpdateable } from '@engine/components';
import { stringify } from '@engine/utils';
export class GameServer {
private sessions: Map<string, Session>;
private server?: Server;
private game: Game;
private messageReceiver: ServerSocketMessageReceiver;
private messagePublisher: ServerSocketMessagePublisher;
private sessionManager: SessionManager;
constructor(
game: Game,
messageReceiver: ServerSocketMessageReceiver,
messagePublisher: ServerSocketMessagePublisher
messagePublisher: ServerSocketMessagePublisher,
sessionManager: SessionManager
) {
this.sessions = new Map();
this.game = game;
this.messageReceiver = messageReceiver;
this.messagePublisher = messagePublisher;
this.sessionManager = sessionManager;
}
public serve() {
@ -64,10 +67,12 @@ export class GameServer {
private closeWebsocket(websocket: ServerWebSocket<SessionData>) {
const { sessionId } = websocket.data;
const sessionEntities = this.sessions.get(sessionId)!.controllableEntities;
this.sessions.delete(sessionId);
const sessionEntities =
this.sessionManager.getSession(sessionId)!.controllableEntities;
this.sessionManager.removeSession(sessionId);
if (!sessionEntities) return;
sessionEntities.forEach((id) => this.game.removeEntity(id));
this.messagePublisher.addMessage({
type: MessageType.REMOVE_ENTITIES,
@ -79,28 +84,51 @@ export class GameServer {
websocket.subscribe(Constants.GAME_TOPIC);
const { sessionId } = websocket.data;
if (this.sessions.has(sessionId)) {
if (this.sessionManager.getSession(sessionId)) {
return;
}
this.sessions.set(sessionId, {
const newSession: Session = {
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.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,
body: [
{
entityName: EntityNames.Player,
args: { playerId: sessionId, id: player.id }
id: player.id,
entityName: player.name,
args: player.serialize()
}
]
});
this.sessions.get(sessionId)!.controllableEntities.add(player.id);
};
this.messagePublisher.addMessage(addNewPlayer);
}
private fetchHandler(req: Request, server: Server): Response {
@ -110,7 +138,7 @@ export class GameServer {
headers.set('Access-Control-Allow-Origin', '*');
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 });
const sessionId = crypto.randomUUID();
@ -127,10 +155,6 @@ export class GameServer {
const sessionId = cookie.split(';').at(0)!.split('SessionId=').at(1);
if (url.pathname == '/game') {
headers.set(
'Set-Cookie',
`SessionId=${sessionId}; HttpOnly; SameSite=Strict;`
);
server.upgrade(req, {
headers,
data: {