RakNet学习(48) -- Replica Manager 3

来源:互联网 发布:网络词语老干部啥意思 编辑:程序博客网 时间:2024/05/01 22:57

Replica Manager 3 插件接口实现(复制管理器)

 

Replica Manager 3实现概览

       任何在游戏进行期间有对象进行创建和销毁的游戏,也就是几乎所有的大型游戏,最少面临如下的三个问题:

       1. 如何将已存的游戏对象广播给新的玩家

       2. 如何将新游戏对象广播给已存的玩家

       3. 如何将删除的游戏对象广播给已存在的玩家

 

       根据游戏的复杂性和优化,还可能遇到如下的几个问题:

       1. 在玩家在游戏世界进行移动时,如何动态创建和销毁对象。

       2. 由于编程或图形绘制的原因(例如射击子弹),如何允许客户端在必要的时候立即在本地创建对象。

       3. 当游戏对象随着时间变化时,如何更新这些对象。

 

       对于这些问题的解决方法通常很直接,即使这样仍然需要大量的编程工作和调试,每一个对象大概要二十几行的代码。

       ReplicaManager3就是为了给用户提供一个一般的,可覆盖的插件,尽可能多地自动护理这些细节。ReplicaManager3自动创建和销毁对象,给新的玩家下载地图,管理玩家以及进行一些数据的自动序列化操作。这个插件同时还包含了自动中继消息的高级功能,以及当序列化的成员数据改变时,自动序列化你的对象。

 

操作顺序:

       对象是按序远程创建的,使用ReplicaManager3::Reference()方法进行注册。在一个tick创建或销毁的对象是在同一个packet创建或销毁的,意味着对于构建或销毁对象的调用都是由相同的RakPeerInterface::Receive()调用触发。

      Serialize()在构造之后发生。因此,所有对象都会被创建,会调用DeserializeConstruction()函数,这些操作是在任何Serialize()方法调用发生之前。不像构造函数,Serialize()调用会被扩展到多个调用,一直到RakPeerInterface::Receive()调用,依赖于带宽能力。因此,对于这些在初始化的SerializeConstructioni()调用中设置的对象,确保发送了所有必需的数据,因为插件并不保证你在同一个tick上会接收到Deserialize()方法。

       对象第一次被发送到远端系统时,一旦所有对象被构造且Desialized(),你会得到Connection_RM3::DeserialzeOnDownloadComplete()方法调用。

 

依赖解决方案:

       如果一个对象参考到其他的对象(例如,一个枪要有一个指针指向他自己),那个依赖对象需要首先创建。在这个枪有他自己的拥有者的情况下,拥有者需要首先被创建。枪应该序列化拥有者的NetworkID,在对象拥有者的DeserialzedConstruction调用中可查找拥有者。这个可以通过使用RepelicaManager3::Reference()方法按序注册对象。

       有时你有依赖链,这个链不能通过重新调序来解决。例如,玩家有一个物品清单,在清单中每一个项目都有一个指针指向它的拥有者。否则你或许会有一个环形链,也就是A依赖BB依赖于C,并且C依赖于A。否则重新排序这些对象不再可行。对于这些情况,可以在Replica3::PostDeserializeConstruction()回调中分解这些依赖关系。在一个给定的更新tick中,如果一个DeseializeConstruction()方法对于所有的对象都完成了然后调用PostDeserializeConstruction()方法,因此所有将被创建的对象都将会被创建。

 

       静态对象:

       有时,在系统中有一个已存的对象,这个对象可以被所有系统感知到。例如,在等级加载中的门。在这些情况中,你不希望这些服务器传递对象创建消息,因为它会创建两次。然而你依旧想要访问和序列化这个对象。例如门打开或关闭,或者门的剩余血量。

       1. Replica3派生对象

       2. 对一个对象调用replicaManager3->Reference()之前,调用replica3Object->SetNetworkIDManager(replicaManager3->GetNetworkIDManager());

       3. 对一个对象调用replicaManager3->Reference()之前,调用replica3Object->SetNetworkID(unique64BitDLoadedWithLevel);

       4. 从主机/服务器的QueryConstruction()返回REM3CS_ALREADY_EXISTS_REMOTELY值,否则返回RM3CS_ALREDY_EXISTS_REMOTELY_DO_NOT_CONSTRUCT

       5. WriteAllocationID(),SerializeConstruction(), DeserializeConstruction()并不是用于静态对象,他们都是空实现。

       6. 如果对象不需要通过网络销毁,实现QueryActionOnPopConnection()方法,返回REM3AOPC_DO_NOTHING值。SerializeDestruction()DeserializeDestruction()DeallocReplica()方法可以保持空实现。

       7. 时间QuerySerialization()方法,从序列化对象的系统返回RM3QSR_CALL_SERIALIZE值,典型的端到端主机或服务器。如果系统当前并不是主机,稍后可能成为主机,例如端到端的主机迁移,返回RM3QSR_DO_NOT_CALLSERIALIZE值,返回RM3QSR_DO_NOT_CALL_SERIALIZE。否则返回RM3QSR_NEVER_CALL_SERIALIZE

       8. SerializeConsturctionExisting()方法中将你自己的初始化数据写到BitStream中,在DeseriallizeConstructuionExisting()方法中读取这个数据。当且仅当QueryConstruction()方法返回RM3CS_ALREADY_EXISTS_REMOTELY值,然后才会调用这个函数。

       9. Serialize()方法中写per-tick的序列化数据,在Deserialize()方法中读取这些数据。

       RM3CS_ALREADY_EXISTS_REMOTELY使得ReplicaManager3认为这个门已经在其他的系统上存在,那么当Serialize()函数被调用时,更新仍然会被发送到这个系统。但是调用SerializeConnection()调用,对象创建会被跳过。

      

FullyConnectedMesh2结合使用

       如果你正在使用FullyConnectedMesh2用于主机选定决策,ReplicaManager3依赖于这个插件,那么你需要延迟加入参与者,直到游戏中决定了主机服务器。下面是如何实现:

 

1. replicaManager3->SetAutoManageConnections(false, [false or true, depends on your preference]);

2. fullyConnectedMesh2->SetAutoparticipateConnections(false); (可选)

3. 开始连接到所有的其他的系统。

4. 接收到ID_CONNECTION_REQUEST_ACCEPTED或者ID_NEW_INCOMING_CONNECTION消息执行:

fullyConnectedMesh2->AddParticipant(packet->guid);

if (fullyConnectedMesh2->GetConnectedHost()!=UNASSIGNED_RAKNET_GUID)
{

DataStructures::List<RakNetGUID> participantList;

fullyConnectedMesh2->GetParticipantList(participantList);

RM3AllocConnections(participantList);

}

5. 接收到ID_FCM2_NEW_HOST 消息,执行:

BitStream bsIn(packet->data, packet->length, false);

bsIn.IgnoreBytes(sizeof(MessageID));

RakNetGUID oldHost;

bsIn.Read(oldHost);

if (oldHost==UNASSIGNED_RAKNET_GUID)

{

DataStructures::List<RakNetGUID> participantList;

fullyConnectedMesh2->GetParticipantList(participantList);

RM3AllocConnections(participantList);

}

6. void RM3AllocConnections(DataStructures::List<RakNetGUID> &participantList)

{

for (unsigned int i=0; i < participantList.Size(); i++)

{

Connection_RM3 *connection = replicaManager3->AllocConnection(rakPeer->GetSystemAddressFromGuid(participantList[i]), participantList[i]);

if (replicaManager3->PushConnection(connection)==false)

replicaManager3->DeallocConnection(connection); 
}

}

解释:

       Line1ReplicaManager::SetAutoManageConnections()方法的第一个参数设置为false,目的是让ReplicaManager3不能自动调用PushConnection()方法,为何如此做,原因就是在确定游戏主机之前,你要让系统延迟,从而不让它参与到ReplicaManager3中来。

       Line2,如果连接到你的每一个系统是另外一个游戏实例,可以保持SetAutoparticipateConnections()函数默认是true,不需要调用fullyConnectedMesh2->AddPaticipant(packet->guid); 后面也同样如此处理。原因是你在一些情况下想要连接到profiling工具或者其他的非游戏程序。

       Line3FullyConnectedMesh2要求一个完整的连接网拓扑,因此你需要在游戏实例中连接到每一个人。简单地使用rakPeer->Connect()调用就可以实现,或许根据你的需要,在系统中需要加入其他的系统,例如NAT Punchthrough。这里的代码在每一个人同时从大厅开始的情况下起作用,和游戏中加入的情况。参考帮助手册的Connecting获得更多信息。

       Line4,知道确定了主机,fullyConnectedMesh2->GetConnectedHost()会返回UNASSIGNED_RAKNET_GUID。在这种情况下,需要延迟调用replicaManager3->PushConnection(),直到返回了ID_FCM2_NEW_HOST消息。然而,如果已经知道了主机是谁(例如,游戏已经在进行之中了),那么远端系统可以立即加入。

       Line5,一旦第一次知道了主机,所有的连接系统可以加入到ReplicaManager3中,oldHost==UNASSIGNED_RAKNET_GUID意味着前面没有主机。

       Line6,唯一地给RaplicaManager3添加连接的一个函数给出了远端RakNetGUIDs的列表。

 

与基于系统的组件集成

       基于组件的系统,我的意思是一个游戏的actor带有一系列的附加类,每一个都包含了actor自己的一个属性。例如,玩家有位置,生命值,动画,以及物理组件。

       1. 相同的actor实例既有有相同的类型,序列以及组件数,也有需要提供相同的方法序列化类似的组件。实现Serialize()函数,首先Serialize()自己的英雄。然后按序序列化(持久化)组件。

       2. 如下是一个在端到端游戏中QuerySerialization()的例子,在端到端游戏中,主机控制对象加载等级(静态对象)。否则,创建实例的对等端序列化这个实例。然而,组件可以重写这个对象,可以让主机不关注对象的序列化。例如,如果一个玩家在地面上放了一个武器,如果我们自己的系统是主机系统,那么武器会返回RM3QSR_CALL_SERIALIZE消息。否则它返回RM3QSR_DO_NOT_CALL_SERIALIZE消息。

       if (IsAStaticObject())

{

       // 在关卡中加载的对象被主机序列化

       if (fullyConnectedMesh2->IsHostSystem())

              return RM3QSR_CALL_SERIALIZE;

       else

              return RM3QSR_DO_NOT_CALL_SERIALIZE;

}

else

{

       // 允许组件重写序列化方法

       for (int i=0; i < components.Size(); i++)

       {

              RM3QuerySerializationResult res = components[i]->QuerySerialization(destinationconnection);

              if(res != RM3QSR_MAX)

                     return res;

       }

       return QuerySerialization_PeerToPeer(destinationconnection);

}

       3. 这个QueryConstruction()函数的变量,使得组件返回Replica3P2PMode。例如枪的那个例子,当枪在地上放着时,枪是由一个主机控制,或者在被捡起来时,由玩家的actor控制。如果组件返回了R3P2PM_MULTI_OWNER_CURRENTLY_AUTHORITATIVE或者是R3P2PM_MULTI_OWNER_NOT_CURRENTLY_AUTHORITATIVE,然后QueryConstruction_PeerToPeer()方法会使用这个值返回一个适当的值用于PM3ConstructionState。如果我们控制对象,那么QueryConstruction_PeerToPeer()方法会返回RM3CS_SEND_CONSTRUCTION消息,如果我们没有控制对象或没有人可以控制该对象,组件会返回RM3CS_NEVER_CONSTRUCT消息,如果其他人控制了对象,但是拥有者可以修改,那么组件会返回RM3CS_ALREADY_EXISTS_REMOTELY消息。

 

if (destinationConnection->HasLoadedLevel() == false)

       return RM3CS_NO_ACTION;

if (IsAStaticObject())

{

       if(fullyConnectedMesh2->IsHostSystem())

            return RM3CS_ALREADY_EXISTS_REMOTELY;

       else

            return RM3CS_ALREADY_EXISTS_REMOTELY_DO_NOT_CONSTRUCT;

}

else

{

       Replica3P2PMode p2pMode = R3P2PM_SINGLE_OWNER;

       for (int i=0; i < components.Size(); i++)

       {

              p2pMode = components[i]->QueryP2PMode();

              if(p2pMode != R3P2PM_SINGLE_OWNER)

                     break;

       }

       return QueryConstruction_PeerToPeer(destinationconnection, p2pMode);

}

 

virtual Replica3P2PMode BaseClassComponent::QueryP2PMode() {return R3P2PM_SINGLE_OWNER;}

 

virtual Replica3P2PMode GunComponent::QueryP2PMode()

{

       if (IsOnTheGround())

              if(fullyConnectedMesh2->IsHostSystem())

                     return R3P2PM_MULTI_OWNER_CURRENTLY_AUTHORITATIVE;

              else

                     return R3P2PM_MULTI_OWNER_NOT_CURRENTLY_AUTHORITATIVE;

       else

              if (WeOwnTheGun())

                     return R3P2PM_MULTI_OWNER_CURRENTLY_AUTHORITATIVE;

              else

                     return R3P2PM_MULTI_OWNER_NOT_CURRENTLY_AUTHORITATIVE;   

}

 

4. 如果需要使用合成对象(compostion)而不是(derivation),参考ReplicaManager3.h中的Replica3Composite。它是一个模板类,仅仅有一个成员r3CompositeOwner。所有的Replica3结构可以从r3CompositeOwner查询。

 

对象序列化的方法:

参考手册发送dirty标记

描述:当一个变量变化时,变量已经变化的一些标记设置依赖于你。下一个Serialize() tick,要发送所有的dirty 标记集。

优点快速,内存有效利用

缺点所有复制的变量必须通过访问者修改,那样标记集可以被设置。这样编程人员的劳动量非常大,因为需要编程人员编程设置dirty标记,在设置过程中有可能产生bug

例如:

void SetHealth(float newHealth)

{

if (health==newHealth)

return;

health=newHealth;

serializeHealth=true;

}

void SetScore(float newScore)

{

if (score==newScore)

return;

score=newScore;

serializeScore=true;

}

virtual RM3SerializationResult Serialize(SerializeParameters *serializeParameters)

{

bool anyVariablesNeedToBeSent=false;

if (serializeHealth==true)

{

serializeParameters->outputBitstream[0]->Write(true);

serializeParameters->outputBitstream[0]->Write(health);

anyVariablesNeedToBeSent=true;

}

else

{

serializeParameters->outputBitstream[0]->Write(false);

}
if (serializeScore==true)

{

serializeParameters->outputBitstream[0]->Write(true);

serializeParameters->outputBitstream[0]->Write(score);

anyVariablesNeedToBeSent=true;

}

else

{

serializeParameters->outputBitstream[0]->Write(false);

}

if (anyVariablesNeedToBeSent==false)

serializeParameters->outputBitstream[0]->Reset();

// Won't send anything if the bitStream is empty (was Reset()).

M3SR_SERIALIZED_ALWAYS skips default memory compare

return RM3SR_SERIALIZED_ALWAYS;

}

virtual void Deserialize(RakNet::DeserializeParameters *deserializeParameters)

{

bool healthWasChanged, scoreWasChanged;

deserializeParameters->serializationBitstream[0]->Read(healthWasChanged);

if (healthWasChanged)

deserializeParameters->serializationBitstream[0]->Read(health);

deserializeParameters->serializationBitstream[0]->Read(scoreWasChanged);

if (scoreWasChanged)

deserializeParameters->serializationBitstream[0]->Read(score);

}

 

基于对象变化的序列化:

描述:这个是ReplicaManager3所带有的功能。如果一个bitStream信道的对象的状态完全变化了,那么整个信道将被重置。

优点:方便编程人员

缺点:发送一些不必要的变量,浪费带宽。CPU和内存的使用也比较多。

例如:

void SetHealth(float newHealth)

{

health=newHealth;

}

virtual RM3SerializationResult Serialize(SerializeParameters *serializeParameters)

{

serializeParameters->outputBitstream[0]->Write(health);

serializeParameters->outputBitstream[0]->Write(score);

// Memory compares against last outputBitstream write. If changed, writes everything on the changed channel(s), which can be wasteful in this case if only health or score changed, and not both

return RM3SR_BROADCAST_IDENTICALLY;

}

virtual void Deserialize(RakNet::DeserializeParameters *deserializeParameters)

{

deserializeParameters->serializationBitstream[0]->Read(health);

deserializeParameters- >serializationBitstream[0]->Read(score);

}

 

序列化每一个变量

描述:正在讨论的是一个可选模块。每一个变量在内部拷贝,与最后一个状态对比。

优点:最大化带宽利用率

缺点:CPU和内存的使用非常严重

例子(也可以参考RplicaManager3例子工程)

virtual RM3SerializationResult Serialize(SerializeParameters *serializeParameters)

{

VariableDeltaSerializer::SerializationContext serializationContext;

// All variables to be sent using a different mode go on different channels

serializeParameters->pro[0].reliability=RELIABLE_ORDERED;

variableDeltaSerializer.BeginIdenticalSerialize( &serializationContext,serializeParameters->whenLastSerialized==0,&serializeParameters->outputBitstream[0]);

variableDeltaSerializer.SerializeVariable(&serializationContext, var3Reliable);

variableDeltaSerializer.SerializeVariable(&serializationContext, var4Reliable);

variableDeltaSerializer.EndSerialize(&serializationContext);

return RM3SR_SERIALIZED_ALWAYS_IDENTICALLY;

}

 

virtual void Deserialize(RakNet::DeserializeParameters *deserializeParameters)

{

VariableDeltaSerializer::DeserializationContext deserializationContext;

variableDeltaSerializer.BeginDeserialize(&deserializationContext, &deserializeParameters->serializationBitstream[0]);

if (variableDeltaSerializer.DeserializeVariable(&deserializationContext, var3Reliable))

printf("var3Reliable changed to %i\n", var3Reliable);

if (variableDeltaSerializer.DeserializeVariable(&deserializationContext, var4Reliable))

printf("var4Reliable changed to %i\n", var4Reliable);

variableDeltaSerializer.EndDeserialize(&deserializationContext);

}

 

快速开始:

1. Connection_RM3派生,实现Connection_RM3::AllocReplica()方法。这是一个工厂函数,参数传递了类的标示符(例如名字),返回一个该类的实例。应该可以返回你游戏中的任何的网络对象。

2. ReplicaManager3派生,实现AllocConnection()DeallocConnection()函数,返回在第一步创建的类。

       3. Replica3派生你的网络游戏对象。所有的纯虚方法必须实现,然而默认根据你的网络结构提供了Replica3::QueryConstruction()Replica3::QueryRemoteConstruction()方法。

       4. 在本地系统上创建了一个新的游戏对象,将它传递给ReplicaManager3::Reference()方法。

       5. 本地系统上一个游戏对象销毁时,想要其他的系统知道该对象被销毁,调用Replica3::BroadcastDestruction()方法。

       6. ReplicaManager3作为一个插件附加到RakPeer上。

 

       所有函数列表,以及函数参数的详细文档,参考ReplicaManager.h文件。

 

       主要样例位于Samples\ReplicaManager3中。

 

ReplicaManager3ReplicaManager2的区别

ReplicaManager3应该更简单,更加透明化

1. Connection_RM2::Construct现在是两个函数:Connection_RM3::AllocReplica()Connection_RM3::DeserializeConstruction()。先前,在Connection_RM2::Construct中给的是原始数据,需要你自己创建和销毁对象的构建。现在AllocRelica会创建对象,DeserializeConstruction会为对象填充数据。

       2. 由于上述变化,NetworkIDcreatingSystemGUID,和replicaManager等变量在你得到DeserializeConstruction回调之前,已经被设置为成员变量了。这个简化使用主要是因为对象已经准备被使用了。

       3. 同一个tick创建的对象前面是使用单独的消息发送。这意味着对于连接的用户,他可能在不同的远端游戏ticks接收到两个对象。如果开始工作之前,两个对象相互依赖,那将出现问题。现在,在同一个tick创建的对象在同一个消息中发送(RakPeerInterface::Receive()调用定义,这个函数会调用PluginInterface2::Update()方法)

       4. 先前,你需要使用一个特殊的连接工厂类调用ReplicaManager2::SetConectionFactory()方法创建Connction_RM2实例。现在,ReplicaManager3自己有纯虚函数AllocaConnection()DeallocConnection()

       5. 先前,对对象的访问是隐式的。如果对象不存在,调用RelicaManager2::SendConstructionReplicaManager2::SendSerialize,或者RelicaManager2::SendVisiblity可以注册实例。现在访问对象是显示的,使用ReplicaManager3::Reference()替代了ReplicaManager2的这三个调用。这是先前的混乱的源头,这些Send函数(或者Broadcast替代函数)没有校验对应的Relica2::Query*函数。ConstructionSerialization函数现在没有了,通过自动的更新tick实现。

       6. ReplicaManager2没有支持每一个连接不同的SerializationReplicaManager3做到了,通过从ReplicaManager3::Serialize函数返回RM3SR_SERIALIZED_UNIQUELY消息实现。如果对所有的连接,Serializations是相同的,那么返回RM3SR_SERIALIZED_IDENDICALLY更加高效。

       7. ReplicaManager3不支持可见命令,例如ReplicaManager2::SendVisibility,以保持系统更加简单,更加透明化。要支持这个功能,增加一个布尔类型可见标记。在Serialize中转换一次,使用RM3SR_SERIALIZED_UNIQUELY来转化。在远端系统上,如果可见标记是false,隐藏这个对象。在发送系统上,如果可见标记是false,从ReplicaManager3::Serialize函数返回RM3SR_DO_NOT_SERIALIZE。你可以验证这个replica/connection对的可见标记是不是已经被SerializeParameter::lastSerializationSent改变了,lastSerializationSent里面包含了SerializeParameters::outputBitstream函数中最后传递的值。

       8. RelicaManager3不支持Connection_RM2::SerializeDownloadStarted函数,使得系统更加简单和透明。可以再ReplicaManager3::SerializeConstruction函数中使用destinationConnection->IsInitialDownload()函数进行验证。更多的复杂操作,也可以在注册远端系统时发送数据。参数autoCreate = false调用RelicaManager3::SetAutoManageConnections函数。发送你的数据,然后调用ReplicaManager3::PushConnection函数。

       9. QueryDestruction 不在存在。QueryConstruction现在返回值表明析构。

10. QueryIs*Authority不在存在,从ReplicaManager3函数中返回值达到相同的结果。

0 0
原创粉丝点击