干了这么多年虚幻引擎项目,从Demo到正式产品,踩过的坑、攒下的窍门,零零散散记了一堆。这篇清单不是从文档里抄出来的,是实打实摸出来的101条经验。我不按引擎版本目录走,直接按工作场景分块,先说设置了再说坑,最后补环境,每一条都能直接落地。
1. 项目性能优化:先看瓶颈再动手
刚接手项目的人最容易犯的毛病,就是一上来疯狂删模型面数、压贴图尺寸,结果帧率一点没涨。性能优化这件事,顺序不对等于白干。
1.1 日常性能排查的启动姿势
1. 用stat命令组先定位瓶颈,而不是猜。
打开控制台,按顺序敲stat unit、stat game、stat streaming、stat rhi。stat unit看的是GameThread、DrawThread、GPU总耗时,一旦某项数值明显偏高,就说明瓶颈在哪一层。GameThread高就去查蓝图和Actor数量,DrawThread高就去减DrawCall,GPU高才轮到材质和贴图。这个顺序颠倒了,优化工作九成是白忙。
2.stat scenerendering里的Mesh Draw Calls 数值值得盯紧。
这个数字是整个场景渲染批次的风向标。移动端项目我一般控制在400以下,PC项目看目标帧率定,60帧下最好别超过1500。数值突然翻倍的时候,八成是新增的植被或建筑物没做合并,逐帧检查最近改动的关卡Actor就能找到元凶。
3. 测量帧耗时别只看平均帧率。
用t.maxfps锁到目标帧率再观察。比如你目标30帧,就在控制台跑t.maxfps 30,然后打开stat unit看每帧是否稳定在33毫秒附近。如果开了垂直同步帧率显示是满的,但游戏体感卡顿,通常是单帧峰值过高,这时候要开stat fps查看帧耗时曲线,而不是看平均FPS。
1.2 DrawCall与渲染开销的实战处理
4. 能静态合批就别动态合批。
StaticMesh默认的静态路径会利用引擎的实例化合批机制。但要注意,合批的坑在于模型必须使用同一材质,UV重叠不受影响,可材质参数变化会直接打断合批。比如一棵树的颜色变了个淡旺季,合批就断了。检查方法是在编辑器里选中模型,看属性面板里的Num Triangles和实际场景渲染的DrawCall数量是否接近。
5. 动态物体别忽略bUseAsOccluder标志位。
这个标志默认是开的,但多数人根本不知道它影响什么。它决定这个物体是否参与遮挡剔除计算,如果一个纯装饰性的小道具长期开启遮挡,GPU每帧都在为它做无谓的深度计算。我在一个开放世界项目里把几百个小石头关闭遮挡标志后,DrawCall没降,但GPU时间省了两毫秒。
6. 打包后出现渲染闪烁,先查LOD切换距离。
出现这种问题,别怀疑光照,直接看静态网格体的LOD设置。LOD0和LOD1的临界距离设置太近,切换瞬间顶点位置跳变就会闪烁。稳妥做法是让LOD切换距离拉开两倍以上,并且开启Use LOD Material让每个LOD层级的材质品质也递减。
7. 植被系统(Foliage)刷多了之后,整体CPU开销飙升,先查MinInstanceRadius。
大量小实例会让引擎做很细的裁剪计算。把MinInstanceRadius调大,小于该半径的实例直接被丢弃,不再参与可见性判断,CPU负载立刻下降。刷草的时候尤其管用。
1.3 纹理、内存与流送
8. 移动端贴图格式别用默认的BC系列,用ASTC。
ASTC在同等质量下压缩比更高,带宽占用更低。工程设置里改Targeted RHIs时顺手把纹理格式从DXT改成ASTC,体感非常明显。改完记得重新保存所有贴图资产,该项修改不会自动生效。
9. 大场景打开卡顿,第一步查纹理流送池。
控制台输入r.Streaming.PoolSize,默认可能是1024,也就是1GB上限。当你的关卡同时加载几套大型建筑贴图,池子满了就开始疯狂抖动加载,帧率时高时低。按实际项目资源量把这个值提升到2048或4096,能明显缓解顿挫。
10. 关卡里放了一堆不常变化的纹理,可以关闭它的流送。
选中纹理资产,去掉Never Stream以外不要乱钩,正确操作是把LOD Group改成UI或者直接强制关闭流送选项。世界正中央的招牌、广告牌这类不会被频繁察看的贴图,关掉流送后加载更稳定,也不容易在视角快速转动时弹出模糊。
2. 材质与渲染:把GPU花在刀刃上
材质写不好,场景再漂亮都是空架子。这一章讲的都是材质节点和渲染层面的常用技巧,照着做效果立竿见影。
2.1 材质函数与复用结构
11. 多用Material Function,而不是到处复制节点网络。
引擎自带的材质函数库里有大量现成的噪声、扭曲、滚边工具,不要自己重新组装。自己写材质函数时,注意输入节点要带描述,时间久了连自己都认不出来参数含义。
12. 材质编辑器里不要混用WorldPosition和ActorPosition,不然材质不对称。
像水波纹、边缘衰减这类要用绝对世界坐标的效果,直接把World Position节点接到Vertex Position,再放进正弦或噪声函数。如果混用了局部坐标,模型旋转后材质效果会对不上。
13. 制作地形材质混合(Landscape Layer Blend)时,权重贴图务必统一尺寸。
我吃过这个亏:不同层的权重图大小不一样,刷出的地形过渡带边缘出现明显锯齿。标准做法是每张权重图保持同一像素尺寸,比如全套2048,而且同一地形内部不要混合不同生成器刷出来的图。
14. 半透明材质的性能坑在Overdraw。
半透明物体堆叠越多,GPU片元着色负担越重。粒子系统里大量半透明面片叠加,性能就崩了。这种情况下把粒子渲染改成扭曲材质加深度预写,或者在远处用不透明冒名顶替,远处看不太出来,性能却差出几倍。
2.2 常用渲染技巧与效果完成度
15. 做贴花(Decal)用Deferred Decal配合DBuffer渲染路径,安全省心。
不需要自己搞投影矩阵,Material里设置Blend Mode = Translucent,Shading Model = Deferred Lighting,然后挂到场景就行。这个设置下贴花会自动处理与地表的高度贴合。如果看到贴花在边缘有半透明叠影,检查Fade Actor的淡出时长,调短点。
16. 想让某个物体只被特定相机看到,用Custom Depth/Stencil标记。
先开启项目的Custom Depth-Stencil Pass,在目标物体的渲染属性里勾选Render CustomDepth Pass,然后在后处理材质里读取CustomDepth做Mask。这是做描边、闪烁高亮、X光透视效果的标准套路,不用动Shader。
17. 做积雪、灰尘覆盖效果,用顶点着色器按法线方向叠加。
制作思路是采样世界坐标,把法线朝上的区域混合成白色。我通常在材质编辑器里这样搭建:把Vertex Normal取绝对值,再和World Position的Y轴做点乘,用来控制颜色蒙版的强度,最后用Lerp混合原贴图和积雪贴图。这个方法不增加任何额外渲染Pass,性能零损耗。
18. 视差贴图(Parallax Occlusion Mapping)开启后,模型切线空间必须烘焙正确。
如果模型从其他软件导入时没有正确导出切线,视差效果会出现明显的抖动和错位。检查模型导入设置的Compute Tangents选项,保证它在导入时重新计算切线数据,否则效果就是花屏。
19. 平铺纹理的接缝问题,用Triplanar投影方式绕开UV。
地形、悬崖、岩石这种大物体,UV展开效率低还容易拉伸,直接用三平面投影,按世界坐标采样三次,再按法线权重混合。做悬崖面时几乎天然无缝,不用花时间展UV。
2.3 材质调试与错误排查
20. 材质编辑器预览时卡顿,先禁用实时更新。
预览窗口默认开实时刷新,节点改动多的时候电脑立刻卡死。在预览窗口的工具栏里关掉Realtime按钮,改用手动刷新,调完一个阶段再按一次刷新。
21. 材质结果偏色,先检查色彩空间。
Texture节点里的采样设置有个sRGB开关,贴图格式如果是线性空间却用了sRGB,整体会泛灰。法线贴图的采样器固定不勾选sRGB,颜色贴图一般勾选,这个顺序反了,出来的颜色就是错的。
22. 材质编辑器节点错乱,最实用的方法是按住1、2、3、4快捷键切换节点预览模式。
1是纯颜色,2是UV,3是法线,4是自定义。排查某个数值是否正常时,按对应数字就能隔离查看该通道的值,比肉眼猜节点输出快得多。
3. 蓝图开发:从能跑到跑得稳
蓝图写多了你就知道,性能问题往往不是蓝图本身,而是节点组织方式。以下这些技巧帮你在设计阶段就避开常见的弯道。
3.1 蓝图结构设计与事件驱动
23. 蓝图中大量使用Tick,等于慢性自杀。
编排逻辑时优先考虑用Event、Timer和Dispatch,不要每帧轮询。比如检查玩家是否进入某个区域,不要用On Tick里每帧做距离判断,用触发器体积的OnActorBeginOverlap事件一劳永逸。
24. 能用Cast To就别全局搜索,但滥用Cast会让代码脆弱。
设计模式上,接口(Interface)比Cast更能解耦。比如多类型物体都要响应交互时,定义一个IInteractive接口,物体实现接口即可,调用方只做接口判断,不关心具体类型。模块化程度和可维护性会好很多。
25. 事件分发器(Event Dispatcher)是蓝图解耦的利器。
多个系统都要监听“玩家死亡”事件,直接在角色蓝图上挂事件分发器,各系统绑定自己的处理函数。后续新增监听者,不用改动事件源本身,比满世界做Cast干净得多。
26. 蓝图里的Get All Actors Of Class少用。
这个节点在关卡内Actor数量上百万的场景下,耗时是线性爆炸的。如果确实需要查找,改用GameplayDebugger的分区查询,或者自己维护一个“注册表”数组,把需要被查询的对象动态加入数组。
3.2 蓝图运行机制与优化
27. 蓝图节点执行顺序不是你以为的从上到下。
事件图内从左到右流动,但分支结构会并行推进。比如一个Sequence节点后面挂两个运算,两边的结果是同时执行的,如果节点里有全局变量的读写,顺序就会产生难以察觉的Bug。复杂逻辑尽量拆成纯函数,避免副作用。
28. 所有耗时操作都用异步节点,不让游戏卡死。
加载关卡用Open Level的异步版本,或者Load Stream Level。资源加载同理。如果旧项目还有大量同步加载逻辑,打包后打开存档会白屏好几秒,就是因为这些同步操作卡了主线程。
29. 蓝图变量类型能用结构体就别散装。
一堆零散的布尔、实数变量,读起来混乱,复制传递也浪费。把相关变量打包成一个Structure,比如“角色状态”包含血量、耐力、属性增益,既方便做存档,也能整体在网络复制中传递。
30. 经常需要瞬间判断位置时,把距离判断的平方值存好。
Vector的Distance函数每次调用都开一次根号运算,而Distance Squared不用。连续对几百个Actor做距离判断时,做平方比较可以省掉大量CPU开根号的负载。判断是否小于某个阈值时,比较前先给阈值本身平方就行。
3.3 蓝图调试实战
31. 断点调试时Step Into和Continue会让人看不明白执行流,打开Blueprint Debugger的“断点列表”,一次看完所有断点。
这个面板还能右键断点条件化,只在某个变量等于特定值时中断。排查难复现的Bug,这是我用得最多的技巧,不用一遍遍手工触发条件。
32. 蓝图拉了大量连线,试试选中节点后按C键注释分组。
一张图几百个节点的时候,没有注释等于天书。C键快速分组注释,比拖拽节点框选再输入备注快得多。时间久了你会感激当初的注释。
33. 蓝图变量每次用Get节点取值,其实是有开销的。
循环里获取同一个变量,引擎会在每次迭代解引用一次。复杂循环里把变量先取到局部,每次迭代直接用局部值,能省下不少函数调用开销。蓝图里不明显,但几千次循环的差异立刻体现在帧耗时上。
4. C++模块化开发:用好工具链和反射机制
纯蓝图搞大型项目迟早卡脖子。C++参与进来之后,工程组织方式和调试手段都要跟上。
4.1 C++与蓝图交互的正确姿势
34. UPROPERTY反射宏记得带好EditDefaultsOnly和BlueprintReadOnly。
这俩组合是经常组合出现的。纯C++变量不给反射,编辑器属性面板里看不到,蓝图也访问不了,等于白写。赋值类型尽量用VisibleAnywhere避免被意外改动。
35. UFUNCTION(BlueprintCallable) 函数里,参数名要加描述。
蓝图侧连接节点时,看到的参数名就是C++函数签名里的名字。不加注释,别人(包括三个月后的你)根本不知道这个参数该填什么。函数前写清楚UPARAM(DisplayName="目标位置")是职业习惯。
36. 写模块时养成“瘦核心、胖接口”的习惯。
核心逻辑放C++,便于测试与复用;蓝图只做表现层拼装。比如技能系统,数值计算放在C++对象里,动画表现、音效反馈用蓝图实现。改动技能数值不用翻蓝图,改表现也不用动C++,两边的迭代互不干扰。
4.2 编译与构建优化
37. 改C++后编译慢,打开Live Coding后仍慢,就查项目的Unity Build配置。
大型工程默认会合并多个CPP文件到一个翻译单元加快构建。如果某个文件里的static变量影响了全局,编译器会报诡异的链接错误。这时候把bUseUnityBuild关掉试一次,定位到具体文件再重新打开,别一刀切关掉整个加速。
38. 把常用模块写成独立的Runtime模块,拆出插件。
比如网络通信、存档系统、UI框架都独立成插件,主工程只放游戏逻辑。好处是单模块编译时间短,能在多个项目间复用。同时插件内部改动不会导致主工程全量重编。
39. 每次改头文件都会触发全世界重编,原因在于头文件依赖。用前置声明减少include。
只在头文件里写类指针或引用时,声明class APlayerState;、class UGameInstance;这类前置声明,到cpp里再include具体头文件。改其中一个头文件,重编范围立刻小一半。
4.3 数据与内存实践经验
40. 项目中大量产生的小对象用ObjectPool,别反复构造和销毁。
子弹、飘字、连击提示这类高频实体,用一个池数组管理。取用、归还都只做标志位切换,不调用构造析构,堆分配次数降下来之后,GC压力明显减轻。
41. 字符串拼接用FString::Printf而不是无数个+。
FString的重载 + 会反复产生临时对象,循环里做拼接就是一场内存灾难。Printf一次性格式化,性能和经济性都好得多。项目里全局搜索+= FString,会发现能优化一大片。
42. 存档、配置类数据结构用FJsonObject的序列化,而不是手写解析。
UE里用FJsonObjectConverter::UStructToJsonObjectString转换一个USTRUCT到JSON字符串,直接写文件。读取同理。代码量少且不容易出错,比手拼字符串稳定太多。
43. 控制台命令是调试神器,写游戏逻辑时顺手加几个。
ConsoleCommand自动注册到控制台,开发期能用它调数值、触发剧情事件、刷怪,不用每次重编重启。做到这一点,日常效率至少高两倍。
5. 动画、物理与关卡细节
表现层的东西,看似不难,实际问题都藏在细节里。动画跟物理的坑,多数是结构性原因。
5.1 动画系统实用技巧
44. 动画蓝图每帧取值开销大,把常量动画数据缓存到变量。
比如不同速度对应的移动动画混合参数,放在蓝图里做一次获得,以后直接读取变量。避免每个动画蓝图里都计算同一个参数。动画蓝图数量多的时候,这个优化对GameThread很友好。
45. 动画重定向(Retarget)出问题,根骨骼必须改名匹配。
从其他项目导入角色模型时,如果根骨骼名称不一致,重定向后角色要么浮空要么戳地。检查源模型和目标模型的根骨骼名字,比如都是Root或都是Hips,而不是root、hips这种大小写不一致的写法。
46. 别忽略Enable Root Motion。
很多初学者做击退、翻滚、攀爬动画,角色原地滑动后来发现原因是动画里没有勾选Root Motion。动画资产里的根骨骼动画如果没启用,程序只能跨帧插值算移动距离,结果就是滑步。把动画资产勾为Root Motion From Animation,程序里配合GetRootMotionTransform做实际位移,角色就踏实了。
47. 物理资产(Physics Asset)精准度不够,角色布娃娃摔得怪。
布娃娃物理模拟要正常,前提是物理资产里的胶囊体或者球体形状贴合骨骼包围盒。从自动生成开始,按身体部位逐个调整碰撞体粗度和旋转朝向,可以解决绝大多数“关节反折”问题。
5.2 物理交互与查询优化
48. 射线检测用ECC_Visibility还是自定碰撞通道,别混淆。
做玩家瞄准检测时,通常用ECC_Visibility通道检测静态网格体,而交互提示用自定通道,避免射线穿透UI假体或者非交互物。通道设错会导致本该能点击的物体点不到。
49. 物理模拟的动态物体大量堆叠时,用CCD(连续碰撞检测)防穿透。
道具、子弹、碎块这类高速运动的物体按默认离散检测,经常出现“穿过墙壁”的Bug。在物体碰撞设置中开启CCD,让引擎在半步内做连续检测,子弹穿墙问题立即解决。不要全项目开启CCD,性能扛不住,只给高速物体开。
50. 蓝图发物理力的方向,记得先转换成世界空间。
外部导入的角色骨骼朝向是本地空间,直接用骨骼向量的Forward Vector加力,物体会往意想不到的斜后方飞。正确做法是先取骨骼组件GetComponentRotation,再从这个旋转量提取世界空间的朝向向量。
5.3 关卡编排与流送策略
51. 触发机关用延迟回调实现顺序门,别用蓝图Timer硬拼。
连续压下几个机关才开门,如果机关A触发五分钟后再压机关B,A状态要一直保存在变量里。这个变量放在关卡蓝图里容易被GC回收,放在GameInstance或者触发器自己的组件里,状态管理才稳定。
52. 关卡流送(World Partition/Level Streaming)加载时的淡入淡出,用Level Sequence控制。
直接塞Open Level做切换,会看到植被、建筑凭空冒出来。把子关卡加载事件挂到Sequence的关键帧上,加一个半透明的渐入效果。这类体验问题卡关时间久了,玩家感知很强,务必处理。
53. 自动保存节点别放Tick里。
我看到过新人把“每隔X时间自动存档”的逻辑直接写在Tick里判断,一存档就卡顿。写成Timer节点,5分钟触发一次保存,存档瞬间再隐藏UI提示,不要选在玩家开车的瞬间触发。
54. 做大量可拾取物时,别给每个物体都挂脚本。
几百个金币每个Actor都跑一遍Tick逻辑简直是灾难。做法是使用一个统管系统,拾取物只挂静态网格体和触发器,由系统的回调统一执行拾取逻辑。逻辑集中,性能也好。
55. 关卡编辑器里随手挪动过的灯光位置,常导致光照烘焙出现黑斑。
检查每个光源在属性面板里Mobility是否为Stationary或Static,动态光源参与烘焙容易产生一块块黑污。全静态关卡尽可能全部固定光源。
6. 光照与烘焙:别在地下室里调色
光照好看与否,一半在下项目时的设置,另一半在环境搭建。调灯之前先确认基本设置,而不是对着一个窗口狂试。
6.1 光照基础设置
56. 项目里所有光源的Intensity单位必须统一。
UE5以后默认是lux(流明/平方米),但老项目可能沿用旧的cd单位。在偏好设置里统一光源单位,不然同一个数值在不同光源上亮度差异会让人头大。团队协作时,这个设定应该在第一天就统一。
57. 室外场景烘焙泛白,检查Auto Exposure设置。
默认的自动曝光在明暗对比强烈的场景里容易过曝,把户外环境压成灰白一片。在后处理体积里把Auto Exposure Method改为手动,调好Min EV100和Max EV100范围,画面立刻有了实景感。
58. 使用Sky Atmosphere时,场景色温偏蓝,调Ground Albedo。
材质的颜色取决于整个天空反射,地面参数对这些整体的色调倾向影响非常大。把Ground Albedo调成暖色,阴影处立刻转暖,能对冲掉大量冷蓝感。
6.2 烘焙与环境质感调试
59. 光烘焙的“漏光”九成是几何体UV重叠。
光照贴图展开时,两块重叠的UV会让烘焙器算错亮度信息。检查Lightmap UV是否正确展开:选中静态网格体资产,在网格体编辑器的UV通道2里看展开结果,任何重叠都修正掉再烘焙。
60. 动态物体不参与烘焙光照,需要实时光源。
角色、载具这类动态物体,如果场景主要用烘焙光源,它们会阴森森的没光照感。要么给角色自身挂个移动光源模拟主光,要么接受混合光照模式下用Lumen(UE5)做实时间接光,再补充一个补光点。
61. 静态光照烘焙完出现噪点,提高Lightmass Importance Volume。
光线不充足区域噪声大,说明烘焙计算时这个区域被近似处理了。把Lightmass Importance Volume拉大包住主要视觉区,烘焙时间会增加,但中间的噪点明显减少。工作时需要反复试验大小。
62. 实时全局光照(Lumen)在室内偏暗,配合反射捕获反射球。
Lumen虽好,但CG感的反射仍需要反射捕获辅助。不要在室内全依赖Lumen,放几个SphereReflectionCapture半径贴合房间,金属物体表面立刻有层次感。
63. 开光追时噪点多,不要盲目把Samples拉满。
先看r.RayTracing.SamplesPerPixel的数值,比如2改为4后噪点下降明显,更大的数字边际收益变缓且性能下降严重。正确做法是开Denoiser配合4~8采样,画面干净性能又稳。
6.3 后期优化与特殊需求
64. 用Render Target做动态贴花,内存会随时间增长。
动态血腥脚印、弹痕这类贴花连续生成,Render Target尺寸持续膨胀。定时清理调度里,把旧贴花Clear一下,或者限制同一时刻最大贴花数,避免内存一路攀升。
65. 帧率不稳的时候,先关掉泛光(Bloom)/环境光遮蔽。
这两个后处理是最耗GPU的常用效果。排查前端表现问题时临时关闭,能快速判断是后处理导致还是模型负载导致。全场景泛光降为0外加SSAO关掉,帧率回升不到10%,再考虑降分辨率、减面数。
66. 曝光积雪模拟别用后处理硬刷,用材质遮罩。
后处理方案在极端角度会破绽百出。标准做法是给地面积雪材质加一个世界空间噪声遮罩,让雪只在朝上的表面出现,并按遮挡程度衰减。材质的World Position配Noise实现即可。
7. 调试与分析工具:用对工具比写对代码重要
项目卡死、内存泄、崩溃报告一大堆,着急没用,得按流程来。
7.1 卡死与性能分析
67. 游戏突然卡死,按~打开控制台输入stat startfile,跑5秒卡顿场景再输入stat stopfile。
这会在项目Saved文件夹里生成一个.ueprof文件,用Unreal Insights打开即可看到耗时分布。不要靠肉眼看动画卡在哪,工具会告诉你真相。
68. Unreal Insights里GameThread耗时异常,向下钻取<GameThread>类别,查找那些耗时的Blueprint节点。
Trace工具可以把C++和蓝图的耗时点都记录到,动画蓝图和事件图表一目了然。很多“迷之卡顿”就是某个节点在特殊条件下太耗时,之前完全没注意。
69. 项目运行久了内存持续上涨,怀疑泄漏,开Debug Console打obj list分类统计。
按obj list的输出,对比运行前后各类资源的数量,能发现累计UPROPERTY引用下未被回收的对象。找到增长最猛的对象类,再去查引用链,通常是某处静态引用一直保持,GC无法清理。
7.2 渲染与逻辑断点排查
70. 运行时画面出现黑色方块或莫名裁剪,用r.VisualizeOccludedPrimitives 1高亮可见性计算。
开这个模式会看到哪些物体被引擎认为不可见,如果关键物体被错误剔除,就得调整它的包围盒大小或者遮挡标记,比如某个道具被墙挡住但玩家需要看到它,它的追踪距离设置就要特殊处理。
71. 崩溃报告看不懂,用ReadBack转储。
崩溃时C++调用堆栈在PC上比较容易看,但打包后无从下手。勾选项目的CrashReporter并且开启符号文件上传,拿到崩溃堆栈后,第一步看崩溃发生在哪个模块,然后按模块查找最近的提交记录,定位代码变更。
72. 复杂逻辑Bug进入循环出不来,用条件断点代替满屏Print。
在C++或者蓝图的断点窗口设置条件表达式,比如Health < 30 && IsAlive == true,满足条件再中断。肉眼检查几百帧的数据,不如让调试器自动帮你在关键位置停住。
73. Shader编译卡顿,不是机器问题,是shader库太大。
做项目过程中材质和shader数量疯长,运行时首次打开卡半天,是因为要编几千个变体。打开ShaderCompileWorker日志,查看是否全部闲置编译但明显CPU跑满,考虑精简材质减少变体,或者在做培训、展示版本时关闭一些高级特性。
7.3 数据对比与迭代方法
74. 用A/B对比测试调试画质。
改完画面表现,迅速按F5截图(默认高分辨率截图),保存到ScreenShots文件夹,把改前改后的截图放在一起对比色阶、曝光、亮度曲线,别在手机或屏幕上凭印象。
75. 版本管理里总出灵异问题,用Content Browser右键资产Size Map查看资源驾驶舱。
一个主城关卡常因为误拖入超大贴图或者高模,打包体积悄悄涨几十兆。定期按Size Map排序,自动揪出大头资产,简化警告栏里经常提示的冗余资产。
76. 局域网多人协同调试时,用Log文件的唯一可读标记。
把所有调试日志输出到Saved/Logs,命名含时间戳。提交问题单时把这个日志附上,协同伙伴省掉大量沟通成本,直接读日志定位反馈。
8. 编辑器日常操作与团队协作
这些命令和习惯,决定了你在编辑器里待一天会不会抓狂。
8.1 编辑器提速与常用操作
77. 在编辑器中搜索资产别傻等缩略图,按Ctrl+Shift+F直接开Content Browser全文搜索。
全项目搜类名/贴图名,明确输入关键词比在文件夹里一层层翻快无数倍。推荐平时把常用目录在左侧收藏,效率更高。
78. 视图模式切到Lit调试光照,切到Unlit查材质自发光比例。
想在光照不到位时查看模型轮廓,需要Unlit。查找材质是否过亮,用Lit。常驻快捷键分别是Alt+4和Alt+3,切换速度决定工作流顺畅度。
79. 长期在编辑器里操作同一组关卡,学会用Bookmark功能。
按Ctrl+1~Ctrl+9存书签,再按数字键跳转回的书签视角。刷地形、做光照,在几个固定观察点之间来回跳,鼠标中键拖视角的效率远不如书签秒切。
80. 做关卡时大量调整物体位置,偶尔要用到精确数值。
选中物体后右键 →Pivot或属性面板里直接输入坐标。更好的方式是打开“可移动模式”下手拖同时按住W/E/R,在拖拽时输入精确数值,省去反复开关面板。
81. 多个人同时编辑一个关卡,提交时冲突,解决这类冲突用Level Snapshots。
UE的Level Snapshots可以把当前关卡状态归档成快照,冲突时对比两份快照差异,手动挑用户想保留的Actor。比在版本管理里直接文本解决靠谱得多,文本冲突几乎看不懂。
8.2 资产整理与脚本批处理
82. 资产整理用文件夹带_Deprecated后缀归档,不要物理删除。
裁员式清理资产,旧资源大清理前先打个快照,或者把废弃内容移入_Deprecated目录,编译一次确认无误再彻底删除物理文件。直接删,引用找不到就白屏,回滚都难。
83. Python脚本批处理资产命名。
UE的Python API可以批量修改资源名、批量替换贴图尺寸、批量清理Unused资产。一个自动整理脚本跑完几百个资产,几分钟能干完手动一小时的活。常用功能封装成菜单按钮,团队协作时更好用。
84. 养成用Collections分类资产的习惯,而不是依赖文件夹。
Collections可以跨目录聚合,比如“Boss战可用资源”“近期更新”这种逻辑分组。文件夹会随着发展变得又多又乱,Collections按用途分组不受物理目录限制,整理和维护成本更低。
85. 版本管理提交信息写清楚“为什么改”,不要写“调整”。
回滚排查时,一条 “修复传送门对墙角碰撞体误判” 比 “调整” 有用一万倍。开发者本人回看三个月前的提交都需要辅助线索,更不用说别人接手了。
9. 多平台适配与发布坑点
发布环节翻车是最伤的:单机调试好好的,一上真机和低端配置就各种问题。这块是实战里的重灾区。
9.1 客户端与真机适配
86. 安卓包体安装好但启动白屏,检查引擎版本和GPU驱动兼容性。
部分旧设备GPU不支持ASTC,贴图花屏或直接加载失败。项目包体里带上兼容性检测,在启动时对GPU能力做判断,然后禁用个别高开销特性。
87. 手柄按键在PC上正常,主机上失灵,原因是输入映射的Key没绑定主机按键。
工程设置里Input界面需要为每个主机平台单独绑一次按键,不能只做PC一套。检测标准是按主机手柄的A/B/X/Y键是否能触发对应Action。
88. 分辨率不同导致UI被截断,用Widget Blueprint的Size Box加比例锁。
不锁定屏幕比例时,UI在21:9宽屏上左右离屏幕超远。设计稿采用基准比例,再配合Scale Box控制缩放,界面在多种设备上就不至于崩。
89. 画面推流/录制时帧率被限制到30,检查项目里的Frame Rate Lock设置。
不少项目为了功耗限制帧率上限,开发者自己忘了解锁,录制出来的视频就锁死30帧。发布前录制宣传片,记得在控制台输入t.maxfps 60验证录制帧率上限。
9.2 打包与生产环境
90. Windows打包没声音,检查Audio Mixer的采样率设置。
部分旧声卡不支持高采样率输出,引擎自动降级失败,就会无声。在工程设置里把音频采样率调到标准48kHz试试,独立声卡和集成声卡的兼容性明显改善。
91. 打包文件过大,第一刀看贴图/音频资源平台上是否有专属压缩格式。
比如Switch换用OPUS压缩音频,体积能降一半;贴图平台选择对应格式开压缩,包体立减几百MB。查Cook日志中每个资源的size,按占用排序,一下看清体积大头。
92. 打包报 “Shader Code Library” 错误,优先查材质节点是否包含非法操作平台指令。
自定义节点用了桌面端特性但打包主机时没加平台宏,编译shader变体时会报错。解法是在材质节点中按平台宏做分支,比如ifdef PLATFORM_ANDROID和ifndef,只对目标平台执行有效代码。
93. 服务器/寻路/AI跑得和客户端不一样,多半是帧率不同步。
服务器帧率默认30,客户端可能60,网络同步逻辑里用了GetWorld()->DeltaTime就会时序错乱。关键逻辑用固定步长模拟或者同步服务器时间,就能跨端一致。
9.3 构建与CI发布流程
94. 项目每天构建,把UnrealPak和Cook时间错开,缩短构建瀑布。
典型误区是每次构建都全量Cook。做增量Cook策略,只有资源变更时才重新Cook,包体上传快得多。配合版本提交哈希做Cook缓存,能看到构建时间大幅缩短。
95. 自动化冒烟测试,用长Automation命令。
引擎自带自动化测试框架,能在打包后的包内跑脚本做开关关卡、简单寻路等测试。上线前一天夜里跑一轮,早上看报告,比自己手动进游戏跑Case靠得住。
96. 多平台打包三件套:
- Cook平台专属资源(对应平台的贴图格式,音频格式)
- 打对应平台的包体(iOS/Android/Windows)
- 平台认证(主机平台尤其要过合规检查)
这三步里任何一步误操作,轻则Store打回,重则证书过期无法装包。做发布流程表,每一步都打勾,避免上线当天手忙脚乱。
97. 版本号规则尽早规范化,拆成Major.Minor.Patch三段。
热更新也要在这个版本号内做小步提交,不然玩家端缓存和资源更新的错位判断会引发日志刷屏,还不好排查。
98. 代码分支使用Release和Develop分离,修复问题不要直接在主线改。
修完的重修单提交到Develop分支,再合并回Release,保证线上包稳定不乱。一旦某个补丁忘了合并,线上Bug修了但下一版又出现,就是分支合并没做透。
99. 使用第三方插件时,锁定版本到具体Commit,别用master最新版。
插件更新是破解兼容性的热门原因,今天能用,明天更新完开不了。团队里统一提交流水线,插件锁定某个测试通过的Commit号。
100. 硬件兼容性差的报错,收集DxDiag信息最管用。
遇到玩家反馈启动闪退或花屏,第一步让对方跑dxdiag导出配置。GPU型号、驱动版本一目了然。玩家自己的配置信息远比你瞎猜修复得快。
101. 团队Wiki或者文档中心,每次更新引擎版本后,把变更点记录到“踩坑表”里。
比如升级到新版本后光照单位变了、移动端贴图格式换了,这些已经在项目里趟过的坑记录成章,下一个人接手时不用重新趟一遍。我在的实际经验是,这份记录常常比引擎官方文档更适合项目内部迭代使用。
最后一条不是技术,是协作习惯。这个清单从头写到这里,每一条背后都是实实在在的调试时间和现场翻车。技术工具会迭代,但排查问题的思路和协作方式不会过时。如果你照着它解决了项目里的一个问题,这个清单就没白写。