even more
This commit is contained in:
parent
82e092ba55
commit
8f50ebdb97
1 changed files with 1842 additions and 3095 deletions
|
|
@ -1,38 +1,35 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { Renderer } from 'sdf-2d';
|
||||
import {
|
||||
Circle,
|
||||
Command,
|
||||
CommandExecutors,
|
||||
CommandReceiver,
|
||||
CreateObjectsCommand,
|
||||
CreatePlayerCommand,
|
||||
DeleteObjectsCommand,
|
||||
GameObject,
|
||||
Id,
|
||||
PropertyUpdatesForObjects,
|
||||
RemoteCallsForObjects,
|
||||
settings,
|
||||
UpdatePropertyCommand,
|
||||
} from 'shared';
|
||||
import { BeforeDestroyCommand } from '../commands/types/before-destroy';
|
||||
import { RenderCommand } from '../commands/types/render';
|
||||
import { StepCommand } from '../commands/types/step';
|
||||
import { FeedbackHud } from '../feedback-hud';
|
||||
import { Game } from '../game';
|
||||
import { serverTimeline } from '../helper/server-timeline';
|
||||
import { PredictablePlanet } from '../helper/prediction/client-character-world';
|
||||
import { localCharacterPredictor } from '../helper/prediction/local-character-predictor';
|
||||
import { Camera } from './types/camera';
|
||||
import { CharacterView } from './types/character-view';
|
||||
import { PlanetView } from './types/planet-view';
|
||||
import { View } from './view';
|
||||
|
||||
export class GameObjectContainer extends CommandReceiver {
|
||||
protected objects: Map<Id, GameObject> = new Map();
|
||||
public player!: CharacterView;
|
||||
public camera: Camera = new Camera(this.game);
|
||||
public player?: CharacterView;
|
||||
public readonly camera: Camera;
|
||||
public planets: Array<PlanetView> = [];
|
||||
|
||||
private objects: Map<Id, View> = new Map();
|
||||
private wasLocalPlayerAlive = false;
|
||||
|
||||
// Create/delete are admitted/retired on the render timeline (not the wire) so interpolated objects don't pop at the muzzle.
|
||||
// Creates and deletes take effect on the render timeline, not on arrival.
|
||||
private visibleFrom = new Map<Id, number>();
|
||||
private deleteAt = new Map<Id, number>();
|
||||
private awaitingCreateStamp: Array<Id> = [];
|
||||
|
|
@ -50,42 +47,10 @@ export class GameObjectContainer extends CommandReceiver {
|
|||
|
||||
[CreateObjectsCommand.type]: (c: CreateObjectsCommand) =>
|
||||
c.objects.forEach((o) => {
|
||||
this.addObject(o as GameObject);
|
||||
this.awaitingCreateStamp.push((o as GameObject).id);
|
||||
this.addObject(o as View);
|
||||
this.awaitingCreateStamp.push(o.id);
|
||||
}),
|
||||
|
||||
[StepCommand.type]: (c: StepCommand) => {
|
||||
// Fall back to newest snapshot if a batch had no property updates, so nothing is stranded.
|
||||
this.stampPending(serverTimeline.snapshotTime);
|
||||
this.retireDueObjects();
|
||||
this.defaultCommandExecutor(c);
|
||||
|
||||
// Alive = object exists AND health > 0 (player pointer outlives the body).
|
||||
const bodyPresent = !!this.player && this.objects.has(this.player.id);
|
||||
const alive = bodyPresent && this.player.health > 0;
|
||||
|
||||
if (this.wasLocalPlayerAlive && !alive) {
|
||||
FeedbackHud.showElimination();
|
||||
}
|
||||
this.wasLocalPlayerAlive = alive;
|
||||
|
||||
if (bodyPresent) {
|
||||
// Override the local player's interpolated pose with the predicted one; suppressed while dead (server ignores dead input).
|
||||
localCharacterPredictor.setAlive(alive);
|
||||
localCharacterPredictor.setStrength(
|
||||
this.player.strengthFraction * settings.playerMaxStrength,
|
||||
);
|
||||
if (
|
||||
localCharacterPredictor.update(this.predictablePlanets(), c.deltaTimeInSeconds)
|
||||
) {
|
||||
this.player.head = localCharacterPredictor.head;
|
||||
this.player.leftFoot = localCharacterPredictor.leftFoot;
|
||||
this.player.rightFoot = localCharacterPredictor.rightFoot;
|
||||
}
|
||||
this.camera.follow(this.player.position, c.deltaTimeInSeconds);
|
||||
}
|
||||
},
|
||||
|
||||
[RemoteCallsForObjects.type]: (c: RemoteCallsForObjects) =>
|
||||
c.callsForObjects.forEach((c) =>
|
||||
this.objects.get(c.id)?.processRemoteCalls(c.calls),
|
||||
|
|
@ -93,11 +58,11 @@ export class GameObjectContainer extends CommandReceiver {
|
|||
|
||||
[PropertyUpdatesForObjects.type]: (c: PropertyUpdatesForObjects) => {
|
||||
serverTimeline.onSnapshot(c.timestamp);
|
||||
// Create/delete precede property updates in a batch — timestamp is known here.
|
||||
this.stampPending(c.timestamp);
|
||||
c.updates.forEach((u) => {
|
||||
u.updates.forEach((au) => this.objects.get(u.id)?.handleCommand(au));
|
||||
if (this.player && u.id === this.player.id) {
|
||||
const object = this.objects.get(u.id);
|
||||
u.updates.forEach((au) => object?.updateProperty?.(au));
|
||||
if (object && object === this.player) {
|
||||
this.feedPredictor(u.updates);
|
||||
}
|
||||
});
|
||||
|
|
@ -107,19 +72,15 @@ export class GameObjectContainer extends CommandReceiver {
|
|||
c.ids.forEach((id: Id) => this.awaitingDeleteStamp.push(id)),
|
||||
};
|
||||
|
||||
constructor(private game: Game) {
|
||||
constructor(game: Game) {
|
||||
super();
|
||||
this.camera = new Camera(game);
|
||||
}
|
||||
|
||||
/**
|
||||
* A reconnect is a new server-side player that never sends deletes for the
|
||||
* old session's objects — clear them or they stay as frozen ghosts. `player`
|
||||
* is left stale: reads are gated on `objects`, and the next CreatePlayerCommand
|
||||
* replaces it.
|
||||
*/
|
||||
public reset() {
|
||||
this.objects.forEach((o) => o.handleCommand(new BeforeDestroyCommand()));
|
||||
this.objects.forEach((o) => o.beforeDestroy?.());
|
||||
this.objects.clear();
|
||||
this.planets = [];
|
||||
this.visibleFrom.clear();
|
||||
this.deleteAt.clear();
|
||||
this.awaitingCreateStamp = [];
|
||||
|
|
@ -127,32 +88,43 @@ export class GameObjectContainer extends CommandReceiver {
|
|||
this.wasLocalPlayerAlive = false;
|
||||
}
|
||||
|
||||
public get localPlayerPosition(): vec2 | undefined {
|
||||
return this.player && this.objects.has(this.player.id)
|
||||
? this.player.position
|
||||
: undefined;
|
||||
public get localPlayer(): CharacterView | undefined {
|
||||
return this.player && this.objects.has(this.player.id) ? this.player : undefined;
|
||||
}
|
||||
|
||||
public get planets(): Array<PlanetView> {
|
||||
const planets: Array<PlanetView> = [];
|
||||
this.objects.forEach((o) => {
|
||||
if (o instanceof PlanetView) {
|
||||
planets.push(o);
|
||||
public step(deltaTimeInSeconds: number) {
|
||||
this.stampPending(serverTimeline.snapshotTime);
|
||||
this.retireDueObjects();
|
||||
this.objects.forEach((o) => o.step(deltaTimeInSeconds));
|
||||
|
||||
const player = this.localPlayer;
|
||||
const alive = !!player && player.health > 0;
|
||||
if (this.wasLocalPlayerAlive && !alive) {
|
||||
FeedbackHud.showElimination();
|
||||
}
|
||||
this.wasLocalPlayerAlive = alive;
|
||||
|
||||
if (player) {
|
||||
localCharacterPredictor.setAlive(alive);
|
||||
localCharacterPredictor.setStrength(
|
||||
player.strengthFraction * settings.playerMaxStrength,
|
||||
);
|
||||
if (localCharacterPredictor.update(this.planets, deltaTimeInSeconds)) {
|
||||
player.head = localCharacterPredictor.head;
|
||||
player.leftFoot = localCharacterPredictor.leftFoot;
|
||||
player.rightFoot = localCharacterPredictor.rightFoot;
|
||||
}
|
||||
});
|
||||
return planets;
|
||||
this.camera.follow(player.position, deltaTimeInSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
protected defaultCommandExecutor(c: Command) {
|
||||
const isRender = c.type === RenderCommand.type;
|
||||
public render(renderer: Renderer, overlay: HTMLElement, shouldChangeLayout: boolean) {
|
||||
this.objects.forEach((o) => {
|
||||
// Stepping runs even for not-yet-visible objects so they enter the scene already moving.
|
||||
if (isRender && !this.isVisible(o.id)) {
|
||||
return;
|
||||
if (this.isVisible(o.id)) {
|
||||
o.render(renderer, overlay, shouldChangeLayout);
|
||||
}
|
||||
o.handleCommand(c);
|
||||
});
|
||||
this.camera.handleCommand(c);
|
||||
this.camera.draw(renderer);
|
||||
}
|
||||
|
||||
private isVisible(id: Id): boolean {
|
||||
|
|
@ -173,52 +145,41 @@ export class GameObjectContainer extends CommandReceiver {
|
|||
}
|
||||
|
||||
private retireDueObjects() {
|
||||
if (this.deleteAt.size === 0) {
|
||||
return;
|
||||
}
|
||||
const renderTime = serverTimeline.renderTime;
|
||||
for (const [id, at] of [...this.deleteAt]) {
|
||||
if (renderTime >= at) {
|
||||
this.deleteAt.delete(id);
|
||||
this.deleteObject(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Feed the predictor the raw (not interpolated) authoritative pose.
|
||||
private feedPredictor(updates: Array<UpdatePropertyCommand>) {
|
||||
let head: Circle | undefined;
|
||||
let leftFoot: Circle | undefined;
|
||||
let rightFoot: Circle | undefined;
|
||||
const pose: Partial<Record<'head' | 'leftFoot' | 'rightFoot', Circle>> = {};
|
||||
for (const u of updates) {
|
||||
if (u.propertyKey === 'head') head = u.propertyValue as Circle;
|
||||
else if (u.propertyKey === 'leftFoot') leftFoot = u.propertyValue as Circle;
|
||||
else if (u.propertyKey === 'rightFoot') rightFoot = u.propertyValue as Circle;
|
||||
if (
|
||||
u.propertyKey === 'head' ||
|
||||
u.propertyKey === 'leftFoot' ||
|
||||
u.propertyKey === 'rightFoot'
|
||||
) {
|
||||
pose[u.propertyKey] = u.propertyValue;
|
||||
}
|
||||
}
|
||||
if (head && leftFoot && rightFoot) {
|
||||
localCharacterPredictor.setAuthoritative(head, leftFoot, rightFoot);
|
||||
if (pose.head && pose.leftFoot && pose.rightFoot) {
|
||||
localCharacterPredictor.setAuthoritative(pose.head, pose.leftFoot, pose.rightFoot);
|
||||
}
|
||||
}
|
||||
|
||||
private predictablePlanets(): Array<PredictablePlanet> {
|
||||
return this.planets.map((p) => ({
|
||||
id: p.id,
|
||||
vertices: p.vertices,
|
||||
center: p.center,
|
||||
radius: p.radius,
|
||||
rotation: p.predictionRotation,
|
||||
rotationSpeed: p.predictionRotationSpeed,
|
||||
}));
|
||||
}
|
||||
|
||||
private addObject(object: GameObject) {
|
||||
private addObject(object: View) {
|
||||
this.objects.set(object.id, object);
|
||||
if (object instanceof PlanetView) {
|
||||
this.planets.push(object);
|
||||
}
|
||||
}
|
||||
|
||||
private deleteObject(id: Id) {
|
||||
const object = this.objects.get(id);
|
||||
object?.handleCommand(new BeforeDestroyCommand());
|
||||
this.objects.get(id)?.beforeDestroy?.();
|
||||
this.objects.delete(id);
|
||||
this.planets = this.planets.filter((p) => p.id !== id);
|
||||
this.visibleFrom.delete(id);
|
||||
this.deleteAt.delete(id);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue