在虚幻引擎5.5中实现MQTT协议通信是连接物联网设备、云服务和游戏逻辑的常见需求。本文将基于实际项目经验,详细解析如何通过C++在UE5.5中实现MQTT客户端的订阅/发布功能,特别是处理JSON消息和音频数据的完整流程。
首先需要在项目的Build.cs文件中添加必要的模块依赖。根据实际需求,MQTT通信通常需要以下核心模块支持:
csharp复制PublicDependencyModuleNames.AddRange(new string[] {
"Core",
"CoreUObject",
"Engine",
"InputCore",
"EnhancedInput",
"WebSockets", // 基础网络通信支持
"Json", // JSON序列化/反序列化
"JsonUtilities", // JSON工具类
"MQTTCore", // MQTT核心功能
"AudioMixer" // 音频数据处理
});
关键提示:确保MQTTCore模块已正确安装。可以通过Epic Games Launcher的引擎插件管理器或手动编译源码集成。
创建一个继承自UObject的类作为MQTT客户端封装。以下是核心头文件定义:
cpp复制// MyObject.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "IMQTTClient.h"
#include "Json.h"
#include "JsonUtilities.h"
#include "Misc/Base64.h"
#include "Misc/FileHelper.h"
UCLASS(BlueprintType, Blueprintable)
class METAHUMANCHARACTERHEIXI_API UMyObject : public UObject
{
GENERATED_BODY()
public:
TSharedPtr<IMQTTClient, ESPMode::ThreadSafe> MQTTClient;
UFUNCTION(BlueprintCallable, Category = "Demo")
void HelloWorld();
void SaveWav(const FString& FilePath, const TArray<uint8>& AudioBytes,
int32 SampleRate, int32 NumChannels);
TArray<uint8> ReceivedAudioBuffer;
protected:
virtual void BeginDestroy() override;
};
在实现文件中,首先需要初始化MQTT客户端并建立连接:
cpp复制// MyObject.cpp
void UMyObject::HelloWorld()
{
IMQTTCoreModule& MQTTModule = FModuleManager::LoadModuleChecked<IMQTTCoreModule>("MQTTCore");
FMQTTURL URL;
URL.Host = TEXT("127.0.0.1"); // 替换为实际MQTT broker地址
URL.Port = 1883;
MQTTClient = MQTTModule.GetOrCreateClient(URL);
if (!MQTTClient.IsValid())
{
UE_LOG(LogTemp, Error, TEXT("MQTTClient初始化失败!"));
return;
}
// 连接成功回调
MQTTClient->OnConnect().AddLambda([this](EMQTTConnectReturnCode ReturnCode) {
AsyncTask(ENamedThreads::GameThread, [this, ReturnCode]() {
if (ReturnCode == EMQTTConnectReturnCode::Accepted) {
UE_LOG(LogTemp, Warning, TEXT("MQTT连接成功"));
// 订阅主题
TArray<TPair<FString, EMQTTQualityOfService>> Topics;
Topics.Add(MakeTuple(FString("ue/messages"), EMQTTQualityOfService::Once));
Topics.Add(MakeTuple(FString("ue/audio"), EMQTTQualityOfService::Once));
MQTTClient->Subscribe(Topics);
}
});
});
MQTTClient->Connect();
}
设置消息接收回调是核心环节,需要注意线程安全问题:
cpp复制MQTTClient->OnMessage().AddLambda([](const FMQTTClientMessage& Msg) {
// 拷贝数据到GameThread处理
FString TopicCopy = Msg.Topic;
TArray<uint8> PayloadCopy = Msg.Payload;
AsyncTask(ENamedThreads::GameThread, [TopicCopy, PayloadCopy]() {
if (TopicCopy == "ue/audio") {
// 处理音频数据
FString FilePath = FPaths::ProjectSavedDir() / TEXT("received.wav");
SaveWav(FilePath, PayloadCopy, 44100, 1);
}
else if (TopicCopy == "ue/messages") {
// 处理JSON消息
FString PayloadStr(PayloadCopy.Num(), UTF8_TO_TCHAR((const char*)PayloadCopy.GetData()));
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(PayloadStr);
if (FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid()) {
FString Type = JsonObject->GetStringField("type");
if (Type == "command") {
FString CommandData = JsonObject->GetStringField("data");
// 处理命令逻辑
}
}
}
});
});
处理音频数据时需要正确封装WAV文件头:
cpp复制void UMyObject::SaveWav(const FString& FilePath, const TArray<uint8>& AudioBytes,
int32 SampleRate, int32 NumChannels)
{
if (AudioBytes.Num() == 0) return;
const int32 BitsPerSample = 16;
const int32 BlockAlign = NumChannels * BitsPerSample / 8;
const int32 ByteRate = SampleRate * BlockAlign;
const int32 DataSize = AudioBytes.Num();
const int32 ChunkSize = 36 + DataSize;
TArray<uint8> Wav;
Wav.Reserve(44 + DataSize);
// 辅助lambda函数
auto AppendInt32 = [&Wav](int32 V) {
Wav.Append(reinterpret_cast<uint8*>(&V), sizeof(int32));
};
auto AppendInt16 = [&Wav](int16 V) {
Wav.Append(reinterpret_cast<uint8*>(&V), sizeof(int16));
};
// WAV头写入
Wav.Append(reinterpret_cast<const uint8*>("RIFF"), 4);
AppendInt32(ChunkSize);
Wav.Append(reinterpret_cast<const uint8*>("WAVE"), 4);
Wav.Append(reinterpret_cast<const uint8*>("fmt "), 4);
AppendInt32(16);
AppendInt16(1);
AppendInt16(NumChannels);
AppendInt32(SampleRate);
AppendInt32(ByteRate);
AppendInt16(BlockAlign);
AppendInt16(BitsPerSample);
Wav.Append(reinterpret_cast<const uint8*>("data"), 4);
AppendInt32(DataSize);
// 添加PCM数据
Wav.Append(AudioBytes);
// 保存文件
FFileHelper::SaveArrayToFile(Wav, *FilePath);
}
对于大音频数据,建议采用分块接收和拼接的方式:
cpp复制void UMyObject::AppendAudioChunk(const TArray<uint8>& Chunk)
{
ReceivedAudioBuffer.Append(Chunk);
// 达到阈值后保存
if (ReceivedAudioBuffer.Num() > 1024 * 1024) { // 1MB
FString FilePath = FPaths::ProjectSavedDir() / TEXT("chunk.wav");
SaveWav(FilePath, ReceivedAudioBuffer, 44100, 1);
ReceivedAudioBuffer.Empty();
}
}
原始代码中提到的长消息崩溃问题通常由以下原因导致:
解决方案:
cpp复制// 在消息回调中添加安全检查
MQTTClient->OnMessage().AddLambda([](const FMQTTClientMessage& Msg) {
// 添加长度检查
if (Msg.Payload.Num() > 10 * 1024 * 1024) { // 限制10MB
UE_LOG(LogTemp, Warning, TEXT("消息过大,已忽略"));
return;
}
// ...其余处理逻辑
});
cpp复制// 二进制数据发布示例
void PublishAudioData(const TArray<uint8>& AudioData)
{
if (MQTTClient.IsValid() && MQTTClient->IsConnected()) {
FMQTTClientMessage Message;
Message.Topic = "ue/audio_publish";
Message.Payload = AudioData;
MQTTClient->Publish(Message);
}
}
确保在对象销毁时正确释放MQTT资源:
cpp复制void UMyObject::BeginDestroy()
{
if (MQTTClient.IsValid()) {
MQTTClient->Disconnect();
MQTTClient.Reset();
}
Super::BeginDestroy();
}
以下是典型的MQTT通信工作流程:
初始化阶段:
连接阶段:
订阅阶段:
消息处理阶段:
发布阶段:
清理阶段:
在实际项目中,建议将MQTT客户端封装成独立的子系统,便于全局管理和状态维护。对于高频消息场景,还需要考虑消息队列和流量控制机制。