2020-01-29 03:51:15 +00:00
|
|
|
|
2020-02-16 00:52:00 +00:00
|
|
|
export const INPUT_FREQUENCY = 33; // roughly 30fps
|
|
|
|
|
|
|
|
export interface LockstepProcessor<Input, State> {
|
2020-02-15 18:54:59 +00:00
|
|
|
compareInput(a: Input, b: Input): boolean;
|
|
|
|
cloneState(source: State): State;
|
|
|
|
advanceState(state: State, input: Input): void;
|
|
|
|
}
|
2020-01-29 03:51:15 +00:00
|
|
|
|
2020-02-15 18:54:59 +00:00
|
|
|
export class LockstepState<Input, State> {
|
2020-01-29 03:51:15 +00:00
|
|
|
|
|
|
|
private inputIndex = -1;
|
|
|
|
private inputLog: Input[] = [];
|
|
|
|
private canonIndex = -1;
|
|
|
|
private canonState: State;
|
|
|
|
private renderIndex = -1;
|
|
|
|
private renderState: State;
|
|
|
|
|
2020-02-15 18:54:59 +00:00
|
|
|
constructor(private initialState: State, private engine: LockstepProcessor<Input, State>) {
|
|
|
|
this.canonState = engine.cloneState(initialState);
|
|
|
|
this.renderState = engine.cloneState(initialState);
|
2020-01-29 03:51:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public addCanonInput(input: Input): void {
|
|
|
|
this.canonIndex++;
|
|
|
|
|
|
|
|
// advance canonical game state
|
2020-02-15 18:54:59 +00:00
|
|
|
this.engine.advanceState(this.canonState, input);
|
2020-01-29 03:51:15 +00:00
|
|
|
|
|
|
|
if(this.canonIndex <= this.renderIndex) {
|
|
|
|
// we're rendering predicted states, so if the input changes we need to invalidate the rendered state
|
2020-02-15 18:54:59 +00:00
|
|
|
if(!this.engine.compareInput(this.inputLog[this.canonIndex], input)) {
|
|
|
|
this.renderState = this.engine.cloneState(this.canonState);
|
2020-01-29 03:51:15 +00:00
|
|
|
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++;
|
2020-02-15 18:54:59 +00:00
|
|
|
this.engine.advanceState(this.renderState, this.inputLog[this.renderIndex]);
|
2020-01-29 03:51:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return this.renderState;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-15 19:12:53 +00:00
|
|
|
export class Playback<Input, State> {
|
|
|
|
private frame = 0;
|
|
|
|
|
|
|
|
public constructor(
|
|
|
|
private state: State,
|
|
|
|
private inputLog: Input[],
|
|
|
|
private engine: LockstepProcessor<Input, State>,
|
|
|
|
) {}
|
|
|
|
|
|
|
|
public getNextState(): State {
|
|
|
|
if(this.frame < this.inputLog.length) {
|
|
|
|
this.engine.advanceState(this.state, this.inputLog[this.frame]);
|
|
|
|
this.frame++;
|
|
|
|
}
|
|
|
|
return this.state;
|
|
|
|
}
|
|
|
|
}
|