export const INPUT_FREQUENCY = 33; // roughly 30fps
export interface LockstepProcessor {
compareInput(a: Input[], b: Input[]): boolean;
predictInput(prev: Input[] | null, localPlayer: number, localInput: Input): Input[];
cloneState(source: State): State;
advanceState(state: State, input: Input[]): void;
}
export class LockstepState {
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) {
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;
}
/** Warning: this only supports one player input per frame. There is no support for two players using the same lockstep simulation instance. */
public addLocalInput(player: number, 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] = this.engine.predictInput(this.inputLog[this.inputIndex - 1] ?? null, player, 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 {
private frame = 0;
public constructor(
private state: State,
private inputLog: Input[][],
private engine: LockstepProcessor,
) {}
public getNextState(): State {
if(this.frame < this.inputLog.length) {
this.engine.advanceState(this.state, this.inputLog[this.frame]);
this.frame++;
}
return this.state;
}
}