base2020/src/Ecs/Lockstep.ts

94 lines
3 KiB
TypeScript
Raw Normal View History

export interface DeepCopy<T> {
deepCopy(patch: Partial<T>): T;
}
export interface Equals {
equals(other: this): boolean;
}
function equals<T extends Equals | string | number>(a: T, b: T): boolean {
if(typeof a === "string"){
return a == b;
}
if(typeof a === "number"){
return a == b;
}
return (a as Equals).equals(b as Equals);
}
// TODO: probably an incoherent idea. Instead consider having two state objects,
// a synchronized state object that can be rolled back / predicted, and
// a cosmetic state object that glosses the synchronized state and never rolls back
export const enum TickType {
/// a "canonical" update that will not be rolled back;
/// this must be fully deterministic based on the state and input.
CANON,
/// a "predicted" update that may or may not be rolled back;
/// if possible, avoid changes that could be distracting if rolled back.
PREDICTED,
}
export type Advancer<Input, State> = (state: State, input: Input) => void;
export class LockstepState<Input extends Equals, State extends DeepCopy<State>> {
private inputIndex = -1;
private inputLog: Input[] = [];
private canonIndex = -1;
private canonState: State;
private renderIndex = -1;
private renderState: State;
constructor(private initialState: State, private advancer: Advancer<Input, State>) {
this.canonState = initialState.deepCopy({});
this.renderState = initialState.deepCopy({});
}
public addCanonInput(input: Input): void {
this.canonIndex++;
// advance canonical game state
this.advancer(this.canonState, input);
if(this.canonIndex <= this.renderIndex) {
// we're rendering predicted states, so if the input changes we need to invalidate the rendered state
if(!equals(this.inputLog[this.canonIndex], input)) {
this.renderState = this.canonState.deepCopy({});
this.renderIndex = this.canonIndex;
}
}
this.inputLog[this.canonIndex] = input;
}
public addLocalInput(input: Input): void {
this.inputIndex++;
// ensure that we don't overwrite the canon input with local input somehow
// (probably only possible in situations where game is unplayable anyways? but still for sanity.)
if(this.inputIndex > this.canonIndex) {
this.inputLog[this.inputIndex] = input;
}
}
/**
* Do any necessary simulation to catchup the predicted game state to the frame
* that should be rendered, then return it.
*/
public getStateToRender(): State {
// TODO: input lag by X frames
const targetIndex = this.inputLog.length - 1;
while(this.renderIndex < targetIndex) {
this.renderIndex++;
this.advancer(this.renderState, this.inputLog[this.renderIndex]);
}
return this.renderState;
}
}
export class LockstepLoop<Input, State extends DeepCopy<State>> {
}