diff --git a/plan.txt b/plan.txt
index eebd8b1..0c202c7 100644
--- a/plan.txt
+++ b/plan.txt
@@ -1,10 +1,12 @@
Open:
-- Refactor input messages for more than one player
+- 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 messages for more than one player
- Rework State implementation for easier cloning/deserialization
- Test Lockstep/rollback
- Smarter typings for Join/Lookup functions
diff --git a/src/Ecs/Lockstep.ts b/src/Ecs/Lockstep.ts
index cb959d6..711b76b 100644
--- a/src/Ecs/Lockstep.ts
+++ b/src/Ecs/Lockstep.ts
@@ -2,15 +2,16 @@
export const INPUT_FREQUENCY = 33; // roughly 30fps
export interface LockstepProcessor {
- compareInput(a: Input, b: Input): boolean;
+ compareInput(a: Input[], b: Input[]): boolean;
+ predictInput(prev: Input[] | null, localPlayer: number, localInput: Input): Input[];
cloneState(source: State): State;
- advanceState(state: State, input: Input): void;
+ advanceState(state: State, input: Input[]): void;
}
export class LockstepState {
private inputIndex = -1;
- private inputLog: Input[] = [];
+ private inputLog: Input[][] = [];
private canonIndex = -1;
private canonState: State;
private renderIndex = -1;
@@ -21,7 +22,7 @@ export class LockstepState {
this.renderState = engine.cloneState(initialState);
}
- public addCanonInput(input: Input): void {
+ public addCanonInput(input: Input[]): void {
this.canonIndex++;
// advance canonical game state
@@ -37,13 +38,14 @@ export class LockstepState {
this.inputLog[this.canonIndex] = input;
}
- public addLocalInput(input: Input): void {
+ /** 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 {
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;
+ this.inputLog[this.inputIndex] = this.engine.predictInput(this.inputLog[this.inputIndex - 1] ?? null, player, input);
}
}
@@ -69,7 +71,7 @@ export class Playback {
public constructor(
private state: State,
- private inputLog: Input[],
+ private inputLog: Input[][],
private engine: LockstepProcessor,
) {}
diff --git a/src/Game/GameComponents.ts b/src/Game/GameComponents.ts
index b6460ad..a289019 100644
--- a/src/Game/GameComponents.ts
+++ b/src/Game/GameComponents.ts
@@ -23,7 +23,7 @@ export class World {
phase = GamePhase.TITLE;
score = 0;
- constructor() {}
+ constructor() { }
/*
* Drawing Layers
@@ -50,9 +50,9 @@ export class World {
const score = `Score: ${this.score}`;
cx.fillStyle = "#000";
- cx.fillText(score, this.width/3 + 1, this.height - 18 + 1, this.width/4);
+ cx.fillText(score, this.width / 3 + 1, this.height - 18 + 1, this.width / 4);
cx.fillStyle = "#0ff";
- cx.fillText(score, this.width/3, this.height - 18, this.width/4);
+ cx.fillText(score, this.width / 3, this.height - 18, this.width / 4);
}));
}
}
@@ -71,6 +71,7 @@ export class Data extends EcsData implements StateForSchema {
hp: Store;
lifetime: Store;
message: Store;
+ playerControl: Store;
// globals
debugLayer = new Layer(2);
@@ -82,6 +83,7 @@ export class Data extends EcsData implements StateForSchema {
this.hp = copySparse(from.hp);
this.lifetime = copySparse(from.lifetime);
this.message = copySparse(from.message);
+ this.playerControl = copySparse(from.playerControl);
}
clone() {
@@ -124,7 +126,7 @@ export class Hp extends Component {
export class Lifetime extends Component {
time: number;
- constructor(from: Partial & {time: number}) {
+ constructor(from: Partial & { time: number }) {
super(from);
this.time = from.time;
}
@@ -163,35 +165,55 @@ export class Message extends Component {
}
}
+export class PlayerControl extends Component {
+ playerNumber: number;
+ constructor(from: Partial & {playerNumber: number}) {
+ super(from);
+ this.playerNumber = from.playerNumber;
+ }
+ clone(): PlayerControl {
+ return new PlayerControl(this);
+ }
+}
+
export class Engine implements LockstepProcessor {
cloneState(old: Data) {
return new Data(old);
}
- compareInput(a: KeyName[], b: KeyName[]): boolean {
- if (a.length != b.length) return false;
-
- let matches = true;
- a.forEach((keyA, i) => {
- if (keyA != b[i]) {
- matches = false;
- }
- });
-
- return matches;
+ predictInput(prev: KeyName[][] | null, localPlayer: number, localInput: KeyName[]): KeyName[][] {
+ return prev?.map((prevInput, player) => (player == localPlayer) ? localInput : prevInput) ?? [];
}
- advanceState(state: Data, input: KeyName[]) {
+ compareInput(a: KeyName[][], b: KeyName[][]): boolean {
+ if (a.length != b.length) return false;
+
+ for (let i = 0; i < a.length; i++) {
+ if (a[i].length != b[i].length) return false;
+ for (let j = 0; j < a[i].length; j++) {
+ if (a[i][j] != b[i][j]) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ advanceState(state: Data, input: KeyName[][]) {
DumbMotion(state, INPUT_FREQUENCY);
- Join(state, "location").forEach(([location]) => {
- let dir = 0;
- if(input.indexOf("left") != -1) {
- dir -= 1;
+ Join(state, "playerControl", "location").forEach(([player, location]) => {
+ const playerInput = input[player.playerNumber];
+ if(playerInput) {
+ let dir = 0;
+ if (playerInput.indexOf("left") != -1) {
+ dir -= 1;
+ }
+ if (playerInput.indexOf("right") != -1) {
+ dir += 1;
+ }
+ location.VAngle = dir * 0.01;
}
- if(input.indexOf("right") != -1) {
- dir += 1;
- }
- location.VAngle = dir * 0.01;
});
}
}
diff --git a/src/Game/Main.ts b/src/Game/Main.ts
index 7e477af..28fb918 100644
--- a/src/Game/Main.ts
+++ b/src/Game/Main.ts
@@ -8,7 +8,7 @@ import { Create } from "../Ecs/Data";
import { RunRenderBounds } from "../Ecs/Renderers";
import { LockstepClient } from "../Net/LockstepClient";
import { Loopback } from "../Net/LoopbackServer";
-import { Data, Engine } from "./GameComponents";
+import { Data, Engine, PlayerControl } from "./GameComponents";
import { Buttons } from "./Input";
export class Main extends LockstepClient {
@@ -43,8 +43,11 @@ export class Main extends LockstepClient {
const newState = new Data(patch);
Create(newState, {
+ playerControl: new PlayerControl({
+ playerNumber: 0
+ }),
location: new Location({
- X: 200,
+ X: 100,
Y: 200,
}),
bounds: new PolygonComponent({points: [-30, 0, 30, 0, 0, 40]}),
@@ -54,6 +57,21 @@ export class Main extends LockstepClient {
}),
});
+ Create(newState, {
+ playerControl: new PlayerControl({
+ playerNumber: 1
+ }),
+ location: new Location({
+ X: 400,
+ Y: 200,
+ }),
+ bounds: new PolygonComponent({points: [-30, 0, 30, 0, 0, 40]}),
+ renderBounds: new RenderBounds({
+ color: "#f0a",
+ layer: 0
+ }),
+ });
+
return newState;
}
diff --git a/src/Net/LockstepClient.ts b/src/Net/LockstepClient.ts
index ad22e97..43ea9df 100644
--- a/src/Net/LockstepClient.ts
+++ b/src/Net/LockstepClient.ts
@@ -43,8 +43,8 @@ export type ClientMessage =
;
export type ServerMessage =
- | Packet }>
- | Packet
+ | Packet }>
+ | Packet
| Packet
| Packet
;
@@ -53,6 +53,7 @@ export type Server = Callbag, ServerMe
export abstract class LockstepClient {
+ private playerNumber = -1;
private state: LockstepState;
public constructor(
@@ -75,8 +76,10 @@ export abstract class LockstepClient {
const sampleInput = () => {
if (serverTalkback) {
const input = this.gatherInput();
- this.state.addLocalInput(input);
- serverTalkback(1, { t: MessageTypes.INPUT, i: input });
+ if(this.playerNumber >= 0) {
+ this.state.addLocalInput(this.playerNumber, input);
+ serverTalkback(1, { t: MessageTypes.INPUT, i: input });
+ }
setTimeout(sampleInput, INPUT_FREQUENCY);
}
};
@@ -95,6 +98,7 @@ export abstract class LockstepClient {
case MessageTypes.SET_STATE:
const resetState = this.initState(message.s);
this.state = new LockstepState(resetState, this.engine);
+ this.playerNumber = message.u;
break;
case MessageTypes.INPUT:
this.state.addCanonInput(message.i);
diff --git a/src/Net/LoopbackServer.ts b/src/Net/LoopbackServer.ts
index 9e1deb1..323b858 100644
--- a/src/Net/LoopbackServer.ts
+++ b/src/Net/LoopbackServer.ts
@@ -15,10 +15,13 @@ export function Loopback(start: number, data?: Client;
switch(message.t) {
case MessageTypes.INPUT:
- sink(1, message);
+ sink(1, {
+ t: MessageTypes.INPUT,
+ i: [message.i],
+ });
break;
}
}
});
- sink(1, {t: MessageTypes.SET_STATE, s: {}});
+ sink(1, {t: MessageTypes.SET_STATE, u: 0, s: {}});
};