Networked Games -- Sharing Objects

  • Jian Huang
  • Networked Games
  • The Notes Page

    Objects

    All of our previous discussion of graph applies to how we would code up the game logic. With that in mind, let us now consider how we can share objects in a networked environment. Here, the term, object, is used in its general sense.

    The objects in a networked game will obviously fall under different classes, derived from different base class as well. One practice that is prevalent in this domain is to design a scheme to assign an unique ID to each object. The ID could be used as the key into a database maintained on the server, or just as a universal "name". Besides identifying objects, it must contain enough information to parse out what class the object is. For instance, it is common to break up a 32-bit unsigned integer into multiple fields, and use each field for a different purpose.

    Another important mechanism is each object must know how to serialize itself. I.e., to format itself into an endian safe byte stream and to "regenerate" itself from a byte stream. This mainly applies to when objects get created. What other things could happen with objects? Well, for that let's look at Object Lifecycle.


    Object Lifecycle

    In networked games, we need to consider the following for all objects:

    On the server, object creation takes the following steps:

    This should be reminding us of the "Observer" pattern that we have learned before. To use an object, first game code would retrieve the object by ID, modifies the object and then call update on all listeners. If game code first retrieves the object and simply removes that from the database, the object is then destroyed.

    On the client, however, each shared object is created on receiving a creation packet. Note, creation is just a creating an empty object. Then, on receipt of an update paccket, the object is actually instantiated from the serialized byte stream. If an object is to observe/listen to some other subject object, it must be registered. Whereas, if there are observers of the new object (the new object is a subject, all those observers must be notified. This process is a lot more tricky when an object is to be destroyed. Make sure you handle all the subject/observer relationship correctly.


    Dirty Bitfield

    For heavy classes/objects, always having to send a new copy is too expensive. Updates are best to sent incrementally. In other words, only send what's changed. It is ideal to use a different bit in a bitfield to represent each attribute in an object. Of course you may not have as many bits as needed. In that case, a common approach is to partition all your attributes into groups and use one bit for each group. In this case, on notification you send two integers per object: its universal ID and its dirty bitfield. The bitfield could be masked by the server to restrict knowledge of variations in certain fields to just the server.


    Listeners

    Up till now, we haven't really defined what listeners are. If our concern is entirely within the same process address space, of course listeners would simply be a method to be called. But introducing the network makes it a bit complicated. There are many ways to deal with this problem. The following is just one of them, chosen to show here for its simplicity.

    Assuming our framework includes a server process (multi-threaded) and a set of client processes (multi-threaded as well), with each process running on a different computer. In each process, there is a special listener thread. When a network message is received, from the ID the listener can quickly tell what class this object belongs to, and calls the corresponding update() method on the right object. The dirty bitfield is an input to the update method. Depending on the implementation of the very object, the update could be simply igored or could lead to "pulling" more data across the network.

    But what if the observers are on the same machine as the subject? Should we treat them differently? Probably not. You can still use the same interface. I.e. send a notification message to the same listerner thread as above. When you implement the code, just have each notification include a new request of connection to the listener. After the connection is granted, send the message and close the connection. Most web servers operate in this fashion. It is not as slow as you might expect. The speed to do so definitely surprised myself during our own research dealing with large data visualization.

    In this design, when registering observer with a subject, you then need to record the IP address as well as the port number of each listening thread. Make sure you keep a reference counter for each listener, though, since objects can move from one server to another. When the reference counter becomes zero, no more objects served by that listener still need to hear.

    Update and event are very similar. They both mean "something's new", but at different levels of severity. Update could mean that playerA is losing blood level, event is exemplified by playerA losing his favorite weapon during a fight. It makes more sense from software design point of view in the next section, though.


    Unreliable/Reliable Updates

    Here update simply means a message to be transfer across the network. TCP/IP has an evil twin brother, UDP/IP. TCP is the good one that everybody adores, because it is behaving and considerate of others. UDP is not so considerate and penaltied to be unreliable, hence often shunned.

    However, since UDP is not so well-behaviing traffic-wise, it is likely to deliver your message with a short latency. When you have continuous updates happening at a relatively fast rate, say to continuously update the level of gold accumulation every 2 seconds, we could care less whether one message in the middle gets lost. But if your ship sunk, that message better reach everybody.

    Normal updates could be sent using UDP, whereas events are better suited for TCP. It is notable that this distinction is not iron clad. More important updates should be sent with TCP as well. Just that this aspect is highly dependent on your game story. One more constraint is setting up a connection. It probably doesn't make much sense to set up a new connection every time you need to send a UDP packet. This is indeed a trade-off for you to decide.


    Summing Up

    Today's discussion is to get you exposed to the concept of sharing objects across networks. Our main design makes use of the Observer (Publisher/Subscriber) pattern. Every time a subject needs to notify its observers, the subject actually loops through all the registered listeners and initiate the communication.

    The underlying assumption is that all objects "visible" on a client are locally replicated, and a notifications are uniformly sent to all objects locally available within a class. If you want to distinguish within a class, then you will have to register with each listener a list of ID's that subscribes to a subject. This is of course an additional overhead. If your story needs this functionality, do so. Otherwise, don't bother.

    Finally, this design is not comprehensive, nor is it already a scalable and fault-tolerant solution. It is a good starting point, though.

    Do graph nodes need to be shared? Just a question :-).