ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

UE实战进阶:从C++编程模型到渲染管线调优的深度解析

UE实战进阶:从C++编程模型到渲染管线调优的深度解析 1. 从能跑到能扛UE实战阶段真正要跨过的坎很多人学Unreal Engine的路径都差不多跟着官方教程把Third Person模板跑起来拖几个Actor进场景连上Blueprint让角色能走能跳然后觉得自己会UE了。但真正进项目之后才发现之前那套东西顶多算能跑离能扛还差着十万八千里。所谓能扛是帧率稳得住、内存不爆、多人同步不飘、打包出来不崩、策划改需求的时候你不用重写半个系统。这一篇是游戏引擎架构深度解析系列的第五部分前面几篇我们把引擎的渲染管线、资源管理、Gameplay框架的底层逻辑拆得比较细了。到了这一篇视角要切换一下——从引擎是怎么设计的转到我在项目里怎么用它。关键词里出现了UE、Unreal Engine、C、Gameplay、渲染管线还有一堆C相关的热搜词比如vscode配置c/c环境、c回调函数、c引用指针和值传递、c结构体链表基本语法等等。这说明什么说明大量从C基础转过来的开发者正在往UE实战这个方向走但中间有一道明显的鸿沟C语法你会但UE的C不是标准C。这篇文章就围绕这道鸿沟展开。我会从UE的C编程模型讲起聊到Gameplay框架的实战用法再深入到渲染管线的性能调优最后讲一些Lyra这类官方示例项目里值得抄的设计思路。适合已经能跑通基础Demo、准备进真实项目或者正在项目中踩坑的开发者。如果你还在纠结vscode怎么配置c环境这种问题建议先把C基础打牢再回来因为UE的C门槛不在语法而在框架思维。先说一个我踩过的坑。刚转UE的时候我习惯性地用标准C的思路去写类构造函数里做一堆初始化结果编辑器一打开就崩。后来才明白UE有自己的对象生命周期管理UObject的构造、初始化、序列化是分开的你乱来它就敢崩给你看。这个认知转变是UE实战的第一课。2. UE的C不是标准C编程模型的核心差异2.1 UObject体系为什么让标准C开发者水土不服标准C里你new一个对象用完delete生命周期清清楚楚。UE不这么玩。UE有一套自己的对象系统根是UObject所有继承自它的类都由引擎的垃圾回收机制管理。你不能随便new一个UObject得用NewObjectT()或者CreateDefaultSubobjectT()而且什么时候被回收你说了不算得看引用计数和GC策略。这带来的第一个不适应就是构造函数里不能做重活。UE的对象在编辑器加载、CDOClass Default Object创建、反序列化等场景下都会被构造你的构造函数可能被调用很多次而且调用时机你控制不了。正确做法是把初始化逻辑放到BeginPlay()或者PostInitProperties()里。我见过太多新手在构造函数里加载资源、绑定委托结果编辑器一开就报一堆莫名其妙的错。第二个不适应是反射系统。UE用UCLASS()、UPROPERTY()、UFUNCTION()这些宏给类、属性、函数打标记然后UHTUnreal Header Tool在编译前扫描这些标记生成额外的代码。这些生成的代码负责序列化、GC引用追踪、Blueprint暴露等等。你不加UPROPERTY()这个成员变量就不会被GC追踪指向的UObject可能被回收掉然后你访问的时候就崩了。这个坑我踩过不止一次排查起来特别费劲因为崩的地方和出问题的地方往往不在一起。第三个是命名规范。UE的C有一套约定俗成的前缀U开头是UObject派生类A开头是Actor派生类F开头是普通结构体或类I开头是接口E开头是枚举。这不是强迫症是UHT和引擎内部工具依赖这些前缀来识别类型。你不按规范来轻则编译警告重则反射系统识别不了。2.2 引用、指针与值传递在UE里的实际选择热搜词里有c 引用 指针 和 值传递这个问题在UE语境下更复杂。标准C里我们讲究能用引用就不用指针能传const引用就不传值。UE里要分情况。对于UObject及其派生类永远用指针而且推荐用TObjectPtrTUE5之后。为什么因为UObject可能被GC回收引用没法处理对象没了这种情况。TObjectPtr是UE5引入的智能指针包装在编辑器构建下能提供访问追踪打包后和裸指针性能一样。你写UObject*也能跑但TObjectPtr更安全尤其是做大型项目的时候。对于非UObject的普通结构体比如FVector、FRotator这些小对象传值大对象传const引用。FVector就三个float传值比传引用还快因为不用解引用。但如果是大的配置结构体传const引用避免拷贝。这里有个容易忽略的点Blueprint和C之间的参数传递。Blueprint里传结构体是按值拷贝的如果你有个大结构体在Blueprint里频繁传递性能会很难看。我做过一个项目策划在Blueprint里传一个包含几十个字段的配置结构体每帧传好几次后来改成传指针或者只传需要的字段帧率直接上了一个台阶。2.3 回调函数与委托UE的事件驱动机制热搜词里有c回调函数例子UE里的回调主要靠**委托Delegate**实现。UE的委托分几种单播委托、多播委托、动态委托。动态委托才能暴露给Blueprint因为需要反射支持。我刚开始用的时候搞不清楚什么时候用DECLARE_DELEGATE什么时候用DECLARE_DYNAMIC_MULTICAST_DELEGATE。简单记要给Blueprint用的必须用DYNAMIC版本纯C内部用的用非DYNAMIC版本性能更好。多播就是可以有多个监听者单播只能绑一个。绑定的时候也有讲究。BindUObject用于绑定UObject的成员函数BindStatic用于静态函数BindLambda用于lambda。lambda绑定要小心生命周期如果lambda捕获了局部变量而委托在局部变量销毁后才触发就崩了。我一般建议能用BindUObject就用BindUObject生命周期由引擎管理省心。还有一个实战经验委托的广播时机。比如你在Actor的BeginPlay里广播一个初始化完成的委托但监听者可能还没绑定上来就漏掉了。这种情况要么用AddDynamic之后手动检查状态要么把广播延迟到下一帧。Lyra项目里大量使用了GameplayMessageSubsystem这种基于消息总线的机制来解耦比直接绑委托更灵活后面会细讲。3. Gameplay框架实战从Actor到Lyra的分层设计3.1 Actor与Component组合优于继承的落地UE的Gameplay框架核心是Actor-Component模式。Actor是场景里的实体Component是功能模块。一个Actor可以挂很多Component每个Component负责一块功能。这个设计的好处是组合优于继承你不用为了加一个功能去改继承链。但实际项目里我看到很多团队还是习惯性地写大Actor把所有逻辑塞在一个类里。结果就是几千行的Actor类改一处崩三处。正确的做法是按职责拆Component。比如移动相关的逻辑拆成MovementComponent血量相关的拆成HealthComponent交互相关的拆成InteractionComponent。Component之间通过委托或者接口通信不要互相直接引用。这里有个坑Component的Tick顺序。UE默认不保证Component的Tick顺序如果你的逻辑依赖某个Component先Tick得手动设置AddTickPrerequisiteComponent。我做过一个载具项目车轮的物理计算和车身的移动逻辑有依赖关系没设Tick顺序的时候偶尔会抖动设了之后就好了。另一个坑是Component的复制。多人游戏里Component的复制规则要仔细配。SetIsReplicated(true)只是第一步还得在GetLifetimeReplicatedProps里注册哪些属性要复制。漏了这一步客户端拿不到数据表现就是服务器上好好的客户端没反应。3.2 GameplayAbilitySystemLyra的核心武器Lyra是Epic官方放出来的UE5示例项目它的核心之一就是GameplayAbilitySystemGAS。GAS是一套用于管理技能、属性、效果的框架最初是为MOBA和ARPG设计的后来在Lyra里被用到了射击游戏上。GAS的核心概念有几个Ability技能、Attribute属性、GameplayEffect效果、AbilityTask技能任务。Ability定义能做什么Attribute定义数值是多少GameplayEffect定义数值怎么变AbilityTask定义技能执行过程中的异步步骤。我一开始觉得GAS太重了一个小项目用不上。但后来发现只要你涉及到技能有冷却属性会被Buff影响技能执行到一半要等动画这些需求GAS就能帮你省掉大量重复代码。Lyra里连开火、换弹、冲刺这些基础操作都是用GAS实现的扩展性非常好。不过GAS的学习曲线确实陡。我建议的入门路径是先理解Attribute和GameplayEffect的关系再理解Ability的激活流程最后看AbilityTask怎么处理异步。Lyra的源码是最好的教材但不要一上来就啃先跑起来改几个参数看看效果再回去读代码。3.3 从Lyra抄什么值得借鉴的架构决策Lyra里有几个设计我觉得特别值得抄。第一个是Experience系统。Lyra用Experience来定义一局游戏加载哪些资源、启用哪些系统。这个设计的好处是不同模式比如团队死斗和占点可以有不同的Experience加载不同的资源互不干扰。传统做法是在GameMode里写一堆if-elseLyra的做法干净得多。第二个是GameplayMessageSubsystem。这是一个基于消息总线的解耦机制系统之间不直接引用而是通过发消息通信。比如UI系统监听血量变化消息而不是直接引用HealthComponent。这样UI和游戏逻辑完全解耦换UI不用改逻辑。第三个是Input系统。Lyra用的是Enhanced Input把输入映射和输入处理分开。输入映射是数据资产可以在编辑器里配输入处理是Ability用GAS管理。这样改按键不用改代码做多套操作方案也很方便。抄的时候要注意Lyra是为展示引擎能力设计的不是为小项目设计的。它的架构有大量抽象层小项目直接抄会显得过度设计。我的建议是理解它的设计思路按需取用不要照搬。4. 渲染管线调优从Draw Call到Lumen的实战取舍4.1 渲染管线的基本流程与性能瓶颈定位UE的渲染管线大致分几个阶段应用阶段CPU端做可见性剔除、排序、提交Draw Call、几何阶段顶点着色、裁剪、光栅化、像素阶段像素着色、混合、输出。性能瓶颈可能出现在任何一个阶段定位方法不同。CPU瓶颈的典型表现是帧率上不去但GPU占用不高。用stat unit命令看如果GameThread或者DrawThread的时间远大于GPUTime就是CPU瓶颈。常见原因是Draw Call太多、Tick逻辑太重、物理计算太多。优化方向是合并Draw Call用Instanced Static Mesh、减少Tick、简化物理。GPU瓶颈的表现是GPU占用高帧率上不去。用stat gpu看各个Pass的耗时。常见原因是Overdraw太多、Shader太复杂、分辨率太高。优化方向是减少半透明物体、简化材质、用动态分辨率。我做过一个开放世界项目最初帧率只有30多。用stat unit一看GameThread占了20多毫秒DrawThread也高。排查发现是场景里几千个独立Actor每个都在Tick而且每个都是独立Draw Call。后来把静态物体合并成Instanced Static Mesh把不需要Tick的Actor关掉Tick帧率直接翻倍。4.2 Lumen与NaniteUE5的两把双刃剑UE5最大的卖点就是Lumen全局光照和Nanite虚拟几何体。这两个技术确实强大但也是性能杀手用不好帧率直接崩。Lumen的问题在于它需要大量的光线追踪计算对GPU要求很高。而且Lumen的质量设置对性能影响巨大从Low到Epic性能差距可能有几倍。我的经验是中低端设备不要开Lumen用传统的烘焙光照或者简单的SSGI代替。如果非要开把Lumen的质量调低或者限制Lumen的作用范围。Nanite的问题在于它对材质和顶点动画的支持有限。Nanite不支持World Position Offset的复杂计算不支持半透明材质不支持骨骼动画UE5.1之后支持了一部分。如果你的场景大量使用这些特性Nanite反而会拖慢性能因为它要回退到传统渲染路径。还有一个坑Nanite和Lumen一起用的时候显存占用会飙升。Nanite的几何数据本身就占显存Lumen的Surface Cache也占显存两个加起来中端显卡很容易爆显存。我建议在项目早期就确定目标硬件根据硬件能力决定开不开这两个特性不要等到后期才发现跑不动。4.3 材质与Shader优化的实战技巧材质优化是渲染调优里最容易被忽略的一块。很多美术做的材质看着效果不错但Shader指令数几百条一个像素算半天。优化材质有几个原则能用顶点着色器算的不要用像素着色器算。顶点数量远少于像素数量顶点着色器的计算成本低得多。比如一些渐变、UV动画可以放到顶点着色器里。减少纹理采样次数。每次纹理采样都有开销能合并的纹理就合并。比如把Roughness、Metallic、AO打包到一张纹理的不同通道里一次采样拿三个值。慎用半透明材质。半透明材质不能写深度会导致Overdraw而且排序开销大。能用Masked就不用Translucent能用Opaque就不用Masked。Shader复杂度分级。UE支持材质质量级别你可以为不同画质设置不同的Shader复杂度。比如高质量用复杂的PBR低质量用简单的Lambert。这个功能在跨平台项目里特别有用。我踩过的一个坑是材质里的Static Switch用太多。Static Switch会在编译时生成多个Shader变体变体太多会导致编译时间爆炸打包体积也会变大。我见过一个项目一个材质有十几个Static Switch组合出来的变体上千个打包打了一整天。后来精简到三四个打包时间降到几小时。5. 工程化与协作让团队少踩坑的实践5.1 项目结构设计与模块划分UE项目的默认结构是Content目录下按类型分文件夹比如Meshes、Materials、Blueprints。但项目一大这种分法就乱了。我推荐按功能模块分比如Content/Characters、Content/Weapons、Content/UI每个模块下面再按类型分。这样找东西方便也方便做模块化加载。C代码的模块划分更重要。UE支持把代码拆成多个Module每个Module可以独立编译、独立加载。我建议至少拆成几个核心ModuleGameCore核心框架、Gameplay游戏逻辑、UI界面、Editor编辑器扩展。Module之间通过Public和Private目录控制可见性避免循环依赖。这里有个坑Module的依赖关系要理清。A依赖BB依赖CC又依赖A编译就挂了。UE的Build.cs文件里配依赖配错了编译报错还算好的有时候能编过但运行时崩排查起来很痛苦。我的经验是画一张Module依赖图确保是单向的树状结构不要有环。5.2 版本控制与资源管理UE项目的版本控制是个老大难。.uasset文件是二进制的不能合并两个人同时改一个资源就冲突。解决办法是锁定机制谁改谁锁定别人不能改。Perforce对UE的支持最好Git也能用但需要配LFS而且大文件多了之后很慢。资源命名规范也要统一。我推荐用前缀标识类型比如SM_开头是Static MeshM_开头是MaterialBP_开头是BlueprintT_开头是Texture。这样在Content Browser里排序、搜索都方便。还有一个经验定期清理未使用的资源。UE有个Reference Viewer可以看资源引用关系但项目大了之后总有一些孤儿资源没人引用但占着空间。我一般每个大版本做一次清理用Size Map看哪些资源占空间大用Reference Viewer确认没引用后删掉。有一次清理出好几个G的废弃资源打包体积直接小了一圈。5.3 性能分析与自动化测试UE自带了一套性能分析工具Unreal Insights、stat命令、Profiler。Unreal Insights是最强大的能看CPU、GPU、内存、网络各个维度的数据还能做Trace对比。我建议在项目早期就接入Insights定期做性能回归测试不要等到后期才发现性能问题。自动化测试方面UE支持Functional Test和Automation Test。Functional Test是在场景里放测试Actor跑一遍看结果对不对。Automation Test是纯代码测试测逻辑、测算法。我一般把核心玩法逻辑写成Automation Test每次提交前跑一遍防止改A崩B。还有一个实用技巧用Commandlet做批量处理。比如批量重命名资源、批量修改材质参数、批量导出数据都可以写Commandlet。我写过一个Commandlet自动检查所有Blueprint的命名规范不符合的报出来省了人工检查的时间。6. 一些没人告诉你但迟早会遇到的坑6.1 打包与发布环节的常见故障打包是UE项目最容易出问题的环节。常见问题有几个Shader编译卡住、资源丢失、崩溃。Shader编译卡住通常是因为变体太多或者Shader编译缓存没配好。解决办法是开Shared Shader Cache团队共享编译结果。另外打包前用RecompileShaders命令预编译一遍能提前发现问题。资源丢失通常是因为引用链断了。编辑器里能跑打包后找不到资源多半是软引用Soft Reference没配好或者资源在打包时被剔除了。检查方法是看打包日志里的WarningUE会提示哪些资源没找到。崩溃的话先看Crash Log再看Callstack。UE的Crash Reporter能自动上传崩溃信息配好Symbol Server就能看到具体的代码行。我遇到过一次打包后崩溃查了半天发现是某个Plugin在Shipping模式下有代码被条件编译掉了导致空指针。这种问题只能靠仔细检查条件编译宏。6.2 多人同步的常见陷阱多人游戏里网络同步是最容易出Bug的地方。常见问题有属性不同步、RPC调用时机不对、预测和纠正打架。属性不同步先检查GetLifetimeReplicatedProps里有没有注册再检查SetIsReplicated有没有开最后检查同步条件比如COND_OwnerOnly对不对。RPC调用时机不对通常是在Actor还没复制到客户端的时候就调用了。解决办法是用OnRep回调或者等BeginPlay之后再调。预测和纠正打架是客户端预测没做好。UE的CharacterMovement自带预测但自定义的逻辑要自己处理预测。我的建议是能不用预测就不用预测用服务器权威模式客户端只做表现。虽然手感差一点但Bug少很多。6.3 从C基础到UE实战的思维转换最后聊一个心态问题。很多C基础不错的开发者转UE之后反而觉得别扭因为UE的很多做法和标准C的最佳实践是冲突的。比如标准C讲究RAIIUE里UObject不归你管标准C讲究用智能指针UE里用TObjectPtr标准C讲究异常处理UE里基本不用异常。我的建议是把UE当成一门新语言来学不要用标准C的思维去套。UE有自己的哲学它的很多设计是为了编辑器友好、热重载、跨平台。你理解了它的设计目标就能理解它为什么这么做。另外多读引擎源码。UE的源码是开源的遇到不懂的地方直接去看源码比看文档快。我刚开始的时候遇到问题就搜论坛后来发现大部分问题源码里都有答案。Lyra项目更是把很多最佳实践直接写成了代码值得反复读。7. 我个人的一些实战体会写了这么多最后分享几个我自己的体会不一定对但都是踩坑踩出来的。第一不要过早优化。我见过太多项目早期就在纠结用不用Nanite、用不用Lumen结果玩法还没跑通性能优化无从谈起。正确的顺序是先让玩法跑起来再测性能再优化。优化要有数据支撑不要凭感觉。第二工具链比技术点重要。一个团队如果版本控制混乱、命名规范不统一、没有自动化测试技术再强也做不出好项目。我花在工具链上的时间比花在具体技术点上的时间多得多但我觉得值。第三多和策划、美术沟通。程序不是孤岛很多性能问题、同步问题根源在需求设计。比如策划要一个全屏AOE技能如果程序不提前说清楚性能影响做出来可能直接卡死。早期沟通能避免大量返工。第四保持学习但不要追新。UE版本更新很快每个版本都有新特性。但项目不是试验田不要为了用新特性而用新特性。我一般等新版本稳定一两个小版本之后再升级升级前做好兼容性测试。第五文档和注释要写。UE的Blueprint和C混编逻辑分散在两边不写文档根本理不清。我现在的习惯是每个系统写一个README每个复杂函数写注释每个Blueprint写描述。看起来费时间但后期维护省的时间更多。这个系列写到第五篇基本上把UE从架构到实战的主要脉络过了一遍。后面如果还有第六篇可能会聊一些更垂直的话题比如网络同步的深入实现、渲染管线的自定义Pass、或者编辑器扩展开发。但那些都是锦上添花把前面这些基础打牢大部分项目都能应付了。
返回列表