目录(Contents)
前言:为什么要学习异步编程?
想象一下这样的场景:你的游戏需要加载一个大型地图,如果让玩家干等几十秒,他们可能会失去耐心。异步编程就是解决这类问题的钥匙——它让程序在等待耗时操作(如加载资源、网络请求)时,不会“卡住”整个游戏。
Unreal Engine提供了一套叫做Future/Promise的工具,让你能以清晰、简洁的方式编写异步代码。别担心,我们会从最基础的概念讲起,一步步带你掌握这项强大技能!
第一部分:基础知识(小白也能懂)
1.1 什么是Future和Promise?
让我们用生活中的例子来理解:
比喻理解:
- Promise(承诺):就像你在网上订了一份外卖,商家承诺会在30分钟内送达
- Future(未来):你拿到的订单号,通过它可以查询外卖的送达状态
代码中的对应关系:
cpp
// 商家(Promise)承诺给你一份披萨 TPromise<FString> PizzaPromise; // "承诺"一个披萨 // 你拿到订单号(Future),可以用来查披萨状态 TFuture<FString> PizzaFuture = PizzaPromise.GetFuture(); // "未来"能拿到披萨
1.2 最简单的例子:数数
让我们从最简单的例子开始,看看如何使用Future/Promise:
cpp
// 创建一个Promise,承诺会给你一个数字
TPromise<int> MyPromise;
// 获取对应的Future(就像拿到订单号)
TFuture<int> MyFuture = MyPromise.GetFuture();
// 在另一个地方(或另一个线程)履行承诺
void FulfillPromise()
{
// 商家说:"披萨做好了!数字是42"
MyPromise.SetValue(42);
}
// 你随时可以检查披萨是否送达
void CheckPizza()
{
if (MyFuture.IsReady()) // 检查是否准备好
{
int Number = MyFuture.Get(); // 拿到数字42
UE_LOG(LogTemp, Log, TEXT("我拿到了数字:%d"), Number);
}
else
{
UE_LOG(LogTemp, Log, TEXT("还没准备好呢..."));
}
}
第二部分:实际应用(从简单到复杂)
2.1 场景一:延迟显示消息(最简单)
需求:3秒后显示”Hello World”
cpp
// 传统方式(你可能已经会了):
FTimerHandle TimerHandle;
GetWorld()->GetTimerManager().SetTimer(TimerHandle, []() {
UE_LOG(LogTemp, Log, TEXT("Hello World!"));
}, 3.0f, false);
// 使用Future的方式:
void ShowDelayedMessage()
{
TPromise<void> Promise; // 创建一个void类型的Promise(不返回具体值)
TFuture<void> Future = Promise.GetFuture();
// 启动一个延迟任务
AsyncTask(ENamedThreads::AnyThread, [Promise = MoveTemp(Promise)]() mutable {
// 等待3秒
FPlatformProcess::Sleep(3.0f);
// 履行承诺
Promise.SetValue();
});
// 当Future完成时执行操作
Future.Next([]() {
UE_LOG(LogTemp, Log, TEXT("Hello World!"));
});
}
2.2 场景二:加载资源并显示
需求:异步加载一张图片,加载完成后显示
cpp
// 第1步:定义加载函数
TFuture<UTexture2D*> LoadTextureAsync(const FString& TexturePath)
{
// 创建Promise
TPromise<UTexture2D*> Promise;
// 获取Future(立刻就能返回,不用等加载完成)
TFuture<UTexture2D*> Future = Promise.GetFuture();
// 在后台线程加载
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask,
[Promise = MoveTemp(Promise), TexturePath]() mutable
{
// 模拟耗时的加载过程
FPlatformProcess::Sleep(1.0f); // 假装加载需要1秒
// 实际加载代码(简化版)
UTexture2D* Texture = LoadObject<UTexture2D>(nullptr, *TexturePath);
// 履行承诺:告诉调用者图片加载好了
Promise.SetValue(Texture);
});
return Future;
}
// 第2步:使用加载函数
void UseTexture()
{
// 开始加载(不会阻塞!)
TFuture<UTexture2D*> TextureFuture = LoadTextureAsync("/Game/Textures/MyTexture");
// 设置加载完成后的回调
TextureFuture.Next([](UTexture2D* LoadedTexture) {
if (LoadedTexture)
{
// 显示图片(注意:需要在游戏线程操作UI)
AsyncTask(ENamedThreads::GameThread, [LoadedTexture]() {
if (UImage* MyImage = GetMyImage())
{
MyImage->SetBrushFromTexture(LoadedTexture);
UE_LOG(LogTemp, Log, TEXT("图片加载成功并显示!"));
}
});
}
});
// 程序会立刻继续执行这里,不会等待加载完成
UE_LOG(LogTemp, Log, TEXT("已经开始加载图片,我继续做其他事情..."));
}
2.3 场景三:多个异步任务组合
需求:同时加载玩家头像和名字,都完成后显示完整信息
cpp
// 传统方式可能很复杂,但用Future可以很优雅:
// 1. 加载头像
TFuture<UTexture2D*> LoadAvatarAsync(int32 PlayerID)
{
TPromise<UTexture2D*> Promise;
// ... 异步加载代码
return Promise.GetFuture();
}
// 2. 加载名字
TFuture<FString> LoadPlayerNameAsync(int32 PlayerID)
{
TPromise<FString> Promise;
// ... 异步加载代码
return Promise.GetFuture();
}
// 3. 组合两个任务
void ShowPlayerInfo(int32 PlayerID)
{
// 同时开始两个加载任务
TFuture<UTexture2D*> AvatarFuture = LoadAvatarAsync(PlayerID);
TFuture<FString> NameFuture = LoadPlayerNameAsync(PlayerID);
// 方法A:分别处理(简单)
AvatarFuture.Next([PlayerID](UTexture2D* Avatar) {
UE_LOG(LogTemp, Log, TEXT("玩家%d的头像加载完成"), PlayerID);
// 显示头像...
});
NameFuture.Next([PlayerID](const FString& Name) {
UE_LOG(LogTemp, Log, TEXT("玩家%d的名字是:%s"), PlayerID, *Name);
// 显示名字...
});
// 方法B:等两个都完成再处理(使用Then链式调用)
AvatarFuture.Then([NameFuture = MoveTemp(NameFuture), PlayerID](TFuture<UTexture2D*> AvatarFutureInner) mutable
{
// 等待名字也加载完成
return NameFuture.Next([AvatarFutureInner = MoveTemp(AvatarFutureInner), PlayerID](const FString& Name) mutable
{
UTexture2D* Avatar = AvatarFutureInner.Get();
UE_LOG(LogTemp, Log, TEXT("玩家%d的完整信息:名字=%s,头像已加载"), PlayerID, *Name);
// 两个都准备好了,可以一起显示
return FPlayerInfo{ Name, Avatar };
});
});
}
第三部分:核心概念深入理解
3.1 Future的三种状态
cpp
TFuture<int> MyFuture = SomeAsyncFunction();
// 状态1:检查是否有效
if (MyFuture.IsValid()) // 类似判断指针是否为空
{
// Future是有效的(有对应的Promise)
}
// 状态2:检查是否完成
if (MyFuture.IsReady()) // 类似检查文件是否下载完
{
// 结果已经准备好了,可以立即获取
int Result = MyFuture.Get();
}
// 状态3:等待结果
MyFuture.Wait(); // 阻塞等待,直到结果准备好
// 或者带超时的等待
bool bReady = MyFuture.WaitFor(FTimespan::FromSeconds(5));
if (bReady)
{
int Result = MyFuture.Get();
}
3.2 重要的区别:Get() vs Consume()
cpp
TFuture<FString> GetPlayerNameAsync();
// 方法1:Get() - 只读,不转移所有权
{
TFuture<FString> Future = GetPlayerNameAsync();
FString Name1 = Future.Get(); // 可以多次调用Get()
FString Name2 = Future.Get(); // 再次获取,Future仍然有效
// Future在这里仍然可以用
}
// 方法2:Consume() - 消费,转移所有权
{
TFuture<FString> Future = GetPlayerNameAsync();
FString Name = Future.Consume(); // 获取结果,Future变为无效
// 不能再使用Future了!下面的代码会崩溃:
// FString Name2 = Future.Get(); // 错误!
}
// 比喻理解:
// Get()就像查看快递状态(可以反复查看)
// Consume()就像签收快递(只能签收一次)
3.3 链式调用的魔法:Then和Next
cpp
// 假设有三个异步函数
TFuture<int> GetPlayerLevelAsync();
TFuture<FString> GetRankNameAsync(int Level);
TFuture<float> CalculatePowerAsync(const FString& Rank);
// 传统嵌套回调(回调地狱!)
GetPlayerLevelAsync().Next([](int Level) {
GetRankNameAsync(Level).Next([](const FString& Rank) {
CalculatePowerAsync(Rank).Next([](float Power) {
// 三层嵌套,难以阅读和维护
DisplayPower(Power);
});
});
});
// 使用链式调用(清晰!)
GetPlayerLevelAsync()
.Next(GetRankNameAsync) // 自动传递结果给下一个函数
.Next(CalculatePowerAsync) // 继续传递
.Next(DisplayPower); // 最终处理
// 或者更灵活的方式:
GetPlayerLevelAsync()
.Next([](int Level) {
// 这里可以做一些额外处理
return Level * 2;
})
.Next(GetRankNameAsync) // 传递处理后的结果
.Next([](const FString& Rank) {
UE_LOG(LogTemp, Log, TEXT("玩家等级是:%s"), *Rank);
return Rank;
})
.Next(CalculatePowerAsync);
第四部分:实际游戏开发示例
4.1 示例:登录系统
cpp
// 游戏登录流程:验证→加载配置→初始化游戏
TFuture<bool> LoginAsync(const FString& Username, const FString& Password)
{
TPromise<bool> Promise;
// 第一步:验证用户名密码(网络请求)
VerifyCredentialsAsync(Username, Password)
.Next([Username](bool bValid) -> TFuture<FPlayerConfig>
{
if (!bValid)
{
throw std::runtime_error("登录失败");
}
// 第二步:加载玩家配置
return LoadPlayerConfigAsync(Username);
})
.Next([Username](const FPlayerConfig& Config) -> TFuture<void>
{
// 第三步:初始化游戏状态
return InitializeGameAsync(Config);
})
.Next([Promise = MoveTemp(Promise)]() mutable
{
// 所有步骤成功完成
Promise.SetValue(true);
return true;
})
.Next([](TFuture<bool> Future)
{
// 错误处理
try
{
Future.Get();
}
catch (const std::exception& e)
{
UE_LOG(LogTemp, Error, TEXT("登录失败: %s"), UTF8_TO_TCHAR(e.what()));
ShowErrorMessage("登录失败,请重试");
}
});
return Promise.GetFuture();
}
// 使用登录系统
void StartLogin()
{
LoginAsync("Player1", "password123")
.Next([](bool bSuccess) {
if (bSuccess)
{
UE_LOG(LogTemp, Log, TEXT("登录成功!"));
StartGame();
}
});
}
4.2 示例:成就系统
cpp
// 检查并解锁成就
void CheckAchievement(int32 PlayerScore)
{
// 并行检查多个成就条件
TArray<TFuture<bool>> AchievementFutures;
// 成就1:分数超过1000
AchievementFutures.Add(CheckScoreAchievementAsync(PlayerScore, 1000));
// 成就2:连续登录7天
AchievementFutures.Add(CheckLoginStreakAsync(7));
// 成就3:完成所有教程
AchievementFutures.Add(CheckTutorialCompletionAsync());
// 等待所有检查完成
WhenAll(AchievementFutures).Next([](TArray<bool> Results)
{
for (int32 i = 0; i < Results.Num(); i++)
{
if (Results[i])
{
UnlockAchievement(i); // 解锁成就
}
}
});
}
// 辅助函数:等待所有Future完成
template<typename T>
TFuture<TArray<T>> WhenAll(const TArray<TFuture<T>>& Futures)
{
TPromise<TArray<T>> Promise;
auto State = MakeShared<FWhenAllState<T>>();
State->Results.SetNum(Futures.Num());
State->RemainingCount = Futures.Num();
for (int32 i = 0; i < Futures.Num(); i++)
{
Futures[i].Next([State, i](T Result) mutable
{
State->Results[i] = MoveTemp(Result);
if (--State->RemainingCount == 0)
{
State->Promise.SetValue(MoveTemp(State->Results));
}
});
}
return Promise.GetFuture();
}
4.3 示例:资源预加载
cpp
// 游戏启动时预加载常用资源
TFuture<void> PreloadCommonResources()
{
// 需要预加载的资源列表
TArray<FString> ResourcesToLoad = {
"/Game/Textures/Common/ButtonNormal",
"/Game/Textures/Common/ButtonHovered",
"/Game/Sounds/UI/Click",
"/Game/Sounds/UI/Hover",
"/Game/Fonts/MainFont"
};
TArray<TFuture<UObject*>> LoadFutures;
// 并行加载所有资源
for (const FString& ResourcePath : ResourcesToLoad)
{
LoadFutures.Add(LoadResourceAsync(ResourcePath));
}
// 显示加载进度
TSharedPtr<int32> LoadedCount = MakeShared<int32>(0);
for (auto& Future : LoadFutures)
{
Future.Next([LoadedCount, Total = ResourcesToLoad.Num()](UObject* Resource)
{
(*LoadedCount)++;
float Progress = static_cast<float>(*LoadedCount) / Total;
UpdateLoadingScreen(Progress); // 更新进度条
if (Resource)
{
CacheResource(Resource); // 缓存资源
}
});
}
// 所有资源加载完成后
return WhenAll(LoadFutures).Next([](TArray<UObject*> Resources)
{
UE_LOG(LogTemp, Log, TEXT("预加载完成,共加载%d个资源"), Resources.Num());
ShowMainMenu(); // 显示主菜单
});
}
第五部分:常见问题与调试技巧
5.1 新手常见错误
cpp
// 错误1:忘记调用Get()或Wait()
TFuture<int> Future = CalculateAsync();
// int Result = Future; // 错误!不能直接赋值
int Result = Future.Get(); // 正确!需要调用Get()
// 错误2:在无效的Future上调用方法
TFuture<int> Future;
Future.Get(); // 崩溃!Future是无效的
// 正确做法:先检查有效性
if (Future.IsValid() && Future.IsReady())
{
int Result = Future.Get();
}
// 错误3:在错误的线程操作
Future.Next([](int Result) {
// 这个回调可能在非游戏线程执行
UpdateUI(Result); // 可能崩溃!
});
// 正确做法:切换到游戏线程
Future.Next([](int Result) {
AsyncTask(ENamedThreads::GameThread, [Result]() {
UpdateUI(Result); // 安全!
});
});
5.2 调试技巧
cpp
// 技巧1:添加日志跟踪
TFuture<int> DebuggableFuture = SomeAsyncFunction()
.Next([](int Value) {
UE_LOG(LogTemp, Log, TEXT("第一步完成,值=%d"), Value);
return Value * 2;
})
.Next([](int Value) {
UE_LOG(LogTemp, Log, TEXT("第二步完成,值=%d"), Value);
return Value + 10;
});
// 技巧2:使用Promise的构造函数添加完成回调
TPromise<int> Promise([]() {
UE_LOG(LogTemp, Log, TEXT("Promise完成了!"));
});
5.3 性能优化建议
cpp
// 建议1:避免不必要的拷贝 TPromise<FBigStruct> Promise; // 不好:会产生临时对象和拷贝 FBigStruct Data = CreateBigData(); Promise.SetValue(Data); // 拷贝发生在这里 // 好:使用EmplaceValue直接构造 Promise.EmplaceValue(CreateBigData()); // 直接构造,无拷贝 // 建议2:合理使用共享Future TFuture<FString> OriginalFuture = LoadConfigAsync(); // 如果多个地方需要读取结果 TSharedFuture<FString> SharedFuture1 = OriginalFuture.Share(); TSharedFuture<FString> SharedFuture2 = OriginalFuture.Share(); // SharedFuture1和SharedFuture2共享同一个结果
第六部分:进阶话题(可选学习)
6.1 自定义Future适配器
cpp
// 创建带超时的Future
template<typename T>
TFuture<T> WithTimeout(TFuture<T>&& InnerFuture, float TimeoutSeconds)
{
TPromise<T> Promise;
// 设置超时计时器
FTimerHandle TimeoutHandle;
GetWorld()->GetTimerManager().SetTimer(TimeoutHandle,
[Promise = Promise]() mutable {
Promise.SetValue(T()); // 返回默认值
}, TimeoutSeconds, false);
// 正常完成处理
InnerFuture.Next([Promise = MoveTemp(Promise), TimeoutHandle](T Result) mutable {
// 取消超时计时器
GetWorld()->GetTimerManager().ClearTimer(TimeoutHandle);
Promise.SetValue(Result);
});
return Promise.GetFuture();
}
// 使用示例
TFuture<FString> Future = LoadDataAsync()
.Then(WithTimeout<FString>, 5.0f); // 5秒超时
6.2 错误处理模式
cpp
// 封装带错误处理的Future
template<typename T>
class TResult
{
public:
bool bSuccess;
T Value;
FString ErrorMessage;
};
template<typename T>
TFuture<TResult<T>> SafeAsync(TFunction<T()> Operation)
{
TPromise<TResult<T>> Promise;
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask,
[Operation = MoveTemp(Operation), Promise = MoveTemp(Promise)]() mutable
{
TResult<T> Result;
try
{
Result.Value = Operation();
Result.bSuccess = true;
}
catch (const std::exception& e)
{
Result.bSuccess = false;
Result.ErrorMessage = UTF8_TO_TCHAR(e.what());
}
Promise.SetValue(Result);
});
return Promise.GetFuture();
}
// 使用安全版本
SafeAsync<int>([]() -> int {
if (SomeCondition())
{
throw std::runtime_error("出错了!");
}
return 42;
}).Next([](TResult<int> Result) {
if (Result.bSuccess)
{
UE_LOG(LogTemp, Log, TEXT("成功:%d"), Result.Value);
}
else
{
UE_LOG(LogTemp, Error, TEXT("失败:%s"), *Result.ErrorMessage);
}
});
总结:学习路径建议
🎯 初学者(第一周)
- 理解Promise/Future的基本概念(订外卖的比喻)
- 学会创建简单的异步任务(3秒后显示消息)
- 掌握
Get()和IsReady()的基本使用
📈 进阶者(第二周)
- 在实际项目中使用异步资源加载
- 学会使用
Then()和Next()进行链式调用 - 理解线程安全,学会在正确的线程操作UI
🚀 熟练者(第三周)
- 处理复杂的异步流程(登录、成就系统)
- 组合多个Future(WhenAll等模式)
- 添加错误处理和调试信息
🏆 专家(第四周及以后)
- 创建自定义Future适配器
- 优化异步代码性能
- 设计基于Future的架构模式
最后的提醒
- 从简单开始:先在一个小功能上尝试,成功了再应用到复杂场景
- 多写多练:异步编程需要实践才能掌握
- 利用调试工具:UE提供了强大的调试功能,善加利用
- 阅读官方源码:UE的Future/Promise实现非常优秀,是学习的好材料
记住:异步编程是现代游戏开发的必备技能。虽然开始可能有些挑战,但一旦掌握,你将能够创建更加流畅、响应更快的游戏体验。加油!🎮
