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.