11. Basic Duplicated Object Features

The duplicated object features automatically handle communication for game objects. The application simply changes the parameters of in-game objects, and the library automatically sends and receives the necessary data.

This chapter explains how to manipulate, manage, and migrate duplicated objects. For more information about the Data Definition Language (DDL) for duplicated objects, see Chapter 14 Data Definition Language (DDL) for Duplicated Objects.

11.1. Creating Duplicated Objects

The NetZ API has methods for instantiating and destroying duplicated objects. By default, when a duplicated object is instantiated, the local object is initialized as the duplication master, and the duplicated objects created on the remote stations are initialized as duplicas. You must implement an RMC to create objects remotely.

As an alternative, you can use the DuplicatedObject::Emigrate and DuplicatedObject::AttemptEmigration member functions to migrate the duplication master of an object between stations. If a player joins a session that is already in progress, duplicas of all the existing duplicated objects in the session are automatically created on the new player's station.

You can create objects in two ways: by calling the DuplicatedObject::Create member function, or by calling the DuplicatedObject::CreateWellKnown member function. Both these methods publish objects globally. However, WellKnown objects are also guaranteed to be discovered before the session is created. Even though objects are published globally, you do not need to update all of the datasets for a duplica on every station. By using station filters, you can control which information is updated on each station. This practice helps to reduce bandwidth use and enables you to implement security restrictions on specific datasets. For more information about the station filter update policy, see Section 14.1 DDL File Syntax.

The user must use the following declaration to implement all duplicated object classes that are declared in the DDL file.

Code 11.1 Implementing Classes

class ObjectName : public DOCLASS(ObjectName) {
// User specified variables and methods
// User-specified variables and methods.
};

After you implement the duplicated object classes, a duplicated object is created and published directly within its duplicated object class—by using the Create and Publish member functions—to instantiate the duplicated object classes generated by the DDL compiler for the object. NetZ generates the Create and Publish member functions for each duplicated object class declared in the DDL. The Create member function creates the duplication master for an object, and can be called before calling the CreateSession or JoinSession member functions of the Session class. Until the object is published, the duplication master can only be referenced by using the pointer returned by the Create member function. After a duplicated object is published, it is automatically discovered by all remote stations connected to the session that require the object, and by any station that joins the session, and can be referenced using a DOHandle or a Ref. Use the IsPublished member function to determine whether an object has been published or not. The method returns true if the object has been published, and false if it has not. Although the DuplicatedObject::Create member function has the following syntax, note that the DuplicatedObject* Create(DOID idUserDefinedID) member function is defined by a specific class.

Code 11.2 Member Functions for Creating Duplicated Objects

static DuplicatedObject* Create(DOID idUserDefinedID);
static DuplicatedObject* Create(DOClassID idDOClass,
                                qUnsignedInt32 uiTimeout = WAIT_INFINITE);
static DuplicatedObject* Create(DOClassID idDOClass, DOID idUserDefinedID);
static DuplicatedObject* Create(DOHandle &dohHandle);

idUserDefinedID is a user-defined ID. This parameter is optional, but when it is supplied, the created duplicated object is assigned the specified DOID rather than the one assigned by using the automatic DOID generation mechanism of NetZ. Note that a unique DOID must be assigned to each duplicated object within the same class, regardless of its location.

idDOClass is the DOClassID of the object.

uiTimeout is the maximum time that the system will wait.

dohHandle is the DOHandle of the object.

Because the DOClassID of an object—and therefore its DOHandle—may change when you recompile your project, alter your DDL file, or upgrade to a new version of NetZ, it is safer to store and retrieve DOIDs in the database instead of DOHandles. If a class named Avatar is declared as a doclass in the .ddl file, an Avatar object can be created and published in any of the following ways.

Code 11.3 Creating and Publishing an Avatar Object

// 1. Let NetZ assign the IDs for the object.
{
    Avatar* pNewAvatar=Avatar::Create();
    pNewAvatar->Publish();
}

// 2. Assign the DOID.
{
    ID idNewAvatar=0;
    IDGenerator::Ref refIDGenerator(Avatar::GetIDGenerator());
    if (refIDGenerator->GenerateID(&idNewAvatar, 0)) {
        Avatar* pNewAvatar=Avatar::Create(idNewAvatar);
        pNewAvatar->Publish();
    } else {
        return false;
    }
}

// 3. Assign the DOClassID and let NetZ assign the DOID.
{
    DuplicatedObject* pNewAvatar=DuplicatedObject::Create DOCLASSID(Avatar));
    pNewAvatar->Publish();
}

// 4. Assign both the DOClassID and DOID.
{
    ID idNewAvatar=0;
    IDGenerator::Ref refIDGenerator(Avatar::GetIDGenerator());
    if (refIDGenerator->GenerateID(&idNewAvatar, 0)) {
        Avatar* pNewAvatar=(Avatar*)DuplicatedObject::Create(
                    DOCLASSID(Avatar), idNewAvatar);
        pNewAvatar->Publish();
    } else {
        return false;
    }
}

// 5. Assign a DOHandle.

// Assume there is a database from which we
// save/retrieve information.

DOHandle hPreviouslySavedAvatar = ReadHandleFromDatabase();
Avatar* pNewAvatar=(Avatar*) DuplicatedObject::Create(hPreviouslySavedAvatar);
// Initializes the attributes of pNewAvatar with the
// info saved previously in the database, and then publish
// the new avatar.
pNewAvatar->Publish();

To create a sphere in the SphereZ example, use the following code to instantiate the NetZ object, create a session, and then create and publish the Sphere duplicated object.

Code 11.4 Creating and Publishing a Sphere Duplicated Object

// Initialize NetZ
NetZ* pNetZ = new NetZ();

// Create a session
Session::CreateSession();

// Create and publish a Sphere Duplicated Object
Sphere* pNewSphere = Sphere::Create();
pNewSphere->Publish();

11.2. Duplication Master Authority

Most of the time, each duplicated object has one duplication master and multiple duplicas. However, when a duplication master migrates there are two duplication masters for a short period. NetZ requires that there is always a duplication master for each object. During a migration, the migrating duplication master does not become a duplica until it receives confirmation that the migration was successful and a new duplication master exists on another station, as illustrated in Figure 11.1 Transferring Object State and Object Authority During an Object Migration. For some time during a migration, there are two duplication masters for the same object. Nevertheless, at any time, each station considers only one duplication master to exist, even though two separate stations may consider the master to reside on different stations. This state occurs because it takes time to send a message to notify stations of a migration, and latency means that not all stations receive this information at the same time.

Because the existence of two duplication masters can lead to conflicting calls, NetZ attaches a notion of authority to a duplication master: It is impossible for two duplication masters to have authority over an object at the same time. When a migrating duplication master sends a message to the station it wants to migrate to, it gives up authority over the object. When the object on the second station becomes a duplication master, it takes up that authority, as shown in Figure 11.1. If the migration is not successful, the original duplication master reassumes authority after it receives confirmation that the migration failed. While a migration is in progress, there is a short period, depending on latency, during which no duplication master has authority over the object and some calls cannot be made. The DuplicatedObject::HasAuthority member function returns a value indicating whether a duplication master has authority. A duplication master with authority has the right to migrate and delete the main reference of the object. In addition, the developer can add other restrictions to the notion of authority. For example, say you have a factory object in your game that generates objects, each with a unique ID. To prevent two objects from being created with the same ID, it makes sense to allow only the authoritative duplication master to create an object. To ensure that a method can only be called on a duplication master that has authority, you need to use the TargetObjectMustHaveAuthority flag within the DOCallContext of the member function call. For more information, see Section 11.9 Call Contexts.

_images/Fig_DO_Basic_ObjectStateTransaction_In_Migration.png

Figure 11.1 Transferring Object State and Object Authority During an Object Migration

11.3. Duplicated Object Operations

Within the DuplicatedObject class, there are several member functions you can use to determine the properties of a particular object. Use the IsADuplicationMaster and IsADuplica member functions to determine whether a duplicated object is a duplication master or a duplica. Use the IsAWellKnownDO member function to determine whether the object is a WellKnown object. To get information about the class a particular object belongs to, use the IsA and IsAKindOf member functions to determine whether an object is a member of or inherits from a particular class. Use the GetClassID and GetClassNameString member functions to get the ID and name of the object's class. Use the IsACoreDO and IsAUserDO member functions to get whether the duplicated object is a core object or a user-defined object declared by the user in the DDL. A core object is a duplicated object created by NetZ when a session is created, or when stations join or leave the session. These objects are essential for NetZ to operate correctly, and they include the root classes for all global and non-global duplicated objects; the Station, SessionClock, and IDGenerator classes; and the system classes related to the fault tolerance mechanisms.

Use the IDGenerator class to generate unique IDs that can be assigned to objects or other entities in your game. IDs generated by an IDGenerator instance are guaranteed to be unique, but two IDGenerator instances may generate the same ID. A typical use of the IDGenerator class, as illustrated previously, is to generate specific IDs that can be assigned to duplicated objects when they are created, rather than having NetZ assign the IDs automatically. However, an instance of this class must be created for each duplicated object class for which IDs are to be generated. When using IDGenerator, you must set the range within which to generate IDs by calling the SetIDRange member function, and then call the GenerateID member function to generate an ID. When a station joins a session, all IDGenerator objects are guaranteed to be discovered by the station before the session commences on that station.

Duplicated objects are referenced across the network by using a DOHandle, that refers to the local instance of the duplicated object. NetZ generates a DOHandle when a duplicated object is either created or published — whichever occurs first — after a session is created or joined. The DOHandle of a duplicated object comprises the class ID (DOClassID) and duplicated object ID (DOID) of the object. Get the values of the two parts of the DOHandle by calling the GetDOClassID and GetDOID member functions. A DOClassID is a unique identifier that the DDL compiler generates automatically for each duplicated object class declared in the DDL file. Conversely, a DOID can be generated automatically by NetZ, or it can be assigned by the user. It is only unique within a particular duplicated object class. Note that if the user sets the DOID for duplicate objects, the user must ensure that each DOID is unique within each duplicated object class. A DOHandle can be passed across the network as an RMC, as an action argument, or as a dataset. Use the DuplicatedObject::GetHandle member function to return a DOHandle to the local instance of a duplicated object. Note that you can call this method only after the object has been published. Use the IsAWKHandle member function to determine whether the handle references a WellKnown object. As with the DuplicatedObject class, use the IsA and IsAKindOf member functions to determine whether a particular handle refers to an object that is a member of or inherits from a particular class. Use the IsAUserDO and IsACoreDO member functions to determine whether a particular handle refers to a user-defined or core duplicated object. You can also use the DOHandle::GetClassNameString member function to get a string that represents the class name of the duplicated object to which a particular DOHandle refers.

You can create a Ref to reference an object only on a local station. This creates a direct reference to an object, and this reference may be used to call any method or group of methods on the object. The reference is created before the methods are performed and is destroyed after the block is exited. (A block is a section of code delimited by a { } pair.) To ensure that there is never a reference to an object that does not exist, NetZ guarantees that the object that is referenced cannot be deleted until the block is exited, while using an appropriate thread mode. It is important to ensure that a reference to an object is kept for as short a period as possible. There are two ways to create a reference to an object: by specifying a DOHandle to an object, or by specifying a reference to an object. You can also use the default constructor, and then use the assignment operator to assign an object to a Ref. Then use the DORef::IsValid member function to verify whether the construction works correctly. You can use the DORef::Poll and DORef::Wait member functions to verify whether a reference is valid. Wait continually checks for validity until the timeout. Use the DORef::GetHandle member function to get the DOHandle used to construct the reference, and use the DOHandle member function to return a pointer to a specific duplicated object. For example, use the following code to execute a method locally on the MySphere object by specifying the DOHandle of the object.

Code 11.5 Running a Method Locally on the MySphere Object by Specifying a DOHandle

{
    Sphere::Ref refMySphere(hMySphere);
        if (refMySphere.IsValid()){
            // call a method
            refMySphere->method();
        }
}

Alternatively, you can use either of the following examples to create a reference by specifying a reference to an object.

Code 11.6 Specifying a Reference to an Object to Create a Reference

// create a Ref using the default constructor and then assign it a value
Sphere::Ref refToASphere;
RefToASphere=refMySphere;

// or create a Ref by specifying a Ref to an object
Sphere::Ref refToASphere(refMySphere);

Use the DuplicatedObject::DeleteMainRef member function to delete a duplicated object. This member function can only be called on a duplication master, and it can be called on an unpublished object. The DeleteMainRef member function deletes the main reference to the duplicated object held in the duplicated object store. If this was the only reference to the object, the destructor is called and the duplication master and all of its duplicas are deleted by this function call. If there are other references to the object, the destructor is called until the last reference is released. For example, if a reference to the object created using RefTemplate exists, the object’s destructor is not called until this reference to the object is deleted. You can use the DuplicatedObject::MainRefIsDeleted member function to determine whether the main reference to the duplicated object has been deleted. For the complete sequence of events that occurs when an object is deleted, see the RemoveFromStore operation section in Section 13.1 Remote Method Calls. For example, to destroy the MySphere object, use the following code.

Code 11.7 Destroying a MySphere Object

{
    Sphere::Ref refMySphere(hMySphere);
    refMySphere->DeleteMainRef();
}

When a station leaves the session, NetZ calls the DuplicatedObject::ApproveEmigration member function on all duplication masters on the station to determine whether to approve the migration of objects to another station so that they can remain in the session. If this member function returns false for a duplicated object, NetZ automatically deletes the main reference to the object. Similarly, if a station leaves the session because a fault occurred, the DuplicatedObject::ApproveFaultRecovery member function is called on the duplicated objects on that station. If this member function returns false for a duplicated object, NetZ automatically deletes the main reference to the object.

NetZ defines a DynamicCast member function for casting (explicit type conversion) during run time for each specific DuplicatedObject class. This method functions in the same manner as the standard C++ dynamic_cast operator, but using NetZ run-time information. For example, say you have an Avatar class that inherits from a Duplicated3DObject class, which in turn inherits from the root DuplicatedObject class. You can return a pointer to an Avatar object if pObj points to an Avatar object, as follows.

Code 11.8 Getting a Pointer to an Avatar Object Using the DynamicCast Method

Avatar *pAvatar = Avatar::DynamicCast(pObj)

11.4. WellKnown Objects

A WellKnown object is effectively a global object that is common to all stations that are participating in the session. These objects typically include objects such as the game environment that must be discovered before a player can take part in the session. WellKnown objects are typically computer-controlled objects. When they are created, their duplication master resides on the session master.

The difference between a duplicated object and a WellKnownDO object is that a WellKnown object is created before the session and must also be created on the session master. When a station joins a session, the station is guaranteed to discover all WellKnown objects before the Session::JoinSession member function returns on the station. WellKnown objects are also fault-tolerant by default, and migrate to a new station if their duplication master resides on a station that fails or leaves the session.

WellKnown duplicated objects must be defined in the DDL (Section 14.1.) to ensure that the number of well-known instances in a session is known before the session is created. For example, the WellKnown object World and the global variable g_hTheWorld are defined in the DDL as follows.

Code 11.9 Definitions of the WellKnown Object World and Its Global Variable

doclass World {
};
wellknown World g_hTheWorld;

When a WellKnownDO is declared in the DDL, the DDL compiler generates a global variable of the type WKHandle. The WKHandle variable refers to a WellKnown object. In the example, g_hTheWorld is the generated WKHandle global variable. The WKHandle instance inherits from the DOHandle class.

All WellKnownDO objects specified in the DDL must be built and published before the session is created. This is before the Session::CreateSession function is called. 'WellKnownDO objects are constructed similarly to duplicated objects by calling the DuplicatedObject::CreateWellKnown member function. This member function returns a pointer to the newly created duplicated object. The pointer can be used until the DuplicatedObject::Publish member function is called, after which the pointer belongs to NetZ and may be invalidated at any time because objects can be created and destroyed during a session. The duplication master of a WellKnownDO object must reside on the station that is the session master. Consequently, the CreateWellKnown member function is only valid on the station that calls the CreateSession member function. In SphereZ, the following code is used to create and publish the World.

Code 11.10 Creating and Publishing the World

{
    World* pNewWorld;
    pNewWorld = World::CreateWellKnown(g_hTheWorld);
    pNewWorld->Publish();
}

You can use the DuplicatedObject::IsAWellKnownDO member function to determine whether a duplicated object is a WellKnown object. This member function returns true if the object that it is called on is WellKnown, and false if it is not.

11.5. Iterators

Methods called and performed locally on duplicated objects are implemented using standard C++ programming techniques. To perform a method on a particular object, you simply create a reference to the object and then call the method on it directly, as described in Section 11.1 Creating Duplicated Objects. Iterators are used to call a method over a group of duplicated objects.

On each station, NetZ keeps a list of all duplicated objects, in addition to two subsets of this list that contain all duplication masters and all duplicas. These lists may be accessed by using the SelectionIteratorTemplate class. This class enables you to iterate over a specific class of objects. You can construct three different selection iterators: an iterator that can hold all duplicated objects, one that holds all duplication masters, or one that holds all duplicas. The default constructor creates an iterator that holds all duplicated objects. Use the GotoStart, GotoEndGotoEnd, EndReached, and Count member functions within an iterator loop to set the pointer of the iterator to point to the first or last object in the object list, determine when the end of the list is reached, and to return the total number of objects in the list, respectively.

For example, to call the DoControl member function on all duplication masters in the Sphere class, use the following code to first create a selection iterator that holds all the duplication masters, point it at the first duplication master in the iterator list, and finally call DoControl on each object until you reach the end of the iteration list.

Code 11.11 Calling the DoControl Member Function on All Duplication Masters

Sphere::SelectionIterator itDMSphere(DUPLICATION_MASTER);
    itDMSphere.GotoStart();
        while(! (itDMSphere.EndReached() ));
            itDMSphere->DoControl();
            itDMsphere++;
        }

Likewise, if you want to list all the stations in a session, use the following code.

Code 11.12 Listing All of the Stations in a Session

Station::SelectionIterator   itStations;
    itStations.GotoStart();
        while (!itStations.EndReached()) {
            itStations->Trace();
        }

11.6. Datasets

The user must implement any dataset class that is declared in the DDL file, and also declare that the dataset inherits from the DDL generated DATASET class as follows.

Code 11.13 Implementing the Dataset Class

class DatasetName : public DATASET(DatasetName) {
    // User specified variables and methods
};

For example, the datasets Position and SphereData of the SphereZ example are declared in the DDL as follows.

Code 11.14 Declaring the Position and SphereData Datasets in the SphereZ Example

dataset Position {
    double x;
    double y;
    double z;
} extrapolation_filter, unreliable;

dataset SphereData {
    uint16 m_ui16Texture;
} upon_request_filter;

The user-specified classes Position and SphereData are implemented as follows. A dataset may take a container as a parameter.

Code 11.15 Implementing Position and SphereData Datasets in the SphereZ Example

class Position : public DATASET(Position) {
    void Set(qDouble dx, qDouble dy, qDouble dz);
};

class SphereData : public DATASET(SphereData) {
    Set (qUnsignedInt16 ui16Texture);
};

A dataset can take a container as a parameter. NetZ uses membervector, memberlist, and memberqueue for its containers. The supported types for the containers are any of the simple DDL data types detailed in Section 14.1 Creating Duplicated Objects, in addition to any valid user-defined type. If a container is used, depending on its kind, operations such as remove, add, insert, and iterate over may be performed on the dataset members of the container. In NetZ, the methods available to perform such operations are found in the classes MemberVector, MemberList, and MemberQueue and are similar to the methods available for standard containers in the C++ standard library. For example, the NetZ method Pop_Back removes the last element in the container and functions in a similar manner to the standard C++ method pop_back. The way that you declare an iterator for a container differs slightly from standard techniques. Assume that the dataset MyContainer is declared in the DDL as follows.

Code 11.16 Declaring the MyContainer Dataset

dataset MyContainer {
    membervector<real> vec;
};

The following syntax is then used to create an iterator for this container.

Code 11.17 Creating an Iterator for MyContainer

MemberVectorIterator<DDLTYPE(qReal)> it;

11.6.1. Propagating Datasets

The contents of a dataset may be sent over the network by using a reliable or unreliable channel. Set the channel in the update policy for the dataset, defined in the DDL file. For more information about update policies, see Section 14.1 Creating Duplicated Objects. Use the DataSet::ReliableUpdate member function to determine whether updates for a particular dataset are sent over a reliable or unreliable channel.

When a reliable channel is used and the use of multiple reliables has been initialized, you can use DataSet::SetSubStreamID to specify a SubStreamID that can be used to send during Update. For the initialization settings for the substream, see Section 7.3.4. For messages other than the SubStreamIDDefine::SYSTEM (=0) shared with the system, message handling with BundlingPolicy is disabled. However, packet handling for each stream set with StreamBundling is enabled.

Use the DuplicatedObject::Update member function to propagate the contents of a duplicated object's datasets from the duplication master to its duplicas. This member function is called on the duplication master of an object, and can be called on either a specific dataset or on all datasets of the duplicated object. Changes to a dataset are always made from the duplication master to the duplica, so this member function must be called on the duplication master. The Update member function is a virtual function. If an object is cast into a pointer of a base class, calling Update on the base class updates the datasets of the objects in both the base and inheriting classes. When you call Update and the dataset is unbuffered, the dataset variables on the duplica are updated directly. When the dataset is buffered, the buffer on the duplica is updated. When the dataset is buffered, you must call the DuplicatedObject::Refresh member function on the duplica to transfer the content of the buffer to the duplica of the duplicated object. You can call this member function on either a specific dataset or on all buffered datasets of a duplica. A dataset is buffered if in the DDL the buffered DDL property is declared for the dataset. In addition, if the extrapolation_filter DDL property is declared for a dataset, you must call the DuplicatedObject::Refresh member function to update the datasets. This call is necessary because the extrapolation model is stored in a special buffer when using extrapolation.

To maintain a coherent prediction model for extrapolated data when the dataset of a duplicated object uses an extrapolation filter, you must call the Update member function, even if the contents of the dataset do not change. The frequency at which the member function needs to be called depends on how often the dataset changes. For most games, calling the member function each physics loop suffices. If the dataset uses upon_request_filter, calling the Update member function to update the dataset does not generate a network message unless you first call the DataSet::RequestUpdate member function. You must therefore call the RequestUpdate member function each time the dataset must be updated. When using upon_request_filter, it is up to the user to define the conditions under which the dataset is updated. For example, the user can define that a specific variable must change by a certain amount before updating.

The station_filter dataset property enables a dataset to be updated on a subset of duplicas, rather than all duplicas, mainly for bandwidth optimization and security reasons. When this filter is set, you must have implemented the DataSet::UpdateIsRequired system callback to specify which stations the given dataset is updated on. This system callback is called each time the given dataset is updated, and determines which stations to update. The two major reasons to use a station filter are to apply update bandwidth optimizations, or to apply security restrictions. In the first case a station filter can be used to prevent unnecessary information being sent across the network. For example, for an object that is a long way away, you probably do not need to know the orientation of the object because you probably only represent the object as a point. Because this dataset does not need to be updated, you can save on bandwidth. In the second case, you can define data as being either public, semi-private, or private, or any other categorization you desire, and limit the stations where certain data is updated accordingly. For example, the position of your avatar needs to be known by all players, so this data should be public and updated on all stations. However, data about your items only needs to be known by your fellow guild members, so that should be semi-private and only updated on guild members' stations. The following example illustrates how data could be kept completely private, or shared only with the server.

Code 11.18 DataSet::UpdateIsRequired System Callback for Data Shared Only With the Server

qBool UpdateIsRequired(const DuplicatedObject * pDO, const Station * pStation)
{
    Avatar( pDO );
    // Only update this DataSet if the Station is a server.
    return pStation->GetProcessType()==Station ::ServerProcess;
}

Containers can be updated in either content mode or operation mode, by using either MemberContainer::SetContentUpdate or SetOperationUpdate. In content mode, all content of the container is updated on each duplica each time Update is called, while in operation mode, the operations that were performed since the last update are sent to and reapplied on the duplica. Note that when the operation mode is used, it is the user’s responsibility to ensure that data is accessed with standard container operations so that the operation can be reapplied on the duplica. When the Update member function is called on a dataset that consists of a container, to reduce bandwidth use, generally only the changes made to the dataset members are propagated from the duplication master to its duplicas. When a station joins a session, this update method is not used, and all dataset members are updated. When this happens, a complete update of the container is sent to all duplicas upon discovery of the duplicated object. Similarly, if the list of operations performed on the container members is greater than the number of members in the container, a complete update is sent.

11.6.2. Message Handles

The BundlingPolicy class can bundle messages for duplicated objects sent across the network. Rather than sending each message separately, messages can be sent as a group, which reduces bandwidth usage. Call the BundlingPolicy::GetInstance member function to access the BundlingPolicy class. This member function returns a pointer to the BundlingPolicy object.

Message bundling is enabled by default and can be enabled or disabled during run time by calling the BundlingPolicy::Enable and BundlingPolicy::Disable member functions. Call the BundlingPolicy::IsEnabled member function to get whether message bundling is enabled. When message bundling is enabled, a message bundle is created on the local station for each remote station that the local station is aware of. All messages to send to a particular remote station are then stored in the message bundle for that particular remote station. For reliable transmission, messages other than SubStreamIDDefine::SYSTEM (=0) shared with the system are not handled.

Message bundles may be sent either automatically or manually. If bundles are sent automatically, the frequency at which bundles are sent is set by either specifying a time delay or the maximum size that the bundle may attain. When either of these two conditions is met the message bundle is sent. Set a time delay by calling the BundlingPolicy::SetMaximumFlushDelay member function. The value for this function is the maximum time that can pass between sending two bundles. With the default, the time delay is set to 0, and sending occurs with each Dispatch. We recommend that you always use the default settings.

Get the time delay by calling the BundlingPolicy::GetMaximumFlushDelay member function. A fixed value of 1250 bytes is used for the maximum handle size. When a message bundle reaches this size, the bundle is sent.

Call either the Station::FlushBundle or Station::FlushAllBundles member function to send message bundles manually. Calling the Station::FlushBundle member function sends all messages in the message bundle on the local station associated with that Station object. Similarly, calling the Station::FlushAllBundles member function sends all messages in all message bundles on the local station. These two methods cause message bundles to be sent regardless of the time delay and maximum bundle size settings.

11.7. Migrating Duplicated Objects

NetZ allows control of duplicated objects—in other words, the duplication master—to migrate between stations participating in a session.

When a duplication master migrates, the only change observed by duplicas is a change in the station where their duplication master is located and where they receive updates from. Object migration can be performed automatically by the system or manually at any time by the developer. Specifically:

  1. Fault tolerance to ensure that an object survives a fault.
  2. Load balancing to distribute duplication master objects, and thus the processing load and bandwidth.
  3. Masking the effect of latency by migrating the duplication master for an object (such as a non-player character) to the local station during interactions with that object (such as during a battle).

 In the last case, migration of NPC objects ensures that events such as collisions are always calculated locally (between master objects) and are always shown accurately.

Migration is the process by which control of a duplicated object is transferred to another station. Migration involves two operations: emigration and immigration. Emigration is the process by which a station reassigns control of a duplicated object to another station, and immigration is the process by which a station accepts control of a duplicated object that was previously controlled by another station. Use the DuplicatedObject::ApproveEmigration system callback to migrate a duplicated object automatically when a station quits a session. If this system callback returns true, the duplication master migrates to another station in the session when the station on which it resides leaves the session. The station to which the object migrates is determined by NetZ. If the callback returns false, the object cannot migrate and ceases to exist when the station on which its duplication master resides quits the session. The default behavior of an object is to return false. If the developer wants to control the station to which an object migrates, the developer must call the DuplicatedObject::AttemptEmigration member function on the object before the station quits the game. The following code shows an implementation of the DuplicatedObject::ApproveEmigration system callback that allows Sphere objects to automatically migrate from one station to another to ensure that the objects survive even when stations quit the game.

Code 11.19 Sample Implementation of the ApproveEmigration System Callback

Sphere: public DOCLASS(Sphere)
{
    public:
        Sphere();
        virtual ~Sphere();

        qBool ApproveEmigration(qUnsignedInt32 uiReason)
        {
            if (uiReason==MIGRATION_REASON_LEAVING_SESSION)
            {
                return true;
            } else {
                return false;
            }
        }
    // Game specific methods and variables
};

When an object migrates from one station to another, a ChangeMasterStation system operation is performed to ensure that the information that each station holds for the object is correct. The operation is called on all the stations that have a copy of the migrating object. For more information about system operations, see Section 13.3 System Operation.

During a migration, NetZ ensures that all relevant network information is transferred to the new object master; however, depending on the object, the master may have access to additional data (such as physics or AI models) that its duplicas know nothing about. If the duplication master requires this additional information to continue, or so that the migration is transparent to the players, you must ensure that this information is transferred to the new duplication master. How and what information is transferred depends entirely on the object and its requirements. For example, you may want to send the target position, direction, or behavior of an object so that the new master can continue to calculate the behavior of the object correctly.

If a station quits the session before a migration was completed, an object could be erroneously dropped from the system. To prevent this from occurring, NetZ ensures that neither station involved in the migration can quit the session while a migration is in progress until the object migration has completed.

11.8. Fault Tolerance

Fault tolerance is the ability to recover from faults, such as the disconnection of a station, with little or no effect on the ongoing session. A station may fail, but the duplicated objects continue to exist as if nothing had happened. The disconnection of a station may be caused by a player quitting the session, or because of failure of the network, power supply, or another fault. NetZ implements fault tolerance by giving the station that is the session master the ability to assume control of a fault-tolerant duplicated object with a duplication master that has failed. When a station fails, all the duplicas of the fault-tolerant objects from the failed station are promoted to duplication masters on the session master. If the session master fails, NetZ automatically elects a new session master. When a station failure occurs, objects associated with that station may be migrated to redistribute the load. As a result, stations may fail but duplicated objects continue to exist as if nothing had happened.

Duplicated objects are not fault-tolerant by default. It is up to you to decide whether to make an object fault-tolerant. Use the DuplicatedObject::ApproveFaultRecovery system callback to define an object as fault-tolerant. If the callback returns true, the object is fault-tolerant. If it returns false, the object is not fault-tolerant and ceases to exist if and when the station on which its duplication master resides fails.

The following code shows an implementation of the ApproveFaultRecovery system callback that specifies that Sphere objects are fault-tolerant.

Code 11.20 Sample Implementation of the ApproveFaultRecovery System Callback

Sphere:: public DOCLASS(Sphere)
{
    public:
        Sphere();
        ~Sphere();

        qBool ApproveFaultRecovery()
        {
            return true;
        }
    // Game specific methods and variables
};

Conversely, when a station quits the session without a fault having occurred, the duplication masters on the station are destroyed unless the DuplicatedObject::ApproveEmigration system callback returns true. Use this callback to allow control of a duplication master to migrate between stations. The callback must return a reason for the object to migrate. In NetZ, this reason can only be migration caused by leaving the session (MIGRATION_REASON_LEAVING_SESSION). If the callback returns true, the object can migrate between stations. If the callback returns false, the object cannot migrate and ceases to exist when the station on which its duplication master resides leaves the session. If you want an object to survive even if its duplication master resides on a station that leaves the session because of a fault or simply quits the session, both the ApproveFaultRecovery and ApproveEmigration system callbacks must return true. For example, if you want Spheres to survive when the duplication masters reside on a station that quits the game, and when a fault occurs, implement the following code.

Code 11.21 Making Objects Survive Even When a Fault Has Occurred

Sphere:: public DOCLASS(Sphere)
{
    public:
        Sphere();
        ~Sphere();

        qBool ApproveFaultRecovery()
        {
            return true;
        }
        qBool ApproveEmigration(qUnsignedInt32 uiReason)
        {
            if (uiReason==MIGRATION_REASON_LEAVING_SESSION)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
    // Game specific methods and variables
};

If a station loses only the connection to the session it is participating in rather than losing all functionality (such as occurs when the LAN connection of a station is disabled rather than loses power), the session may continue to run on the faulting station. When this situation occurs, there are effectively two instances of the same session. Both sessions can be joined by other stations. However, the two sessions cannot rejoin each other’s session.

When a station fails, the PromotionReferee system object uses affinity levels to decide which station fault-tolerant objects migrate to. When the duplication master of a fault-tolerant object resides on a station that quits the session, because of a failure or otherwise, each station that holds a duplica of that object calls the PromotionReferee member function using the ComputeAffinityCallback callback. This call indicates the affinity level of the station for the object and indicates that the station wants to become the new duplication master. By default, the station with the highest affinity level becomes the new duplication master. The default affinity level for each process is: server=100, client=50, and tool=0. In addition, the session master is assigned 10 bonus points, and by default unknown processes are assumed to be client processes. Objects are preferentially migrated to server processes rather than client processes, and to client processes rather than tool processes. If two or more processes of the same type call the PromotionReferee member function and none of the processes is the session master, the first station to call the PromotionReferee member function is elected as the new duplication master.

Call the ComputeAffinityCallback callback to change the default behavior of the PromotionReferee member function. This callback assigns the affinity for a particular object of a particular station, and how long the station wants the PromotionReferee member function to wait before assigning the new duplication master. The default wait time is 1000 milliseconds for a tool process (to provide more time for another process to take control of the object), 500 milliseconds for a client process, and 0 milliseconds for a server process. Consequently, the call returns as soon as a server process notifies the PromotionReferee member function that it wants to take control of an orphaned object. This callback must be called by one of the stations of the PromotionReferee, using the RegisterAffinityCallback member function. For example, to always assign orphaned objects to a server process as long as a server process exists, implement the following code. In this case, objects are randomly assigned to a server process to avoid one server taking control of all objects and risking an overload. To distribute server load, load balancing can be implemented across the server cluster.

Code 11.22 Specifying a Server Process as Long as One Exists

void ServerTakeOverAllObjects(DOHandle hObject, qByte* pbyAffinity,
                              TimeInterval* ptiWaitTime)
{
    // No matter what hObject is, we do the following. In a real
    // application, the actual affinity would probably depend on
    // the type of hObject.
    Station::Ref refLocal(Station::GetLocalStation());
    SYSTEMCHECK(refLocal.IsValid());
    if (refLocal->GetProcessType()==Station::ServerProcess)
    {
        // If I'm a server, then I select a random value between
        // 1 and 101. This is my affinity. Using a random value
        // will cause a uniform distribution of the objects on
        // the failed station over the servers.
        * pbyAffinity = 1 + static_cast<qByte>(Platform::GetRandomNumber(100));
        // Since this is a server process we use a low wait time
        // as it doesn't really matter which server we choose.
        * ptiWaitTime = 200;
    }
    else
    {
        // The station is not a server. We do not want this
        // station to take control of the object so we set
        // a low affinity.
        * pbyAffinity = 0;
        // And we set a long wait time as we want to wait for
        // a server to see the fault as well.
        * ptiWaitTime = 2000;
    }
}

To ensure that the ComputeAffinityCallback that a station registers is valid, the PromotionReferee can call the RegisterAffinityValidationCallback callback on a particular station. This callback verifies if the affinity of a particular station for a particular object is valid. For example, you could implement the following when the valid affinity level is between 1 and 101 for a server process, and zero for a client process.

Code 11.23 Assuming Affinity Levels of 1 to 101 for a Server Process and 0 for a Client Process

qByte DoubleCheckThatServerTakesOver(DOHandle hObject, DOHandle hCalleeStation,
                                     qByte byAffinity)
{
    // The callee station hCalleeStation proposes an affinity,
    // so we check if it is a valid value. Clients are cheating
    // if the value is not zero. Servers are cheating (or most
    // probably there is a bug in the callback that reports
    // affinity) if the value is not between 1 and 101.
    Station::Ref refCallee(hCalleeStation);
    SYSTEMCHECK(refCallee.IsValid());
    if (refCallee->GetProcessType()==Station::ServerProcess)
    {
        SYSTEMCHECK(byAffinity>0);
        return byAffinity;
    }
    else
    {
        // This is not a server.
        if (byAffinity > 0)
        {
            // Then the station must be cheating. At this point,
            // we could decide to flag the station as a cheater
            // and take action. We also report 0 as the affinity
            // and ignore whatever value the station claimed.
            return 0;
        }
        return byAffinity;
    }
}

PromotionReferee specifies the duplica that is promoted to the duplication master of the object after a fault. The developer decides whether to ensure that the duplica contains all the necessary information for that object to take over as the duplication master. During duplicated object migration, NetZ ensures that all relevant network information is transferred to the new object master, but depending on the object, the master may have access to additional data (such as physics calculations or AI models) that its duplicas know nothing about. If the duplication master requires this additional information to continue, or so that the migration is transparent to the players, you must ensure that this information is transferred to the new duplication master. How and what information is transferred depends entirely on the object and its requirements. For example, you may need to send the target position, direction, or behavior of an object so that the new master can continue to compute the behavior of the object correctly. When implementing station filters on datasets, as described in Section 14.1 Creating Duplicated Objects, ensure that the datasets of the promoted duplica are up-to-date enough for the object to seamlessly take over as master.

11.9. Call Contexts

To invoke an RMC or to migrate objects, the context of the call must be defined using either the RMCContext or MigrationContext classes. These two classes inherit from the DOCallContext class, which in turn inherits from the CallContext class. By using these classes you set the target station of the call context and the appropriate flags. You can also get information about the state and the outcome of the call context, and you can also cancel or reset a call context. Use the QSUCCEEDED macro or cast to qBool to determine whether the result stored in Outcome indicates that the process was successful.

You can use the DOCallContext class to set flags that affect how a particular call is made, as listed in Table 8.1. Note that the SendUnreliableMessage, CallOnDuplicas, and CallOnNeighbouringStations flags can only be set if the OneWayCall flag is also set. If either the TargetObjectMustBeMaster or TargetObjectMustHaveAuthority flag is set on an RMC, the call is made on the station where the duplication master resides regardless of the specified target station. Note that these two flags are guaranteed to be true only the first time that an RMC is called. When postponing the call by using the CallMethodOperation::PostponeOperation member function, these flags are not rechecked and may no longer be true. Moreover, you can set the RetryOnTargetValidationFailure flag in conjunction with only one of these two flags. If the SynchronousCall flag is not set, the system operates in a synchronous manner by default. The thread from which the call context was made waits for the call to return a value, or an error, before performing the next task.

Table 11.1 DOCallContext Flags
Flag Feature
CallOnDuplicas The method is called on all duplicas. RMCContext only.
CallOnLocalStation The method is called on the local station. RMCContext only.
CallOnMaster The method is only called on the duplication master RMCContext.
CallOnNeighbouringStations The method is called on all neighboring stations of the local station. RMCContext only.
CallOnTargetStation This flag cannot be set directly. It is set automatically when DOCallContext::SetTargetStation is called.
CancellationNotAllowed The call cannot be canceled. MigrationContext only.
DeleteOnCompletion The CallContext of the call is deleted upon the completion of the call.
OneWayCall The call is one-way. The call does not return the result of the called method. RMCContext only.
RetryOnTargetValidationFailure If the outcome of a call is ErrorInvalidRole or ErrorNoAuthority, the call tries to resend the message to the duplication master until the outcome is Success, or until it returns an error other than ErrorInvalidRole or ErrorNoAuthority. RMCContext only.
SendUnreliableMessage The call is sent over an unreliable channel. RMCContext only.
SendUnbundledMessage The call message is not bundled and is sent as soon as possible. RMCContext only.
SynchronousCall The call is synchronous. The system waits for the call to return a value or an error before performing the next task.
TargetObjectMustBeMaster The call is only invoked on an object that is a duplication master. RMCContext only.
TargetObjectMustHaveAuthority The call is only made on an object that has authority. RMCContext only.

When a response is received, the State and Outcome of the call context change. When a call context is made asynchronously, you can use the DOCallContext::Wait member function to set a timeout that specifies how long the thread calling the DOCallContext::Wait member function waits for the call context to be processed before proceeding. If a response is received before the end of the timeout, the Wait call terminates. When the call context is made synchronously, the thread where the call context was initiated waits until it receives a response from the call context or until an error occurs before proceeding. Effectively, this is equivalent to setting the wait time to infinite on an asynchronous call. Use the DOCallContext::SetFlag and DOCallContext::ClearFlag member functions to set and clear the flag. Use the DOCallContext::FlagIsSet member function to determine whether a particular flag is set. You can also use the DOCallContext::SetTargetStation and DOCallContext::GetTargetStation member functions to set and get the target station of the call context. The significance of the target station depends on the specific call context. For an RMCContext, the target station is the station that the RMC is called on. For a MigrationContext, it is the station that you want to migrate an object to.

A call context may be in one of five states, as shown in Figure 11.2 Call Context States: Available, CallInProgress, CallSuccess, CallFailure, or CallCancelled. A call context is initially in the Available state. When it is called by the user, it transitions to the CallInProgress state, after which it can transition to CallSuccess, CallFailure, or CallCancelled. When a call context is in the CallInProgress state, the user can use the DOCallContext::Cancel member function to cancel the call context, after which the state changes to CallCancelled. When the state is CallSuccess, CallFailure, or CallCancelled, you can call the DOCallContext::Reset member function to reset the state to Available. The DOCallContext::GetState member function returns the current state of the call context. If DOCallContext::Cancel is called and the DOCallContext is not allowed to be canceled, as is the case for a migration call, the member function locks until the call context terminates.

_images/Fig_DO_Basic_State_RMCContext.png

Figure 11.2 Call Context States

The manner in which a call context terminates can be obtained by using the GetOutcome method. Termination has one of the following outcomes.

  • Success: The call context terminated successfully.
  • ErrorInvalidRole: The call context was called on a duplica when the TargetObjectMustBeMaster flag was set.
  • ErrorObjectNotFound: The object could not be found on the target station, either because the object was deleted or because it does not currently exist on that station.
  • ErrorLocalStationLeaving: The local station is leaving the session.
  • ErrorStationNotReached: The request was sent but it did not reach the target station.
  • ErrorTargetStationDisconnect: The request was sent, but the target station disconnected before a response was received.
  • ErrorInvalidParameters: The call was made with invalid parameters.
  • ErrorAccessDenied: The particular call was denied by the target.
  • ErrorNoAuthority: The target object did not have authority to perform the call.
  • ErrorRMCDispatchFailed: The request could not be sent from the station initiating the call. (Only valid for an RMC.)
  • ErrorMigrationInProgress: A migration call is in progress. (Only valid for a migration call.)
  • ErrorCallTimeout: The call failed because it did not complete before the configured timeout.
  • ErrorReliableSendBufferFull: The send buffer is temporarily full and the call cannot be made.
  • ErrorPacketBufferFull: The packet buffer is temporarily full and the call cannot be made.
  • CallPostponed: The call was postponed.
  • UnknownOutcome: The outcome of the call context is unknown because it has not returned a response.

Use the CallContext class to call user-defined callbacks upon completion of a CallContext. Completion callbacks present an easy and efficient way to ensure that a particular function is called when a CallContext completes. When member functions are called asynchronously, using a completion callback avoids having to continually poll the CallContext to determine whether it has completed. When multiple callbacks are registered, by default they are kept and then called in the order registered. The callback that was registered first is the first one called. To change the order, you must set the bAddToEnd parameter when you register a callback. Use the RegisterCompletionCallback member function to register a completion callback to a particular CallContext. The member function has the following two syntaxes.

Code 11.24 Member Function Syntaxes for RegisterCompletionCallback

void CallContext::RegisterCompletionCallback(
                    CompletionCallback pfCompletionCallback,
                    const UserContext & oContext, qBool bAddToEnd=true);
void CallContext::RegisterCompletionCallback(CallbackRoot * pCallback,
                    qBool bCallOnSuccess=true, qBool bAddToEnd=true)

This member function takes either a pfCompletionCallback pointer to the CompletionCallback function called when the CallContext completes, or a pContext pointer to a CallbackRoot object. If you choose to use the second syntax, you must use the Callback template class to define the CallbackRoot object.

A following code shows a partial implementation of a completion callback. For this example, first declare the ChangeOwner RMC in the DDL.

Code 11.25 ChangeOwner RMC Declaration

// DDL declarations for Item class.
doclass Item : Duplicated3DObject {
    ItemOwnership m_dsOwner;
    bool ChangeOwner(int dohandle hCurrentOwner, int dohandle hNewOwner,
                     dohandle hTargetItemMaster);
};

Next, implement the PickUp member function that uses the ChangeOwner RMC in the Inventory.cpp file.

Code 11.26 Implementing the PickUp Member Function

qBool Inventory::Pickup(RMCContext *pContext, qBool* pbResult, DOHandle hItem,
                        qBool bMigrate) {
    // We create a Callback to the method Inventory::AddItem.
    // This callback will be called when the RMC completes. The
    // CallChangeOwner method prepares and performs the actual
    // RMC call.
    return CallChangeOwner(pContext, pbResult, hItem, INVALID_DOHANDLE,
                           m_pContainer->GetHandle(), qNew Callback<Inventory,
                           DOHandle > (this, &Inventory::AddItem, hItem),
                           bMigrate);
}

Then call the RMC, as follows. The Inventory::AddItem member function is called when the RMC completes.

Code 11.27 Calling the ChangeOwner RMC

qBool Inventory::CallChangeOwner(RMCContext* pContext, qBool *pbResult,
            DOHandle hItem, DOHandle hRequiredCurrentOwner,
            DOHandle hNewOwner, CallbackRoot* pCallback, qBool bMigrate)
{
    // Perform a series of checks to ensure that the call is
    // valid. Details are given in the framework source code.
    SYSTEMCHECK(m_pContainer!=NULL);
    if (!m_pContainer->IsADuplicationMaster()) {
        qDelete pCallback;
        return false;
    }
    Item::Ref refItem(hItem);
    if (!refItem.IsValid()) {
        qDelete pCallback;
        return false;
    }
    if (refItem->GetOwner()!=hRequiredCurrentOwner) {
        qDelete pCallback;
        return false;
    }

    // Once we've ensured that the call is valid we proceed to
    // the real call.
    pContext->RegisterCompletionCallback(pCallback);
    pContext->SetTargetStation(refItem->GetMasterStation());
    pContext->SetFlag(DOCallContext::TargetObjectMustHaveAuthority);
    return refItem->CallChangeOwner(pContext, pbResult, hRequiredCurrentOwner,
        hNewOwner, bMigrate ? m_pContainer->GetMasterStation():
                              INVALID_DOHANDLE);
}

11.10. User-Defined Contexts

The user-defined value passed by the UserContext class can be an unsigned integer, double, Boolean, or pointer to an object. This is typically used to pass a value that is not automatically retained by NetZ, but that the user wants to retain. The UserContext class is typically used in conjunction with the Operation class, but it can also be used together with any other class in a game. For example, when a ChangeMasterStationOperation occurs, NetZ does not retain the original session master ID of the object. However, if you want to retain this information it can be passed as user-defined data before the operation begins, and retrieved after the operation ends. When user-defined data is passed in the context of an operation, you must use the SetUserData and GetUserData member functions of the Operation class, as shown in Section 13.1 Remote Method Calls.


CONFIDENTIAL