I defined packets for creating and removing objects on the client machine, and for updating properties of those objects. Here, the object system I designed a few months ago really helps, because public properties all have known types (long, double, quaternion, string, object reference, etc), and objects are all created from a known template.
Thus, the packets I need are:
- create object
- template name
- object id
- start object
- object id
- remove object
- object id
- update property of type X
- object id
- component index
- property index
- property value
Because all objects are created from the same templates, the component index, and the property index within the components, will be the same, which simplifies addressing. I assume I won't have more than 255 components per object, or more than 255 properties per component, so I use a byte for each.
The "start object" message is there to make sure that I don't start an object that's half configured. The way object start-up actually works is:
1. Send object creation, including object template.
2. For each component.
2.1 For each property.
2.1.1 If the property differs from the template.
18.104.22.168 Send a property update.
2.1.2 Subscribe to property updates for future sends.
3. Send object start.
When a property updates after being started, a dirty flag is set, and when time comes to update object state, dirty properties are queued for sending. I prioritize property updates such that closer objects get higher priority. I also use a send queue with replacement capability, so if property X updates while it's already queued, I replace that piece of data in the queue with the newer data. This means I can also remove pending updates for objects as those objects are being removed -- no need sending network updates for things that die.
The neat thing is: Pretty much all of this code used infrastructure I already had written for the editor and composable object model!
I haven't debugged it yet, but looking at the code, it's quite simple. There are, however, some inefficiencies that I might want to address later (if profiling shows I need to):
- The code that determines whether a property has changed from the template or not is inefficient. This affects CPU cost when starting up a new object, or when a new player connects. Bad N-squared cost.
- Each player connection subscribes to all properties of all objects. This causes lots of allocations (one per subscription). A system of generation/change counts would probably be more efficient, to the point where each player connection stores the latest generation seen, and each property stores the latest generation updated.
- Each updated property is prefixed with the object id. Some bytes could possibly be saved by grouping updates per object, then per component. This is mainly a network usage issue, and I think it'll be OK as is.
- Properties are sent as generic pieces of data -- "a double" or "an int". If I had more information about the properties, I could marshal using less network data -- say, compressing floats to a byte when I know they are in the range [0..1] with precision 0.01.
Despite this, I'm pretty psyched! This part really looks straightforward, once all the infrastructure was there (components, properties, subscriptions, etc). Now I need to define some more interesting object kinds than "rock" -- like, for example, "player" :-)