Improve netcode and simplify (#4)
- Simplify the logic wherever possible to remove overengineering and simplify the netcode - Actually improve the netcode - The new lines are mainly the package locks and tests Reviewed-on: https://home.schmelczer.dev/git/andras/decla-red/pulls/4
This commit is contained in:
parent
5d854440ec
commit
93a7fe01ab
1 changed files with 15073 additions and 4754 deletions
206
test/client-object-lifecycle.test.ts
Normal file
206
test/client-object-lifecycle.test.ts
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createRequire } from 'node:module';
|
||||
const {
|
||||
CreateObjectsCommand,
|
||||
DeleteObjectsCommand,
|
||||
PropertyUpdatesForObjects,
|
||||
RemoteCallsForObjects,
|
||||
RemoteCallsForObject,
|
||||
RemoteCall,
|
||||
UpdatePropertyCommand,
|
||||
CharacterTeam,
|
||||
} = createRequire(import.meta.url)('../shared/lib/main.js');
|
||||
import { GameObjectContainer } from '../frontend/src/scripts/objects/game-object-container';
|
||||
import { PlanetView } from '../frontend/src/scripts/objects/types/planet-view';
|
||||
import { serverTimeline } from '../frontend/src/scripts/helper/server-timeline';
|
||||
import { LinearInterpolator } from '../frontend/src/scripts/helper/interpolators/linear-interpolator';
|
||||
import { ProjectileView } from '../frontend/src/scripts/objects/types/projectile-view';
|
||||
|
||||
vi.mock('../frontend/node_modules/sdf-2d', () => ({
|
||||
CircleLight: class {
|
||||
constructor(
|
||||
public center: number[],
|
||||
public color: number[],
|
||||
public intensity: number,
|
||||
) {}
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../frontend/src/scripts/objects/types/camera', () => ({
|
||||
Camera: class {
|
||||
draw() {}
|
||||
follow() {}
|
||||
},
|
||||
}));
|
||||
vi.mock('../frontend/src/scripts/objects/types/planet-view', () => ({
|
||||
PlanetView: class {
|
||||
beforeDestroy = vi.fn();
|
||||
render = vi.fn();
|
||||
processRemoteCalls = vi.fn();
|
||||
step() {}
|
||||
constructor(public id: number) {}
|
||||
},
|
||||
}));
|
||||
vi.mock('../frontend/src/scripts/feedback-hud', () => ({ FeedbackHud: {} }));
|
||||
|
||||
let nowMs = 0;
|
||||
beforeEach(() => {
|
||||
nowMs = 0;
|
||||
vi.spyOn(performance, 'now').mockImplementation(() => nowMs);
|
||||
serverTimeline.reset();
|
||||
});
|
||||
|
||||
describe('remote effects', () => {
|
||||
it('plays remote effects on the render timeline and local feedback immediately', () => {
|
||||
const container = new GameObjectContainer({} as never);
|
||||
const remote = new PlanetView(1, []);
|
||||
const local = {
|
||||
id: 2,
|
||||
health: 100,
|
||||
position: [0, 0],
|
||||
snapshotStrength: 80,
|
||||
step() {},
|
||||
processRemoteCalls: vi.fn(),
|
||||
};
|
||||
container.handleCommand(new CreateObjectsCommand([remote, local]));
|
||||
container.player = local as never;
|
||||
const calls = [new RemoteCall('onShoot', [40])];
|
||||
container.handleCommand(
|
||||
new RemoteCallsForObjects([
|
||||
new RemoteCallsForObject(remote.id, calls),
|
||||
new RemoteCallsForObject(local.id, calls),
|
||||
]),
|
||||
);
|
||||
container.handleCommand(new PropertyUpdatesForObjects([], 5));
|
||||
expect(local.processRemoteCalls).toHaveBeenCalledWith(calls);
|
||||
step(0);
|
||||
container.step(0);
|
||||
expect(remote.processRemoteCalls).not.toHaveBeenCalled();
|
||||
step(0.11);
|
||||
container.step(0);
|
||||
expect(remote.processRemoteCalls).toHaveBeenCalledWith(calls);
|
||||
});
|
||||
|
||||
it('discards queued effects when a view is replaced or the game resets', () => {
|
||||
const container = new GameObjectContainer({} as never);
|
||||
const original = new PlanetView(1, []);
|
||||
container.handleCommand(new CreateObjectsCommand([original]));
|
||||
container.handleCommand(
|
||||
new RemoteCallsForObjects([
|
||||
new RemoteCallsForObject(1, [new RemoteCall('onFlipped', ['red'])]),
|
||||
]),
|
||||
);
|
||||
container.handleCommand(new PropertyUpdatesForObjects([], 5));
|
||||
const replacement = new PlanetView(1, []);
|
||||
container.handleCommand(new CreateObjectsCommand([replacement]));
|
||||
container.handleCommand(new PropertyUpdatesForObjects([], 5.04));
|
||||
step(0.3);
|
||||
container.step(0);
|
||||
expect(original.processRemoteCalls).not.toHaveBeenCalled();
|
||||
expect(replacement.processRemoteCalls).not.toHaveBeenCalled();
|
||||
|
||||
container.handleCommand(
|
||||
new RemoteCallsForObjects([
|
||||
new RemoteCallsForObject(1, [new RemoteCall('onFlipped', ['blue'])]),
|
||||
]),
|
||||
);
|
||||
container.reset();
|
||||
container.step(0);
|
||||
expect(replacement.processRemoteCalls).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
const step = (deltaSeconds: number) => {
|
||||
nowMs += deltaSeconds * 1000;
|
||||
serverTimeline.step(deltaSeconds);
|
||||
};
|
||||
|
||||
describe('snapshot object lifetime', () => {
|
||||
it('keeps creation and deletion aligned with the delayed render timeline', () => {
|
||||
const container = new GameObjectContainer({} as never);
|
||||
const planet = new PlanetView(1, []);
|
||||
container.handleCommand(new CreateObjectsCommand([planet]));
|
||||
container.handleCommand(new PropertyUpdatesForObjects([], 5));
|
||||
step(0);
|
||||
container.render({} as never, {} as never, true);
|
||||
expect(planet.render).not.toHaveBeenCalled();
|
||||
|
||||
step(0.11);
|
||||
container.render({} as never, {} as never, true);
|
||||
expect(planet.render).toHaveBeenCalledOnce();
|
||||
|
||||
container.handleCommand(new DeleteObjectsCommand([1]));
|
||||
container.handleCommand(new PropertyUpdatesForObjects([], 5.2));
|
||||
container.step(0);
|
||||
expect(planet.beforeDestroy).not.toHaveBeenCalled();
|
||||
step(0.3);
|
||||
container.step(0);
|
||||
expect(planet.beforeDestroy).toHaveBeenCalledOnce();
|
||||
expect(container.planets).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not delete a replacement when an object re-enters before delayed retirement', () => {
|
||||
const container = new GameObjectContainer({} as never);
|
||||
const original = new PlanetView(1, []);
|
||||
container.handleCommand(new CreateObjectsCommand([original]));
|
||||
container.handleCommand(new PropertyUpdatesForObjects([], 5));
|
||||
step(0);
|
||||
container.handleCommand(new DeleteObjectsCommand([1]));
|
||||
container.handleCommand(new PropertyUpdatesForObjects([], 5.04));
|
||||
|
||||
const replacement = new PlanetView(1, []);
|
||||
container.handleCommand(new CreateObjectsCommand([replacement]));
|
||||
container.handleCommand(new PropertyUpdatesForObjects([], 5.08));
|
||||
step(0.3);
|
||||
container.step(0);
|
||||
container.render({} as never, {} as never, true);
|
||||
|
||||
expect(original.beforeDestroy).toHaveBeenCalledOnce();
|
||||
expect(replacement.beforeDestroy).not.toHaveBeenCalled();
|
||||
expect(replacement.render).toHaveBeenCalledOnce();
|
||||
expect(container.planets).toEqual([replacement]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remote render timeline', () => {
|
||||
it('samples projectile strength on the server timeline, including slowed fading', () => {
|
||||
const projectile = new ProjectileView(1, [0, 0] as never, 10, CharacterTeam.red, 100);
|
||||
serverTimeline.onSnapshot(5);
|
||||
projectile.updateProperty(new UpdatePropertyCommand('strength', 100, -10));
|
||||
serverTimeline.onSnapshot(5.2);
|
||||
projectile.updateProperty(new UpdatePropertyCommand('strength', 98, -10));
|
||||
step(0);
|
||||
projectile.step(2);
|
||||
expect(projectile.strength).toBeCloseTo(99, 6);
|
||||
expect([...projectile.center]).toEqual([0, 0]);
|
||||
});
|
||||
|
||||
it('does not accumulate an extra frame of lead while chasing its target', () => {
|
||||
serverTimeline.onSnapshot(10);
|
||||
step(0);
|
||||
for (let i = 0; i < 600; i++) step(1 / 60);
|
||||
expect(serverTimeline.renderTime).toBeCloseTo(19.9, 8);
|
||||
});
|
||||
|
||||
it('interpolates positions and bounds extrapolation while snapshots are missing', () => {
|
||||
const position = new LinearInterpolator(0);
|
||||
serverTimeline.onSnapshot(10);
|
||||
position.addFrame(0, 100);
|
||||
serverTimeline.onSnapshot(10.1);
|
||||
position.addFrame(10, 100);
|
||||
step(0);
|
||||
step(0.05);
|
||||
expect(position.getValue(0.05)).toBeCloseTo(5, 6);
|
||||
step(0.4);
|
||||
expect(position.getValue(0.4)).toBeCloseTo(16, 6);
|
||||
});
|
||||
|
||||
it('ages a new snapshot from its arrival, not the beginning of the render frame', () => {
|
||||
nowMs = 15;
|
||||
serverTimeline.onSnapshot(10);
|
||||
nowMs = 16;
|
||||
serverTimeline.step(0.016);
|
||||
expect(serverTimeline.renderTime).toBeCloseTo(9.901, 6);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue