October Project: Going backward and forward on backward and forward
Posted by Jeff Disher
October Project: Going backward and forward on backward and forward
While trying to figure out how to handle timed actions (like mining a block: You start at one point and finish later, or crafting, as it will work similarly), I came up with an idea to allow an entity change to schedule a later follow-up change (as opposed to the usual "on the next tick" changes).

This, however, complicated the client-server state eventual consistency design since there was no way to describe this follow-up change in a way which could be rationalized without creating yet another mechanism to track these smaller operations (so that the client would know that they passed, failed, or were even attempted). Further, this created a problem where the existing "reverse change stack" approach for the client's speculative state projection couldn't be easily maintained as these delayed changes don't have the more obvious dependent-operation clustering the rest of the system was using. Clearly, this was either going to require a lot of complexity and ugly special cases or a re-thinking of the approach.

I realized that part of this problem's complexity was due to that same "reverse change stack" design. The original reason for it was to make it easy for the client to keep 1 speculative interpretation of the game state which it could easily reverse to the server state in order to apply new authoritative changes before re-applying its own changes which weren't captured in that update. Originally, this seemed simple and elegant: If every change can construct a reverse change, then any sequence of changes could be run forward to create the client's state or backward to create the server's state.

However, this become increasingly complicated when changes needed to schedule their own immediate follow-up changes and substantially complicated some changes so that they knew how to reverse themselves (since they could never destroy information). Needing to add this delayed follow-up would compound this complexity, substantially. Then, I got to wondering if this approach makes any sense, to begin with, and decided to change direction.

Instead, I am going to try replacing this with a client-side projection which only goes in one direction and just keeps 2 copies of the state: The client's projection and server's authoritative state. With some refactoring, it should also be possible to directly use the server's logic to implement both of these. This way, client-side changes can be directly applied to its own copy and input from the server can update that copy from which a new client-side state can be derived (discarding the previous client state). This also further leverages the immutable parallel design to avoid duplication and theoretically run the client's projection updates in parallel (probably not needed).

Tracking the delayed follow-up changes will still require some special tracking but they can be more easily identified and managed, as they will no longer be a special-case in the middle of the reverse-change logic.

If this works correctly, I should be able to design a solution which will never have the Minecraft-esque "blocks reappearing" problem when the server is under heavy load. You may be very out-of-sync, but everything should still seem internally consistent (only suddenly shifting to account for actual conflicts - rarely the problem).

I won't be fully confident that this approach is working until I have these 2 components working well enough that they can be integrated to demonstrate non-trivial entity interactions on a shared world.

The work goes on,
Jeff.