使用Protobuf+Websocket构建Unreal与Python服务器的通信

构建Unreal可用的protobuf库

这里有一篇现成可用的教程
UE4随笔:接入 Google Protobuf 库
需要注意的点:

  1. Protobuf和Protoc的版本要一样,Protoc可以不用按照上面教程中自己生成解决方案(只生成libprotobuf就可以),直接从git下载然后配置环境变量就可以

  2. 这个方法不适用 3.22之后的版本,因为下载的文件内容差别比较大,以最新的3.23.4为例
    (1)3.22之后不再有单个语言的版本,需要下载完整版
    (2)完整版的CMakeList是在根目录下,不在cmake文件夹中,所以cmake的时候src要改成根目录
    (3)新版本的Protobuf不再自带googletestabseil,需要单独下载到third_party下对应的文件夹里
    ·使用Protobuf+Websocket构建Unreal与Python服务器的通信_第1张图片

    (4)cmake的时候可能会报缺少zlib,就需要再下载zlib,然后cmake并生成库文件(操作和构建protobuf完全一样),再将生成的Debug和Release库路径添加到当前cmake的配置选项中,具体可以看这篇文章的前半部分
    (5)cmake完生成的文件夹中不再有extract_include.bat文件,无法提取头文件库,lib的生成按照上述步骤是可以正常生成的

  3. 上面的文章中是将Protobuf构建为了第三方库,如果要构建为插件的话,build.cs应该这么配置,注意include和lib文件夹路径和实际路径匹配就可以了

	public Protobuf(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
		//Type = ModuleType.External;
		if (Target.Platform == UnrealTargetPlatform.Win64)
		{
			PublicSystemIncludePaths.Add(Path.Combine(ModuleDirectory, "include"));
			PublicSystemIncludePaths.Add(Path.Combine(ModuleDirectory, "lib"));
			PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "lib", "libprotobuf.lib"));
		}

		ShadowVariableWarningLevel = WarningLevel.Off;
		bEnableUndefinedIdentifierWarnings = false;
		bEnableExceptions = true;
		PublicDefinitions.Add("_CRT_SECURE_NO_WARNINGS");
		PublicDefinitions.Add("GOOGLE_PROTOBUF_NO_RTTI=1");
		PublicDefinitions.Add("GOOGLE_PROTOBUF_CMAKE_BUILD");
	}

Protobuf使用注意事项

  1. Protobuf的版本在客户端和服务端一定要保持一致,不然可能会出现各种奇怪的问题
  2. Protobuf的消息序列化和反序列化在c++端api有两个,但只是存储形式有差别而已,实际存储的内容都是以bytes字节流的格式;而在python端只有一个api,只接收bytes类型的参数,所以要注意在Unreal端使用Websocket或其他协议进行发送时要注意以字节流形式发送,如果以string形式发送的话python端就会解析报错
bool SerializeToArray(
    void* data, 
    int size) const
bool SerializeToString(
    std::string* output) const
(function) ParseFromString: Any

以webscoket为例,unreal端的发送应该是这样,如果不将isBinary设置为true或者使用SerializeToString +Send(FString&)以字符串形式发送都会在python服务器端解析出错

char buf[2048];
ClientMsg.SerializeToArray(buf, ClientMsg.ByteSizeLong());
WebSocket->Send(Data, ClientMsg.ByteSizeLong(), true);

同理,因为protobuf消息为字节流形式,所以unreal端在接收时需要使用OnBinaryMessage而不是OnMessage,需要处理下一个包过大被分成多个Fragment的情况

WebSocket->OnBinaryMessage().AddRaw(this, &AIGameCoreNetwork::OnReceiveMessage);

void AIGameCoreNetwork::OnReceiveMessage(const void* Data, SIZE_T Size, bool bIsLastFragment)
{
	//UE_LOG(LogAIGameCoreNetwork, Log, TEXT("[ReceiveMessage]: %s"), *ReceivedMsg);
	
	if(ReceivedIndex + Size >= MAX_RECEIVE_BUFFER_SIZE)
	{
		UE_LOG(LogAIGameCoreNetwork, Error, TEXT("Receive Buffer runs out!"));
		return;
	}
	memcpy(&ReceiveBuffer[ReceivedIndex], Data, Size);
	ReceivedIndex += Size;
	
	if(bIsLastFragment)
	{
		ReceiveAIServerMsgEvent.Broadcast(ReceiveBuffer, ReceivedIndex);
		ReceivedIndex = 0;
	}
}
  1. Debug打印pb消息可以使用message.DebugString()获取debug字符串,格式和proto文件中定义的类似,非常易读
  2. 可以利用MessageToJson和JsonStringToMessage两个库函数将pb消息和json进行转换
  3. protobuf消息中表示任意类型的消息一定要用google.protobuf.Any类型,使用Pack和Unpack解析,不能使用string然后自行ParseFromString解析,原因应该和前面一样消息是以bytes格式存储而不是字符串
  4. python访问pb中的枚举时不需要加枚举类型名,像下面这种直接使用Msg.A就可以了
message Msg{
    enum MsgType{
        A = 0;
        B = 1;
    }
    
    MsgType MsgType = 1;
    google.protobuf.Any MsgContent = 2;
}
  1. Unreal端不建议直接include生成的proto生成的类文件使用对应的类型,一方面是过多类型都直接依赖proto类文件可能存在隐患,另一方面是proto生成的是c++类型,但unreal中很多类型都和c++有区别,直接使用起来会很不方便。所以建议加一层转译,由某个管理类负责,通过构造函数将proto类型转为传统的unreal类型(比如NPCAction(protoClass)转为FNPCAction(UStruct))

Websocket注意事项

  1. Unreal引擎中自带了Websocket模块,在build.cs中添加依赖就可以直接使用
  2. 在创建websocket前需要先加载模块,可以在GameInstance或者Gamemode中加载
	FModuleManager::Get().LoadModuleChecked(TEXT("WebSockets"));

	WebSocket = FWebSocketsModule::Get().CreateWebSocket(ServerURL, ServerProtocol);
  1. python服务器调用serve函数后,只有当收到消息后handler函数才会被调用(包括客户端连接消息),然后可以把对应的websocket存起来,之后就可以用这个websocket主动发消息给客户端
self.__server = websockets.serve(self.__handler, self.__host, self.__port)

async def __handler(self, websocket):
	self.__websocket = websocket

    async for message in websocket:
    	await self.msglistener(message)
  1. python server端需要以异步方式调用发送消息函数,可以将消息加入队列中用单独的线程发送
    async def send(self, message):
        await self.__websocket.send(message)
  1. websocket可以通过try-except的方式来检测客户端断开连接,当客户端断开连接时send和recv都会抛出websockets.exceptions.ConnectionClosed
        try:
            await self.__websocket.send(msg)
        except websockets.exceptions.ConnectionClosed:
            self.__on_client_disconnect()
        try:
        	async for message in websocket:
            	self.msglistener(message)
        except websockets.exceptions.ConnectionClosed:
            self.__on_client_disconnect()

总结

在unreal客户端这边,主要需要注意下protobuf的解析为字节流形式的问题,以及websocket使用binarymessage进行发送和接收
python服务器不需要关心类型问题,注意下需要先收到客户端消息才能获取到对应的websocket,然后需要保存下来用于之后主动发消息就可以了。因为发消息本身是异步,如果直接由逻辑类型发送就需要大部分函数都是async,可以考虑逻辑类型仅将消息放进队列中,然后有一个独立的线程以一定时间间隔发送消息

你可能感兴趣的:(Unreal网络,websocket,python,unreal,engine,游戏开发,c++)