UE5 客户端对时实现 & 非玩家角色移动问题解决

379 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第13天,点击查看活动详情


对时

联动:从零开始的MMORPG游戏服务器(5) - Scene Server(3) - 对时 - 掘金 (juejin.cn)

服务器端的对时逻辑在上面的文章中已经写好,本节说明客户端的实现方式。

客户端与服务器的 Socket 通信使用虚幻商城中免费的 TcpSocketConnection,通过子类化实现发送自定义 Proto 数据的目的。子类化后的类名为 AMP5DemoTcpSocketConnection,需要定义函数 SendTimeSync,用来向服务器发送 TimeSync 数据包。

对于本系列来说,Proto协议被定义为顶层全部为 Packet 类型的消息,通过 oneof 关键字区分载荷内容,TimeSync 就是其中一种。目前 TimeSync 消息内没有任何内容,以后想到了再完善。

协议包装与发送代码:

void AMP5DemoTcpSocketConnection::SendTimeSync(int64 Timestamp)
{
   TimeSync *TS = new TimeSync();
   Packet *P = new Packet();
   P->set_id(GetPacketId());
   P->set_timestamp(Timestamp);
   P->set_allocated_time_sync(TS);
   SendProtoData(P);
}

bool AMP5DemoTcpSocketConnection::SendProtoData(Packet *P)
{
   size_t PacketSize = P->ByteSizeLong();
   TArray<uint8> DataArray1;
   DataArray1.SetNum(PacketSize);
   P->SerializeToArray(DataArray1.GetData(), PacketSize);
   
   // 数据包序列化过程
   std::string str = P->SerializeAsString();
   FString FData(str.c_str());
   TArray<uint8> DataArray;
   DataArray.SetNum(FData.Len());
   memcpy(DataArray.GetData(), TCHAR_TO_ANSI(*FData), FData.Len());
   
   SendData(connectionIdGameServer, DataArray1);
   
   return SendResult;
}

其中数据包的序列化过程,从 C++ 原生类型向 UE 类型转化,搜索方法花了不少功夫。

这个类还需要存放一个回调列表,以及一个注册回调函数,区分不同类型的消息收到后应该由谁负责处理:

TMap<Packet::PayloadCase, FMessageDelegateBase*> MessageDelegates;

void BindMessageDelegate(Packet::PayloadCase PC, FMessageDelegateBase *MD);

此处回调的类型为 FMessageDelegateBase,是一个抽象类,所有要处理接收到的信息的类都需要继承这个类实现多态:

class MP5DEMO_API FMessageDelegateBase
{
public:
   FMessageDelegateBase();
   virtual ~FMessageDelegateBase() = 0;

   virtual void OnMessageReceived(Packet *P) = 0;
};

AMP5DemoTcpSocketConnection 类收到消息后,会根据回调列表中对应消息类型的对象,调用 OnMessageReceived 方法。

之后再创建一个 Actor 的子类 ANetDelayManagerActor,目前暂时用来专门与服务器对时。

实现一个 UFUNCTION 方法用来给蓝图类调用,并实现上面 FMessageDelegateBase 类的 OnMessageReceived 方法:

UFUNCTION(BlueprintCallable)
void StartTimeSync(FTimeSyncCompleteCallback Callback);
void ANetDelayManagerActor::StartTimeSync(FTimeSyncCompleteCallback Callback)
{
   this->TimeSyncCompleteCallback = Callback;
   OldTimestamp = TcpConnection->GetTimestamp();
   this->TcpConnection->SendTimeSync(OldTimestamp);
}

void ANetDelayManagerActor::OnMessageReceived(Packet* P)
{
   const int64 NewTimestamp = TcpConnection->GetTimestamp();

   const int32 TempDelay = (NewTimestamp - OldTimestamp) / 2;

   if (Delay != 0)
   {
      Delay = (TempDelay + Delay) / 2;
   }
   else
   {
      Delay = TempDelay;
   }

   UE_LOG(LogTemp, Warning, TEXT("New delay: %dms"), Delay);
   TcpConnection->SendTimeSync(TcpConnection->GetTimestamp());
}

代码实现的部分基本完成,下面在引擎里测试一下:

微信截图_20221025194819.png

非玩家角色移动问题

多人测试的时候需要另一个不被 PlayerController 控制的角色,但是我使用 AIControllerAIMoveto 方法让这个角色向玩家控制角色移动,但是怎么也不动。一开始我以为 AIController 的生效是需要有什么手动设置或者初始化才会生效。但是经过一番查找资料后,发现如果一个 Character 不是玩家控制,那么它就默认被 AIController 控制,但是要想让其正常工作还需要一个东西—— NavMesh,地图中必须有这个东西角色才能在 AIController 的控制下移动。于是我在场景中添加了一个 NavMeshBoundVolumn,这个Volumn会在范围内把场景中可以站立移动的地方标出来,就像这样:

image.png

加入这个之后,非玩家控制的角色就能动起来了。