Jump to content
  • Advertisement
Sign in to follow this  
magwo

Elegant design pattern for multiplayer net code?

This topic is 3612 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I'm wondering if there are any nice and elegant "patterns" for messaging/object synchronization out there. It doesn't have to be a design pattern - a simple model or concept will suffice. My problem is with the way that multiplayer games' packet code turns out. I have worked with the net code of several multiplayer games.. and they have all been ugly, non-robust, bug prone and hard to manage. Basically, there are several problems with the classic "serialized object state" approach: 1. You manually write the serialization and deserialization code for every single serializable type. LOTS of room for programmer error with mismatching read/write procedures. Bugs, especially with a binary protocol, are hard to find. 2. After a while, there are usually MANY "packet" classes, and they are all ugly and a pain to maintain. Delicate stuff. Some day you might be forced to implement all these "packets" in another language.. major headache. 3. It's hard to integrate into the game. Either you do it with some singleton hideousness and perform function calls from the singleton that receives packets. Or you can do some big complex event system that requires tons of manually written boilerplate code. Many opportunities for hard-to-find virtual shadowing bugs etc etc. So.. what I DON'T want to see is something like this: public class PlayerStatePacket extends Packet { PlayerStatePacket(Stream s) { ... } void write(Stream s) { ... } } Is there a remedy?

Share this post


Link to post
Share on other sites
Advertisement
How about having a base Packet class, and each inherited class registers its data members to that class, which then only needs a single read and write function?
You can register them with the << operator, if you like, for that extra C++-ness.

In the end, since C++ is not reflective, you have to specify every individual data member anyway. There's no getting around that.

Share this post


Link to post
Share on other sites
Quote:
Original post by magwo

I have worked with the net code of several multiplayer games.. and they have all been ugly, non-robust, bug prone and hard to manage.


Real projects or emulators?

Quote:
1. You manually write the serialization and deserialization code for every single serializable type. LOTS of room for programmer error with mismatching read/write procedures.


"public class" sounds like Java or C#. Those support reflection. That makes it trivial to marshal everything automatically.

Quote:
2. After a while, there are usually MANY "packet" classes, and they are all ugly and a pain to maintain. Delicate stuff. Some day you might be forced to implement all these "packets" in another language.. major headache.


Define them externally. There's no end to various IDLs, even google recently published one.

Quote:
Bugs, especially with a binary protocol, are hard to find.


Serialization can be implement in a completely type-safe manner, with many additional security checks in Java or C#. In C++, it's possible to achieve the same, but it requires one extra definition per member.

If you're willing to throw macros into your code, serialization can come almost for free, since by defining a serializable variable you define how it's written.

Or perhaps this refers to passing stuff around via char * and void *. And hopefully, all peers will be built from same sources, or from same protocol definitions, so that mismatching versions can be detected during handshake.

Quote:
3. It's hard to integrate into the game. Either you do it with some singleton hideousness and perform function calls from the singleton that receives packets. Or you can do some big complex event system that requires tons of manually written boilerplate code. Many opportunities for hard-to-find virtual shadowing bugs etc etc.


If your logic is event driven, then events is how it works.
If your game is packet based, then that's used.

Or, if it's packets that dictate the architecture, the architecture will likely be cumbersome.

Otherwise, it's usually possible to implement some abstraction layer suitable for the overall design. Depending on your requirements you may need to manipulate packets manually, for some uses, it's possible to completely abstract that away. Real-time will need finer grained control (packet loss will likely be factored in into logic as well at some level) than just async communication, which can be fully abstracted.


Quote:
So.. what I DON'T want to see is something like this:


Why not? Unless you're using a language that supports reflection, that's what you'll have. It may be hidden by macros, auto-generated, involve template magic - but you will need to define fields manually.

Quote:
Is there a remedy?


It all comes down to how the logic itself works. Network is just messages being sent using either reliable or unreliable transport.

Share this post


Link to post
Share on other sites
How about bit/byte packing? You basically fill an array of bytes with your data... It gets quite simple...
(warning, pascal code ahead....)
Writing the data:

Writebytetopack(MyBytepack,5); //packet type: 5, position
Writefloattopack(mybytepack,pos.x);
Writefloattopack(mybytepack,pos.y);
Writefloattopack(mybytepack,pos.z);
Send...


Reading the data:

Readbytefrompack(mybytepack,msgtype)
Case msgtype of
1:handleDynamicobject(mybytepack)
2:handleXYZ
[...]
5:handleposition(mybytepack)
end;

procedure handleposition(pack:Tbytepack);
[...]
readfloatfrompack(pack,enemy.position.x);
readfloatfrompack(pack,enemy.position.y);
readfloatfrompack(pack,enemy.position.z);
end;


It's kinda neat, because you only need to write message specific read/write functions, and you can pack multiple messages in one packet, without more UDP overhead.

P.S:I forgot the code for the byte packing, but that should be easy...

Share this post


Link to post
Share on other sites
Quote:
Original post by Gagyi
How about bit/byte packing? You basically fill an array of bytes with your data... It gets quite simple...


If you have some scattered data, that you just need to move between places, than explicit writing to buffers works.

If you wish to avoid this, and give structures the knowledge of how to serialize themselves, then this would be considered optimal. Same topic is covered in Game Programming Gems series with similar solution, boost serialization is another reference.

Using such approach allows one to use arbitrary destination for serialization, byte or bit stream, a file, database.

Functions can also be defined in this way, allowing them to be exported to scripting languages. Again, details vary depending on language used.

But most "high-level" serialization solutions will define, in one way or another a list of type-name pairs that make up a structure. This is then traversed, and built-in types are written to some destination.

That however covers only the mechanical part.

For some systems its beneficial to transfer whole logical units. For example, writing 'new Entity()' will cause the instance to be created on all peers, but on each a different implementation.

It's also sometimes viable to support some form of automatic state replication. When a peer connects, some base state is sent (or assumed by client from some local cache), afterwards only changes to that state are sent.

Quote:
Case msgtype of


Once you have logical structures, this type of lookup is unavoidable. The decision that needs to be made is whether it's compile-time or run-time defined.

While switch() is probably the most efficient, it may become cumbersome to maintain, especially for logic itself, which can result in hundreds of different messages. One solution is to use IDLs.

Alternative approach is to have a hash table of sorts. When an object wishes to receive data from network, it puts the types it can handle into that table, along with handler functions. Here, the responsibility is put on the user, with networking being black box.

This type of approach is also used with various class-loaders in VMs, where classes do not exist within VM itself, and may be created during run-time.

But some form of hashtable, either optimal (switch/if-else/array-of-functors) or some run-time defined table is unavoidable.

Share this post


Link to post
Share on other sites
Entanglar (http://entanglar.dunnchurchill.com) uses dynamically generated proxies to update a network state bag when synchronised properties are set or methods are called. These changes are then pulled out when a synchronisation manager deems it appropriate (depending on predicted inertial error in the remote sim) and serialized over the network.

At the other end its pulled out and stuffed back into the appropriate entity using reflection.

Advantages of this approach is that you just need to stick an attribute on the front of synchronised properties/methods (and declare them virtual), and everything works in the background.

Disadvantages are that you need to instantiate classes using a factory (EntityFactory<T>.New), so the framework can hand you back a proxy which inherits from T with the inteceptor code hooked up. Generating the proxy has a fair bit of overhead here. Intercepted method calls & property setters have a small overhead, as it has to proceed via the interceptor. Most of these speed issues could be overcome by using a post-compile aspect weaver, but I haven't focussed on performance as yet.

So, yes, its possible to boil it down to [Sync] virtual int foo {...}, but it won't be quite as fast or efficient as hand rolled serialization (although I'm aiming to get close enough) ;)

Share this post


Link to post
Share on other sites
google protocol buffers looks nice although i havent tried them myself.
http://code.google.com/apis/protocolbuffers/docs/overview.html

It basically autogenerates the serialization for you and supports versioning.
The nice thing here is that you make a template for you object and the tool can autogenerate a template for you in Java, Python or c++.

boost serialize also does the same, but here you have to insert the serialization code programatically. There is only one serialize method, so its not as error prone as the write/read method of doing it.
http://www.boost.org/doc/libs/1_35_0/libs/serialization/doc/index.html


I've been looking into these much for the same reason as you (ie. all hell breaks loose when a lower level object changes serialization code, especially when you forget to make it with version support in mind..), but so far i've only looked, not tried.


Hope this helps :)

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

Participate in the game development conversation and more when you create an account on GameDev.net!

Sign me up!