UE学习资料整理:索引表、DDC缓存与高频问题排查
2026/9/18 4:03:40 网站建设 项目流程

UE学习资料整理这件事,我前后推倒重来过三次。第一次是老老实实往浏览器书签里塞链接,塞到三百多条之后再也没打开过;第二次是按"教程/文档/示例工程"分了三个文件夹,结果找"半透明怎么吃景深"这种具体问题时,还是得重新搜一遍;第三次我才想明白,资料整理的核心不是"收藏",而是"能在需要的时候三分钟内定位到答案,最好还能附上我自己验证过的结论"。这篇就把我现在在用的这套方法摊开讲,顺带把几个高频卡点——安装包与缓存目录、Size to Content、投掷物抛物线、平面反射的倒影渐变、半透明景深、FString/FText/FName 的区别和换行符、帧率排查、策略游戏方向——按我自己的笔记原样整理进来。不管你是刚装完引擎的新手,还是已经做过一两个项目的从业者,这里面的目录结构和排查顺序都能直接抄。

1. 我的UE资料库为什么从"收藏夹"改成"索引表"

1.1 按引擎版本和系统模块做双轴归档

UE 的资料有个很讨厌的特点:同一个节点、同一个参数,在 4.27、5.0、5.3、5.5 里的行为可能完全不一样。我在 4.26 时代记的"Translucency 里没有 Output Depth"这条笔记,到 5.x 就变成错的,如果不标版本,过半年自己都会把错误结论当成常识用。

所以我的归档轴是两条:版本轴模块轴。版本轴很简单,我现在维护5.35.45.5三个目录,跨版本通用的结论单独放在common里。模块轴我固定成八类:渲染与材质、UMG与UI、Gameplay框架、AI与导航、物理与碰撞、网络同步、性能分析、打包与部署。为什么不按教程来源分?因为来源只解决"我从哪学的",不解决"我要解决什么问题",而后者才是你真正会去检索的东西。

每条笔记我强制写成同一个格式,一共五行:问题描述 → 最小复现步骤 → 结论 → 适用版本 → 工程路径。举个例子:问题描述"半透明水面不吃景深";最小复现步骤"新建空场景,放一个半透明平面,加 Post Process Volume 开 DOF,把焦距拉到很近";结论"看似无效,实际是 Separate Translucency 在 DOF 之后合成";适用版本"5.0 以上";工程路径"D:\UE_Notes\Proj_TranslucencyDOF"。这个格式的关键是最后一项,它是把笔记从"死文字"变成"活证据"的唯一手段。

提示:写笔记的时候,凡是没亲手复现过的结论,一律加一个[未验证]标签。我踩过太多次从论坛搬来的参数直接用在项目里、结果发现是旧版本的默认值。

1.2 三类清单:必读、备查、待验证

我把所有资料分成三个清单,这个分法帮我砍掉了至少一半的无效阅读。

必读清单只放那些"不读就做不下去"的东西:官方文档里对应模块的概览页、引擎自带的 Content Examples 对应关卡、以及我自己写的模块流程图。这个清单常年保持在 15 条以内,超过就说明我在囤积。

备查清单是工具型资料,比如各个stat命令的含义、材质 Blend Mode 的对照表、打包常见报错的解决方案。这类东西不该"读完",只该"能搜到",所以我会把它们整理成 Markdown 表格,用关键词命名文件,比如stat命令速查.md材质BlendMode对照.md

待验证清单最有意思,专门放那些"听起来很有道理但我还没试过"的说法。比如"用 MassEntity 能把一万个单位跑到 120 帧",这话我信,但不在我的项目场景里跑一遍我不会写进结论。这个清单我会定期清空,把它变成必读或直接删掉。

还有一条经验:官方文档一定要先切版本再读。文档站右上角有版本切换,默认往往不是你在用的版本,很多人照着默认版本读完,回去发现细节面板里根本没那个选项,白白浪费半小时。

2. 安装包、缓存目录与多版本共存:先把地基打平

2.1 一个UE版本到底占多少空间,装机前怎么规划

很多人搜"UE安装包",其实混了两件事:一是引擎安装包(Epic 启动器下载的那个几十 GB 的东西),二是项目打包产出的安装包(打包成 exe)。这两个完全不同,先说前者。

一个 UE5 版本,只勾选 Windows 平台、不含调试符号的情况下,装完大约 30 到 50 GB。如果你在安装时顺手勾了 Android、iOS、Linux 这些目标平台,体积很容易翻倍。我的做法是:装机时只勾当前要用的平台,需要的时候再回来补勾,补勾的代价远小于长期占着一堆不用的 SDK。另外引擎安装时强烈建议用自定义安装路径,放在独立的大容量 SSD 上,例如D:\Program Files\Epic Games\UE_5.4,别放在 C 盘——后面缓存一涨,C 盘直接报警。

打包那一侧,关键点在Project Settings → Packaging。Development 配置带调试信息,包体大、性能也不是真实表现;Shipping 才是线上该看的。测性能我一般直接打 Shipping,或者至少在编辑器里用独立进程模式跑(-game),因为编辑器本体有一大堆开销,在编辑器里测出来的帧率对上线没什么参考价值。

2.2 改DDC缓存目录的三种做法与选型理由

这是"ue改缓存目录"最常指的东西:DDC(Derived Data Cache,派生数据缓存)。它的作用是缓存着色器编译结果、贴图中间格式这些"算出来就不想再算一遍"的数据。默认位置在C:\Users\<用户名>\AppData\Local\UnrealEngine\Common\DerivedDataCache,一个项目反复迭代下来,这里涨到几十 GB 是常态,而且是全项目共享的一个坑。

改法有三种,我把它们放在一起对比:

做法操作位置优点缺点
环境变量UE-LocalDataCachePath系统环境变量不动引擎文件,升级引擎不丢配置每台机器要单独设一次
BaseEngine.ini[DerivedDataBackendGraph]Engine/Config/BaseEngine.ini全局生效,一目了然引擎升级/校验文件时可能被覆盖
启动参数-ddc=快捷方式或命令行临时、灵活、可做多套环境每次启动都要带参数

我选的是第一种,环境变量UE-LocalDataCachePath指向D:\UE_DDC。理由很实在:引擎版本我经常换,改 ini 会在升级时被冲掉,而环境变量一次设好,所有版本都吃这个配置。引擎的BaseEngine.ini里其实已经把EnvPathOverride=UE-LocalDataCachePath写好了,也就是说它天生就支持你用环境变量覆盖,这是最"正规"的入口。

还有几个坑必须说清楚。第一,路径里千万不要有中文和空格,着色器编译偶尔会在这种路径上报莫名其妙的错。第二,绝对不要放在网络盘或者机械盘上,DDC 是高频随机读写,放网络盘会让第一次打开项目慢到你以为死机。第三,删 DDC 不是"清理垃圾",删完之后所有项目要重新编译一次着色器,你可能等上半小时到一小时,所以只在确认缓存损坏时才删。

注意:团队协作会用到共享 DDC,对应环境变量是UE-SharedDataCachePath,通常是局域网内一台机器的共享目录。它和本地 DDC 是叠加关系,本地命中优先,没命中才去共享里找,共享里再没有才自己编译。

2.3 多版本共存时的目录命名与切换习惯

多版本共存本身不是问题,混乱的是命名。我的习惯是严格跟着官方命名走:UE_5.3UE_5.4UE_5.5,不加任何自定义后缀,这样.uproject里的EngineAssociation和目录名能对上,右键项目切换版本的时候不容易选错。

.uproject右键菜单里的Switch Unreal Engine Version只改EngineAssociation字段,不动任何资产。但要注意:从高版本切回低版本,如果项目里用了新版本的资产特性,会直接报错甚至资产损坏。所以我所有项目的切换原则是只向上、不向下,向下切换一律从版本控制里拉一份干净副本。

3. UMG里那些"看着会、用着错"的节点:Size to Content

3.1 Size to Content到底改的是什么

"size to content 在 ue 里"这个搜索我遇到过太多次,多数人是这么踩的:在 Canvas Panel 上勾了 Size to Content,然后发现画面里的东西一点没变,只是外面套着的那个容器突然塌了或者撑开了。

要理解这个,得先知道 Canvas Panel 是个绝对定位面板。它里面的子控件靠锚点(Anchor)加偏移(Offset)决定位置和大小,Canvas Panel 自己则默认占满父级。勾上 Size to Content 之后,Canvas Panel 的期望尺寸会变成所有子控件的期望尺寸之和,而子控件的绝对坐标不会因此改变。所以你会看到两种典型现象:一是子控件本来就在面板范围内,勾了之后视觉上什么都没发生,只是外层面板变小了;二是子控件的坐标超出面板范围,面板变大但内容位置纹丝不动,因为锚点和偏移没变。

它真正好用的场景是内容驱动的容器:比如一个根据文字长度自适应的提示气泡、一个列表长度不定的设置面板。这时候的正确组合是Size Box或者Vertical Box在外,Canvas Panel 在内并勾 Size to Content,让面板"包住"内容。

3.2 和DPI缩放、文本换行一起用时容易出问题的点

和 Size to Content 关系最紧密的是文本换行。UE 的 Text Block 有一个Auto Wrap Text,很多人勾了发现还是不换行,原因不是设置没生效,而是它的父级没有给出宽度约束。放在 Horizontal Box 或者勾了 Size to Content 的 Canvas Panel 里,宽度是"无限"的,文本自然永远不换行。解决办法很土但很有效:在 Text Block 外面套一个 Size Box,把 Width Override 定死,或者把它塞进一个 Fill 的 Vertical Box 槽位里。

DPI 缩放是第二个坑。Project Settings → User Interface → DPI Scaling里有个 DPI Scale Rule,常见选项是 Shortest Side、Longest Side、Horizontal、Vertical。如果你的 UI 在 16:9 显示器上调好了,拿到 21:9 或者竖屏上就错位,八成是缩放参考轴选错了。我一般做跨平台项目就选 Shortest Side,做宽屏为主的桌面项目选 Longest Side,然后在 Canvas Panel 上勾Size to Content配合 DPI Curve 的断点,让不同分辨率下走不同的分辨率档位。

Safe Zone是第三个。做移动端或者带刘海屏设备适配时,用 Safe Zone 控件包住主要内容,但 Safe Zone 的 Padding 是跟着 DPI 走的,如果你同时用了 Size to Content,会出现"内边距把内容挤出去了但面板没变大"的情况。我的处理方式是 Safe Zone 在最外层且不勾任何尺寸自适应,内部再按需使用 Size to Content。

提示:同名选项在不同控件里的含义是不一样的,点之前先看清你选中的是哪个控件。在细节面板里点一下控件名,确认层级关系,比事后调半天布局划算得多。

4. 投掷物抛物线:从Projectile Movement到轨迹预览

4.1 ProjectileMovementComponent的关键参数与重力常数

做"ue投掷物抛物线",起点一定是ProjectileMovementComponent。核心参数就几个,但每一个都值得说明白:

参数作用我的常用值
Initial Speed初始速度大小,配合 Velocity 方向按射程反推,见下文
Projectile Gravity Scale重力倍数,1 表示吃世界重力手雷 0.9-1.0,箭矢 0.6 左右
Max Speed速度上限一般设为 0(不限速)
bRotationFollowsVelocity朝向跟随速度方向子弹开,投掷物看需求
bShouldBounce / Bounciness反弹开关与弹性手雷开,弹性 0.4 左右
bForceSubStepping强制子步进高速物体必须开

世界重力的默认值是-980 cm/s²(在Project Settings → Physics → Default Gravity Z里能看到)。有了这个常数,抛物线就能反推参数。举个我在项目里实际算过的例子:想让投掷物以 45 度仰角打出 60 米(6000 厘米)的水平射程,用公式R = v² × sin(2θ) / g,θ=45° 时 sin90°=1,于是6000 = v² / 980,得出v = √(6000 × 980) ≈ 2425 cm/s。这个数直接填进 Initial Speed 再给一个 45 度方向的 Velocity,一次就能打到目标附近,不用靠手感瞎调。

如果你要的是最大高度,用H = v² × sin²θ / (2g),同样的 2425 cm/s 和 45 度,高度大约是 1500 厘米,也就是 15 米。把这两个公式记住,调投掷物的效率会高一个数量级。

4.2 PredictProjectilePath:瞄准弧线预览的正确做法

策略游戏和射击游戏里都需要"瞄准时显示一条虚线弧",标准做法是PredictProjectilePath。蓝图里叫Predict Projectile Path By Trace Channel,C++ 里是UKismetSystemLibrary::PredictProjectilePath,参数结构是FPredictProjectilePathParams,返回FPredictProjectilePathResult

几个必须配的参数:StartLocation用枪口或手部位置,LaunchVelocity用和实际发射一致的初速度,ProjectileRadius填碰撞半径,MaxSimTime限制预测时长(一般填 3 到 5 秒就够),SimFrequency决定采样密度,默认 15 在近距看起来会有点折线感,我一般调到 30。返回结果里的PathData.PathPoints就是一条折线上的采样点,可以直接画DrawDebugLine调试,也可以塞进 Spline Component 用样条网格渲染成实体弧线。

这里有个很容易忽略的坑:预测出来的路径和实际飞行的路径不一定完全重合。原因是ProjectileMovementComponent的积分方式、子步进设置、碰撞响应和预测用的简化模型之间会有偏差,尤其是开了反弹、或者物体半径比较大的时候。我的处理办法是,把预测的采样点数量、半径、重力都跟实际投射物对齐,然后在实测中对比落点,如果偏差稳定存在,就在预测的OverrideGravityZ上做一点微调补偿。

4.3 反弹、命中与同步的注意点

高速投掷物必须开子步进,否则会直接穿墙。相关参数是bForceSubSteppingMaxSimulationTimeStepMaxSimulationIterations,思路是把一帧的大位移切成若干小步做碰撞检测。代价是 CPU 开销上升,所以别全局无脑开,只给高速物体开。

命中判定建议一律走服务端权威OnComponentHit里直接扣血、生成特效这种写法在单机没问题,联机就会出现两边表现不一致。正确做法是客户端只做预测表现(Tracer、音效),真正的伤害结算放在服务端,客户端收到确认后再播最终特效。

还有一种情况值得单独拎出来:你其实并不需要物理。如果只是做一段固定的抛物线动画(比如掉落拾取物的表现),用 Timeline 驱动一个曲线位移,开销远小于开一个物理组件。我在一个策略游戏里就把所有金币掉落的表现换成了 Timeline 曲线,同屏两百个的情况下省了不少 CPU。判断标准很简单:需要和场景碰撞交互就用物理组件,不需要就用曲线。

5. 画面类效果:平面反射倒影渐变与半透明景深

5.1 平面反射的倒影渐变:思路与材质节点

"ue平面反射倒影渐变"这个需求,本质上是做水面的反射衰减——靠近岸边的水面反射弱,越往远处反射越清晰,或者反过来。实现链条是这样的:场景里放一个Planar Reflection组件,材质里必须使用PlanarReflection 材质函数节点去取反射纹理,而不是普通的 Scene Capture。

拿到反射结果之后,"渐变"就交给遮罩了。我常用三种遮罩来源,各有适用场景:

第一种是距离遮罩。用像素的 World Position 到反射中心点的距离做归一化,再送进一条曲线,越远越清晰或越远越淡都可以。优点是全自动,缺点是弯曲河道会算不准。

第二种是 Fresnel。视角越平越强,适合做"远处水面像镜子、脚下水面能看到底"的经典效果。这个几乎零成本,是最通用的方案。

第三种是美术手绘遮罩。用一张贴图或者顶点色,让美术直接在岸边画一条淡出带。这是效果最好、也最常见的做法,尤其是风格化项目。代价是要额外维护一张遮罩图,河道改形状就得重画。

然后就是把这些遮罩跟基础水面颜色做 Lerp,也可以用遮罩去控制反射的模糊程度,让远处更锐利、近处更糊,视觉上会更自然。

性能上要说清楚:平面反射是额外渲染一遍场景,代价不小。策略游戏里如果有一大片水域,直接开全分辨率平面反射基本是把帧率砍半。我的做法是:反射分辨率降到一半甚至更低、配合距离剔除只在近处启用,或者干脆用 SceneCapture2D 低频更新(比如每三帧更新一次)来伪装。真要极致省,就上屏幕空间反射加噪声扰动,水面动起来的时候破绽不明显。

注意:平面反射渲染的是镜像相机,它默认不包含半透明物体,也不会显示水面自身。调试的时候如果发现"倒影里少了一棵树",先去检查那个物体的 ShowFlags 和材质是否被反射排除掉了。

5.2 半透明物体为什么不吃景深:Separate Translucency那点事

"ue半透明景深"这个组合让很多人困惑:明明开了景深,远处的半透明玻璃、水雾、粒子却还是清清楚楚。

根因是Separate Translucency(分离半透明)。这个功能默认是开的,它的工作方式是:不透明几何先渲染并接受后处理(包括景深),半透明物体单独走一趟,在景深之后才合成到画面上。也就是说,半透明物体根本没参与景深那道工序,自然不会被模糊。你可以用控制台命令r.SeparateTranslucency 0关掉它验证一下,关掉之后半透明就会和场景一起被模糊——但同时会带来一堆副作用,比如半透明和粒子的排序、光照表现都会变,所以这是个全局开关,不建议在正式项目里长期关。

5.3 让半透明参与景深的代码级做法与代价

正确的做法是按材质逐个打开。在材质的细节面板里找到 Translucency 分组,里面有Output DepthOutput Velocity这两个开关(不同版本位置略有差异,5.x 基本都在这一组)。打开 Output Depth 之后,这个半透明材质会把自己的深度写出去,后处理里的景深就能拿到正确深度去模糊它;Output Velocity 则是给运动模糊用的,如果你的项目开了运动模糊而且半透明物体在动,这个也得开。

代价是:这类材质不能再依赖默认的半透明排序,而且会有一点点额外的渲染开销。所以我的策略是只给真正需要的材质开——水雾、玻璃、需要被景深模糊的魔法特效,普通发光贴片就不开。

顺带把三种方案的取舍整理成表:

方案改动范围效果风险
材质开 Output Depth单个材质精准,只影响目标物体需检查排序,开销略增
全局关 Separate Translucency整个项目所有半透明都吃景深排序、粒子、光照表现变化大
用不透明材质伪装单个材质零风险,最省失去半透明边缘过渡

还有一个常见误判:改了材质发现没生效,先确认三点——材质有没有编译保存、场景里是不是有 Post Process Volume 覆盖了景深参数、以及是不是在移动端渲染路径下(移动端和桌面端的后处理能力差异很大,很多桌面能做的在移动端根本没有)。另外景深强度本身受焦距和光圈影响,把焦距拉得离物体很近再调,比来回改代码有效得多。

6. 文本与字符串:FString、FText、FName的分工和换行符陷阱

6.1 三者的定位与选择标准

"ue中的字符串和文本的区别"是个问得非常对的问题,因为这三个类型混用是新手代码里最常见的坏味道。先把定位摆清楚:

类型可变性大小写本地化典型用途
FString可变敏感不支持拼接、解析、日志输出、文件路径
FName不可变不敏感不支持资源名、行名、标签、字典键
FText不可变敏感支持所有面向玩家的显示文本

FString是干活用的。日志用UE_LOG(LogTemp, Warning, TEXT("%s"), *MyString),注意那个星号,它是把 FString 转成宽字符指针。文件读取、字符串切割、拼 URL 这类都用它。

FName是为查找优化的。它在全局名字表里存一份,比较两个 FName 实际上是比较索引,所以极快。代价是创建 FName 有开销,而且不区分大小写——"Fire""fire"会被认为是同一个。所以用它当键非常合适,当显示文本就完全不合适。

FText才是给玩家看的。它支持本地化,写法是LOCTEXT("Key", "文本")或者带命名空间的NSLOCTEXT("Namespace", "Key", "文本"),格式化的标准写法是FText::Format(LOCTEXT("KillMsg", "{0} 击杀了 {1}"), KillerName, VictimName)。有个必须注意的点:从 FString 转 FText 用FText::FromString得到的文本是无法被本地化收集的,拿来做临时调试可以,做正式 UI 文案绝对不行。同理,C++ 里要让一个 FText 进入本地化流程,它必须是被UPROPERTY标记的成员或者出现在LOCTEXT宏里。

另外 FText 不能用==直接比较,要用FText::EqualTo,这也是容易翻车的地方。

6.2 换行符:\n、\r\n与编辑器里的Shift+Enter

"ue的换行符"看着是小问题,实际坑人不少。分三层来说。

C++ 层。UE 提供了宏LINE_TERMINATOR,在 Windows 上它是\r\n,在 Linux 上是\n。如果你在做跨平台的文件写出,用这个宏比自己敲"\n"稳妥。反过来,读取文件的时候要小心\r:Windows 生成的 CSV、TXT 行尾是\r\n,按\n切割之后,每行末尾会多一个看不见的\r,导致字符串比较失败、或者数值解析报错。这种 bug 最气人的地方是肉眼看不出区别,我第一次遇到的时候对着两个"一模一样"的字符串查了一下午。解决办法是用FString::TrimStartAndEnd()清一遍,或者显式替换掉\r

数据表层。CSV 导入 DataTable 时,最后一行如果没有换行符,某些版本会少读一行;字段里的引号和逗号也要按 CSV 规范转义。我现在养成的习惯是,DataTable 的源文件一律用工具生成而不是手写,从源头避免这类问题。

编辑器层。Text Block 里输入多行文本时,直接敲 Enter 会产生真正的换行符,Shift+Enter 是软换行——在编辑器里看起来都是换行,但行为不同。富文本控件里换行要用<br>标签。还有一点:在细节面板里输入\n这两个字符,某些属性框会把它当字面量处理,需要展开成多行编辑框才能真正换行,遇到"文本里出现了反斜杠 n"就是这个原因。

最后一个容易被忽略的点:做本地化的时候尽量别用硬回车断行。不同语言的句子长度差异很大,你按中文长度切好的换行,到了另一种语言里可能把单词劈成两半。让控件自动换行,比手动敲回车靠谱得多。

7. 帧率低了从哪查:我的固定排查顺序

7.1 第一步永远是stat unit,先分清CPU还是GPU

"ue排查帧率低的原因"这件事,最大的忌讳是凭感觉猜。"是不是特效太多了""是不是模型面数太高",这类猜测十有八九是错的。我的第一步永远是stat unit,看那四个数:Frame、Game、Draw、GPU。

判读逻辑很朴素:Frame 大致等于 Game、Draw、GPU 三者里最大的那个。所以哪个数最接近 Frame,瓶颈就在哪边。Game 高说明游戏线程忙,通常是蓝图逻辑、物理、AI、动画计算;Draw 高说明渲染线程忙,常见于绘制调用太多、UI 太复杂;GPU 高说明显卡扛不住,通常是像素着色、后处理、阴影、半透明 Overdraw。

这里有个我踩过的坑:Game 和 Draw 会互相掩盖。游戏线程卡的时候,Draw 看起来很低,你就以为渲染没问题,其实只是渲染线程在等游戏线程。所以一定要看完整的三个数,别看单个。

另外一个前提条件:必须在固定场景、固定视角、固定位置测。边走边看数字跳来跳去,什么结论都得不出来。我一般是加载一个测试关卡,把相机对准最重的那个视角,站定,等五秒让数据稳定,再截图记录。

7.2 CPU侧常见元凶与Blueprint层面的自查清单

确定是 Game 高之后,我会按这个清单过一遍:

  • Actor 的 Tick 数量stat game配合 Unreal Insights 能看出 Tick 占比,几百个 Actor 每帧 Tick 是常见死因。能改 Timer 就改 Timer,能用事件驱动就别轮询,实在要 Tick 的就把Tick Interval拉长。
  • 每帧的全场景查询GetAllActorsOfClassGetAllActorsWithTag这类调用如果出现在 Tick 里,是典型的性能炸弹。正确做法是在 BeginPlay 里查一次存起来,或者用事件通知维护列表。
  • 蓝图里的循环加 CastForEachLoop里套Cast,数量一上去开销非常可观。能用接口就用接口,能把 Cast 提前到循环外就提前。
  • 物理模拟物体的数量stat physics看物理耗时,策略游戏里满地掉落的物品如果都开了模拟,很容易成为瓶颈。解决办法是掉落后进入休眠、或者用非物理的表现替代。
  • 垃圾回收停顿。频繁创建销毁 UObject 会导致 GC 周期性卡顿,表现是帧率突然掉一下然后恢复,而不是持续低。stat gc可以看 GC 时间,减少对象创建、用对象池是主要手段。
  • 动画stat anim看动画耗时,大量 Skeletal Mesh 同时更新骨骼是很贵的,远景单位该降频就降频。

这里补一个方法论上的细节:stat unit只能告诉你"哪条线程忙",具体是谁忙要靠 Unreal Insights。操作是控制台输入stat startfile,跑十秒左右再stat stopfile,然后打开 Insights 加载那个.utrace文件,直接看 Game Thread 里耗时最高的函数排行。这个流程我建议每个人都跑一次,跑完你会发现很多你以为是"渲染问题"的东西其实是某一段蓝图逻辑。

7.3 GPU侧与几个常用命令

GPU 高的话,先看stat gpu,它会按渲染阶段列出耗时:Base Pass、Lighting、Shadow、PostProcess、Translucency 等。哪个阶段最贵,方向就清楚了。

阴影是最常见的 GPU 大头。虚拟阴影贴图、级联阴影的距离设置、动态光源数量都会影响。半透明 Overdraw也很常见,尤其是有大量粒子叠在一起的时候,这时候要去看stat scenerendering里的绘制调用和三角面数,再配合 GPU Visualizer(快捷键 Ctrl+Shift+,或者控制台ProfileGPU)看具体是哪个 Pass 在烧时间。

分辨率是最粗暴也最有效的杠杆。r.ScreenPercentage降到 80 甚至 70,视觉上很多人看不出来,帧率立刻上去了。此外r.VolumetricFogr.Lumen相关的开关都可以临时关掉做对照测试,用来确认某个效果是不是主要开销。

我总结的对照表大致是这样:

现象先看什么常见原因
Game 明显最高stat game / Unreal InsightsTick 过多、全场景查询、蓝图循环
Draw 明显最高stat scenerendering绘制调用过多、UI 复杂、材质槽太多
GPU 明显最高stat gpu / GPU Visualizer阴影、后处理、半透明 Overdraw
帧率周期性抖动stat gc频繁对象创建导致的 GC 停顿
只有特定视角掉帧固定视角对比该视角下物体集中、遮挡剔除失效

最后说一句最重要的话:永远在 Shipping 或者独立进程里做最终确认。编辑器里有大量调试开销,你在编辑器里优化到 60 帧,打包后可能是 90,也可能因为别的因素变成 45,两边都不可互相替代。

8. 策略游戏方向:资料该怎么挑

8.1 大规模单位的渲染与逻辑:ISM/HISM、MassEntity

"ue策略游戏"这个方向最大的技术分水岭就是单位数量。几十个单位,怎么实现都行;几百上千个,架构选择就直接决定项目能不能做下去。

渲染侧的结论很明确:不要给每个单位一个 Actor + Skeletal Mesh。正确路径是InstancedStaticMeshComponent(ISM)或者它的层级版本HISM,把同一种单位合并成一次绘制调用。需要动画的话,用顶点动画贴图(Vertex Animation Texture)把骨骼动画烘进贴图,在材质里按时间采样;更远距离的单位可以用粒子或者带动画的贴片顶替——City Sample 里的大规模人群就是这么分层的:近处是完整骨骼动画,中距离切简化动画,远处直接变粒子。

逻辑侧的答案是MassEntity。它是一套面向数据的设计(ECS 思路),把单位拆成一组稀疏的 Fragment 数据,由 Processor 批量处理,不再需要每个单位一个 Actor 一个 Tick。配套的还有 MassAI 做行为、MassCrowd 做群体导航。这套东西上手曲线陡,但它是目前 UE 里做"几千个单位同时跑逻辑"最正经的方案。我的建议是先在 City Sample 里跑一遍看效果,再决定要不要在自己的项目里上——因为它会改变你整个 Gameplay 层的写法,中期切换代价极大。

8.2 值得拆的官方示例与拆解顺序

策略游戏能参考的官方资源不少,但很多人的问题是"下载了、打开了、不知道该看哪"。我按用途列一下我实际拆过的几个:

示例主要参考价值适合解决的问题
Content Examples各系统的最小示例想搞懂某个功能怎么用
LyraGAS、输入、UI、网络技能系统、角色框架怎么搭
City SampleMassEntity、MassCrowd大规模单位与人群
Cropout小体量策略玩法、破坏单位选择、资源循环、小团队做法
Action RPGGAS 实战、AI技能与战斗数值落地

拆解顺序我有一套固定流程,四步:先跑通,再找入口,再看数据流,最后做最小复现。第二步"找入口"是最容易卡住的地方,因为官方示例动辄几百个类,你不知道从哪开始。我的做法是打开 GameMode 和 PlayerController,顺着 BeginPlay 往下追,把关键系统列成一张"入口类清单"写在笔记里。这张清单比任何教程都值钱,因为它是我自己在那个项目里走通的路线。

顺带说一个策略游戏专属的常见需求:框选。思路是在鼠标按下和抬起之间画一个矩形,把这个矩形投影到世界空间,用GetActorsInSelectionRectangle之类的接口筛选范围内的单位。地面点选则用 LineTrace 打到地面,再通过导航系统把落点校正到可行走区域。这两个功能都不难,但要在几百个单位下保持流畅,就需要配合前面说的实例化渲染和批量逻辑处理,否则光选中高亮就能把帧率吃掉一半。

这套资料库我维护了差不多两年,最大的体会是:整理的价值不在于"存了多少",而在于"下次遇到同类问题时,我能多快拿到一个自己验证过的结论"。我现在查一个半透明景深的问题,从打开笔记到定位到那个最小工程,大概两分钟;换成两年前翻收藏夹,可能已经跑去搜第三遍了。另外有个小技巧很管用,就是每次解决完一个坑,顺手在笔记里加一行"下次先看什么"——这行字通常是整篇笔记里最省时间的部分,因为它记录的是我当时的思路顺序,而不是结论本身。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询