base2020/src/Ecs/Lockstep.ts

104 lines
3.0 KiB
TypeScript

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);
}
interface LockstepProcessor<Input, State> {
compareInput(a: Input, b: Input): boolean;
cloneState(source: State): State;
advanceState(state: State, input: Input): void;
}
export class LockstepState<Input, State> {
private inputIndex = -1;
private inputLog: Input[] = [];
private canonIndex = -1;
private canonState: State;
private renderIndex = -1;
private renderState: State;
constructor(private initialState: State, private engine: LockstepProcessor<Input, State>) {
this.canonState = engine.cloneState(initialState);
this.renderState = engine.cloneState(initialState);
}
public addCanonInput(input: Input): void {
this.canonIndex++;
// advance canonical game state
this.engine.advanceState(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(!this.engine.compareInput(this.inputLog[this.canonIndex], input)) {
this.renderState = this.engine.cloneState(this.canonState);
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.engine.advanceState(this.renderState, this.inputLog[this.renderIndex]);
}
return this.renderState;
}
}
export class Playback<Input, State> {
private frame = 0;
public constructor(
private state: State,
private inputLog: Input[],
private engine: LockstepProcessor<Input, State>,
) {}
public getNextState(): State {
if(this.frame < this.inputLog.length) {
this.engine.advanceState(this.state, this.inputLog[this.frame]);
this.frame++;
}
return this.state;
}
}
export class LockstepLoop<Input, State extends DeepCopy<State>> {
}