don't update controllable entities on the client
This commit is contained in:
parent
8a4ab8d79b
commit
fd1bb1cca9
@ -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<Input>(
|
||||
SystemNames.Input
|
||||
).clientId;
|
||||
const control = entity.getComponent<Control>(
|
||||
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<Input>(
|
||||
SystemNames.Input
|
||||
).clientId;
|
||||
const control = entity.getComponent<Control>(
|
||||
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)
|
||||
|
@ -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/, '')
|
||||
}
|
||||
|
@ -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<T>(name: string): T {
|
||||
return this.systems.get(name) as unknown as T;
|
||||
}
|
||||
|
||||
public doGameLoop(timeStamp: number) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ export class Player extends Entity {
|
||||
}
|
||||
|
||||
public setFrom(args: Record<string, any>) {
|
||||
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),
|
||||
|
@ -11,4 +11,5 @@ export interface RefreshingCollisionFinderBehavior {
|
||||
insert(boxedEntry: BoxedEntry): void;
|
||||
getNeighborIds(boxedEntry: BoxedEntry): Set<string>;
|
||||
setTopLeft(topLeft: Coord2D): void;
|
||||
setDimension(dimension: Dimension2D): void;
|
||||
}
|
||||
|
@ -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<string, number>;
|
||||
private entityUpdateInfo: Map<string, EntityUpdateInfo>;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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 = <T>(str: string) => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user