Network synchronization - object replication

The act of transferring object state from one host to another is called replication.

The host must serialize three types of data before serializing the internal data of the object:

    1. Mark that the current packet is a packet that contains the state of the object.

    2. Identifier that uniquely identifies the object. (network identifier of the object)

    3. Identifier of the class to which the object belongs.

 

1, Mark that the current packet is a packet that contains the state of the object

enum PacketType
{
    PT_Hello,
    PT_Replication,
    PT_Disconnect,
    PT_Max
};

Each packet may contain different functions. The function of the packet is indicated in the first field of the packet.

2, Identifier that uniquely identifies the object

Each object is recorded through the link context, and a unique identifier, also known as network identifier, can be generated for the next object.

class LinkingContext
{
public:
    LinkingContext():mNextNetworkId(1)
    {

    }


    uint32_t GetNetworkId(const GameObject* inGameObject, bool inShouldCreateIfNotFound)
    {
        auto it = mGameObjectToNetworkIdMap.find(inGameObject);
        if(it != mGameObjectToNetworkIdMap.end())
        {
            return it->second;
        }
        else if(inShouldCreateIfNotFound)
        {
            uint32_t newNetworkId = mNextNetworkId++;
            AddGameObject(inGameObject, newNetworkId);
            return newNetworkId;
        }
        else
        {
            return 0;
        }
    }

    void AddGameObject(const GameObject *inGameObject, uint32_t inNetworkId)
    {
        mNetworkIdGameObjectMap[inNetworkId] = inGameObject;
        mGameObjectToNetworkIdMap[inGameObject] = inNetworkId;
    }


    void RemoveGameObject(GameObject *inGameObject)
    {
        uint32_t networkId = mGameObjectToNetworkIdMap[inGameObject];
        mGameObjectToNetworkIdMap.erase(inGameObject);
        mNetworkIdGameObjectMap.erase(networkId);
    }



private:
    std::unordered_map<uint32_t, GameObject*> mNetworkIdGameObjectMap;
    std::unordered_map<const GameObject*, uint32_t> mGameObjectToNetworkIdMap;
    uint32_t mNextNetworkId;
};

3, The class identifier to which the object belongs

Why do I need to serialize the identifier of the class to which the object belongs?

When the remote host receives a packet, it will first read the first part of the data and know that it is a packet containing an object. Then read the data in the second part and get the object identifier (object network identifier). The remote host uses the object identifier to find out whether it already has the object. If so, deserialize the received object data to update the object state.

If the object is not found, you need to create it. To create an object, you need the information of this object class. Therefore, the sender serializes the identifier of the object class after the object identifier to provide the information that the remote host creates the object.

Object Creation Registry

#define CLASS_IDENTIFICATION(inCode, inClass)\
    enum {kClassId = inCode};\
    virtual uint32_t GetClassId() const {return kClassId;}\
    static GameObject *CreateInstance() {return new inClass();}

class GameObject
{
public:
    CLASS_IDENTIFICATION('GOBJ', GameObject)
    virtual ~GameObject();
};


class RoboCat:public GameObject
{
public:
    CLASS_IDENTIFICATION('RBCT', RoboCat)
};


typedef GameObject* (*GameObjectCreationFunc)();

class ObjectCreationRegistry
{
public:
    static ObjectCreationRegistry &Get()
    {
        static ObjectCreationRegistry sInstance;
        return sInstance;
    }


    template<class T>
    void RegistryCreationFunction()
    {
        assert(mNameToGameObjectCreationFunctionMap.find(T::kClassId) == mNameToGameObjectCreationFunctionMap.end());
        mNameToGameObjectCreationFunctionMap[T::kClassId] = T::CreateInstance;
    }

    GameObject *CreateGameObject(uint32_t inClassId)
    {
        GameObjectCreationFunc creationFunc = mNameToGameObjectCreationFunctionMap[inClassId];
        GameObject *gameObject = creationFunc();
        return gameObject;
    }

private:
    ObjectCreationRegistry(){}
    //value Is a function pointer to the creation function of each object class
    std::unordered_map<uint32_t, GameObjectCreationFunc> mNameToGameObjectCreationFunctionMap;
};

//Call the following function in the appropriate place to register the creation function of each class object
void RegisterObjectCreation()
{
    ObjectCreationRegistry::Get().RegistryCreationFunction<GameObject>();
    ObjectCreationRegistry::Get().RegistryCreationFunction<RoboCat>();
}

The object creation registry actually puts the creation functions of all classes into the map data structure. Call RegistryObjectCreation() where appropriate to put the class creation function into the map.

In this process, many tips are used to simplify the code.

  1. The definition of quaternion is basically consistent with the code for obtaining the class identifier and calling the creation function, so a macro definition (CLASS_IDENTIFICATION) is used

  2. Through template implementation, only one function can be called for all classes, and the class creation function can be put into the map container.

 

Replication and synchronization of game world state

1, Synchronize the state of all objects in the server world

If the state of all game objects on the server can be stuffed into a data packet, one of the synchronization methods is to send the state of all objects in the world to the client in a data packet.

When the receiving end detects that the data packet is in the state of synchronization object, it circularly accesses each serialized game object in the data packet.

If the game object exists, the client finds the object and deserializes the state into the object. If the game object does not exist, the client creates the object through the following class ID and puts the serialized state into the object.

When the client finishes processing the packet, destroy all local data objects that do not appear in the packet.

2, Only synchronize the state of the object whose state changes on the server

Since each client keeps a copy of its own world state, it is unnecessary and unlikely to copy all game objects in one packet.

A better way is to choose a method called world state delta. The server creates data packets representing changes in the world state and sends them to the client. The client updates these states in its own world.

Each world state increment package contains the object state delta of the object to be changed.

Therefore, each object state increment represents one of the following three replication behaviors.

  1. Create game object

  2. Update game object

  3. Destroy game objects

Since the method of updating object state increment is adopted, a new packet header type should be added.

Replication of local object state

When sending the status update of an object, because a copy of the object status is kept on the client host, the server does not need to send all the status of the object. The server can choose to serialize some data updated since the last time.

Therefore, each attribute data of the object can be represented by bit fields. Before serializing the object data, the combination of these bit fields should be serialized to represent which numbers

//The following enumeration values are combined into a number to indicate which data the next data contains
//For example: inProperties=MSP_Name|MSP_LegCount
enum MouseStatus
{
    MSP_Name = 1 << 0,
    MSP_LegCount = 1 << 1,
    MSP_HeadCount = 1 << 2,
    MSP_Heath = 1 << 3,
    MSP_MAX
};

//Time basis for serializing data inProperties Determine which data to serialize
void MouseStatus::Write(OutputMemoryBitStream &inStream, uint32_t inProperties)
{
    inStream.Write(inProperties, GetRequireBits<MSP_MAX>::Value);
    if ((inProperties & MSP_Name) != 0)
    {
        inStream.Write(mName);
    }
    if ((inProperties & MSP_LegCount) != 0)
    {
        inStream.Write(mLegCount);
    }
    if ((inProperties & MSP_HeadCount) != 0)
    {
        inStream.Write(mHeadCount);
    }
    if ((inProperties & MSP_Heath) != 0)
    {
        inStream.Write(mHeath);
    }
}

void MouseStatus::Read(InputMemoryBitStream &inStream)
{
    uint32_t writtenProperties = 0;
    inStream.Read(writtenProperties, GetRequiredBits<MSP_MAX>::Value);
    if ((writtenProperties & MSP_Name) != 0)
    {
        inStream.Read(mName);
    }
    if ((writtenProperties & MSP_LegCount) != 0)
    {
        inStream.Read(mLegCount);
    }
    if ((writtenProperties & MSP_HeadCount) != 0)
    {
        inStream.Read(mHeadCount);
    }
    if ((writtenProperties & MSP_Heath) != 0)
    {
        inStream.Read(mHeath);
    }
}

  

To sum up, the fields of the data packet of a good synchronization object state are as follows:

3, Synchronization of remote procedure calls

The remote procedure is called as a special RPC object. The serialized and deserialized object network identifier is the PRC identifier, and the data content is the parameter of the function. The class identifier is no longer required.

Because RPC is regarded as a special object, additional fields are required to represent RPC:

enum ReplicationAction
{
    RA_Create,
    RA_Update,
    RA_Destroy,
    RA_RPC,    
    RA_MAX
};

When the value of the deserialization field ReplicationAction is found to be: RA_ During RPC, the input stream is loaded and handed over to the RPC module for processing. In addition, unlike ordinary objects, RPC does not require a class identifier.]

Therefore, when the data packet is RPC transmission, the fields are as follows:

  

RPC function module function:

  1. Mapping from each RPC identifier to the unpacked glue function, which is used to deserialize the parameters of the remote procedure call and call the appropriate function.

typedef void (*RPCUnwrapFunc) (InputMemoryBitStream&);

class RPCManager
{
public:
    void RegisterUnwrapFunction(uint32_t inName, RPCUnwrapFunc inFunc)
    {
        assert(mNameToRPCTable.find(inName) == mNameToRPCTable.end());
        mNameToRPCTable[inName] = inFunc;
    }

    void ProcessPRC(InputMemoryBitStream &inStream)
    {
        uint32_t name;
        inStream.Read(name);
        mNameToRPCTable[name](InputMemoryBitStream);
    }

    unordered_map<uint32_t, RPCUnwrapFunc> mNameToRPCTable;
};


void UnwrapPlaySound(InputMemoryBitStream &inStream)
{
    string soundName;
    Vector3 location;
    float volume;
    inStream.Read(soundName);
    inStream.Read(location);
    inStream.Read(volume);
    PlaySound(soundName, location, volume);
}

void RegisterRPCs(RPCManager *inRPCManager)
{
    inRPCManager->RegisterUnwrapFunction('PSND', UnwrapPlaySound);
}

 

Posted by dragonfly4 on Tue, 17 May 2022 05:42:29 +0300