add entity updates over network!
This commit is contained in:
parent
594921352c
commit
6ce6946a44
@ -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() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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/, '')
|
||||
}
|
||||
|
@ -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)),
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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 = {
|
||||
|
@ -10,41 +10,62 @@ 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);
|
||||
}
|
||||
|
||||
private hasSomeKey(keys?: string[]): boolean {
|
||||
if (keys) {
|
||||
return keys.some((key) => this.keys.has(key));
|
||||
if (this.messagePublisher) {
|
||||
this.messagePublisher.addMessage({
|
||||
type: MessageType.REMOVE_INPUT,
|
||||
body: key
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public update(_dt: number, game: Game) {
|
||||
game.forEachEntityWithComponent(ComponentNames.Control, (entity) => {
|
||||
game.forEachEntityWithComponent(ComponentNames.Control, (entity) =>
|
||||
this.handleInput(entity)
|
||||
);
|
||||
}
|
||||
|
||||
public handleInput(entity: Entity) {
|
||||
const controlComponent = entity.getComponent<Control>(
|
||||
ComponentNames.Control
|
||||
);
|
||||
if (controlComponent.controllableBy != this.clientId) return;
|
||||
controlComponent.isControllable =
|
||||
controlComponent.controllableBy === this.clientId;
|
||||
|
||||
if (!controlComponent.isControllable) return;
|
||||
|
||||
if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_RIGHT))) {
|
||||
controlComponent.controlVelocityComponent.velocity.dCartesian.dx +=
|
||||
@ -56,13 +77,20 @@ export class Input extends System {
|
||||
-PhysicsConstants.PLAYER_MOVE_VEL;
|
||||
}
|
||||
|
||||
if (entity.hasComponent(ComponentNames.Jump)) {
|
||||
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 (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.JUMP))) {
|
||||
if (jump.canJump) {
|
||||
this.actionTimeStamps.set(Action.JUMP, performance.now());
|
||||
|
||||
@ -88,7 +116,11 @@ export class Input extends System {
|
||||
?.forces.push(jumpForce);
|
||||
}
|
||||
}
|
||||
|
||||
private hasSomeKey(keys?: string[]): boolean {
|
||||
if (keys) {
|
||||
return keys.some((key) => this.keys.has(key));
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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 { 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;
|
||||
}
|
||||
|
@ -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: {
|
||||
|
Loading…
Reference in New Issue
Block a user