export const TICK_LENGTH = 33; // roughly 30fps export interface LockstepProcessor { compareInput(a: GlobalInput, b: GlobalInput): boolean; predictInput(prev: GlobalInput | undefined, localPlayer: number, localInput: LocalInput): GlobalInput; cloneState(source: State): State; advanceState(state: State, input: GlobalInput): void; } export class LockstepState { private inputIndex = -1; private inputLog: GlobalInput[] = []; 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: GlobalInput): 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: LocalInput): 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] ?? undefined, 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: GlobalInput[], 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; } }