diff --git a/src/Ecs/Lockstep.ts b/src/Ecs/Lockstep.ts new file mode 100644 index 0000000..c9e54ec --- /dev/null +++ b/src/Ecs/Lockstep.ts @@ -0,0 +1,93 @@ + +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> { + +}