Separating an object from its network representation

Started by
8 comments, last by ggs 19 years ago
I am experimenting with a game that I wrote. I realized that it is written very flexibly - every game I can think of that involves flying balls and players (multiplayer over tcp/ip with DP) can be represented pretty easily. So, I've decided to turn it into more of an engine, geared not so much towards my game (which kinda sucks) but towards other developers who might have better ideas for games based on pong. My design has developed quite nicely, with the exception of network transfer. The problem as I see it is to develop a general representation of game objects that developers can use as transparently as possible. For example: I currently have "Ball" objects. They have position and velocity, and to transfer them over the network I defined a packet type including those two values. Now: what if someone wants to make a "CrazyBall" that flies in a sine pattern? They need to send not only the position and velocity but also the period of the wave. How does the engine know to send the period? Thought 1: Create a "ITransferrable" interface that all objects to be sent over the network must implement. The interface is something like ISerializable, with a function that simply enumerates all the important data in an object. Then, the engine knows how to take that data and send it across the network, and on the other end of the connection the object has another function to accept that data and make changes to reflect it. The benefit of this method is that the developer hardly needs to do anything at all to make his object transferrable - the engine will pick up all of the ITransferrable objects and deal with them automatically. One downside is that it's kinda hard to program. Another is that a lot of extra type information and other overhead will have to be sent with the data to be sure that it can be properly decoded on the other end. Thought 2: Don't separate the network code from the objects quite so thoroughly. Instead, require developers to define a new type of packet for each new type of object they create. Then, the ITransferrable interface will no longer have data enumeration methods, but instead FillPacket and ReadPacket methods that fill that previously defined typoe of packet (or read from it). This has the benefit that it's easy for me (the engine designer) to program, and there won't be as much overhead in the transmission because the structure of the data will be hardcoded on both ends. The obvious downside is that the game developer has to know how to make his own packets, and has to deal with populating them when he wants information to be transmitted. Does anyone have any comments on these ideas, or ideas of their own? I appreciate it if anyone even read this far, and comments would be awesome ;) Thanks, Riley
--Riley
Advertisement
I'm using ISerialisable with a DataStream object. Each object serialises itself to and from the DataStream. When the object is created, it un/serialises all relevant data. For updates it only un/serialises certain fields it knows are important to clients.

It's a work in progress, and I haven't gotten to the stage where I can test it on anything usable, so it's all just theories.

Something like:

void SomeOject::SerialiseTo(DataStream& stream){    stream.WriteByte(mAge);    stream.WriteCString(mName);}void SomeOject::SerialiseFrom(DataStream& stream){    mAge = stream.ReadByte();    mName = stream.ReadCString();}


etc....

I have a Protocol that slaps on the UDP header (for reliability) and sends the raw data to the Socket. On the other side, the Protocol strips off the header, re-assembles the stream and ultimately hands it off to an object manager (in theory) that locates the object and invokes object.SerialiseUpdateFrom(stream).

Does that make sense or help at all? :)
---------------------http://www.stodge.net
Serializing an entire object is usually a bad idea, because even if you need to update only one or two pieces of data (hitpoints, position, current target, etc), you'd have to send the entire object over the network.

What I've used is breaking the view of your object into multiple, small views; let's call them facets. Each facet can marshal itself to binary, and demarshal on the receiving end. The networking layer keeps track of which facets were updated in what packets, and mark them as "known clean" when receiving an ack for a specific packet number. Modifying a facet marks the facet dirty again, either through manual programmer intervention (facet_.makeDirty()) or through overloaded operator goodness. You also have to remove the facet from the list of packet number->facet update acknowledgements pending, to avoid false positives on future acks.

Then, sending updates for an entity becomes as easy as iterating over the facets, and marshalling out data for the facets that are currently dirty, and remembering the packet number this facet update goes into for later acknowledgement processing. The message for updating an entity will then look something like {entity-id} {facet-bitmask} {facet-data ...}. Typically, you'll put entities in some priority queue, and stuff as many entity update messages as you can fit into a single network packet, to amortize the header overhead.

You could also have "facets" which are assumed known from start-up, such as initial hitpoints or the mesh filename to use; these come from template data for the entity, stored on disk. Only if this data actually changes will the facet system have to send updates.

This system is really quite flexible, because it can be used both for always-changing data (it'll simply always be dirty), and for seldom-changing data (it'll be sent when necessary). If you have some really large facets, and a high packet rate (higher than the round-trip-time), you might want to mark some facets as being re-sent only every N packets, to avoid blasting the large facet update two or three times across the network until you can receive acknowledgement for the first packet.

This mechanism is very similar in operation to the Quake III networking model as linked to by the Forum FAQ, by the way.


Now, given this infrastructure, special subclasses (such as CrazyBall) can define facets that are specific to the subclass. One such facet could be a simple pair of floats for the current phase and period of the sine wave. However, because the phase changes all of the time, it's probably better to make it a pair of the period of the sine wave, and the "base time" (time at which it passed zero phase); then you can easily derive the position based on current time in the CrazyBall object.
enum Bool { True, False, FileNotFound };
You may have missed part of my post:

"For updates it only un/serialises certain fields it knows are important to clients."

For example:

void SomeOject::SerialiseUpdateTo(DataStream& stream){    stream.WriteByte(mAge);}void SomeOject::SerialiseUpdateFrom(DataStream& stream){    mAge = stream.ReadByte();}
---------------------http://www.stodge.net
Thanks for the comments - I think this is a pretty interesting problem. I have been doing essentially what your solutions recommend, but it's always nice to hash things out with people.

I hadn't thought of sending a bitmask for extra flexibility - interesting idea. A downside is that each object is really responsible for knowing which members are dirty and when they've been sent (and acknowledged), etc, and one of my goals is to let the developers of those objects ignore as much of the networking aspect as possible. I wonder if there is a solution in which the developer can just publish a list of members that are important for each object that the networking framework can read and update automatically.

I have to do annoying school work for a while, but if I can take this idea any further I'll certainly post back here. And, of course, additional comments are more than welcome ;)

Thanks,
Riley
--Riley
Quote:each object is really responsible for knowing which members are dirty and when they've been sent (and acknowledged), etc


That's not good enough, because different connected players may have different acknowledgement schedules or loss. Thus, you need to keep state per object, per connected player.

In the system I described, I have a templated system where you wrap your interesting data members in a Facet<> template, and you derive your physical simualtion object from FacetUser, which will register with a FacetManager where each connection can allocate a FacetContext. When you change one of the data members of the Facet<>, it is marked as dirty in all contexts that currently see the object, and un-marked when an update has been sent and acknowledged. Through template magic, the appropriate marshalling is determined for each facet. The programmer doesn't need to worry terribly much about this, except by 1) not being stupid about how values are updated and 2) using the right infrastructure classes.

Other ways of doing it that are somewhat similar and somewhat dissimilar are RakNet and the OpenTNL library; you might want to get these and study their documentation (or code) for some inspiration.
enum Bool { True, False, FileNotFound };
Are you using .Net? If so, then you can use attributes to "publish" the members that need to be sent to the other players, and then use reflection to figure out what the members are and their values at runtime.
Quote:Original post by hplus0603

In the system I described, I have a templated system where you wrap your interesting data members in a Facet<> template, and you derive your physical simualtion object from FacetUser, which will register with a FacetManager where each connection can allocate a FacetContext. When you change one of the data members of the Facet<>, it is marked as dirty in all contexts that currently see the object, and un-marked when an update has been sent and acknowledged. Through template magic, the appropriate marshalling is determined for each facet. The programmer doesn't need to worry terribly much about this, except by 1) not being stupid about how values are updated and 2) using the right infrastructure classes.


Do you have any online references, or Google keywords I can search on? I'd like to read more about this.

Thanks
---------------------http://www.stodge.net
Quote:Do you have any online references


No, this is just code I'm messing about with in my "copious" spare time. I would recommend looking at the implementation of OpenTNL and RakNet, which will give you lots of ideas in this area.

Also, I seem to recall a Game Developer article about splitting object replication into something they called Facets -- perhaps 3 years ago? Try searching on gamasutra.com (free registration needed).
enum Bool { True, False, FileNotFound };
Quote:Original post by Holy Fuzz
Are you using .Net? If so, then you can use attributes to "publish" the members that need to be sent to the other players, and then use reflection to figure out what the members are and their values at runtime.

Ugh. You dont want to be looking up attributes at runtime during gameplay for networking. That will result in the creation of the attribute object ever time you get the custom attribute. And reflection has sucky runtime preformance for stuff like that.

If you are using C# 2.0, something hplus0603 suggested is good(you can get most of it using C# 2.0 generics). The biggest problem is assigning new values to data wrapped by a Facet<T> data type is while you can use implicit conversions from it for free (no object creation, just return a copy of the data). Setting requires a cast, which will involve object creation.

This topic is closed to new replies.

Advertisement