![]() |
Overview of the distributed network object system
The distributed network object system is the highest level layer over RakNet. The concept is simple: objects that are instantiated on one system are instantiated on all systems. Objects that are destroyed on one system are destroyed on all systems. Tagged memory is matched between systems. When a new player connects, these objects are also created on his system. This is very useful conceptually because it has a direct analogy to game objects. For example, a one tank in a multiplayer game with twenty players actually has been instantiated twenty times. However, as far as the player is concerned there is only one tank. The player expects that the position, orientation, number of shells left, and amount of armor is the same on all systems. Traditionally, to maintain a tank you would have to craft a series of custom packets for the tank. One packet to describe position, another packet for the tank to shoot, and another to have the tank take damage. Using the distributed network object system, you can synchronize the tank, those 3 member variables, and everything matches automatically. The distributed object system has both strengths and weaknesses. Strengths
Weaknesses
How to implement the distributed object system Before you can use the distributed object system, you have to register your instance of RakClient and/or RakServer with the Distributed Network Object Manager. DistributedNetworkObjectManager::Instance()->RegisterRakClientInterface(rakClient); DistributedNetworkObjectManager::Instance()->RegisterRakServerInterface(rakServer); The multiplayer class will automatically handle distributed object network messages. If you don't use the multiplayer class, you must handle these packet types yourself: ID_UPDATE_DISTRIBUTED_NETWORK_OBJECT ID_DISTRIBUTED_NETWORK_OBJECT_CREATION_ACCEPTED ID_DISTRIBUTED_NETWORK_OBJECT_CREATION_REJECTED You would pass them to the DistributedNetworkObjectManager in exactly the same fashion as is done in Multiplayer.h. Once that is done, then you have several steps per class.
#include "DistributedNetworkObjectHeader.h" class Tank : public DistributedNetworkObject { // Your data and functions here // ... // The update function for the class is a good place to put UpdateDistributedObject void Update(void) {UpdateDistributedObject("Tank");} }; REGISTER_DISTRIBUTED_CLASS(Tank); How to implement synchronized member variables within the distributed object system That tank is nice but would be a lot more useful if we had the data members synchronized automatically as well. There are two ways to do that:
The advantage of the SynchronizeMemory function is that you can add elements at runtime, members do not have to match, and it can support anything. The disadvantages are that it cannot perform interpolation, is more susceptible to user error, and requires one line of code in your cpp file per variable. The REGISTER_X_DISTRIBUTED_OBJECT_MEMBERS macro To use REGISTER_X_DISTRIBUTED_OBJECT_MEMBERS, in the public section of the class definition add a macro that takes the following form: REGISTER_X_DISTRIBUTED_OBJECT_MEMBERS(BaseClass, SynchronizationMethod, AuthoritativeNetwork, VariableType, VariableName, ... ) The first parameter is in REGISTER_X_DISTRIBUTED_OBJECT_MEMBERS. Replace that X with however many member variables you are synchronizing. For example, if you want to synch 3 member variables then you would instead write REGISTER_3_DISTRIBUTED_OBJECT_MEMBERS. The second parameter, BaseClass is the name of the class your class derives from. As we specified earlier, you must derive from DistributedNetworkObject in the basemost class so this always has a value. If you had Apple derive from Fruit derive from DistributedNetworkObject then the first parameter for REGISTER_X_DISTRIBUTED_OBJECT_MEMBERS for Apple would be Fruit, and for Fruit it would be DistributedNetworkObject. The third parameter is a set of parameters - SynchronizationMethod, AuthoritativeNetwork, VariableType, and VariableName. SynchronizationMethod takes one of the following values:
My type is a
VariableName must match the name of the variable being synchronized. These four parameters that compose the third set of parameters can be repeated as many times as you wish, up to the number of types the define pattern was cut and paste in DistributedObjectNetworkHeader.h Here is our tank with two member variables synchronized: #include "DistributedNetworkObjectHeader.h" class Tank : public DistributedNetworkObject { // Your data and functions here // ... // The update function for the class is a good place to put UpdateDistributedObject void Update(void) {UpdateDistributedObject("Tank");} public: float turrentAngle; Vector position; REGISTER_2_DISTRIBUTED_OBJECT_MEMBERS(DistributedNetworkObject, DOM_INTERPOLATE_COMPRESSED, DOM_CLIENT_AUTHORITATIVE, float, turrentAngle, DOM_COPY_UNCOMPRESSED, DOM_SERVER_AUTHORITATIVE, Vector, position) }; REGISTER_DISTRIBUTED_CLASS(Tank); Additional notes
Synchronizing memory at runtime is possible with the SynchronizeMemory and related functions. The outcome is the same as with REGISTER_X_DISTRIBUTED_OBJECT_MEMBERS. Here is how to use it with our tank example: #include "DistributedNetworkObjectHeader.h" class Tank : public DistributedNetworkObject { Tank() {SynchronizeMemory(0, &turrentAngle, sizeof(turrentAngle), false); SynchronizeMemory(1, &position, sizeof(position), true); } // Your data and functions here // ... // The update function for the class is a good place to put UpdateDistributedObject void Update(void) {UpdateDistributedObject("Tank");} public: float turrentAngle; Vector position; }; REGISTER_DISTRIBUTED_CLASS(Tank); This does the same as the code above. The first parameter is a unique unsigned char to identify the variable. The system will then expect all objects with synchronized memory that use that same value to match in size and type. The second parameter is the memory address of the variable. The third parameter is the size of the block to synchronize. The last parameter is true if you want the server to be the authority on the object, false for the client to be the authority. Refer to Samples\CodeSamples\DistributedNetworkObject\DistributedNetworkObjectSample.cpp for an implementation example. User functions of the DistributedNetworkObject Call this every update cycle for every distributed object that you want updated over the network and to interpolate. classID should be a unique identifier for the class that matches the parameter to REGISTER_DISTRIBUTED_CLASS. The obvious choice is the name of the class - however you can use something shorter if you wish to save a bit of bandwidth virtual void UpdateDistributedObject(char *classID, bool isClassIDEncoded=false); Sets the maximum frequency with which memory synchronization packets can be sent. Lower values increase granularity but require more bandwidth virtual void SetMaximumUpdateFrequency(unsigned long frequency); Broadcasts a request to destroy an object on the network. OnDistrubtedObjectDestruction will be called. If you wish to block deletion, override OnDistributedObjectDestruction to not delete itself virtual void DestroyObjectOnNetwork(void); Server only function - By default, when a client creates an object only it can update the client authoritative members of the class it creates. You can also set this manually with SetClientOwnerID This function is called when a client that does not own an object tries to change any fields in that object Return true to allow the update. Return false (default) to not allow the update. virtual bool AllowSpectatorUpdate(PlayerID sender); Tags memory to be synchronized. You can set the server or the client as the authority for this block. Only the authority will write this memory to the network when it is changed. void SynchronizeMemory(unsigned char memoryBlockIndex, void* memoryBlock, int memoryBlockSize, bool serverAuthority); Untags memory that was formerly synchronized. void DesynchronizeMemory(unsigned char memoryBlockIndex); Changes the authority for memory. You probably will never need this. void SetAuthority(unsigned char memoryBlockIndex, bool serverAuthority); Tells you if a block of memory was formerly used. You probably will never need this. bool IsMemoryBlockIndexUsed(unsigned char memoryBlockIndex); Use this to set a maximum update frequency higher than what was specified to SetMaximumUpdateFrequency Lower values have no effect. void SetMaxMemoryBlockUpdateFrequency(unsigned char memoryBlockIndex, int max); When object creation data is needed, WriteCreationData is called. This function is for you to write any data that is needed to create or initialize the object on remote systems virtual void WriteCreationData(BitStream *initialData); When an object is created, ReadCreationData is called immediately after a successful call to OnDistributedObjectCreation This function is for you to read any data written from WriteCreationData on remote systems. If the object is created by the client, this function is also called by the creator of the object when sent back from the server in case the server overrode any settings virtual void ReadCreationData(BitStream *initialData); When distributed data changes for an object, this function gets called. Default behavior is to do nothing. Override it if you want to perform updates when data is changed. On the server it is also important to override this to make sure the data the client just sent you is reasonable. virtual void OnNetworkUpdate(PlayerID sender); This is called when the object is created by the network. Return true to accept the new object, false to reject it. The return value is primarily useful for the server to reject objects created by the client. On the client you would normally return true senderID is the playerID of the player that created the object (or the server, which you can get from RakClientInterface::GetServerID) You must call the base class version of this function when overriding!. virtual bool OnDistributedObjectCreation(PlayerID senderID); This is called when the object is destroyed by the network. Default behavior is to delete itself. You can override this if you want to delete it elsewhere, or at a later time. If you don't delete the object, you should call DestroyObjectOnNetwork manually to remove it from the network. Note it is important to override this on the server for objects you don't want clients to delete. senderID is the playerID of the player that created the object (or the server, which you can get from RakClientInterface::GetServerID) virtual void OnDistributedObjectDestruction(PlayerID senderID); This is called when the server rejects an object the client created. Default behavior is to destroy the object. You can override this to destroy the object yourself. virtual void OnDistributedObjectRejection(void); |
![]() |
Index Bitstreams Timestamping |