I spent an entire day on Sunday thinking about the architecture on how to write a generic system to create, destroy, serialize, and control scope over networked objects in C++. It’s a very hard problem to solve because you know nothing about the game objects themselves, or what complexities the end-users might introduce with their specific architecture.
In the end I decided on 3 classes. One class, ReplicaManager2, is the base plugin for the system. All it does is essentially serialization, deserialization, transmission, and bookkeeping. Stuff you wouldn’t normally care about. I have the user pass a class factory that generates connection instances, one per connection. This controls high-level and non-object specific functionality such as how to create object instances. Lastly, there is the base class Replica2, which game objects should derive from.
The main reason I did it this way, rather than putting everything in the ReplicaManager2 plugin (as I did in its predecessor) was so that the functionality can be easily overridden. This should solve a problem I had with the old system, where I ended up passing a ton of data to the callbacks in order to force it to get it to do what I wanted. By assuming users will override functions, this also gives me the ability to make assumptions and thus much more elaborate functions.
Improvements it has over the old system are:
1. Relay across systems actually works correctly. So you could have a server cluster, with n clients on each server, and everything would just get sent out properly. It automatically deals with cyclical connection graphs up to a depth of 1 away from the source node.
2. Defaults to polling for changes. Each tick, if an object is now constructable, or in scope, and it was not in the last tick (or vice-versa) the object is created or destroyed appropriately. The object is also serialized every n milliseconds, and if it serializes differently under the same context, the newly serialized value is transmitted. This is easier to use than event-based updates (which are also supported) so the user has to write less code.
3. Downloads construct all objects first, then deserializes them, and in the order they were originally created. This is likely what a user would do were they to hand-write the download system, and maximizes the chance that pointers and other dependencies will be valid on deserialization. This also allows you to have objects that cross-reference each other when deserializing, each both have already been created anyway.
4. It’s easier to modify events and more information is given about what triggered those events. For example, it’s now trivial to send using different reliability types based on the context of what is being sent, something that was hard to do before.
5. Changes are easier to initiate on the client, especially construction, which before would have taken custom game code.
Here’s a video that goes over the system. I start out too fast because I was tired of starting over when I made mistakes, but later on it gets better.