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:
|
||||
- Refactor input generics to distinct local/full types
|
||||
- Multiplayer loopback server
|
||||
- Insecured websocket server implementation
|
||||
- Cloneable RNG that goes in state (use MurmurHash3 finalizer in counter mode?)
|
||||
- remove all random() calls
|
||||
|
||||
Done:
|
||||
- Refactor input generics to distinct local/full types
|
||||
- Refactor input messages for more than one player
|
||||
- Rework State implementation for easier cloning/deserialization
|
||||
- Test Lockstep/rollback
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
|
||||
export const INPUT_FREQUENCY = 33; // roughly 30fps
|
||||
|
||||
export interface LockstepProcessor<Input, State> {
|
||||
compareInput(a: Input[], b: Input[]): boolean;
|
||||
predictInput(prev: Input[] | null, localPlayer: number, localInput: Input): Input[];
|
||||
export interface LockstepProcessor<LocalInput, GlobalInput, State> {
|
||||
compareInput(a: GlobalInput, b: GlobalInput): boolean;
|
||||
predictInput(prev: GlobalInput | null, localPlayer: number, localInput: LocalInput): GlobalInput;
|
||||
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 inputLog: Input[][] = [];
|
||||
private inputLog: GlobalInput[] = [];
|
||||
private canonIndex = -1;
|
||||
private canonState: State;
|
||||
private renderIndex = -1;
|
||||
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.renderState = engine.cloneState(initialState);
|
||||
}
|
||||
|
||||
public addCanonInput(input: Input[]): void {
|
||||
public addCanonInput(input: GlobalInput): void {
|
||||
this.canonIndex++;
|
||||
|
||||
// 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. */
|
||||
public addLocalInput(player: number, input: Input): void {
|
||||
public addLocalInput(player: number, input: LocalInput): void {
|
||||
this.inputIndex++;
|
||||
|
||||
// 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;
|
||||
|
||||
public constructor(
|
||||
private state: State,
|
||||
private inputLog: Input[][],
|
||||
private engine: LockstepProcessor<Input, State>,
|
||||
private inputLog: GlobalInput[],
|
||||
private engine: LockstepProcessor<LocalInput, GlobalInput, 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) {
|
||||
return new Data(old);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import { Loopback } from "../Net/LoopbackServer";
|
|||
import { Data, Engine, PlayerControl } from "./GameComponents";
|
||||
import { Buttons } from "./Input";
|
||||
|
||||
export class Main extends LockstepClient<KeyName[], Data> {
|
||||
export class Main extends LockstepClient<KeyName[], KeyName[][], Data> {
|
||||
|
||||
buttons = new Buttons();
|
||||
|
||||
|
|
|
@ -35,29 +35,29 @@ export const enum MessageTypes {
|
|||
|
||||
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.INPUT, { i: Input }>
|
||||
| Packet<MessageTypes.INPUT, { i: LocalInput }>
|
||||
| Packet<MessageTypes.GET_STATE, { c: number, s: State }>
|
||||
| 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.INPUT, { i: Input[] }>
|
||||
| Packet<MessageTypes.INPUT, { i: GlobalInput }>
|
||||
| Packet<MessageTypes.GET_STATE, { c: 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 state: LockstepState<Input, State>;
|
||||
private state: LockstepState<LocalInput, GlobalInput, State>;
|
||||
|
||||
public constructor(
|
||||
public readonly engine: LockstepProcessor<Input, State>,
|
||||
public readonly engine: LockstepProcessor<LocalInput, GlobalInput, State>,
|
||||
) {
|
||||
const initialState = this.initState({});
|
||||
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 gatherInput(): Input;
|
||||
public abstract gatherInput(): LocalInput;
|
||||
|
||||
/**
|
||||
* Connect to a [perhaps emulated] server and return a disconnect callback
|
||||
*/
|
||||
public connect(server: Server<Input, State>): () => void {
|
||||
let serverTalkback: Server<Input, State> | null = null;
|
||||
public connect(server: Server<LocalInput, GlobalInput, State>): () => void {
|
||||
let serverTalkback: Server<LocalInput, GlobalInput, State> | null = null;
|
||||
|
||||
const sampleInput = () => {
|
||||
if (serverTalkback) {
|
||||
|
@ -85,15 +85,15 @@ export abstract class LockstepClient<Input, State> {
|
|||
};
|
||||
|
||||
// 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) {
|
||||
serverTalkback = data as Server<Input, State>;
|
||||
serverTalkback = data as Server<LocalInput, GlobalInput, State>;
|
||||
|
||||
// kickoff input sender
|
||||
setTimeout(sampleInput, INPUT_FREQUENCY);
|
||||
} else if (mode == 1) {
|
||||
// server message
|
||||
const message = data as ServerMessage<Input, State>;
|
||||
const message = data as ServerMessage<GlobalInput, State>;
|
||||
switch (message.t) {
|
||||
case MessageTypes.SET_STATE:
|
||||
const resetState = this.initState(message.s);
|
||||
|
|
|
@ -2,17 +2,18 @@ import { Callbag } from "callbag";
|
|||
|
||||
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;
|
||||
|
||||
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) {
|
||||
// message from client; just reflect for now
|
||||
const message = data as ClientMessage<Input, State>;
|
||||
const message = data as ClientMessage<LocalInput, State>;
|
||||
switch(message.t) {
|
||||
case MessageTypes.INPUT:
|
||||
sink(1, {
|
||||
|
|
Loading…
Reference in a new issue