don't update controllable entities on the client

This commit is contained in:
Elizabeth Hunt 2023-08-29 12:05:02 -06:00
parent 8a4ab8d79b
commit fd1bb1cca9
Signed by: simponic
GPG Key ID: 52B3774857EB24B1
13 changed files with 129 additions and 45 deletions

View File

@ -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)

View File

@ -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/, '')
}

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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),

View File

@ -11,4 +11,5 @@ export interface RefreshingCollisionFinderBehavior {
insert(boxedEntry: BoxedEntry): void;
getNeighborIds(boxedEntry: BoxedEntry): Set<string>;
setTopLeft(topLeft: Coord2D): void;
setDimension(dimension: Dimension2D): void;
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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) => {

View File

@ -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;
}

View File

@ -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);

View File

@ -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;
}
}
}
}

View File

@ -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;