构建Unreal可用的protobuf库
这里有一篇现成可用的教程
UE4随笔:接入 Google Protobuf 库
需要注意的点:
-
Protobuf和Protoc的版本要一样,Protoc可以不用按照上面教程中自己生成解决方案(只生成libprotobuf就可以),直接从git下载然后配置环境变量就可以
-
这个方法不适用 3.22之后的版本,因为下载的文件内容差别比较大,以最新的3.23.4为例
(1)3.22之后不再有单个语言的版本,需要下载完整版
(2)完整版的CMakeList是在根目录下,不在cmake文件夹中,所以cmake的时候src要改成根目录
(3)新版本的Protobuf不再自带googletest和abseil,需要单独下载到third_party下对应的文件夹里
·(4)cmake的时候可能会报缺少zlib,就需要再下载zlib,然后cmake并生成库文件(操作和构建protobuf完全一样),再将生成的Debug和Release库路径添加到当前cmake的配置选项中,具体可以看这篇文章的前半部分
(5)cmake完生成的文件夹中不再有extract_include.bat文件,无法提取头文件库,lib的生成按照上述步骤是可以正常生成的 -
上面的文章中是将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使用注意事项
- Protobuf的版本在客户端和服务端一定要保持一致,不然可能会出现各种奇怪的问题
- 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;
}
}
- Debug打印pb消息可以使用message.DebugString()获取debug字符串,格式和proto文件中定义的类似,非常易读
- 可以利用MessageToJson和JsonStringToMessage两个库函数将pb消息和json进行转换
- protobuf消息中表示任意类型的消息一定要用google.protobuf.Any类型,使用Pack和Unpack解析,不能使用string然后自行ParseFromString解析,原因应该和前面一样消息是以bytes格式存储而不是字符串
- python访问pb中的枚举时不需要加枚举类型名,像下面这种直接使用Msg.A就可以了
message Msg{
enum MsgType{
A = 0;
B = 1;
}
MsgType MsgType = 1;
google.protobuf.Any MsgContent = 2;
}
- Unreal端不建议直接include生成的proto生成的类文件使用对应的类型,一方面是过多类型都直接依赖proto类文件可能存在隐患,另一方面是proto生成的是c++类型,但unreal中很多类型都和c++有区别,直接使用起来会很不方便。所以建议加一层转译,由某个管理类负责,通过构造函数将proto类型转为传统的unreal类型(比如NPCAction(protoClass)转为FNPCAction(UStruct))
Websocket注意事项
- Unreal引擎中自带了Websocket模块,在build.cs中添加依赖就可以直接使用
- 在创建websocket前需要先加载模块,可以在GameInstance或者Gamemode中加载
FModuleManager::Get().LoadModuleChecked(TEXT("WebSockets"));
WebSocket = FWebSocketsModule::Get().CreateWebSocket(ServerURL, ServerProtocol);
- 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)
- python server端需要以异步方式调用发送消息函数,可以将消息加入队列中用单独的线程发送
async def send(self, message):
await self.__websocket.send(message)
- 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,可以考虑逻辑类型仅将消息放进队列中,然后有一个独立的线程以一定时间间隔发送消息