配景
在数字孪生、数据看板、装备监控这类 UE 项目里,常常会遇到一个需求:运行时从服务器下载图片,然后表现到 UI、材质或场景对象上。
比如我们如今在做的是一个UE 运行态下的POI点位资产编辑平台,要可以大概编辑点位的名称,信息,图标,尺寸等,如下图所示,此中图标是从网络加载的:
此中图片资源在平台服务器端,用户自已上传图标,如下图所示:
单张图片下载看起来不复杂,但真正做成一个可复用插件后,会遇到不少 UE 特有的题目:异步回调、蓝图节点、运行时纹理、GC 生命周期、图片格式辨认、中文 URL 编码,以及跨项目打包后的偶发瓦解。
这篇文章复盘一次 ImageDownloader 插件的改造过程。
1. 为什么不做同步下载
最开始的需求是批量下载图片。直觉上好像可以做一个“同步下载”:按序次下载 A、B、C,全部完成后再继续。
但在 UE 运行时,这不是一个好选择。
网络哀求耗时不可控,假如在 GameThread 上同步期待,游戏画面、UI 相应都会被壅闭。多张图片连续下载时,卡顿会叠加;假如哀求超时或失败,期待时间更不可控。
以是终极方案不是同步下载,而是批量异步下载:
- HTTP 哀求异步发起
- 图片二进制解码放到背景线程
- UTexture2D 创建回到 GameThread
- 蓝图通过回调吸收单张完成和全部完成效果
焦点原则是:耗时数据处置处罚离开 GameThread,UObject 和渲染资源操纵回到 GameThread。
2. 批量下载的焦点计划批量下载节点吸收一个 URL 数组,然后为每个 URL 创建独立 HTTP 哀求。
每张图片完成后触发一次单图回调:- OnImageDownloaded(Index, ImageURL, Texture)
复制代码 全部图片都竣事后触发总回调:- OnAllComplete(Textures, SuccessStates)
复制代码 这里有一个很紧张的计划:效果数组保持输入序次。
比如输入是:纵然实际完成序次是 B、C、A,终极效果仍然写回原始索引:- Textures[Index] = Texture;
- SuccessStates[Index] = Texture != nullptr;
复制代码 如许蓝图侧不必要自己维护完成计数,也不必要处置处罚异步完成序次,只要在 OnAllComplete 里读取效果数组即可。
3. 蓝图异步节点的一个潜伏限定
这个插件最开始利用 UBlueprintAsyncActionBase 实现尺度蓝图异步节点。
它的工作方式大抵是:
- 静态工厂函数创建异步署理对象
- 蓝图绑定输失变乱
- UE 调用 Activate()
- 异步任务启动
- 完成后广播委托
这里有一个容易踩坑的点:在 UE 5.2 中,异步节点固然可以有多个 BlueprintAssignable 输失变乱,但数据输出 pin 通常只按照第一个委托署名天生。
也就是说,假如第一个变乱是:第二个变乱是:蓝图节点上大概只表现第一组数据 pin,导致总完成变乱拿不到数组。
办理方式有两个。
第一种是把两个变乱同一成同一个完备署名,让节点一次性天生全部数据 pin。
第二种是新增“变乱参数模式”,利用动态单播委托作为平凡函数参数,让蓝图直接接 Create Event 或自界说变乱。
终极插件生存了两套入口:- Download Images Async
- Download Images With Events
复制代码 前者得当快速利用,后者得当必要明确绑定自界说变乱、差别回调署名更清楚的场景。
4. 运行时创建 Texture2D 的瓦解
这次改造里最关键的瓦解来自运行时纹理创建。
原代码大抵是:- UTexture2D* Texture = UTexture2D::CreateTransient(Width, Height, PF_B8G8R8A8);
- Texture->GetPlatformData()->Mips.Add(new FTexture2DMipMap());
复制代码 题目在于:CreateTransient() 已经创建好了第 0 层 mip。
背面再手动 Mips.Add(),会追加一个没有精确设置尺寸和 BulkData 的无效 mip。等 UpdateResource() 构建纹理资源时,UE 读取到无效 mip,就大概触发 GetMipData failed、invalid GUID,乃至直接断言瓦解。
精确做法是直接写入已有的 Mips[0]:- UTexture2D* Texture = UTexture2D::CreateTransient(Width, Height, PF_B8G8R8A8);
- Texture->SRGB = true;
- Texture->NeverStream = true;
- void* TextureData = Texture->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_WRITE);
- FMemory::Memcpy(TextureData, RawData.GetData(), RawData.Num());
- Texture->GetPlatformData()->Mips[0].BulkData.Unlock();
- Texture->UpdateResource();
复制代码 同时,图片解码格式要和纹理像素格式匹配。
假如纹理格式是:解码时就应利用:否则固然不肯定瓦解,但大概出现红蓝通道庞杂。
5. 图片格式不要只看 URL 后缀
早期格式判定只依赖 URL 后缀,比方 .png、.jpg。
但实际项目里常常遇到这些环境:
- URL 没有扩展名
- URL 带查询参数
- CDN 地点后缀不可靠
- 服务端返回格式和文件名不划一
因今后续改成优先根据图片二进制数据辨认格式:- ImageWrapperModule.DetectImageFormat(ImageData.GetData(), ImageData.Num());
复制代码 假如二进制检测失败,再用 URL 后缀兜底。
这个改动不影响蓝图接口,但能显着进步下载器的容错本事。
6. 中文 URL 必要单独处置处罚
项目里还遇到过一种环境:欣赏器可以打开图片,但插件下载失败。
比方:- http://192.168.31.70:8090/images/球机.png
复制代码 欣赏器通常会自动转成:- http://192.168.31.70:8090/images/%E7%90%83%E6%9C%BA.png
复制代码 UE HTTP 哀求不会总是帮你做这件事,以是中文路径必要举行 UTF-8 百分号编码。
但不能直接对整个 URL 调用通用编码函数,否则 http://、/、: 也会被转义,URL 结构会被粉碎。
终极做法是:只编码中笔墨符,生存 URL 结构字符稳固。
如许:- http://192.168.31.70:8090/images/球机.png
复制代码 会被转换为:- http://192.168.31.70:8090/images/%E7%90%83%E6%9C%BA.png
复制代码 7. 异步对象生命周期比空指针更伤害
在 UE 里,许多瓦解不是真正的 nullptr,而是 UObject 被 GC 后,异步回调还在访问旧对象。
这类题目本质是悬空指针或 use-after-free。
风险紧张来自两点:
- 异步署理对象通过 NewObject 创建,但没有显式生命周期托管
- 背景线程或 GameThread lambda 直接捕捉裸 this
更稳的做法是:
- 利用 RegisterWithGameInstance(WorldContextObject) 托管异步署理
- 完成后调用 SetReadyToDestroy()
- 跨线程 lambda 利用 TWeakObjectPtr
- 回到 GameThread 后先判定对象是否仍然有用
运行时创建的 UTexture2D 也一样必要可达引用。假如蓝图只是暂时拿到纹理并设置 UI,但没有生存到成员变量或数组,后续也大概被 GC 影响。
8. 大块图片数据转达要留意拷贝本钱
图片解码后的原始像素数据通常很大。
比方:- 2048 x 2048 x 4 = 16 MB
- 4096 x 4096 x 4 = 64 MB
复制代码 假如每次跨函数、跨 lambda 都复制一份,内存峰值和性能开销都会上升。
因此在解码后转达 TArray 时,利用 MoveTemp 是公道的:- CreateTextureOnGameThread(Width, Height, MoveTemp(RawData), OnComplete);
复制代码 它不是为了“让代码能跑”,而是为了克制额外复制大块图片数据。
在图片下载、解码、创建纹理这种高吞吐链路里,移动语义优劣常实用的工程优化。
总结
这次图片下载插件改造,看起来只是“下载图片并表现”,实际涉及了 UE 运行时开发的多个关键点:
- 不要在 GameThread 同步期待网络哀求
- 背景线程只做纯数据处置处罚
- UTexture2D 创建和蓝图广播回到 GameThread
- 批量异步效果要按输入索引回填
- 蓝图异步节点存在委托 pin 天生限定
- CreateTransient() 后不要手动追加无效 mip
- 图片格式优先从二进制头辨认
- 中文 URL 必要精确百分号编码
- UObject 异步署理必须思量 GC 生命周期
- 大块像素数据转达应只管利用移动语义
真正稳固的运行时图片下载器,不但是 HTTP 哀求乐成就竣事了。它还要处置处罚线程、纹理资源、蓝图节点、生命周期和各种真实项目里的输入非常。
这些细节做好以后,插件才更得当从一个项目迁移到另一个项目,也更能经得住打包环境和复杂蓝图流程的磨练。
末了,关注公号“ITMan彪叔” 可以添加作者微信举行互换,实时收到更多有代价的文章。
免责声明:如果侵犯了您的权益,请联系站长及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金. |