split generics for local/full input types for now
LoopbackServer still makes the assumption that GlobalInput = LocalInput[]
This commit is contained in:
parent
743905cad4
commit
258c179887
6 changed files with 35 additions and 34 deletions
2
plan.txt
2
plan.txt
|
@ -1,11 +1,11 @@
|
||||||
Open:
|
Open:
|
||||||
- Refactor input generics to distinct local/full types
|
|
||||||
- Multiplayer loopback server
|
- Multiplayer loopback server
|
||||||
- Insecured websocket server implementation
|
- Insecured websocket server implementation
|
||||||
- Cloneable RNG that goes in state (use MurmurHash3 finalizer in counter mode?)
|
- Cloneable RNG that goes in state (use MurmurHash3 finalizer in counter mode?)
|
||||||
- remove all random() calls
|
- remove all random() calls
|
||||||
|
|
||||||
Done:
|
Done:
|
||||||
|
- Refactor input generics to distinct local/full types
|
||||||
- Refactor input messages for more than one player
|
- Refactor input messages for more than one player
|
||||||
- Rework State implementation for easier cloning/deserialization
|
- Rework State implementation for easier cloning/deserialization
|
||||||
- Test Lockstep/rollback
|
- Test Lockstep/rollback
|
||||||
|
|
|
@ -1,28 +1,28 @@
|
||||||
|
|
||||||
export const INPUT_FREQUENCY = 33; // roughly 30fps
|
export const INPUT_FREQUENCY = 33; // roughly 30fps
|
||||||
|
|
||||||
export interface LockstepProcessor<Input, State> {
|
export interface LockstepProcessor<LocalInput, GlobalInput, State> {
|
||||||
compareInput(a: Input[], b: Input[]): boolean;
|
compareInput(a: GlobalInput, b: GlobalInput): boolean;
|
||||||
predictInput(prev: Input[] | null, localPlayer: number, localInput: Input): Input[];
|
predictInput(prev: GlobalInput | null, localPlayer: number, localInput: LocalInput): GlobalInput;
|
||||||
cloneState(source: State): State;
|
cloneState(source: State): State;
|
||||||
advanceState(state: State, input: Input[]): void;
|
advanceState(state: State, input: GlobalInput): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LockstepState<Input, State> {
|
export class LockstepState<LocalInput, GlobalInput, State> {
|
||||||
|
|
||||||
private inputIndex = -1;
|
private inputIndex = -1;
|
||||||
private inputLog: Input[][] = [];
|
private inputLog: GlobalInput[] = [];
|
||||||
private canonIndex = -1;
|
private canonIndex = -1;
|
||||||
private canonState: State;
|
private canonState: State;
|
||||||
private renderIndex = -1;
|
private renderIndex = -1;
|
||||||
private renderState: State;
|
private renderState: State;
|
||||||
|
|
||||||
constructor(private initialState: State, private engine: LockstepProcessor<Input, State>) {
|
constructor(private initialState: State, private engine: LockstepProcessor<LocalInput, GlobalInput, State>) {
|
||||||
this.canonState = engine.cloneState(initialState);
|
this.canonState = engine.cloneState(initialState);
|
||||||
this.renderState = engine.cloneState(initialState);
|
this.renderState = engine.cloneState(initialState);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addCanonInput(input: Input[]): void {
|
public addCanonInput(input: GlobalInput): void {
|
||||||
this.canonIndex++;
|
this.canonIndex++;
|
||||||
|
|
||||||
// advance canonical game state
|
// advance canonical game state
|
||||||
|
@ -39,7 +39,7 @@ export class LockstepState<Input, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Warning: this only supports one player input per frame. There is no support for two players using the same lockstep simulation instance. */
|
/** 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: Input): void {
|
public addLocalInput(player: number, input: LocalInput): void {
|
||||||
this.inputIndex++;
|
this.inputIndex++;
|
||||||
|
|
||||||
// ensure that we don't overwrite the canon input with local input somehow
|
// ensure that we don't overwrite the canon input with local input somehow
|
||||||
|
@ -66,13 +66,13 @@ export class LockstepState<Input, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Playback<Input, State> {
|
export class Playback<LocalInput, GlobalInput, State> {
|
||||||
private frame = 0;
|
private frame = 0;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private state: State,
|
private state: State,
|
||||||
private inputLog: Input[][],
|
private inputLog: GlobalInput[],
|
||||||
private engine: LockstepProcessor<Input, State>,
|
private engine: LockstepProcessor<LocalInput, GlobalInput, State>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public getNextState(): State {
|
public getNextState(): State {
|
||||||
|
|
|
@ -176,7 +176,7 @@ export class PlayerControl extends Component<PlayerControl> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Engine implements LockstepProcessor<KeyName[], Data> {
|
export class Engine implements LockstepProcessor<KeyName[], KeyName[][], Data> {
|
||||||
cloneState(old: Data) {
|
cloneState(old: Data) {
|
||||||
return new Data(old);
|
return new Data(old);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { Loopback } from "../Net/LoopbackServer";
|
||||||
import { Data, Engine, PlayerControl } from "./GameComponents";
|
import { Data, Engine, PlayerControl } from "./GameComponents";
|
||||||
import { Buttons } from "./Input";
|
import { Buttons } from "./Input";
|
||||||
|
|
||||||
export class Main extends LockstepClient<KeyName[], Data> {
|
export class Main extends LockstepClient<KeyName[], KeyName[][], Data> {
|
||||||
|
|
||||||
buttons = new Buttons();
|
buttons = new Buttons();
|
||||||
|
|
||||||
|
|
|
@ -35,29 +35,29 @@ export const enum MessageTypes {
|
||||||
|
|
||||||
export type Packet<TypeId, Payload> = { t: TypeId } & Payload;
|
export type Packet<TypeId, Payload> = { t: TypeId } & Payload;
|
||||||
|
|
||||||
export type ClientMessage<Input, State> =
|
export type ClientMessage<LocalInput, State> =
|
||||||
| Packet<MessageTypes.SET_STATE, { s: Partial<State> }>
|
| Packet<MessageTypes.SET_STATE, { s: Partial<State> }>
|
||||||
| Packet<MessageTypes.INPUT, { i: Input }>
|
| Packet<MessageTypes.INPUT, { i: LocalInput }>
|
||||||
| Packet<MessageTypes.GET_STATE, { c: number, s: State }>
|
| Packet<MessageTypes.GET_STATE, { c: number, s: State }>
|
||||||
| Packet<MessageTypes.PING, { z: number }>
|
| Packet<MessageTypes.PING, { z: number }>
|
||||||
;
|
;
|
||||||
|
|
||||||
export type ServerMessage<Input, State> =
|
export type ServerMessage<GlobalInput, State> =
|
||||||
| Packet<MessageTypes.SET_STATE, { u: number, s: Partial<State> }>
|
| Packet<MessageTypes.SET_STATE, { u: number, s: Partial<State> }>
|
||||||
| Packet<MessageTypes.INPUT, { i: Input[] }>
|
| Packet<MessageTypes.INPUT, { i: GlobalInput }>
|
||||||
| Packet<MessageTypes.GET_STATE, { c: number }>
|
| Packet<MessageTypes.GET_STATE, { c: number }>
|
||||||
| Packet<MessageTypes.PING, { z: number }>
|
| Packet<MessageTypes.PING, { z: number }>
|
||||||
;
|
;
|
||||||
|
|
||||||
export type Server<Input, State> = Callbag<ClientMessage<Input, State>, ServerMessage<Input, State>>;
|
export type Server<LocalInput, GlobalInput, State> = Callbag<ClientMessage<LocalInput, State>, ServerMessage<GlobalInput, State>>;
|
||||||
|
|
||||||
export abstract class LockstepClient<Input, State> {
|
export abstract class LockstepClient<LocalInput, GlobalInput, State> {
|
||||||
|
|
||||||
private playerNumber = -1;
|
private playerNumber = -1;
|
||||||
private state: LockstepState<Input, State>;
|
private state: LockstepState<LocalInput, GlobalInput, State>;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly engine: LockstepProcessor<Input, State>,
|
public readonly engine: LockstepProcessor<LocalInput, GlobalInput, State>,
|
||||||
) {
|
) {
|
||||||
const initialState = this.initState({});
|
const initialState = this.initState({});
|
||||||
this.state = new LockstepState(initialState, engine);
|
this.state = new LockstepState(initialState, engine);
|
||||||
|
@ -65,13 +65,13 @@ export abstract class LockstepClient<Input, State> {
|
||||||
|
|
||||||
public abstract initState(init: Partial<State>): State;
|
public abstract initState(init: Partial<State>): State;
|
||||||
|
|
||||||
public abstract gatherInput(): Input;
|
public abstract gatherInput(): LocalInput;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to a [perhaps emulated] server and return a disconnect callback
|
* Connect to a [perhaps emulated] server and return a disconnect callback
|
||||||
*/
|
*/
|
||||||
public connect(server: Server<Input, State>): () => void {
|
public connect(server: Server<LocalInput, GlobalInput, State>): () => void {
|
||||||
let serverTalkback: Server<Input, State> | null = null;
|
let serverTalkback: Server<LocalInput, GlobalInput, State> | null = null;
|
||||||
|
|
||||||
const sampleInput = () => {
|
const sampleInput = () => {
|
||||||
if (serverTalkback) {
|
if (serverTalkback) {
|
||||||
|
@ -85,15 +85,15 @@ export abstract class LockstepClient<Input, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
// connect to server
|
// connect to server
|
||||||
server(0, (mode: number, data: Server<Input, State> | ServerMessage<Input, State>) => {
|
server(0, (mode: number, data: Server<LocalInput, GlobalInput, State> | ServerMessage<GlobalInput, State>) => {
|
||||||
if (mode == 0) {
|
if (mode == 0) {
|
||||||
serverTalkback = data as Server<Input, State>;
|
serverTalkback = data as Server<LocalInput, GlobalInput, State>;
|
||||||
|
|
||||||
// kickoff input sender
|
// kickoff input sender
|
||||||
setTimeout(sampleInput, INPUT_FREQUENCY);
|
setTimeout(sampleInput, INPUT_FREQUENCY);
|
||||||
} else if (mode == 1) {
|
} else if (mode == 1) {
|
||||||
// server message
|
// server message
|
||||||
const message = data as ServerMessage<Input, State>;
|
const message = data as ServerMessage<GlobalInput, State>;
|
||||||
switch (message.t) {
|
switch (message.t) {
|
||||||
case MessageTypes.SET_STATE:
|
case MessageTypes.SET_STATE:
|
||||||
const resetState = this.initState(message.s);
|
const resetState = this.initState(message.s);
|
||||||
|
|
|
@ -2,17 +2,18 @@ import { Callbag } from "callbag";
|
||||||
|
|
||||||
import { ClientMessage, MessageTypes, ServerMessage } from "./LockstepClient";
|
import { ClientMessage, MessageTypes, ServerMessage } from "./LockstepClient";
|
||||||
|
|
||||||
type Client<Input, State> = Callbag<ServerMessage<Input, State>, ClientMessage<Input, State>>;
|
type Client<LocalInput, State> = Callbag<ServerMessage<LocalInput[], State>, ClientMessage<LocalInput, State>>;
|
||||||
|
|
||||||
export function Loopback<Input, State>(start: number, data?: Client<Input, State> | ClientMessage<Input, State>) {
|
/** Stub loopback server that handles a single client, for schemes where GlobalInput = LocalInput[] */
|
||||||
|
export function Loopback<LocalInput, State>(start: number, data?: Client<LocalInput, State> | ClientMessage<LocalInput, State>) {
|
||||||
if(start != 0) return;
|
if(start != 0) return;
|
||||||
|
|
||||||
const sink = data as Client<Input, State>;
|
const sink = data as Client<LocalInput, State>;
|
||||||
|
|
||||||
sink(0, (type: number, data?: Client<Input, State> | ClientMessage<Input, State>) => {
|
sink(0, (type: number, data?: Client<LocalInput, State> | ClientMessage<LocalInput, State>) => {
|
||||||
if(type == 1) {
|
if(type == 1) {
|
||||||
// message from client; just reflect for now
|
// message from client; just reflect for now
|
||||||
const message = data as ClientMessage<Input, State>;
|
const message = data as ClientMessage<LocalInput, State>;
|
||||||
switch(message.t) {
|
switch(message.t) {
|
||||||
case MessageTypes.INPUT:
|
case MessageTypes.INPUT:
|
||||||
sink(1, {
|
sink(1, {
|
||||||
|
|
Loading…
Reference in a new issue