feat: latest input tick#624
Conversation
|
I see CI failures but have to run now :) Will fix later. |
|
On first read, this PR looks similar to #518, with the idea being to track ticks that are not expected to change anymore, and use that for things that would be jarring to undo ( HP display, score counters, kill mechanics, etc. ). The difference is that this PR also syncs this tick across peers, so it works without input broadcast + actually uses the ground truth, which is what the server says is confirmed. Plus the new primitive. I think it could be nice to try and unify both features, so we get better behavior without increasing API surface by much. In general I try to avoid As for CI, you can run |
e220750 to
b8f7c0b
Compare
|
Could you clarify a bit more how you would like to unify the contract? In #518 the main surface is The reasoning is that knowing latest inputs for a particular node is not enough to confirm the state won't change. Given the example above, we cannot render HP of a player unless we know inputs of other players. The only unification I can see is if the surface already exposed in 518 starts using the global state exposed in this PR under the hood to ensure correctness. I don't see how we can reuse that surface for a global state though since it's already node-based and this has to be global. This is a good point! Refactored the change so that each peer that owns state calculates its own latest complete tick and shares with everyone. Then each peer calculates the combined latest input complete tick. It's a bit more complex now though but will work even if state authority is split between peers. Fixed! |
b8f7c0b to
0e00527
Compare
|
With 518 it's all about flexibility. You don't need input broadcast. If it's off the server still has that information and can delay till a confirm via state properties. The reason it's node based is so you can infer relevance. For example you might only care about players within a certain distance and can act early without waiting for all inputs to arrive. I also didn't want to impose extra data transfer. |
NetworkSynchronizationServer now tracks latest tick for which the server received all inputs. Effectively, it means this tick cannot change due to new inputs coming from the past anymore.
Problem
With real world latency server receives inputs from peers from the past. When server records new ticks, it doesn't yet know all inputs that may've affected the state but it still sends potentially incorrect state to all peers (which is ok). Later, server receives late inputs, re-records corrected state, and sends new snapshots to all peers.
From the perspective of a peer under latency that predicted a state change (such as reduced HP):
So if HP is displayed as per the latest server state, it will jitter. Moreover, simply ignoring local prediction is not enough either. The same situation can happen with the server itself if it renders state for a tick while some inputs for that tick have not arrived yet. For example, host makes a melee swing at another player -> that player made a perfect block -> server receives late input and has to revert the damage it done.
Solution
Track latest tick for which all inputs have been collected and broadcast it to all peers so they can adjust their local display states.
This also adds a small primitive for displaying states and avoid jitter (at the cost of latency). Use like this:
A second primitive to display local effects based on rollbackable state coming later. This will avoid having to undo the effect when it makes it worse. Such as displaying a wrong damage number before it is "confirmed".
Note: my initial take was to add one byte to tick snapshots to mark them as input complete but that would mean we'd have to send "empty" snapshots and in general complicate the general snapshot logic. Seems like a separate channel for this information is less complex
Another note: this solution is technically not bullet proof and could still result in desync because the input complete confirmation may arrive earlier than the snapshot itself. Though it makes this situation unlikely enough to still make it useful for display effects and maybe some gameplay logic. I was not able to simulate such a condition but it is theoretically possible.