summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Hunt <elizabeth.hunt@simponic.xyz>2023-08-26 17:55:27 -0600
committerElizabeth Hunt <elizabeth.hunt@simponic.xyz>2023-08-26 17:55:27 -0600
commit6ce6946a4401d2ee6fa5cb747fab7d4c658a63c8 (patch)
treee60767dc5295edf379cf421e20171dc418e548b7
parent594921352c8d82fe5f1a6201a4d5f9fbd9b719fc (diff)
downloadjumpstorm-6ce6946a4401d2ee6fa5cb747fab7d4c658a63c8.tar.gz
jumpstorm-6ce6946a4401d2ee6fa5cb747fab7d4c658a63c8.zip
add entity updates over network!
-rw-r--r--client/src/JumpStorm.ts94
-rw-r--r--client/vite.config.ts3
-rw-r--r--engine/components/BoundingBox.ts4
-rw-r--r--engine/components/Control.ts2
-rw-r--r--engine/components/NetworkUpdateable.ts8
-rw-r--r--engine/config/constants.ts10
-rw-r--r--engine/entities/Entity.ts32
-rw-r--r--engine/entities/Floor.ts24
-rw-r--r--engine/entities/Player.ts44
-rw-r--r--engine/network/index.ts12
-rw-r--r--engine/systems/Input.ts144
-rw-r--r--engine/systems/NetworkUpdate.ts40
-rw-r--r--engine/systems/Physics.ts2
-rw-r--r--server/src/main.ts41
-rw-r--r--server/src/network/MessageProcessor.ts36
-rw-r--r--server/src/network/SessionInputSystem.ts32
-rw-r--r--server/src/network/SessionManager.ts33
-rw-r--r--server/src/network/index.ts13
-rw-r--r--server/src/server.ts76
19 files changed, 472 insertions, 178 deletions
diff --git a/client/src/JumpStorm.ts b/client/src/JumpStorm.ts
index 92bddcf..6f9e24f 100644
--- a/client/src/JumpStorm.ts
+++ b/client/src/JumpStorm.ts
@@ -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);
}
}
diff --git a/client/vite.config.ts b/client/vite.config.ts
index d8b999c..6f0e1d0 100644
--- a/client/vite.config.ts
+++ b/client/vite.config.ts
@@ -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/, '')
}
diff --git a/engine/components/BoundingBox.ts b/engine/components/BoundingBox.ts
index dbe083e..921feb9 100644
--- a/engine/components/BoundingBox.ts
+++ b/engine/components/BoundingBox.ts
@@ -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)),
diff --git a/engine/components/Control.ts b/engine/components/Control.ts
index beec82c..d3987d7 100644
--- a/engine/components/Control.ts
+++ b/engine/components/Control.ts
@@ -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;
}
}
diff --git a/engine/components/NetworkUpdateable.ts b/engine/components/NetworkUpdateable.ts
index 7fb6d18..014270c 100644
--- a/engine/components/NetworkUpdateable.ts
+++ b/engine/components/NetworkUpdateable.ts
@@ -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;
}
}
diff --git a/engine/config/constants.ts b/engine/config/constants.ts
index 45b0301..dc98ad0 100644
--- a/engine/config/constants.ts
+++ b/engine/config/constants.ts
@@ -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;
}
diff --git a/engine/entities/Entity.ts b/engine/entities/Entity.ts
index b016fc0..63fb370 100644
--- a/engine/entities/Entity.ts
+++ b/engine/entities/Entity.ts
@@ -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>;
}
diff --git a/engine/entities/Floor.ts b/engine/entities/Floor.ts
index 6f9b13b..b4f48e5 100644
--- a/engine/entities/Floor.ts
+++ b/engine/entities/Floor.ts
@@ -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
+ )
+ );
+ }
}
diff --git a/engine/entities/Player.ts b/engine/entities/Player.ts
index 947fbd6..4d91c6f 100644
--- a/engine/entities/Player.ts
+++ b/engine/entities/Player.ts
@@ -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));
}
}
diff --git a/engine/network/index.ts b/engine/network/index.ts
index 1bf95fb..5dc7ece 100644
--- a/engine/network/index.ts
+++ b/engine/network/index.ts
@@ -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 = {
diff --git a/engine/systems/Input.ts b/engine/systems/Input.ts
index 8a68905..9afd1ab 100644
--- a/engine/systems/Input.ts
+++ b/engine/systems/Input.ts
@@ -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);
- }
- }
- }
- });
- }
}
diff --git a/engine/systems/NetworkUpdate.ts b/engine/systems/NetworkUpdate.ts
index bcfb71e..6d13574 100644
--- a/engine/systems/NetworkUpdate.ts
+++ b/engine/systems/NetworkUpdate.ts
@@ -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;
+ }
}
diff --git a/engine/systems/Physics.ts b/engine/systems/Physics.ts
index 35afb3f..b5df459 100644
--- a/engine/systems/Physics.ts
+++ b/engine/systems/Physics.ts
@@ -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();
}
});
diff --git a/server/src/main.ts b/server/src/main.ts
index 965e0d7..0e47491 100644
--- a/server/src/main.ts
+++ b/server/src/main.ts
@@ -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';
+import { Floor } from '@engine/entities';
+import { BoundingBox } from '@engine/components';
+import { Miscellaneous } from '@engine/config';
+
+const game = new Game();
+
+const sessionManager = new MemorySessionManager();
const messageReceiver = new ServerSocketMessageReceiver();
const messagePublisher = new ServerSocketMessagePublisher();
-const messageProcessor = new ServerMessageProcessor();
+const messageProcessor = new ServerMessageProcessor(game, sessionManager);
-const game = new Game();
-
-const server = new GameServer(game, messageReceiver, messagePublisher);
+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());
diff --git a/server/src/network/MessageProcessor.ts b/server/src/network/MessageProcessor.ts
index de42459..2d9f11f 100644
--- a/server/src/network/MessageProcessor.ts
+++ b/server/src/network/MessageProcessor.ts
@@ -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;
+ }
+ }
}
diff --git a/server/src/network/SessionInputSystem.ts b/server/src/network/SessionInputSystem.ts
new file mode 100644
index 0000000..44fba54
--- /dev/null
+++ b/server/src/network/SessionInputSystem.ts
@@ -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);
+ }
+ });
+ });
+ }
+}
diff --git a/server/src/network/SessionManager.ts b/server/src/network/SessionManager.ts
new file mode 100644
index 0000000..dbd4364
--- /dev/null
+++ b/server/src/network/SessionManager.ts
@@ -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);
+ }
+}
diff --git a/server/src/network/index.ts b/server/src/network/index.ts
index 8ffa689..3cbf0ac 100644
--- a/server/src/network/index.ts
+++ b/server/src/network/index.ts
@@ -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;
+}
diff --git a/server/src/server.ts b/server/src/server.ts
index 303d2b5..575e916 100644
--- a/server/src/server.ts
+++ b/server/src/server.ts
@@ -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: {