The NetZ
data definition language (DDL) enables developers to describe the data that needs to be sent over the network in a few simple statements, and then to generate the networking layer. That is, the developer uses one or more DDL files to describe the data structures and game objects for which NetZ
manages the networking layer.
For a single game, the DDL compiler expects one or more DDL files. This chapter explains the DDL syntax, how to use the DDL compiler, and the class generation that is performed by the compiler. In addition, it describes in detail the information that you are required to provide.
The NetZ
DDL is an object-oriented language based on classes. The three most important classes are the DuplicatedObject
, DupSpace
, and DataSet
classes. These classes are the base classes for all game objects and their related datasets. RMCs and actions are the two ways to remotely call a method. All duplicated objects, datasets, RMCs, and actions are declared in the DDL file. Comments within the DDL code are indicated by two backslashes at the beginning of the line.
NetZ
has several commonly used built-in data types, in addition to a number of supplementary data types that can be included if desired. The developer can also define custom data types. Once implemented, custom types can be used within the DDL like any other data type.
Variables declared in the DDL may use any of the built-in data types, or any valid user-defined data type. The supported built-in data types are listed in the following table. An array of any of these types is also supported. For cstr
, the variable is duplicated up to the terminating null character. A dataset sent over an unreliable channel cannot contain more than 3584 bytes of data. The DDL types are mapped to C++ types according to the following table.
DDL | C++ |
---|---|
bool |
qBool |
byte |
qByte |
int8 |
qInt8 |
int16 |
qInt16 |
uint16 |
qUnsignedInt16 |
int32 |
qInt32 |
uint32 |
qUnsignedInt32 |
int64 |
qInt64 |
uint64 |
qUnsignedInt64 |
float |
qFloat |
double |
qDouble |
real |
qReal |
qchar |
qChar |
qchar8 |
qChar8 |
qchar16 |
qChar16 |
cstr |
qChar* |
string |
String |
dohandle |
DOHandle |
stationurl |
StationURL |
datetime |
DateTime |
qresult |
qResult |
qbuffer |
qBuffer |
qlist |
qList |
qvector |
qVector |
qqueue |
qQueue |
memberlist |
MemberList |
memberqueue |
MemberQueue |
membervector |
MemberVector |
To facilitate data manipulation, NetZ
provides several additional data types that you can include when you compile your project. In particular, you can use the following C++ STL data types.
DDL | C++ |
---|---|
std_ string |
qChar |
std_ char8string |
qChar8* |
std_ char16string |
qChar16* |
std_ bitset8 |
std:: bitset<8> |
std_ bitset16 |
std:: bitset<16> |
std_ bitset32 |
std:: bitset<32> |
std_ vector T |
std:: vector T |
std_ list T |
std:: list T |
std_ map Key, Value |
std:: map Key, Value |
To integrate these STL data types into a project, you must use the following code to include the STL.ddl
file in the DDL file of your project.
Code 14.1 Including the STL.dll
File in a DDL File
#include <Extensions/STL.ddl>
Next, specify which of the following STL libraries to link with.
Project Settings | STLExt Library |
---|---|
Unicode debug | STLExtud.lib |
Unicode release | STLExtu.lib |
The developer can define custom data types in the DDL. After these data types are defined in the DDL, they can be used as if they were built-in types. The following syntax is used to define a custom type in the DDL.
Code 14.2 Defining a Custom Type
type TypeName;
TypeName
is the name of the new type. When a new type is declared in the DDL, the developer must implement a C++ class with the name DDLTYPETypeName
to implement marshalling (that is, C++ mapping and in/out streaming) for this new data type. Within this class, the type definition for C++ mapping must have the form typedef TypeName CPPType
. For example, to integrate STL strings as DDL types, you could define the following type.
Code 14.3 Sample Custom Type Definition (DDL)
type std_string;
When this declaration is found in the MyDDL.ddl
file, the MyDDLTypes.h
file must contain a C++ class to marshall std_strings
.
Code 14.4 Sample Custom Type Definition (C++ Class)
class DDLTYPE(std_string) {
public:
// mapping of DDL type to a C++ type
typedef std::string CPPType;
static void Add(Message* pMessageToSend,
const std::string& oString) {
// Code to add the string to the message.
}
static void Extract(Message* pReceivedMessage,
std::basic_string<C>* pString) {
// Code to extract the string from the message.
}
};
You could also integrate a user-defined template as a DDL type by defining the following type.
Code 14.5 Integrating a User-Defined Template (DDL)
type umap <T1, T2, ... >;
Next, use the following code to implement the template. Note that a parameter specified in a template is a DDLType
and not a CPPType
.
Code 14.6 Integrating a User-Defined Template (C++)
template <class DDLKey, class DDLValue>
class DDLTYPE(umap) : public DDLType {
public:
typedef NS_QSTL Map<DDLKey::CPPType,
DDLValue::CPPType> CPPType;
static void Add(Message * pMessageToSend,
const CPPType &uMap){
(*pMessageToSend) << (qUnsignedInt32)uMap.size();
CPPType::const_iterator i;
CPPType::const_iterator end = uMap.end();
for( i = uMap.begin(); i != end; i++ ){
DDLKey::Add( pMessageToSend, (*i).first );
DDLValue::Add( pMessageToSend, (*i).second );
}
};
static void Extract(Message *pReceivedMessage,
CPPType *pUMap ){
qUnsignedInt32 uiSize;
(*pReceivedMessage) >> uiSize;
qUnsignedInt32 i;
for(i = 0; i < uiSize; i++){
DDLKey::CPPType k;
DDLValue::CPPType v;
DDLKey::Extract( pReceivedMessage, &k );
DDLValue::Extract( pReceivedMessage, &v );
(*pUMap)[k] = v;
}
};
}
The distribution includes complete implementations of these examples of custom data types. These examples can be found in files with the .stl
extension, located in the include/Extensions/STL
directory. In the STL.ddl
and STLTypes.h
files, the .cpp
files define and implement standard C++ bitsets and strings.
If a custom data type is assigned to a variable, the developer must implement streaming to and from a message so that the system knows how to treat the data. When this data is sent between stations, the system creates a message, and then the developer must use the methods of the Message
class to add and extract the data. Data can be added to the message by using the stream operators or the AddData
and AddDataString
methods. These methods respectively add data of a specific type or a string. Similarly, use the ExtractData
method to extract data from the message buffer. Call GetPayloadSize
to get the size of the message content. Call the GetLastError
method to get the last error generated by the class. This method is particularly useful in determining whether an error occurred during a stream operation. The distribution includes a sample implementation of custom data types. These sample data types can be found in files with the STL
extension, located in the ../include/Extensions/STL
directory.
When implementing a custom type, you must implement the following methods in your user-implemented C++ class.
operator=
operator==
The copy constructor is required only if one of the following conditions is met.
Custom foo()
void foo(Custom toto)
If you only ever pass the custom type by reference, the copy constructor is not required.
As an alternative to defining custom data types, data types can be declared as a class. This can simplify implementation in some cases. Class types are defined using the following syntax.
Code 14.7 Defining a Class Data Type
class ClassName [: inheritance_class] {
class attributes
} [concrete];
in this example, class attributes
can be any number of data types, as described previously, and inheritance_class
refers to the name of another class from which ClassName
inherits. From this declaration, the DDL compiler generates the ClassNameDDL.h
/.ccp files
and the DDLCLASS(ClassName)
class, from which the ClassName
class inherits. If you want to add functionality to the ClassName
class, you must implement the class yourself. If not, you can use the concrete
property. When this property is declared, the DDL compiler generates the base C++ class for the declared class. This class contains no methods and cannot be modified. For example, say that you declare the following in the DDL.
Code 14.8 Using the concrete
Property
class Request{
int32 m_intPlayerID;
byte m_byType;
} concrete;
This generates the DDLCLASS(Request)
class, and the RequestDDL.h
and RequestDDL.cpp
files. When the concrete
property is specified, the Request
class is generated within the RequestDDL.h
/.cpp
files and cannot be modified. When not using the concrete
property, you are expected to implement the Request.h
/.cpp
files.
Next, consider the built-in DDL properties of NetZ
. The majority of these properties are only valid for dataset declarations. The only incompatible properties are buffered
and extrapolation_filter
, which cannot be declared together. You may also implement your own custom DDL properties as detailed later in this section.
DDL properties are primarily used to specify the manner in which a dataset is updated. The default assumption is that the variables of a dataset change so that each duplica is updated each time the DuplicatedObject::Update
function is called, and that the data is sent over a reliable channel to guarantee delivery. If you want to update a dataset in a way that differs from these assumptions, you must include one or more DDL properties in the DDL declaration of the dataset.
Property | Valid In | Description |
---|---|---|
unreliable |
Dataset | Dataset updates are sent over an unreliable channel. Updates are transmitted faster than over a reliable channel, but delivery is not guaranteed. This property is suitable for datasets that contain variables that change frequently, such as position, where a missing update is not detrimental to the game. A dataset sent over an unreliable channel cannot contain more than 3584 bytes of data. Normally, there is a possibility that data arrives that has duplicated data or for which the order has changed. With RootTransport:: EnableDropDuplicateReorderingUnreliablePackets(true); , data that is duplicated or for which the order has been changed can be lost. |
buffered |
Dataset | The values of the dataset data members on the duplica are not directly updated. When an update message is received by the duplica, it stores the result in a buffer. The buffer content is copied to the dataset member variables when the application calls DuplicatedObject::Refresh . The buffer is automatically locked when data is transferred to or from it. This property cannot be used together with the extrapolation_filter property. |
Constant |
Dataset | The values of the dataset data members are constant. When the duplicated object is created, the dataset values are sent to its duplicas and—because they do not change—they are never updated. |
update_ perf_ counter |
Dataset | A DataSetPerfCounters class, accessed using DataSet::GetPerformanceCounters , is created for the dataset. Performance data for the dataset can be recorded to aid in fine-tuning and optimizing network performance. For more information, see Section 17.1 Performance Fine-Tuning. |
upon_ request_ filter |
Dataset | Dataset updates are sent only when DataSet::RequestUpdate is called. This property is used for variables that do not change often, such as weapons inventories. |
station_ filter |
Dataset | This property is used to filter out the stations that a dataset is updated on, for optimization and security reasons. To specify this filter, you must implement the DataSet::UpdateIsRequired system callback to specify which stations to update the dataset on. |
extrapolation_ filter |
Dataset | This property applies a prediction algorithm to the values of the dataset members on the duplicas. This property is suitable for datasets that change in a continuous way, such as position. NetZ also provides options to indicate that there can be occasional continuity breaks, even though the dataset usually changes in a continuous way. When using an extrapolation filter, you must call the DuplicatedObject::Refresh method to update the dataset members. A default value of zero is used when the error tolerance is not set. The valid types are int32 , int16 , int8 , float , and double . This property cannot be used together with the buffered property. |
Loopback |
Dataset | This property indicates that the values of the dataset members on the duplication master are interpolated. This property can only be used with the extrapolation_filter property. The valid types are int , float , and double . |
Concrete |
Dataset, class | If this property is declared, the DDL compiler generates the base C++ class (the source and header files) for the declared dataset, which contains no methods and cannot be modified. Use this property when you do not wish to add any functionality to the dataset class. When adding functionality, do not specify this property. In this case, you must then implement the class yourself. |
Globaldo |
Doclass | All NetZ duplicated object classes are automatically instantiated as global duplicated objects. This means that they are known globally across the session and are guaranteed to be duplicated to all stations participating in the session. |
nonglobal |
Doclass | This property is not normally used. (These objects are global by default. To make them non-global, use the nonglobal property in the class declaration in the DDL file.) |
If the DDL properties provided with NetZ
are not adequate for your needs, you can implement your own custom dataset or duplicated object properties as described in this section. Custom dataset properties enable you to update your datasets in precisely the manner you choose, and custom duplicated object properties enable you to add custom code in the DDL-generated files.
To implement your own dataset DDL properties, you must perform the following steps.
dsproperty
DDL declaration.A custom dataset DDL property must be declared in the DDL file using the following syntax.
Code 14.9 Declaring a Custom Dataset DDL Property
dsproperty PropertyName : <property qualifier>;
PropertyName
is the name of your custom DDL property. property qualifier
is one or more qualifiers that define the type of DDL property. The specified property qualifiers can be updatefilter
or code
. After you have declared your custom dataset DDL property, you can then assign it to the appropriate datasets in the usual manner.
You can specify the following property qualifiers in a doclassproperty
declaration.
Code 14.10 Property Identifiers
code
This property qualifier enables you to add custom code in the file generated by the DDL compiler for a particular duplicated object. The following four macros are used by the generated code.
Code 14.11 Macros Used by Code Generated for Property Identifiers
// public declaration
_PR_PropertyName_pudecl()
// private declaration
_PR_PropertyName_prdecl()
// source file implementation
_PR_PropertyName_impl(DOCLASS)
// source file implementation of a derived class
_PR_PropertyName_derived_impl(DOCLASS,PARENT)
PropertyName
is the name of your custom duplicated object DDL property. DOCLASS
is the name of the DuplicatedObject
class that uses the property. PARENT
is the name of the DuplicatedObject
class from which the DOCLASS
inherits.
For example, in the DDL file, define the MyDO
class to use a property that specifies the code
property qualifier. The previous four macros are then used by the DDL-generated DATASET(MyDO)
class. More specifically, the first two macros are expanded respectively in the public and private declarations of the MyDODDL.h file
, and the last two macros are expanded in the MyDODDL.cpp
file.
DDLFileNameProperties.h
file. By using the macros, you define the name of the class that implements the associated DDL property.To implement your own duplicated object DDL properties, you must do the following.
doclassproperty
DDL declaration.You must declare custom duplicated object DDL properties in the DDL file using the following syntax.
Code 14.12 Declaring Custom Duplicated Object DDL Properties
doclassproperty PropertyName : <property qualifier>;
PropertyName
is the name of your custom DDL property. property qualifier
is one or more qualifiers that define the type of DDL property. The specified property qualifier can only be code
.
After declaring a custom duplicated object DDL property, you can assign it to the appropriate duplicated objects in the usual manner.
You can specify the following property qualifiers in a doclassproperty
declaration.
Code 14.13 Property Identifiers
code
This property qualifier enables you to add custom code in the file generated by the DDL compiler for a particular duplicated object. The following four macros are used by the generated code.
Code 14.14 Macros Used by Code Generated for Property Identifiers
// public declaration
_PR_PropertyName_pudecl()
// private declaration
_PR_PropertyName_prdecl()
// source file implementation
_PR_PropertyName_impl(DOCLASS)
// source file implementation of a derived class
_PR_PropertyName_derived_impl(DOCLASS,PARENT)
PropertyName
is the name of your custom duplicated object DDL property. DOCLASS
is the name of the DuplicatedObject
class that uses the property. PARENT
is the name of the DuplicatedObject
class from which the DOCLASS
inherits.
For example, in the DDL file, define the MyDO
class to use a property that specifies the code
property qualifier. The previous four macros are then used by the DDL-generated DATASET(MyDO)
class. More specifically, the first two macros are expanded respectively in the public and private declarations of the MyDODDL.h file
, and the last two macros are expanded in the MyDODDL.cpp
file.
DDLFileNameProperties.h
file. By using the macros, you define the name of the class that implements the associated DDL property.The application title defines the type of game. It is necessary to define an application title in the DDL file because it is used to authenticate any station that tries to join the session. If more than one DDL file is used, the application title must be defined in a DDL file that includes or uses all the other relevant DDL files. The application title is also included in the SessionDescription
. It should not be confused with the session name, which is used to identify a specific instance of a type of game. The session name is defined when a session is created using the Session::CreateSession
member function, as detailed in Section 5.1 Session Management. Unlike the application title, the session name does not have to be defined. The application title is defined in the DDL using the following syntax.
Code 14.15 Defining the Application Title
title "string";
To define the application title of a game as SphereZ
, you would make the following declaration in the DDL file.
Code 14.16 Example of Defining the Application Title
title "SphereZ";
In a multiplayer game, the objects of the session must be duplicated across the network for the players to see each other. NetZ
uses object duplication as the mechanism to achieve this. NetZ
duplicated objects are declared using duplicated object classes (DOClasses
). A duplicated object is an instance of a duplicated object class, and you can create multiple instances of the same duplicated object class.
The relationship between a duplicated object and a duplicated object class is very similar to the relationship between an object and a class in standard C++ programming. The distinction between C++ objects and NetZ
duplicated objects is apparent when an object is instantiated. In NetZ
, whenever an instance of a duplicated object class is created on a particular station, copies of the instance are automatically created on all remote stations that require that object. The local duplicated object is called the duplication master and is the controlling instance of the object. The other duplicated objects are its duplicas.
A duplicated object class is declared in a DDL file, which also declares the relevant datasets, RMCs, and actions. The other elements of the DDL file are explained in more detail later.
Use the following syntax to declare a duplicated object in the DDL file.
Code 14.17 Declaring a Duplicated Object
doclass ObjectName [: inheritance_class] {
[declarations];
};
[inheritance_class]
refers to the name of another duplicated object class from which the ObjectName
class inherits. [declarations]
are all of the relevant dataset, RMC, and action declarations for the class.
By default, all objects in NetZ
are global. Each class defined in the DDL is known globally across the session, and is guaranteed to be duplicated to all stations participating in the session. (You do not normally need to make these classes non-global.)
Duplicated objects can also be WellKnown
objects. The difference between a global duplicated object and a WellKnownDO
is that a WellKnown
object is created before the session. 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
instances of a particular duplicated object class are declared in the DDL file. When a duplicated object is declared as a WellKnown
object, the DDL compiler declares and initializes a global variable of type WKHandle
on each station. This variable can be used to construct a DOHandle
to the object. You must use the DuplicatedObject::CreateWellKnown
function to create these instances on the session master, after which they are duplicated to all stations that belong to the session.
Code 14.18 Declaring a WellKnown
Instance
wellknown DOClassName aVariableName;
In our SphereZ
example, we create a WellKnown
instance of the DOClass
World
as follows.
Code 14.19 Example of Declaring a WellKnown
Instance
wellknown World g_hWorld;
One instance of the World
is well-known, so global variable g_hTheWorld
of type WKHandle
is defined in the file generated by the DDL compiler. Similarly, the compiler generates a variable of type DOHandle
for each class defined in the DDL. This variable is used to refer to a duplicated object of a particular class. The WKHandle
instance inherits from the DOHandle
class. For more information about implementing the WellKnownDO
class, see Section 11.4 WellKnown
Objects.
Part of the DDL file for the hypothetical SphereZ
sample program is shown in the following example. Four duplicated object classes are declared in the DDL: GeometricalObject
, Sphere
, AISphere
, and World
. Each of these DOClasses
can contain datasets, RMCs, and actions. The AISphere
datasets, RMCs, and actions are inherited from the Sphere
class. The datasets, RMCs, and actions of the Sphere
class are in turn inherited from the GeometricalObject
class. This example is expanded upon elsewhere in this manual to introduce new concepts.
Code 14.20 Partial DDL for the SphereZ
Sample Program
doclass GeometricalObject {
[declarations];
};
doclass Sphere : GeometricalObject {
[declarations];
};
doclass AISphere : Sphere {
[declarations];
};
doclass World {
[declarations];
};
wellknown World g_hWorld;
You must write the source and header files and then implement the class for each duplicated object class that you define in the DDL file. For more information, see Section 14.5 Developer Responsibilities and Section 11.1 Creating Duplicated Objects.
A dataset is defined in the DDL using the following syntax.
Code 14.21 Defining a Dataset
dataset DatasetName {
Dataset attributes;
}[Update Policy];
Dataset attributes can be any number of simple data types described in Section 14.1 DDL File Syntax. However, when using an extrapolation filter, you can only use the types int
, float
, and double
.[Update Policy] specifies the manner in which the dataset is updated by using any combination of valid DDL properties, as described in Section 14.1.
When more than one DDL property is specified, separate each property by using a comma.
The default assumption is that the variables of a dataset change so that each duplica is updated each time the DuplicatedObject::Update
function is called, and that the data is sent over a reliable channel to guarantee delivery. If you want to update a dataset in a way that differs from these assumptions, you must include one or more DDL properties in the DDL declaration of the dataset.
Continuing the SphereZ
example started previously, define three datasets in the DDL: Position
, SphereData
, and Weather
. The following sections shows the update policies for these datasets.
This dataset contains the variables x, y, and z, which define the location of a sphere. A prediction algorithm is used to update the Position
dataset, and updates are sent over an unreliable channel. The extrapolation filter instructs NetZ
to predict the values for x
, y
, and z
between network updates, which smooths the dataset values.
SphereData
This dataset contains the Texture
variable that defines the appearance of a sphere. The updates for the SphereData
dataset are sent over a reliable channel, and are updated only when requested. This dataset does not change often, but it is important that the object is notified of every change.
World
This dataset contains the FogEnabled
variable that defines whether fog is turned on or off. The World
dataset updates are sent using the default update policy. This means that they are sent over a reliable channel each time the data changes.
Code 14.22 Example of Defining a Dataset
dataset Position {
double x;
double y;
double z;
} unreliable, extrapolation_filter;
dataset SphereData {
uint16 m_ui16Texture;
} upon_request_filter;
dataset World {
int FogEnabled;
}
Any dataset defined in the DDL must be associated with a duplicated object. For example, use the following syntax to associate the Position
dataset with the GeometricalObject
duplicated object class.
Code 14.23 Example of Associating a Dataset With a Duplicated Object Class
doclass GeometricalObject {
Position m_dsPos;
// other declarations
};
Because the Sphere
duplicated object class inherits from GeometricalObject
, the Sphere
class includes the Position
dataset.
When declaring a dataset in the DDL file, the dataset must be implemented by the user, as detailed in Section 11.6 Datasets.
MemberContainers
The containers used in NetZ
function in a very similar manner to the containers of the C++ standard library. For more information about the usage of containers, refer to any good C++ programming book. The kinds of containers used in NetZ
are membervector
, memberlist
, and memberqueue
, and the supported types for the containers are the same as those listed previously for dataset variables, except for the string
type. When declared in the DDL, a container must specify that updates are sent over a reliable channel, which is the default, and they cannot specify the use of an extrapolation filter. The following gives a sample DDL definition of a MyContainer
dataset class that contains three containers and is associated with the Avatar
duplicated object class.
Code 14.24 Sample Container Definitions
dataset MyContainer{
membervector<double> vec;
memberlist<dohandle> lst;
memberqueue<int32> que;
};
doclass Avatar{
MyContainer m_dsContainer;
};
TYPE
is any of the indicated DDL dsts types
. Use the following code to create an iterator for this container.
Code 14.25 Container Iterator
MyContainer::vecIterator
NetZ
uses either remote method calls (RMCs) or actions to remotely call a method on a duplicated object. Remote method calls are described in this section, and actions are described in the following Section 14.1.8 Actions. RMCs are methods that are called on one specific instance of an object, which can be a duplication master or a duplica, and can also return a value. RMCs give the developer significantly more control over the method than actions both in how they are called and the information that the developer may get about the state and the outcome of the call.
Remote method calls are declared in the DDL file, and can contain parameters. The system assigns each declared RMC a unique MethodID
. The valid arguments are the same as the valid arguments for datasets, as detailed in Section 14.1 DDL File Syntax, and any valid custom data type. When a built-in template type (MemberVector
, MemberList
, or MemberQueue
) is defined as an RMC argument, NetZ
expects that the user-defined function uses a parameter type of the following form.
Code 14.26 Available Parameter Types in User-Defined Functions
MemberVector <DDLTYPE(type)>,
MemberList <DDLTYPE(type)>,
or MemberQueue <DDLTYPE(type)>
An RMC uses the following syntax.
Code 14.27 Syntax Used for an RMC
returntype RMCName( [Argument List] );
returntype
is either the parameter type of the return value. or void
. [Argument List]
can be any number of arguments, similar to a C++ function declaration.
The arguments of the input and output of the RMC can be specified using the in
, out
, or in out
modifiers as shown in the following example. In this example, a CarDealer
duplicated object class has the RMCs BestMatch
, ListMatches
, and UpdateList
. The first two methods respectively return the vehicle that best matches the specified criteria, which consists of the vehicle make, model, year, and price, and a list of all the matches for the criteria. The UpdateList
method updates the current list of available vehicles.
Code 14.28 RMC for the CarDealer
Class
doclass CarDealer {
dohandle BestMatch(int32 imake, int32 imodel, int32 iyear, int32 iprice);
void ListMatches(int32 imake, int32 imodel, int32 iyear,
out int32 iNbOfCars, out dohandle hCarArray[16]);
void UpdateList(in out dohandle hCarArray[16]);
// other declarations
};
When declaring an RMC in the DDL file, it must be implemented by the user, as detailed in Section 13.1 Remote Method Calls.
NetZ
uses either remote method calls (RMCs), as described earlier, or actions to remotely call methods on duplicated objects. Actions are methods that are called within a duplicated object class, and are the easiest way to call a method on an object to ensure that the method is executed on all of the duplicas of the object. However, as opposed to an RMC, an action cannot return a value.
Actions can be called on the duplication master or duplica of an object. When an action is called on a duplication master, the duplication master then calls the corresponding method on all of its duplicas. If an action is called on a duplica, that duplica calls the corresponding method on its duplication master. If necessary, the duplication master then updates the datasets of its duplicas.
Actions are declared in the DDL file and can contain parameters. The system assigns each declared action a unique MethodID
. The valid arguments are the same as the valid arguments for datasets, as detailed in Section 14.1 DDL File Syntax, and any valid custom data type. When a built-in template type (MemberVector
, MemberList
, or MemberQueue
) is defined as an action argument, NetZ
expects the user-defined function to use a parameter type of the form MemberVector <DDLTYPE(type)>
, MemberList <DDLTYPE(type)>
, or MemberQueue <DDLTYPE(type)>
.
The following syntax is used for actions. [Argument List]
can be any number of arguments, similar to a C++ function declaration.
Code 14.29 Action Syntax
action ActionName( [Argument List] );
In SphereZ
, the act of updating the weather is defined as an action because it is performed within a duplicated object class and can be called remotely. The following code shows declaration of the UpdateWeather
action in the DDL file.
Code 14.30 UpdateWeather
Action Declaration
doclass World {
// other declarations
action UpdateWeather();
};
When declaring an action in the DDL file, it must be implemented by the user, as detailed in Section 13.2 Actions.
If you want to specify constants that may be shared between the DDL and C++ code, you may use the #define
directive. You must simply define the constant and its value, as shown in the following code sample. ConstantName
is the name of the constant and Value
is its value.
Code 14.31 Defining a Constant
#define ConstantName Value
For example, the hypothetical MyConstant
constant could be defined as follows.
Code 14.32 Example of Defining a Constant
#define MyConstant 15
Note that for DDL files that specify constants, it is usually simpler to include the file using the #include
DDL preprocessor directive instead of the #use
directive, especially if all that is defined in the file is constants.
DDL declarations may be made in one or more DDL files. If multiple DDL files are used, use the #include
and #use
preprocessor directives to ensure that all relevant information is available to the DDL compiler. Preprocessor directives are typically used to make source programs easy to change and easy to compile in different execution environments. The DDL preprocessor directives work the same way as the preprocessor directives in standard C++ programming.
The DDL preprocessor recognizes the following directives.
Property | Description |
---|---|
#define macro |
|
#define macro string |
You can use the #define directive to give a meaningful name to a constant in your program. However, as opposed to standard C++ syntax, a macro cannot be defined to take arguments. The DDL compiler automatically defines the NETZ macro. |
#else |
The #else directive can appear between the #ifdef / #ifndef and #endif directives. Only one #else directive is allowed, and if present, it must be the last directive before #endif . |
#endif |
Each #ifdef and #ifndef directive in a source file must be matched by a closing #endif directive. |
#ifdef identifier |
|
#ifndef identifier |
The #ifdef directive, with the #else and #endif directives, controls compilation of portions of a source file. This constant expression is considered true (nonzero) if the identifier is currently defined; otherwise, the condition is false (zero). The #ifndef directive checks for the opposite of the condition checked by #ifdef . If the identifier has not been defined, the condition is true (nonzero); otherwise, the condition is false (zero). Each #ifdef directive in a source file must be matched by a closing #endif directive. Only one #else directive is allowed, and if present, it must be the last directive before #endif . |
#include filename |
The #include directive tells the preprocessor to treat the statements in the specified file as if they had appeared in the source program at the point where the directive appears. The preprocessor looks for the files in the local directory, searches in the path specified by the -I compiler option, and finally searches the Frameworks subdirectory of the installation directory. |
#use filename |
The #use directive works in a similar manner to #include , except that the DDL compiler does not generate code for the classes and other elements in the DDL files. The preprocessor looks for files in the local directory, searches in the path specified by the -I compiler option, and finally searches the Frameworks subdirectory of the installation directory. |
When a project is compiled, the DDL compiler follows the following rules. These rules ensure that the compiler knows where the generated files are located.
#include
lines of the generated files are the same as the paths to the used files. For example, if #use path/MyFile.ddl
is found in a DDL file, the compiler generates #include “path/MyFileDDF.h”
.#include
lines has no prefix. To specify a prefix, use the –includeprefix path
compiler option.The following syntax is used to invoke the DDL compiler and to generate the code to implement the objects and datasets of the session described in the DDL file.
DDL.EXE [options] filename
filename
specifies the name of the DDL file. The options that can be used with this command are detailed in the following table.
Option | Description |
---|---|
-D macro |
Defines the specified macro. |
-header (or -p ) HeaderFilename |
Specifies the file to include at the beginning of all the generated .cpp files. This option is useful for projects with precompiled headers. For example, when you build a NetZ project that uses the precompiled header stdheaders.h , invoke the DDL compiler with the -header stdheaders.h switch. |
-help (or -h ) |
Displays the Help screen that details the DDL compiler options. |
-I path |
Specifies a path to search in addition to the local directory when a DDL file is either included or used in another DDL file. In general, this path is also passed to the C++ compiler. |
-includeprefix path |
Specifies a path to use as a prefix when the generated code must include a file. For example, when specifying ddl.exe –includeprefix Game MyFile.ddl , the compiler generates #include “Game/MyFileDDF.h” . |
-nousingdirective |
The DDL compiler adds NO_USING_QUAZAL_DIRECTIVE at the beginning of each generated file. Use this option if you do not want to use the namespace by default. |
-skeleton (or –g ) |
The DDL compiler generates a skeleton of the required user files that you can use as a starting point to implement your duplicated object classes. Note that existing files are never overwritten. |
-verbose (or –v ) |
Verbose. |
For the errors and warning codes returned by the DDL compiler, see Section 15.3 DDL Compiler Errors. For example, use the following syntax to compile the SphereZ.ddl
DDL file.
Code 14.33 Sample Compilation
ddl.exe SphereZ.ddl
When you compile an application built with NetZ
, the NetZ
DDL compiler uses the DDL files to generate the DOCLASS
, DATASET
, and DUPSPACE
classes and their relevant source and header files. The compiler generates the source and header files ClassNameDDL.cpp
and ClassNameDDL.h
from the DDL files for each dataset and duplicated object declaration made in the DDL. The generated classes and files, along with the user-implemented classes and game code and the NetZ
library, are then used by the C++ compiler to generate the executable file. The following figure illustrates this process.
Figure 14.1 Generating an Executable File
The NetZ
DDL compiler generates C++ classes corresponding to the description declared in the DDL file. The main user-implemented classes for NetZ
are the duplicated object and dataset classes. These user-implemented classes inherit from the classes generated by the DDL compiler, which in turn inherit from the NetZ
classes DuplicatedObject
and DataSet
. The following figure illustrates the class hierarchy.
Figure 14.2 General Class Inheritance for Duplicated Objects and Datasets
In general, if you specify a duplicated object class UserDOClass
in the DDL, you must implement a UserDOClass
class that inherits from the DDL-generated class. Use DOCLASS(UserDOClass)
to access this generated class. This DOCLASS
is a macro that builds a name that refers to the generated class. Any class generated as a result of a DOClass
declaration in the DDL file automatically ultimately inherits from the root class DuplicatedObject
. Unlike the DataSet
class, you can specify inheritance between user-implemented classes in the DDL for the DuplicatedObject
class.
Similarly, if you specify a UserDataSet
dataset in the DDL, you must implement a UserDataSet
class that inherits from the DDL-generated class. Use DATASET(UserDataSet)
to access this generated class. This DATASET
is a macro that builds a name that refers to the generated class. The DDL-generated class automatically ultimately inherits from the root class DataSet
. The variables declared as part of the UserDataSet
class inherit from the variables declared in the generated class DATASET(UserDataSet)
.
You cannot instantiate duplicated objects or datasets directly. Use the DuplicatedObject::Create
member function to create duplicated objects. This method creates an instance of the class that can be published. Datasets do not need to be directly instantiated. They are implemented automatically by NetZ
according to the declarations in the DDL file. For dataset classes, the DDL-generated code implements the classes as member variables.
For duplicated objects, the class generated by the DDL compiler encapsulates all the network code. This deals with how to instantiate a class and ensure that it is discovered on all remote stations, how to ensure that duplicas are updated appropriately when the variables of a duplicated object change, and how to remotely call methods when an RMC or action is called. The user-implemented class declares the dataset variables, implements methods corresponding to the declared RMCs and actions, and may also add any local variables or methods pertinent to the application.
Similarly, for datasets the class that the DDL compiler generates holds the dataset variables, and is responsible for the dataset update, including marshalling, unmarshalling, and data streaming. If an extrapolation filter is used to update the dataset, a prediction model is also generated. The user-implemented class adds any local variables or methods that are applicable to the game. This local information makes no sense on a remote station and is therefore not replicated.
SphereZ
SamplePrevious sections defined the duplicated objects, datasets, and actions relevant for the SphereZ
example. The following example shows a complete DDL file for SphereZ
.
Code 14.34 SphereZ
DDL File
title “SphereZ”
dataset Position {
double x;
double y;
double z;
} extrapolation_filter, unreliable;
dataset SphereData {
uint16 m_ui16Texture;
} upon_request_filter;
dataset Weather {
int FogEnabled;
};
doclass GeometricalObject {
Position m_dsPos;
};
doclass Sphere : GeometricalObject {
SphereData m_dsSphereData;
};
doclass Sphere : AISphere {
};
doclass World {
Weather m_dsWeather;
action UpdateWeather();
};
wellknown World g_hWorld;
The class inheritance of duplicated objects and datasets—the result of the descriptions in this DDL file—is illustrated in Figure 14.3 and Figure 14.4. The AISphere
user-defined duplicated object class inherits from the Sphere
class, which inherits from the GeometricalObject
class, which in turn inherits from the NetZ
DuplicatedObject
class. Similarly, the Position
user-defined dataset class automatically inherits from the NetZ
DataSet
class.
Figure 14.3 Duplicated Object Class Inheritance
Figure 14.4 Dataset Class Inheritance
For each class that is defined in the DDL, the inheritance of the class is defined in the corresponding header file. In this example, AISphere.h
defines the AISphere
class as inheriting from DOCLASS(AISphere)
, which is generated based on the definition in the DDL file. The generated file has the name AISphereDDL.h
. Similarly, the DDL generates the classes DOCLASS(Sphere)
and DOCLASS(GeometricalObject)
, and the files SphereDDL.h
and GeometricalObjectDDL.h
. The GeometricalObject
class then ultimately inherits from the NetZ
DuplicatedObject
class. The user must include the DDL-generated header files before the corresponding user class is defined. The compiler also generates a source file corresponding to each defined class. These files must be linked to the other classes and compiled with the project. In this example, the source files generated by the DDL are AISphere.cpp
, SphereDDL.cpp
, GeometricalObjectDDL.cpp
, and WorldDDL.cpp
.
The Position
dataset is defined in the Position.h
file. The Position
dataset class inherits from the DDL-generated class DATASET(Position)
, which ultimately inherits from the NetZ
DataSet
class automatically. The variables declared as part of the Position
class inherit from the variables declared in the DATASET(Position)
generated class. The files generated by the DDL have the names PositionDDL.h
and PositionDDL.cpp
. As mentioned previously, the user must include the generated header files before the corresponding user classes are defined, and the source files must be compiled with the project.
When a duplicated object is instantiated, the member variable that corresponds to the DOClass
declaration uses the developer's own class rather than the class generated by the DDL compiler. For example, the DOCLASS(GeometricalObject)
generated by the DDL compiler includes a variable m_dsPos
of type Position
, rather than DATASET(Position)
. As a result, code added in the Position
class is available when the duplicated object is instantiated. The following figure illustrates this situation.
Figure 14.5 Relationship Between Duplicated Object and Dataset Classes
For each class that is defined in the DDL, the inheritance of the class is defined in the corresponding header file. In this example, the Moderator.h
file defines that the Moderator
class inherits from DOCLASS(Moderator)
, generated based on the definition in the DDL file. The generated file has the name ModeratorDDL.h
. Similarly, the DDL generates the classes DOCLASS(Delegate)
, DOCLASS(Sage)
, DOCLASS(Room)
, and DOCLASS(SageBalancing)
, and the files DelegateDDL.h
, SageDDL.h
, RoomDDL.h
, and SageBalancing.h
. The Moderator
class then inherits from the Delegate
class and ultimately from the NetZ
DuplicatedObject
class. The user must include the DDL generated header files before the corresponding user class is defined. The compiler also generates a source file corresponding to each defined class. These files must be linked to the other classes and compiled with the project. In this example, the source files generated by the DDL are DelegateDDL.cpp
, ModeratorDDL.cpp
, SageDDL.cpp
, RoomDDL.cpp
, and SageBalancingDDL.cpp
.
The Location
dataset is defined in Location.h
. The class inherits from the DDL-generated class DATASET(Location)
, which automatically ultimately inherits from the NetZ
DataSet
class. The variables declared as part of the class inherit from the variables declared in the generated class DATASET(Location)
. The DDL-generated files have the names LocationDDL.h
and LocationDDL.cpp
. Likewise, the DDL generates the files StringInfoDDL.h
, StringInfoDDL.cpp
, SageKnowledgeDDL.h
, and SageKnowledgeDDL.cpp
that correspond to the StringInfo
and SageKnowledge
datasets. As always, the user must include the generated header files before the corresponding user classes are defined, and compile the source files with the project.
When a duplicated object is instantiated, the member variable that corresponds to the DOClass
declaration uses the developer's own class rather than the class generated by the DDL compiler. For example, the DOCLASS(Location)
generated by the DDL compiler includes a variable m_dsLocation
of type Location
, rather than DATASET(Location)
. As a result, code added in the Location
class is available when the duplicated object is instantiated, as shown in the following figure.
Figure 14.6 Relationship Between the Duplicated Object and Dataset Classes
The developer must implement several files before an application built with NetZ
can be compiled. It is the developer’s responsibility to write the DDL files in the manner described previously in this chapter. In addition, for each dataset, doclass, and dupspace declaration made in the DDL, you are required to implement the .h
and .cpp
files. Within the header file, you must define the inheritance and the variables of the class, in addition to any non-dataset variables that are used for local processing.
You also need to ensure that the files generated by the DDL compiler are correctly linked to the project. To do this, you must make sure that all DDL generated header files (ClassNameDDL.h
) are included before the class is defined, and that all DDL generated source files (ClassNameDDL.cpp
) are linked and compiled with the project.
You must implement any class that is declared in the DDL file. In other words, you must implement the methods associated with any datasets, remote method calls, or actions. Use the following syntax to declare duplicated object and dataset classes in the header file.
Code 14.35 Declaring a Duplicated Object and Dataset Class
class DuplicatedObjectName : public DOCLASS(DuplicatedObjectName) {
// User specified RMCs, actions, and methods
};
class DataSetName : public DATASET(DataSetName) {
// User specified methods
};
You also need to implement the methods associated with any datasets, RMCs, or actions. For example, use the following syntax to declare the duplicated object class GeometricalObject
in the GeometricalObject
header file.
Code 14.36 Declaring GeometricalObject
class GeometricalObject : public DOCLASS(GeometricalObject) {
// User specified RMCs, actions, and methods
};
All NetZ
system classes are defined in the NEX
namespace. The system uses the NEX
namespace by default when you include NetZ
. If you do not want to use the NEX
namespace, possibly to avoid name collisions, use the –nousingdirective
DDL compiler option and define the NO_USING_QUAZAL_DIRECTIVE
macro before including NetZ
or passing it by the command line.
You have the option of overriding the NetZ
system defaults by implementing the appropriate code in the source and header files. For example, you can decide whether objects are fault-tolerant, whether objects can migrate, or whether the implementation of dead reckoning can be customized.
To facilitate data manipulation, NetZ
has several additional data types that can be included when a project is compiled. In particular, implementation of the following C++ STL string and bitset data types is provided.
DDL | C++ | Comment |
---|---|---|
string |
String |
Implemented internally by using qChar16* (a pointer to a string of 8-bit or 16-bit characters). NetZ converts such strings to UTF-8 when placing them in RMCs. |
cstr |
qChar |
Mapped to qChar16 . No conversion to UTF-8 takes place when placed in RMCs. |
qchar8 |
qchar8 |
|
qchar16 |
Qchar16 |
|
std_ char8string |
qChar8* |
No conversion to UTF-8 takes place when placed in RMCs. |
std_ char16string |
qChar16* |
No conversion to UTF-8 takes place when placed in RMCs. |
std_ bitset8 |
std:: bitset<8> |
|
std_ bitset16 |
std:: bitset<16> |
|
std_ bitset32 |
std:: bitset<32> |
|
qlist T |
qList T |
T is the type (such as int). |
qvector T |
qVector T |
T is the type (such as int). |
qmap Key, Value |
qMap Key, Value |
Key, Value are the key and value of the map items. |
qqueue T |
qQueue T |
T is the type (such as int). |
std_ vector T |
std:: vector T |
T is the type (such as int). |
std_ list T |
std:: list T |
T is the type (such as int). |
std_ map Key, Value |
std:: map Key, Value |
Key, Value are the key and value of the map items. |
To integrate these STL data types into a project, you must use the following code to include the STL.ddl
file in the DDL file of your project.
Code 14.37 Integrating STL Data Types Into a Project
#include <Extensions/STL.ddl>
CONFIDENTIAL