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.
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.
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:
- Inherit from EM_NET_OBJECT.
- Implement the deferred features: serialize, unserialize and serialization_byte_count. Use the stream opperations for basic types like put_integer and read_integer.
-
Register the object in a subclass of
EM_NET_OBJECT_TYPES (eg. MY_NET_OBEJECTS):
- Implement a feature which returns a new instance of your object type each time it is called.
- Set a unique object type ID.
-
Link the object's creation procedure with the object type ID in the
register feature using
net_object_factory.add (see net_object_factory for more details).
You need to redefine register.
Hint:Look at the implementation of EM_NET_OBJECT_TYPES and just copy its structure.
- Use the new class as generic parameter for a descendant of EM_NET_BASE.
-
In case you don't use dynamic object creation,
you need to add
the created object to all peers at design time with a predefined ID.
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.
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.
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.
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:
-
EM_NET_CREATE_OBJECT_REQUEST
Requests an object creation of a specific type. By default the object is requested to be created in the same group the object it sent from. EM_NET_SERVER has implemented a create_object_request_default_handler which you may subscribe to the EM_NET_CREATE_OBJECT_REQUEST event. The default handler does the following:
- Send an EM_NET_CREATE_OBJECT_RESPONSE to all connections in create_group_name.
- Create the object locally (on the server) in the same group.
- All connections that didn't acknowledge the the creation response will be removed.
Note:By default, there is no event handler attached to this request. You must either subscribe your own handler or use the create_object_request_default_handler.
-
EM_NET_CREATE_OBJECT_RESPONSE
This is the response to EM_NET_CREATE_OBJECT_REQUEST that tells the receiver to create a specific object. This object is specified using set_object_to_create. You set the group name, in which the object will be created by set_create_in_group_name.
EM_NET_CLIENT has a pre-registered handler that creates the object automatically.
-
EM_NET_DESTROY_OBJECT_REQUEST
Request an object for destruction. You only need to specify the ID of the object that you want to destroy by set_destroy_id. EM_NET_SERVER has implemented a destroy_object_request_default_handler which you may subscribe to the EM_NET_DESTROY_OBJECT_REQUEST event. The default handler does the following:
- Send an EM_NET_DESTROY_OBJECT_RESPONSE to all connections in the group the request was received from.
- Destroy the object locally (on the server) from the same group.
- All connections that didn't acknowledge the the destruction response will be removed.
Note:By default, there is no event handler attached to this request. You must either subscribe your own handler or use the create_object_destroy_default_handler.
-
EM_NET_DESTROY_OBJECT_RESPONSE
This is the response to EM_NET_DESTROY_OBJECT_REQUEST that tells the receiver to destroy a specific object. This object is specified using set_destroy_id.
EM_NET_CLIENT has a pre-registered handler that destroys the object automatically.
Detailed instructions of how to create and send events are available in the event section.