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.
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();
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.
Figure 11.1 Transferring Object State and Object Authority During an Object Migration
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)
WellKnown
ObjectsA 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.
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();
}
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;
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.
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.
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:
- Fault tolerance to ensure that an object survives a fault.
- Load balancing to distribute duplication master objects, and thus the processing load and bandwidth.
- 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.
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.
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.
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.
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);
}
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