export interface DeepCopy { deepCopy(patch: Partial): T; } export interface Equals { equals(other: this): boolean; } function equals(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 = (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 advancer: Advancer) { 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> { }