How to structure a reusable game networking library

jwatte's picture

I recently have answered several questions about how to structure a networking library such that it can be easy to use for users of the library and/or when expanding the game you're writing. Here are some thoughts on that. (Code examples in C++)

Networking generally ends up needing to do three things:

1) Mirror state updates from one object to another.
2) Request remote services ("RPC").
3) Connection management.

Generally, you'll want to structure your network protocol so that it supports state updates and RPC as native operations. Those, in turn, are invoked on specific targets within the remote endpoint you're talking to -- typically, game objects, or services. (Connection management is generally special and partially hard coded)

Thus, your network library should allow users to define objects that can be targeted and/or replicated, as well as possible RPC methods. The variation points are generally:

Property updates:
- Dropped packets may not matter if later packets send later versions.
- Some properties may need an "eventually consistent" guarantee, such that if they change only sometimes, they still get distributed until the other end actually has the new value.
- Generally are not sequenced. This may turn into momentary hilarity if "body mesh" is a different property from "body texture".

RPC:
- Is generally guaranteed delivery, guaranteed in-order.
- Is generally asynchronous from the caller's point of view -- "fire and forget"
- Generally end up returning results (if needed) using a "reply" RPC.

Connection management:
- Generally may include set-up and tear-down.
- Clock management, packet ack, resend requests etc.
- Encryption, authentication, if you need it.

Thus, you need not only a way to register which objects are available to handle requests, but which properties and methods are available on those objects. Then you need the ability to mirror property updates (if an object is "master") and dispatch RPCs, as well as doing general rules checks (so the user doesn't send a "AddGold(me, 200000)" RPC :-)

Manufacture of objects generally happens through RPC into an object factory (or sometimes through connection management), and objects generally have IDs that are shared across the network (rather than different IDs on different machines).

Thus, one simple API for networked game object management may look something like:

class MyClass : NetworkObject<MyClass> {
  public:
    PROPERTY(int, SomeValue);
    PROPERTY(string, SomeName);
    METHOD(AddValue, ARG(int, delta) END());
    METHOD(MakeRandomName, END());
    MyClass();
};

A lot of the smarts of registering an object, assigning an ID etc will be implemented in NetworkObject. If the user calls new MyClass(), that will likely make the current node the master for that object, and will generate an ID. Once the object is "blessed" (all properties are properly configured, say), that object will be mirrored to other nodes in the network.

Meanwhile, when a factory "new object" message comes in, that will create an object within the network object factory system (hence NetworkObject knows about the MyClass class, hence the template), and this will create a "slave" copy of the object, updates to which are made through the distribution system, not locally.

Generally, for things that don't need an object per se ("ping/pong") in your example, you'll just define a singleton with a bunch of methods.

// Every node has exactly one of these
class ConnectionSingleton : NetworkConnection {
  public:
    METHOD(Ping, END());
    METHOD(Pong, END());
};

NetworkConnection will likely, in turn, also derive from/implement the NetworkObject interface, and will be the first object created, so it will generally have a special ID (like "0" or "1"). To issue "Ping," you simply call the Ping() method.

The trick comes in defining the macros and templates smart enough that it's simple to write the code and use the properties/methods, yet it has all the information necesary to make a "safe" interface for the network packets, and being able to parse/reject badly formatted packets.

If your goal is not to write a fully reusable object oriented networking librarym then registering a "packet handler" for a given code is probably good enough. The interface for that would be dead simple:

class IPacketHandler {
  public:
    virtual void OnPacket(void const *data, size_t size) = 0;
};
 
class Connection {
  public:
    void SetHandler(int code, IPacketHandler handler);
    void SendPacket(int code, void const *data, size_t size);
};

When one end calls SendPacket(), that gets bundled up into a network message (more than one packet per message, typically). When the message arrives on the other end, it gets received and un-packed, and the handler for each message code gets called with the appropriate data. This is sufficient for very simple games (and I think EverQuest used a similar system, actually), but in the end, management of all the handlers in one central place becomes a real pain.

Comments

Implementing macros

I've been thinking for some time about how to implement macros MEHTOD and PROPERTY, but I'm still confused. Could you please give me a piece of advice on this subject?

jwatte's picture

A better description of how

A better description of how to implement macros like that is found in my chapter of the Game Engine Gems 2 book. See it here:
http://www.amazon.com/gp/product/1568814372/ref=as_li_ss_tl?ie=UTF8&tag=...

You may be able to find it at your local library if you don't want to buy it new.

Beat Me To It

I am also making a network library like that for XNA. It works A LOT like the DependencyProperty WPF stuff; e.g.

    public class Tank : NetworkObject
    {
        public static Vector2NetworkProperty<Tank> PositionProperty = new Vector2NetworkProperty<Tank>("Position", Microsoft.Xna.Framework.Net.SendDataOptions.Reliable, 1);
 
        public Vector2 Position
        {
            get
            {
                return PositionProperty.GetValue(this);
            }
            set
            {
                PositionProperty.SetValue(this, value);
            }
        }
    }

The first arg is the name, the second arg is the delivery options (duh) and the third the average time changes wait in the local cache.

On top of that there is object caching (because the objects fly around identified my LUIDs and I don't want to run out of LUIDs); unfortunately that means that something like the following is required:

var tank = NetworkObjectController.Construct<Tank>();

The client and server objects are identical; in fact, calling construct will do nothing on the client and rather call the server.

The whole thing is really elegant, and I get client prediction without thinking about it. You might want to have a second look at the system.

Get a handle on me at: jonathan dot dickinson at k2 dot com if you want to have a look at how it works.

Lately I've been migrating

Lately I've been migrating away from the state update and RPC approaches - they tend to be more difficult to secure... the developer's mentality is engraned in object/function interaction and not the fact that all clients are fundamentally untrusted in a multiplayer game.

I find the "contract first" pattern a bit better - design the protocol first and describe the actual interactions as data, rather than as function calls (RPC) or object sync.

A bit more work in the integration layer, but designed right the work is fairly straightforward and forces the team to think about the client interactions themselves.

In my case, clients generally send "commands" (what they did) while the server sends "state changes". I keep the state changes "event based" rather than full on object sync. For example, if an entity moves an "EntityMoved" event is raised and handled by a communication channel. The abstraction makes it easier to secure and debug.

That's a good observation. I

That's a good observation. I believe that one-way RPC and "commands" are pretty much indistinguishable at the semantic layer :-)
Yes, I agree that command-based, input-synchronous networking is generally a more bandwidth-conserving way of structuring your game.
However, at some point, certain commands like "player changed weapon" is also kind-of indistinguishable from the message "update weapon property of player." In the end, there's a certain structure that these things "want" to take on, and any solution that's optimized for the same requirements will end up doing the same thing, just calling them differently based on where you were coming from at the beginning.