Object synchronisation

PreviousNext

Overview

This page explains how object synchronisation over network is realized. This is achieved using a process called serialization, also known as marshalling.

Implementation

If you start using the network framework you will sooner or later wish to expand its functionality. One of the goals in the design of this framework was to build as few barriers as possible to make extensions as easy as possible. Even though the framework can be used without the knowledge of its implementation details, it's still a good idea to know how things work internally.

Object ID

The object ID is used to identify a single instance of an object in a distributed systen. A unique ID is necessary for the object updating process using serialization.
This works the following way: The updating party, for example the server, wants to update object XY and calls its serialize feature. This will write the object's synchronisation data to a stream in a flat form. Ahead of the this stream, the object's ID, let's say 43, is added. Now the whole thing will be sent through the network. The updated party, which is a client in this case, has its own instance of the object - but with the same object ID. While receiving the stream, the client sees that the data that follows the ID 43 is for object XY and will subsequently call its unserialize feature, passing the incoming stream as argument. This way, the right amount of data will be taken from the stream and the object will be updated.

Object Type-ID

Each effective class whose instances should be synchronized over network needs a unique ID from the object type space (which is the INTEGER_32 space in the standard implementation). The ID must be unique per factory, that will later produce objects based in their type-ID. Since the framework already uses some predefined objects, there is a constraint ID range that can't be used for user specified objects. Check the features lowest_system_id and highest_system_id that represent the borders of this range.
Object type-IDs enable remote object creation. Imagine you would like to create a new object - you have to find out the type-ID of its defining class and send an object creation event to all peers. A create event transmits (at least) the object type-ID and and object ID, which is needed for data synchronisation afterwards, to the remote peers. They will then create a new object, based on a table that maps type-IDs to the corresponding classes. This table must manually be filled by the developer at design time.

Aside:

Such an object registration would not be necessary, if there was a preprocessor available. The Java RMI compiler (rmic), together with the Java Remote Object Registry (rmiregistry) service is an approach that does not need manual registration.

Object creation trough type-IDs with a factory

Every instance of descendants of EM_NET_BASE has its own object creation factory. This factory will be used to create objects by their type-ID. Because type-IDs must be unique per factory, every instance of EM_NET_BASE (in particular instances of EM_NET_SERVER and EM_NET_CLIENT) will have access to a set of objects with unique type-IDs.
Now it should be clear, that peers that communicate with each other need access to the same set of objects. You can assure that by using the same class as generic parameter for EM_NET_BASE.

Note:

The class EM_NET_OBJECT_FACTORY should only be used through net_object_factory to create objects by type-ID.

The framework uses this mechanism to implement the event publish/subscribe pattern over a network and for dynamic object creation.

Object registration

After having learned the concepts, you probably now wonder how to register you own objects. This is actually pretty simple! The class EM_NET_OBJECT_TYPES provides registration for predefined system objects that consists of:

  • The distribution of object type-IDs.
  • The implementation of creation functions.
  • The connection between an object type-ID and the corresponding object creation function. This is achieved using a net_object_factory that is automatically passed as argument to the creation procedure make_and_initialize_factory.

If it's sufficient for you to use predefined objects only, you can just pass EM_NET_OBJECT_TYPES as generic parameter to a descendant of EM_NET_BASE. To register you own network objects, you've to create a new class that inherits from EM_NET_OBJECT_TYPES and redefine the feature register. Look at the implementation of EM_NET_OBJECT_TYPES for sample registrations. Now you'll use this new class as generic parameter of EM_NET_BASE.
The registered object types are later accessible trough object_types.

Serialization/Deserialization of Object Data

To enable network transmission for a specific object, there are several steps necessary:

Hint:

add_object is just a convenience feature: It adds an object to the standard_group. If your software becomes more advanced it might be a good idea to have a look at the group concept.

Note:

If you have objects which need special initialization data, you may want to overwrite the following default features too: make_from_stream, serialise_init_data and init_serialization_byte_count. See dynamic object creation for more details.

To reduce the amount of work for you as a programmer we implemented a few standard objects like an object which carries two instances of type INTEGER. You may inherit from them and rename the features to fit your needs.

Aside:Why must i do this my self?

It's the only way to be platform/compiler independent because Eiffel does not (yet) support introspection. It gives you low-level control over your data which is necessary to build fast network protocol, which is an important topic in game development in general.

Hint:String serialization

Because of the variable length of strings it's best to serialise first its length onto the stream and afterwards the string itself. Using this method, you don't have to scan for a special termination character.

Example illustration and explanation of the current EM_NET_PROTOCOL

  • The current protocol uses UDP only.
  • The grey field is the header of the packet: It contains a protocol version and a timestamp which states when the packet was sent, which is used if time synchronisation is enabled.
  • The next thing is the ID of the object which is synchronized followed by its data. This works now as follows: We search the object with the corresponding id out of our object space and call its unserialize feature while passing the data stream as argument. This procedure is repeated until the whole data is read from the stream (EOF).
  • How an object organizes its data is completely up to the programmer.
  • The illustrations show an abstraction of the serialized state of the class EM_NET_EVENT_CONTAINER_OBJECT.
    • The container needs to now how many events just arrived: That's why there's the green field labeled with count.
    • The next step is to create a new object using the net_object_factory and the type-ID provided. Afterwards the data stream is passed to the feature make_from_stream which initializes the event data.
    • The last step is to put the newly created event into a list of arrived events. From there they will be further processed: Have a look at the event section for more details.

Dynamic Object Creation

Where not to use it

If your system is simple enough you can avoid dynamic object creation and destruction by using prebuilt objects and predefined ID ranges for object types. Imagine a simple space shooter where several players dynamically join and leave the running game. If you want to do something like that, the easiest thing would be to have predefined player-objects and give them the ID range from 20 up to 29. Now your game is capable of handling a maximum of 10 players. If you have three connected players, seven player objects remain unused and you may simply activate them somehow when a new player likes to join the game. The same mechanism can be used for even more dynamic objects like rockets and other small objects of short life: You would have hundreds of prebuilt cached rocket objects to avoid latency and to guarantee a fast gameplay. However, this concept becomes inapplicable if object data size is huge. One might also argue about the fact that you have a lot of unused objects in memory and after all it might become limiting factor if no objects are left (a famous programming principle states that you should not build static limits into your software).
The advantage of predefined objects is speed: They are always available, so if you need them, one object update is enough. This only takes time of about the latency of your network. In comparison, dynamic object creation takes at least four times more time!

Where to use it

In more advanced environments where maybe latency is not the most important factor it may not be satisfying to have thousands of prebuilt cached objects which will be activated in the game engine when needed. This is the point where dynamic object creation and destruction becomes effective.

To fully understand the application of dynamic object creation, you also need to understand events and group management. This is because events are the basis of object creation and destruction, and event publishing makes use of groups.

As it has already been stated above, it is necessary that each object has a unique ID in the whole network. Because of that, a central ID manager must be chosen that assigns an ID to each object. In the client-server architecture of the multiplayer framework, only the server will decide if objects are allowed to be created and will also assign IDs. Clients can only request an object creation or destruction - the final word has the server. EM_NET_BASE provides access to such an ID manager through the feature id_manager.

The following 2PC events are involved in object creation and destruction:

Detailed instructions of how to create and send events are available in the event section.