base2020/src/ecs/Lockstep.ts

86 lines
3.1 KiB
TypeScript

export const TICK_LENGTH = 33; // roughly 30fps
export interface LockstepProcessor<LocalInput, GlobalInput, State> {
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<LocalInput, GlobalInput, State> {
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<LocalInput, GlobalInput, State>) {
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<LocalInput, GlobalInput, State> {
private frame = 0;
public constructor(
private state: State,
private inputLog: GlobalInput[],
private engine: LockstepProcessor<LocalInput, GlobalInput, State>,
) {}
public getNextState(): State {
if(this.frame < this.inputLog.length) {
this.engine.advanceState(this.state, this.inputLog[this.frame]);
this.frame++;
}
return this.state;
}
}