don't update controllable entities on the client
This commit is contained in:
parent
8a4ab8d79b
commit
fd1bb1cca9
@ -8,7 +8,8 @@ import {
|
|||||||
Physics,
|
Physics,
|
||||||
Input,
|
Input,
|
||||||
Collision,
|
Collision,
|
||||||
NetworkUpdate
|
NetworkUpdate,
|
||||||
|
SystemNames
|
||||||
} from '@engine/systems';
|
} from '@engine/systems';
|
||||||
import {
|
import {
|
||||||
type MessageQueueProvider,
|
type MessageQueueProvider,
|
||||||
@ -20,6 +21,7 @@ import {
|
|||||||
type EntityUpdateBody
|
type EntityUpdateBody
|
||||||
} from '@engine/network';
|
} from '@engine/network';
|
||||||
import { stringify, parse } from '@engine/utils';
|
import { stringify, parse } from '@engine/utils';
|
||||||
|
import { ComponentNames, Control, NetworkUpdateable } from '@engine/components';
|
||||||
|
|
||||||
class ClientMessageProcessor implements MessageProcessor {
|
class ClientMessageProcessor implements MessageProcessor {
|
||||||
private game: Game;
|
private game: Game;
|
||||||
@ -32,11 +34,27 @@ class ClientMessageProcessor implements MessageProcessor {
|
|||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case MessageType.NEW_ENTITIES:
|
case MessageType.NEW_ENTITIES:
|
||||||
const entityAdditions = message.body as unknown as EntityAddBody[];
|
const entityAdditions = message.body as unknown as EntityAddBody[];
|
||||||
entityAdditions.forEach((addBody) =>
|
entityAdditions.forEach((addBody) => {
|
||||||
this.game.addEntity(
|
const entity = Entity.from(
|
||||||
Entity.from(addBody.entityName, addBody.id, addBody.args)
|
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;
|
break;
|
||||||
case MessageType.REMOVE_ENTITIES:
|
case MessageType.REMOVE_ENTITIES:
|
||||||
const ids = message.body as unknown as string[];
|
const ids = message.body as unknown as string[];
|
||||||
@ -44,9 +62,22 @@ class ClientMessageProcessor implements MessageProcessor {
|
|||||||
break;
|
break;
|
||||||
case MessageType.UPDATE_ENTITIES:
|
case MessageType.UPDATE_ENTITIES:
|
||||||
const entityUpdates = message.body as unknown as EntityUpdateBody[];
|
const entityUpdates = message.body as unknown as EntityUpdateBody[];
|
||||||
entityUpdates.forEach(
|
entityUpdates.forEach(({ id, args }) => {
|
||||||
({ id, args }) => this.game.getEntity(id)?.setFrom(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;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@ -131,6 +162,7 @@ export class JumpStorm {
|
|||||||
const grid = new Grid();
|
const grid = new Grid();
|
||||||
|
|
||||||
[
|
[
|
||||||
|
new Physics(),
|
||||||
new NetworkUpdate(
|
new NetworkUpdate(
|
||||||
clientSocketMessageQueueProvider,
|
clientSocketMessageQueueProvider,
|
||||||
clientSocketMessagePublisher,
|
clientSocketMessagePublisher,
|
||||||
@ -138,7 +170,6 @@ export class JumpStorm {
|
|||||||
),
|
),
|
||||||
inputSystem,
|
inputSystem,
|
||||||
new FacingDirection(),
|
new FacingDirection(),
|
||||||
new Physics(),
|
|
||||||
new Collision(grid),
|
new Collision(grid),
|
||||||
new WallBounds(),
|
new WallBounds(),
|
||||||
new Render(ctx)
|
new Render(ctx)
|
||||||
|
@ -5,10 +5,9 @@ import { fileURLToPath, URL } from 'node:url';
|
|||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://10.0.0.237:8080',
|
target: 'http://localhost:8080',
|
||||||
ws: true,
|
ws: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, '')
|
rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
}
|
}
|
||||||
|
@ -56,8 +56,8 @@ export class Game {
|
|||||||
this.systems.set(system.name, system);
|
this.systems.set(system.name, system);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSystem(name: string): System | undefined {
|
public getSystem<T>(name: string): T {
|
||||||
return this.systems.get(name);
|
return this.systems.get(name) as unknown as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
public doGameLoop(timeStamp: number) {
|
public doGameLoop(timeStamp: number) {
|
||||||
|
@ -3,16 +3,17 @@ import { Component, ComponentNames, Velocity } from '.';
|
|||||||
export class Control extends Component {
|
export class Control extends Component {
|
||||||
public controlVelocityComponent: Velocity;
|
public controlVelocityComponent: Velocity;
|
||||||
public controllableBy: string;
|
public controllableBy: string;
|
||||||
public isControllable: boolean; // computed each update in the input system
|
public isControllable?: boolean; // updated by the input system
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
controllableBy: string,
|
controllableBy: string,
|
||||||
controlVelocityComponent: Velocity = new Velocity()
|
controlVelocityComponent: Velocity = new Velocity(),
|
||||||
|
isControllable?: boolean
|
||||||
) {
|
) {
|
||||||
super(ComponentNames.Control);
|
super(ComponentNames.Control);
|
||||||
|
|
||||||
this.controllableBy = controllableBy;
|
this.controllableBy = controllableBy;
|
||||||
|
this.isControllable = isControllable;
|
||||||
this.controlVelocityComponent = controlVelocityComponent;
|
this.controlVelocityComponent = controlVelocityComponent;
|
||||||
this.isControllable = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@ export class Player extends Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setFrom(args: Record<string, any>) {
|
public setFrom(args: Record<string, any>) {
|
||||||
const { control, velocity, forces, boundingBox } = args;
|
const { control, forces, velocity, boundingBox } = args;
|
||||||
|
|
||||||
let center = boundingBox.center;
|
let center = boundingBox.center;
|
||||||
|
|
||||||
@ -92,7 +92,8 @@ export class Player extends Entity {
|
|||||||
const distance = Math.sqrt(
|
const distance = Math.sqrt(
|
||||||
Math.pow(center.y - myCenter.y, 2) + Math.pow(center.x - myCenter.x, 2)
|
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),
|
Object.assign(new Control(control.controllableBy), control),
|
||||||
|
@ -11,4 +11,5 @@ export interface RefreshingCollisionFinderBehavior {
|
|||||||
insert(boxedEntry: BoxedEntry): void;
|
insert(boxedEntry: BoxedEntry): void;
|
||||||
getNeighborIds(boxedEntry: BoxedEntry): Set<string>;
|
getNeighborIds(boxedEntry: BoxedEntry): Set<string>;
|
||||||
setTopLeft(topLeft: Coord2D): void;
|
setTopLeft(topLeft: Coord2D): void;
|
||||||
|
setDimension(dimension: Dimension2D): void;
|
||||||
}
|
}
|
||||||
|
@ -8,13 +8,16 @@ import {
|
|||||||
MessageType,
|
MessageType,
|
||||||
EntityUpdateBody
|
EntityUpdateBody
|
||||||
} from '../network';
|
} from '../network';
|
||||||
|
import { stringify } from '../utils';
|
||||||
|
|
||||||
|
type EntityUpdateInfo = { timer: number; hash: string };
|
||||||
|
|
||||||
export class NetworkUpdate extends System {
|
export class NetworkUpdate extends System {
|
||||||
private queueProvider: MessageQueueProvider;
|
private queueProvider: MessageQueueProvider;
|
||||||
private publisher: MessagePublisher;
|
private publisher: MessagePublisher;
|
||||||
private messageProcessor: MessageProcessor;
|
private messageProcessor: MessageProcessor;
|
||||||
|
|
||||||
private entityUpdateTimers: Map<string, number>;
|
private entityUpdateInfo: Map<string, EntityUpdateInfo>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
queueProvider: MessageQueueProvider,
|
queueProvider: MessageQueueProvider,
|
||||||
@ -27,10 +30,20 @@ export class NetworkUpdate extends System {
|
|||||||
this.publisher = publisher;
|
this.publisher = publisher;
|
||||||
this.messageProcessor = messageProcessor;
|
this.messageProcessor = messageProcessor;
|
||||||
|
|
||||||
this.entityUpdateTimers = new Map();
|
this.entityUpdateInfo = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(dt: number, game: Game) {
|
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
|
// 1. process new messages
|
||||||
this.queueProvider
|
this.queueProvider
|
||||||
.getNewMessages()
|
.getNewMessages()
|
||||||
@ -39,34 +52,51 @@ export class NetworkUpdate extends System {
|
|||||||
|
|
||||||
// 2. send entity updates
|
// 2. send entity updates
|
||||||
const updateMessages: EntityUpdateBody[] = [];
|
const updateMessages: EntityUpdateBody[] = [];
|
||||||
|
|
||||||
|
// todo: figure out if we can use the controllable component to determine if we should publish an update
|
||||||
game.forEachEntityWithComponent(
|
game.forEachEntityWithComponent(
|
||||||
ComponentNames.NetworkUpdateable,
|
ComponentNames.NetworkUpdateable,
|
||||||
(entity) => {
|
(entity) => {
|
||||||
let timer = this.entityUpdateTimers.get(entity.id) ?? dt;
|
const newHash = stringify(entity.serialize());
|
||||||
timer -= dt;
|
let updateInfo: EntityUpdateInfo = this.entityUpdateInfo.get(
|
||||||
this.entityUpdateTimers.set(entity.id, timer);
|
entity.id
|
||||||
|
) ?? {
|
||||||
|
timer: this.getNextUpdateTimeMs(),
|
||||||
|
hash: newHash
|
||||||
|
};
|
||||||
|
|
||||||
if (timer > 0) return;
|
// update timer
|
||||||
this.entityUpdateTimers.set(entity.id, this.getNextUpdateTimeMs());
|
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)) {
|
// maybe update if hash is not consitent
|
||||||
updateMessages.push({
|
if (updateInfo.hash == newHash) {
|
||||||
id: entity.id,
|
return;
|
||||||
args: entity.serialize()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
updateInfo.hash = newHash;
|
||||||
|
this.entityUpdateInfo.set(entity.id, updateInfo);
|
||||||
|
|
||||||
|
updateMessages.push({
|
||||||
|
id: entity.id,
|
||||||
|
args: entity.serialize()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.publisher.addMessage({
|
|
||||||
type: MessageType.UPDATE_ENTITIES,
|
if (updateMessages.length)
|
||||||
body: updateMessages
|
this.publisher.addMessage({
|
||||||
});
|
type: MessageType.UPDATE_ENTITIES,
|
||||||
|
body: updateMessages
|
||||||
|
});
|
||||||
|
|
||||||
// 3. publish changes
|
// 3. publish changes
|
||||||
this.publisher.publish();
|
this.publisher.publish();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNextUpdateTimeMs() {
|
private getNextUpdateTimeMs() {
|
||||||
return Math.random() * 70 + 50;
|
return Math.random() * 30 + 50;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
Control
|
Control
|
||||||
} from '../components';
|
} from '../components';
|
||||||
import { PhysicsConstants } from '../config';
|
import { PhysicsConstants } from '../config';
|
||||||
import type { Force2D, Velocity2D } from '../interfaces';
|
import type { Force2D } from '../interfaces';
|
||||||
import { Game } from '../Game';
|
import { Game } from '../Game';
|
||||||
|
|
||||||
export class Physics extends System {
|
export class Physics extends System {
|
||||||
@ -98,7 +98,7 @@ export class Physics extends System {
|
|||||||
? 360 + boundingBox.rotation
|
? 360 + boundingBox.rotation
|
||||||
: boundingBox.rotation) % 360;
|
: boundingBox.rotation) % 360;
|
||||||
|
|
||||||
// clear the control velocity
|
// clear the control velocity if and only if we are controlling
|
||||||
if (control && control.isControllable) {
|
if (control && control.isControllable) {
|
||||||
control.controlVelocityComponent = new Velocity();
|
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) => {
|
const reviver = (_key: any, value: any) => {
|
||||||
if (typeof value === 'object' && value !== null) {
|
if (typeof value === 'object' && value !== null) {
|
||||||
if (value.dataType === 'Map') {
|
if (value.dataType === 'Map') {
|
||||||
@ -18,8 +30,9 @@ const reviver = (_key: any, value: any) => {
|
|||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// "deterministic" stringify
|
||||||
export const stringify = (obj: any) => {
|
export const stringify = (obj: any) => {
|
||||||
return JSON.stringify(obj, replacer);
|
return JSON.stringify(sortObj(obj), replacer);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parse = <T>(str: string) => {
|
export const parse = <T>(str: string) => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export namespace Constants {
|
export namespace Constants {
|
||||||
export const SERVER_PORT = 8080;
|
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 GAME_TOPIC = 'game';
|
||||||
export const MAX_PLAYERS = 8;
|
export const MAX_PLAYERS = 8;
|
||||||
}
|
}
|
||||||
|
@ -30,9 +30,9 @@ const server = new GameServer(
|
|||||||
);
|
);
|
||||||
|
|
||||||
[
|
[
|
||||||
|
new Physics(),
|
||||||
new SessionInputSystem(sessionManager),
|
new SessionInputSystem(sessionManager),
|
||||||
new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor),
|
new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor),
|
||||||
new Physics(),
|
|
||||||
new Collision(new Grid()),
|
new Collision(new Grid()),
|
||||||
new WallBounds()
|
new WallBounds()
|
||||||
].forEach((system) => game.addSystem(system));
|
].forEach((system) => game.addSystem(system));
|
||||||
@ -44,9 +44,9 @@ floor.addComponent(
|
|||||||
new BoundingBox(
|
new BoundingBox(
|
||||||
{
|
{
|
||||||
x: Miscellaneous.WIDTH / 2,
|
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);
|
game.addEntity(floor);
|
||||||
|
@ -29,8 +29,16 @@ export class ServerMessageProcessor implements MessageProcessor {
|
|||||||
session?.inputSystem.keyReleased(message.body as string);
|
session?.inputSystem.keyReleased(message.body as string);
|
||||||
break;
|
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;
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Game } from '@engine/Game';
|
import { Game } from '@engine/Game';
|
||||||
import { SessionManager } from '.';
|
import { SessionManager } from '.';
|
||||||
import { System } from '@engine/systems';
|
import { System } from '@engine/systems';
|
||||||
import { BoundingBox, ComponentNames, Control } from '@engine/components';
|
import { ComponentNames } from '@engine/components';
|
||||||
|
|
||||||
export class SessionInputSystem extends System {
|
export class SessionInputSystem extends System {
|
||||||
private sessionManager: SessionManager;
|
private sessionManager: SessionManager;
|
||||||
|
Loading…
Reference in New Issue
Block a user