Add _rb_last_synced_tick to network-synchronization-server, to preven…#631
Add _rb_last_synced_tick to network-synchronization-server, to preven…#631jburdecki wants to merge 2 commits into
Conversation
…t sending state for already-simulated ticks to clients
|
I think this could lead to unexpected behavior because local simulation might rely on intermittent states being correct, not only the latest one. But the argument about not sending the same tick multiple times is valid and is also addressed in this PR: #618 |
…tations during rollback/resim events on nodes or games using diff states
|
Good find @aromancev 👍 Updated PR to also track the "last synced" snapshot, which covers rollback where intermittent states read/write data. |
|
Sorry, I meant it a bit differently. The current change basically never sends the same tick data over network. My point is that it's a more radical change than never sending the same data. It could matter in cases where local simulation relies on the previous ticks to simulate correctly. For example,
This increases the chances of desync between peers and host simulations, especially if peers rely a lot on predicted behavior such as projectiles. My point is that we could achieve similar bandwidth savings by remembering what was sent in each tick, so that unchanged fields and not sent again. That would still preserve correctness of the simulation on all peers. This is what is done in #618 already. |
|
Is it right to understand the concern here is for state that a game (netfox) doesn't replicate? If so I think this PR (#631) is okay as-is, because my understanding is that for all state that a Looking at the example through that lens:
Step 2. is where "local predicted simulations" diverge between host and peers with this PR. However, in my opinion, that's okay. This divergence of local (non-replicated) state is the result of what state a game chooses to replicate via netfox - and so the affected state in this example is not replicated. As a specific example, I would see this local state divergence to be expected/acceptable for "side-effects" that are dependent on frame specific triggers, like minor SFX, VFX, and lerps/tweens for UI elements, etc. For any "side-effects" that must not diverge, either those should be included/tracked via a |
|
However, maybe more to your point |
|
To #618, I like where it's pointing, as a means to reduce bandwidth - however the field deduplication in it doesn't cover the failure this PR is targeting. Eg: on #618, with a connection where "one-way-latency > |
|
Yeah, this counter point is valid if we assume that synced state cannot interact with non-synced state that is important for local prediction :) Here is a specific example. Two entities:
Syncing every projectile would consume a lot of bandwidth. There can be hundreds in flight at any point, and it is not necessary to sync their position every tick. At the same time, projectiles affect (predicted) gameplay state. They may push a character, reduce HP, break concentration, etc. It's not always better to do it but it is a choice a developer can make. I do it for some cases in my game. Now here is the interesting part. The position of the player at each tick is originally simulated and synced as follows: Effectively, standing still. And the position of the projectile is as follows: Effectively, flying in a straight line, missing the player. After receiving late Inputs, host resimulates the position of the player to the following: So the player was actually at a different position and only moved to position 0 later. So in this PR the server will sync the correct state for the player only from tick 4. This is fine for the player but not for the projectile. The projectile should have hit the player on tick 1, which it did on the host, but not on the client. The client never observed player in position 1 on tick 1 so the projectile missed it. The projectile will not correct itself because it is not synced. This will not lead to a permanently broken state but it will produce a short lived desyc. The probability of this happening will differ from game to game and will depend on the amount of predicted state and the frequency of interactions. With this change, we are forcing a weaker consistency model on all users. The problem we are trying to solve is to reduce the amount of traffic we're sending. My point is that we can get 90% there by not sending the exact same values for the exact same tick, but at least sending what was different after a resimulation for each tick. So there are two ways we can proceed. Either the option I'm suggesting is saving enough traffic and then we can avoid a weaker consistency model or it's not a at the very least we should make it an option so that developers can choose their tradeoff. I'm actually not sure I understand why resending state will ever extend beyond the natural latency of the peer. If latency is 150ms at 20 ticks per second and Input delay is 1 tick, you can expect to sync 3 - 1 = 2 extra ticks worth of state data every tick. If you diff it against previously sent state, it will amount to almost nothing. There is no reason for peers to fall behind even more just because they received state updates from the past. This is exactly what I onserved in my tests as well. Even before the recent fix of not using unreliable channels. If anything, CPU would be the bottleneck there, not network. |
|
The premise of only broadcasting the latest state from a rollback event, and as a result creating "a weaker consistency model", makes sense to me. That said, as I haven't contributed to netfox before and haven't joined architectural conversations, I don't have a strong statement on if it should be:
But I believe one of those should be available to developers. |
|
In the context of the "projectile, player" example, I'm interpreting the projectile as a gameplay node/entity (eg: static mesh), rather than an algo (eg: hitscan). My opinion here is that If a developer wishes for higher consistency/replication on projectiles then that data needs to be replicated, and any effects caused by that projectile on the host also need to be replicated. For instance during resim where the Projectile and the Player intersect at Pos 1, on tick 1,
or:
|
|
Rereading this thread, I think we're closer to agreement than it looks. To revisit some points:
|
|
If you actually observed perpetual resim increase up to history length, would be curious to see an example project. I don't see how it would happen but maybe I'm missing something. As for syncing projectiles or collision events, that's one way to do it, yeah. I prefer not to because that has its own trade-offs. So some optionality would be nice in any case. I think we should wait for Tamas anyway to make a visionary decision 😅 |
Closes #630
Adds
_NetworkSynchronizationServer::_rb_last_synced_tickas a way to gate sending state. Only new (unsimulated) ticks are sent. This prevents issue #630's root cause of increasingly deep rollbacks for clients with "one-way-latency" greater thaninput_delay.Video 1: with
input_delay = 0(fixed)https://github.com/user-attachments/assets/7422a282-9a43-4aaf-a84b-808a009fefd5
Video 2: with
input_delay = 6(fixed)https://github.com/user-attachments/assets/b32dcb15-37b4-4243-bb64-2c113fb59d13