diff --git a/client/src/JumpStorm.ts b/client/src/JumpStorm.ts
index 6f9e24f..1beeb0d 100644
--- a/client/src/JumpStorm.ts
+++ b/client/src/JumpStorm.ts
@@ -8,7 +8,8 @@ import {
Physics,
Input,
Collision,
- NetworkUpdate
+ NetworkUpdate,
+ SystemNames
} from '@engine/systems';
import {
type MessageQueueProvider,
@@ -20,6 +21,7 @@ import {
type EntityUpdateBody
} from '@engine/network';
import { stringify, parse } from '@engine/utils';
+import { ComponentNames, Control, NetworkUpdateable } from '@engine/components';
class ClientMessageProcessor implements MessageProcessor {
private game: Game;
@@ -32,11 +34,27 @@ class ClientMessageProcessor implements MessageProcessor {
switch (message.type) {
case MessageType.NEW_ENTITIES:
const entityAdditions = message.body as unknown as EntityAddBody[];
- entityAdditions.forEach((addBody) =>
- this.game.addEntity(
- Entity.from(addBody.entityName, addBody.id, addBody.args)
- )
- );
+ entityAdditions.forEach((addBody) => {
+ const entity = Entity.from(
+ addBody.entityName,
+ addBody.id,
+ addBody.args
+ );
+ if (entity.hasComponent(ComponentNames.Control)) {
+ const clientId = this.game.getSystem(
+ SystemNames.Input
+ ).clientId;
+ const control = entity.getComponent(
+ ComponentNames.Control
+ );
+
+ if (control.controllableBy === clientId) {
+ entity.addComponent(new NetworkUpdateable());
+ }
+ }
+
+ this.game.addEntity(entity);
+ });
break;
case MessageType.REMOVE_ENTITIES:
const ids = message.body as unknown as string[];
@@ -44,9 +62,22 @@ class ClientMessageProcessor implements MessageProcessor {
break;
case MessageType.UPDATE_ENTITIES:
const entityUpdates = message.body as unknown as EntityUpdateBody[];
- entityUpdates.forEach(
- ({ id, args }) => this.game.getEntity(id)?.setFrom(args)
- );
+ entityUpdates.forEach(({ id, args }) => {
+ const entity = this.game.getEntity(id);
+ if (!entity) return;
+ if (entity && entity.hasComponent(ComponentNames.Control)) {
+ const clientId = this.game.getSystem(
+ SystemNames.Input
+ ).clientId;
+ const control = entity.getComponent(
+ ComponentNames.Control
+ );
+
+ // don't listen to entities which we control
+ if (control.controllableBy == clientId) return;
+ }
+ entity.setFrom(args);
+ });
break;
default:
break;
@@ -131,6 +162,7 @@ export class JumpStorm {
const grid = new Grid();
[
+ new Physics(),
new NetworkUpdate(
clientSocketMessageQueueProvider,
clientSocketMessagePublisher,
@@ -138,7 +170,6 @@ export class JumpStorm {
),
inputSystem,
new FacingDirection(),
- new Physics(),
new Collision(grid),
new WallBounds(),
new Render(ctx)
diff --git a/client/vite.config.ts b/client/vite.config.ts
index 6f0e1d0..d8b999c 100644
--- a/client/vite.config.ts
+++ b/client/vite.config.ts
@@ -5,10 +5,9 @@ import { fileURLToPath, URL } from 'node:url';
// https://vitejs.dev/config/
export default defineConfig({
server: {
- host: '0.0.0.0',
proxy: {
'/api': {
- target: 'http://10.0.0.237:8080',
+ target: 'http://localhost:8080',
ws: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
diff --git a/engine/Game.ts b/engine/Game.ts
index cdd3507..19de398 100644
--- a/engine/Game.ts
+++ b/engine/Game.ts
@@ -56,8 +56,8 @@ export class Game {
this.systems.set(system.name, system);
}
- public getSystem(name: string): System | undefined {
- return this.systems.get(name);
+ public getSystem(name: string): T {
+ return this.systems.get(name) as unknown as T;
}
public doGameLoop(timeStamp: number) {
diff --git a/engine/components/Control.ts b/engine/components/Control.ts
index d3987d7..b6a3dc3 100644
--- a/engine/components/Control.ts
+++ b/engine/components/Control.ts
@@ -3,16 +3,17 @@ 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
+ public isControllable?: boolean; // updated by the input system
constructor(
controllableBy: string,
- controlVelocityComponent: Velocity = new Velocity()
+ controlVelocityComponent: Velocity = new Velocity(),
+ isControllable?: boolean
) {
super(ComponentNames.Control);
this.controllableBy = controllableBy;
+ this.isControllable = isControllable;
this.controlVelocityComponent = controlVelocityComponent;
- this.isControllable = false;
}
}
diff --git a/engine/entities/Player.ts b/engine/entities/Player.ts
index 4d91c6f..a7a41f8 100644
--- a/engine/entities/Player.ts
+++ b/engine/entities/Player.ts
@@ -82,7 +82,7 @@ export class Player extends Entity {
}
public setFrom(args: Record) {
- const { control, velocity, forces, boundingBox } = args;
+ const { control, forces, velocity, boundingBox } = args;
let center = boundingBox.center;
@@ -92,7 +92,8 @@ export class Player extends Entity {
const distance = Math.sqrt(
Math.pow(center.y - myCenter.y, 2) + Math.pow(center.x - myCenter.x, 2)
);
- if (distance < 30) center = myCenter;
+ const clientServerPredictionCenterThreshold = 30;
+ if (distance < clientServerPredictionCenterThreshold) center = myCenter;
[
Object.assign(new Control(control.controllableBy), control),
diff --git a/engine/structures/RefreshingCollisionFinderBehavior.ts b/engine/structures/RefreshingCollisionFinderBehavior.ts
index 573ddd8..2215994 100644
--- a/engine/structures/RefreshingCollisionFinderBehavior.ts
+++ b/engine/structures/RefreshingCollisionFinderBehavior.ts
@@ -11,4 +11,5 @@ export interface RefreshingCollisionFinderBehavior {
insert(boxedEntry: BoxedEntry): void;
getNeighborIds(boxedEntry: BoxedEntry): Set;
setTopLeft(topLeft: Coord2D): void;
+ setDimension(dimension: Dimension2D): void;
}
diff --git a/engine/systems/NetworkUpdate.ts b/engine/systems/NetworkUpdate.ts
index 6d13574..a54be2e 100644
--- a/engine/systems/NetworkUpdate.ts
+++ b/engine/systems/NetworkUpdate.ts
@@ -8,13 +8,16 @@ import {
MessageType,
EntityUpdateBody
} from '../network';
+import { stringify } from '../utils';
+
+type EntityUpdateInfo = { timer: number; hash: string };
export class NetworkUpdate extends System {
private queueProvider: MessageQueueProvider;
private publisher: MessagePublisher;
private messageProcessor: MessageProcessor;
- private entityUpdateTimers: Map;
+ private entityUpdateInfo: Map;
constructor(
queueProvider: MessageQueueProvider,
@@ -27,10 +30,20 @@ export class NetworkUpdate extends System {
this.publisher = publisher;
this.messageProcessor = messageProcessor;
- this.entityUpdateTimers = new Map();
+ this.entityUpdateInfo = new Map();
}
public update(dt: number, game: Game) {
+ // 0. remove unnecessary info for removed entities
+ const networkUpdateableEntities = game.componentEntities.get(
+ ComponentNames.NetworkUpdateable
+ );
+ for (const entityId of this.entityUpdateInfo.keys()) {
+ if (!networkUpdateableEntities?.has(entityId)) {
+ this.entityUpdateInfo.delete(entityId);
+ }
+ }
+
// 1. process new messages
this.queueProvider
.getNewMessages()
@@ -39,34 +52,51 @@ export class NetworkUpdate extends System {
// 2. send entity updates
const updateMessages: EntityUpdateBody[] = [];
+
+ // todo: figure out if we can use the controllable component to determine if we should publish an update
game.forEachEntityWithComponent(
ComponentNames.NetworkUpdateable,
(entity) => {
- let timer = this.entityUpdateTimers.get(entity.id) ?? dt;
- timer -= dt;
- this.entityUpdateTimers.set(entity.id, timer);
+ const newHash = stringify(entity.serialize());
+ let updateInfo: EntityUpdateInfo = this.entityUpdateInfo.get(
+ entity.id
+ ) ?? {
+ timer: this.getNextUpdateTimeMs(),
+ hash: newHash
+ };
- if (timer > 0) return;
- this.entityUpdateTimers.set(entity.id, this.getNextUpdateTimeMs());
+ // update timer
+ updateInfo.timer -= dt;
+ this.entityUpdateInfo.set(entity.id, updateInfo);
+ if (updateInfo.timer > 0) return;
+ updateInfo.timer = this.getNextUpdateTimeMs();
+ this.entityUpdateInfo.set(entity.id, updateInfo);
- if (entity.hasComponent(ComponentNames.NetworkUpdateable)) {
- updateMessages.push({
- id: entity.id,
- args: entity.serialize()
- });
+ // maybe update if hash is not consitent
+ if (updateInfo.hash == newHash) {
+ return;
}
+ updateInfo.hash = newHash;
+ this.entityUpdateInfo.set(entity.id, updateInfo);
+
+ updateMessages.push({
+ id: entity.id,
+ args: entity.serialize()
+ });
}
);
- this.publisher.addMessage({
- type: MessageType.UPDATE_ENTITIES,
- body: updateMessages
- });
+
+ if (updateMessages.length)
+ this.publisher.addMessage({
+ type: MessageType.UPDATE_ENTITIES,
+ body: updateMessages
+ });
// 3. publish changes
this.publisher.publish();
}
private getNextUpdateTimeMs() {
- return Math.random() * 70 + 50;
+ return Math.random() * 30 + 50;
}
}
diff --git a/engine/systems/Physics.ts b/engine/systems/Physics.ts
index b5df459..4f8cc72 100644
--- a/engine/systems/Physics.ts
+++ b/engine/systems/Physics.ts
@@ -11,7 +11,7 @@ import {
Control
} from '../components';
import { PhysicsConstants } from '../config';
-import type { Force2D, Velocity2D } from '../interfaces';
+import type { Force2D } from '../interfaces';
import { Game } from '../Game';
export class Physics extends System {
@@ -98,7 +98,7 @@ export class Physics extends System {
? 360 + boundingBox.rotation
: boundingBox.rotation) % 360;
- // clear the control velocity
+ // clear the control velocity if and only if we are controlling
if (control && control.isControllable) {
control.controlVelocityComponent = new Velocity();
}
diff --git a/engine/utils/coding.ts b/engine/utils/coding.ts
index 3f78889..283844f 100644
--- a/engine/utils/coding.ts
+++ b/engine/utils/coding.ts
@@ -9,6 +9,18 @@ const replacer = (_key: any, value: any) => {
}
};
+const sortObj = (obj: any): any =>
+ obj === null || typeof obj !== 'object'
+ ? obj
+ : Array.isArray(obj)
+ ? obj.map(sortObj)
+ : Object.assign(
+ {},
+ ...Object.entries(obj)
+ .sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
+ .map(([k, v]) => ({ [k]: sortObj(v) }))
+ );
+
const reviver = (_key: any, value: any) => {
if (typeof value === 'object' && value !== null) {
if (value.dataType === 'Map') {
@@ -18,8 +30,9 @@ const reviver = (_key: any, value: any) => {
return value;
};
+// "deterministic" stringify
export const stringify = (obj: any) => {
- return JSON.stringify(obj, replacer);
+ return JSON.stringify(sortObj(obj), replacer);
};
export const parse = (str: string) => {
diff --git a/server/src/constants.ts b/server/src/constants.ts
index a2b3d12..2968d3a 100644
--- a/server/src/constants.ts
+++ b/server/src/constants.ts
@@ -1,6 +1,6 @@
export namespace Constants {
export const SERVER_PORT = 8080;
- export const SERVER_TICK_RATE = (1 / 60) * 1000;
+ export const SERVER_TICK_RATE = (1 / 120) * 1000;
export const GAME_TOPIC = 'game';
export const MAX_PLAYERS = 8;
}
diff --git a/server/src/main.ts b/server/src/main.ts
index 0e47491..ece6823 100644
--- a/server/src/main.ts
+++ b/server/src/main.ts
@@ -30,9 +30,9 @@ const server = new GameServer(
);
[
+ new Physics(),
new SessionInputSystem(sessionManager),
new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor),
- new Physics(),
new Collision(new Grid()),
new WallBounds()
].forEach((system) => game.addSystem(system));
@@ -44,9 +44,9 @@ floor.addComponent(
new BoundingBox(
{
x: Miscellaneous.WIDTH / 2,
- y: Miscellaneous.HEIGHT + floorHeight / 2
+ y: Miscellaneous.HEIGHT - floorHeight / 2
},
- { width: Miscellaneous.WIDTH, height: floorHeight }
+ { width: Miscellaneous.WIDTH / 2, height: floorHeight }
)
);
game.addEntity(floor);
diff --git a/server/src/network/MessageProcessor.ts b/server/src/network/MessageProcessor.ts
index 2d9f11f..c133f67 100644
--- a/server/src/network/MessageProcessor.ts
+++ b/server/src/network/MessageProcessor.ts
@@ -29,8 +29,16 @@ export class ServerMessageProcessor implements MessageProcessor {
session?.inputSystem.keyReleased(message.body as string);
break;
}
- default:
+ 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;
+ }
}
}
}
diff --git a/server/src/network/SessionInputSystem.ts b/server/src/network/SessionInputSystem.ts
index 44fba54..0f7ca6f 100644
--- a/server/src/network/SessionInputSystem.ts
+++ b/server/src/network/SessionInputSystem.ts
@@ -1,7 +1,7 @@
import { Game } from '@engine/Game';
import { SessionManager } from '.';
import { System } from '@engine/systems';
-import { BoundingBox, ComponentNames, Control } from '@engine/components';
+import { ComponentNames } from '@engine/components';
export class SessionInputSystem extends System {
private sessionManager: SessionManager;