elton-okawa / ts-deterministic-lockstep-study

1 stars 0 forks source link

Allow late join #42

Open GTASektor opened 1 year ago

GTASektor commented 1 year ago

I would like to ask you to write an example in which only one room is created without using the Start button, that is, without waiting for the host

You just need to synchronize the states of the connected players and have each new connected player immediately added to scene

You have already implemented onRemove, you need to add onAdd

elton-okawa commented 1 year ago

Hi, @GTASektor thanks for the suggestion!

Unfortunately I'm not planning to keep making changes for now, I created this repository to study some netcode concepts and then moved on.

But, I can give you an overall idea of how to implement it.

  1. First you'll need to also simulate the physics world on the server based on received client inputs (only clients are simulating and server is basically relaying inputs)
  2. Second, on client connection, the server would send the latest world snapshot and current frame
  3. Finally, the client would apply received snapshot and frame and start simulating.
GTASektor commented 1 year ago

Hi, thanks for the reply

I don't understand at all how in your example to synchronize frames from the servers given the delay, the client input is constantly ahead of the server input

elton-okawa commented 1 year ago

I'll try to explain over a simplified example of the server and two clients A and B

Considering the point of view of Client A, its inputs are locally simulated right away in order to avoid input lag but due to network delay, Client A does not have inputs from Client B at the current frame in order to simulate the local world correctly (let's name it frame X) , so Client A assumes that Client B inputs didn't change from the last one received from the server (let's name it frame Y) and locally simulates the world predicting inputs from Client B from frame Y until current frame X. Notice that frame Y < frame X

Some frames later (let's call frame Z), Client A receives Client B inputs from frame Y+1, two things can happen at this moment:

  1. Predicted input from Client B matches, so simulation at frame Y+1 were correctly predicted, nothing to be done here
  2. Predicted input from Client B does not match, so Client A needs to restore the world at frame Y+1 and re-simulate the world using its inputs and Client B inputs at each frame until current frame Z (notice that we have original Client B input of frame Y+1, but other inputs until frame Z are predicted again)

(In the readme video with 200ms ping, it's easy to notice [2] happening because the other player warps from one point to other)

This process keeps going on.

I know that it's a bit tough to understand, if you have any more questions I'll be happy to answer them. It's worth mention that this doc have a more in depth explanation

GTASektor commented 1 year ago

My main problem is that the second client that connects to the server must receive frames from the server and the current snapshot

Let's take an example: some client is already connected and 2000 frames have passed on the server, then the next player connects, I send server frames and start the timer taking into account half the delay, but even in this case, the frames of the second client are ahead of the server frames, that is, it is very difficult for me to understand how to synchronize the frames so that they tick with a lag of 3 frames from the server frames for subsequent connections

Btw, I did a simulation on the server and it actually does not differ from the simulation on the client

Client: `public update(): void { this.timeSinceLastUpdate += Date.now() - this.lastUpdate;

    const delta = FIXED_DELTA;

    while (this.timeSinceLastUpdate >= delta) {
        this.timeSinceLastUpdate -= delta;

        this.simulateGameplayFrame(this.currentFrame);

        this.currentFrame += 1;
    }
    this.lastUpdate = Date.now();

    this.updateTimeout = setTimeout(() => this.update(), 1000 / FPS);
}

public startGame(): void {
    this.room.onMessage('connected', () => {
        this.inputManager = new InputManager(this.currentState.owner, ROLLBACK_WINDOW);
        this.inputManager.addPlayer(this.room.sessionId);

        const startInMs = 1000 - this.ping.ping / 2;
        this.lastUpdate = Date.now() + startInMs;
        this.updateTimeout = setTimeout(() => this.update(), startInMs);
        this.currentFrame = this.currentState.frame;
        this.currentInput = {
            frame: this.currentFrame,
            w: false,
            a: false,
            s: false,
            d: false,
            space: false,
        };

        document.addEventListener('keydown', (event: KeyboardEvent) => this.handleKey(event.key, true));
        document.addEventListener('keyup', (event: KeyboardEvent) => this.handleKey(event.key, false));

        this.currentState.players.forEach(player => {
            player.inputBuffer.inputs.forEach(inputSchema => {
                inputSchema.onChange = (changes: any[]) => {
                    this.inputManager.confirmInput(inputSchema.frame, player.id, inputSchema);
                };
            });
        });
    });
}`

server:
`setTimeout(() => {
    client.send("connected", this.state.frame);
}, 1000);`
GTASektor commented 1 year ago

If you had the opportunity to write a small example of frame synchronization of an already running simulation, I would be very grateful, we do not touch on the topic of restoring snapshots, because it is quite simple and I have already implemented it

I run two simulations one on the server and one on the client, the server simulation exactly replicates all client actions based on input and frames

elton-okawa commented 1 year ago

The main idea would be all clients and server roughly simulation the same frame, the actual code does this by approximately start simulating at the same time.

In order to late join and sync correctly, I'll try to use the code snippet you've sent as example, I didn't test it out:

Client Main changes * Receive `{ frame, state }` from server on connection * Set `this.currentFrame = frame` * Restore `state` * Start simulating right away, taking into account the half ping in order to know how much time elapsed since the server sent the message and set as the `this.lastUpdate` time * Note that in the snippet I used a hardcoded `50ms` value instead of `this.ping.ping / 2` as workaround because the ping value is a mean value of several messages exchanged with the server, right upon connection, we do not have enough info about it * The idea of line `this.lastUpdate = Date.now() - timeSinceLastServerUpdateMs;` is to catch up the current server frame. E.g. connected at time `100ms` but due to network delay, client receives at time `166ms`, so the received state is "2 frames old". In order to simulate at the same frame, the client needs to simulate those frames to catch up. ``` public update(): void { // no changes } public startGame(): void { this.room.onMessage('connected', ({ frame, state }) => { // receive state and frame from server this.inputManager = new InputManager(this.currentState.owner, ROLLBACK_WINDOW); this.inputManager.addPlayer(this.room.sessionId); const timeSinceLastServerUpdateMs = 50; // in place of 50 we should use this.ping.ping / 2 this.lastUpdate = Date.now() - timeSinceLastServerUpdateMs; // restore(state); // restore world state this.currentFrame = frame; // set received frame this.currentInput = { frame: this.currentFrame, w: false, a: false, s: false, d: false, space: false, }; document.addEventListener('keydown', (event: KeyboardEvent) => this.handleKey(event.key, true)); document.addEventListener('keyup', (event: KeyboardEvent) => this.handleKey(event.key, false)); this.currentState.players.forEach(player => { player.inputBuffer.inputs.forEach(inputSchema => { inputSchema.onChange = (changes: any[]) => { this.inputManager.confirmInput(inputSchema.frame, player.id, inputSchema); }; }); }); this.updateTimeout = setTimeout(() => this.update(), 0); // start immediately }); } ```
Server Main changes: * Send current frame * Send physics world state ``` client.send("connected", { frame: this.state.frame, state: }) ```
GTASektor commented 1 year ago

Unfortunately, this frame synchronization code does not work

elton-okawa commented 1 year ago

That's unfortunate :cry: , there might be some cases that I'm not aware of

If you are planning to use this implementation in an actual game, I wouldn't recommend to do it because I created this repository only to study some network concepts, the code here is far from being suitable for a production game.

I suggest you to take a look at netcode implementations backed by a big community or by a company, here some that I know: