1. 从“能跑蓝图”到“敢改引擎”:UE实战的分水岭
很多人学Unreal Engine的路径都差不多:先跟着教程拖几个Actor,连一堆蓝图节点让角色跑起来,再做个简单的UI,然后觉得自己“会UE”了。可一旦项目稍微复杂一点,比如要自定义一个渲染Pass、要写一个高性能的Actor组件、要接第三方C++库,立刻就卡住。这个分水岭其实非常清晰——你能不能读懂并修改引擎的C++层。
我自己最早做UE项目时也踩过这个坑。当时做一个开放世界的地形交互,蓝图里每帧遍历几千个Actor做距离检测,帧率直接掉到20以下。后来改成C++的UActorComponent,用空间哈希做粗筛,同样的逻辑帧率回到80+。这件事让我彻底明白:蓝图是UE的“快速原型工具”,而C++才是UE的“生产工具”。两者不是替代关系,是分工关系。
这篇内容面向的是已经能写基本蓝图、但想往引擎层深入的人。我会把UE实战中最容易卡住的几个高级主题拆开讲:Gameplay框架的C++扩展、渲染管线的介入点、C++与蓝图的混合架构、以及实际项目中的性能排查方法。每个部分都会给出可复现的代码结构和参数选择的理由,不是泛泛而谈“要用C++”。
先明确一个前提:UE的C++不是标准C++。它有一套自己的反射系统(UHT)、垃圾回收机制、以及大量宏(UCLASS、UPROPERTY、UFUNCTION)。你写UE的C++,本质上是在和这套系统协作,而不是单纯写算法。理解这一点,后面很多“为什么这么写”就顺了。
2. Gameplay框架的C++扩展:AActor与UActorComponent的正确打开方式
2.1 为什么你的Actor不该什么都干
新手写UE的C++,最常见的错误是把所有逻辑塞进一个继承自AActor的类里。移动、血量、攻击、UI更新、音效播放全在一个Tick里。这种写法在原型阶段没问题,但一旦要复用或者做网络同步,就会变成灾难。
UE的Gameplay框架核心思想是组合优于继承。AActor是容器,UActorComponent是功能模块。一个角色Actor应该只负责“存在”和“被引用”,具体能力由挂载的Component提供。比如:
UCharacterMovementComponent负责移动UAbilitySystemComponent负责技能- 你自己写的
UHealthComponent负责血量
这样设计的好处是,当你要做一个“会回血的箱子”时,不需要继承角色类,只需要给箱子挂一个UHealthComponent。代码复用率直接翻倍。
2.2 创建一个可复用的HealthComponent
下面是我在实际项目中用的UHealthComponent简化版结构。注意看UPROPERTY和UFUNCTION的用法,这是UE C++和标准C++最大的区别。
// HealthComponent.h #pragma once #include "CoreMinimal.h" #include "Components/ActorComponent.h" #include "HealthComponent.generated.h" DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnHealthChanged, float, NewHealth, float, Delta); UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) class MYGAME_API UHealthComponent : public UActorComponent { GENERATED_BODY() public: UHealthComponent(); UFUNCTION(BlueprintCallable, Category="Health") void ApplyDamage(float DamageAmount); UFUNCTION(BlueprintCallable, Category="Health") void Heal(float HealAmount); UFUNCTION(BlueprintPure, Category="Health") float GetHealthPercent() const; UPROPERTY(BlueprintAssignable, Category="Health") FOnHealthChanged OnHealthChanged; protected: virtual void BeginPlay() override; UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Health") float MaxHealth = 100.0f; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Health") float CurrentHealth = 0.0f; UPROPERTY(EditDefaultsOnly, Category="Health") bool bCanDie = true; };这里有几个关键点值得展开。GENERATED_BODY()宏是UHT(Unreal Header Tool)的入口,它会在编译前生成反射代码。没有这个宏,你的UPROPERTY就不会被引擎识别,蓝图里也看不到。BlueprintAssignable让这个委托可以在蓝图里绑定事件,这样策划不用改C++就能加“血量变化时播放音效”的逻辑。
BeginPlay里做初始化:
void UHealthComponent::BeginPlay() { Super::BeginPlay(); CurrentHealth = MaxHealth; OnHealthChanged.Broadcast(CurrentHealth, 0.0f); }ApplyDamage里要处理边界:
void UHealthComponent::ApplyDamage(float DamageAmount) { if (DamageAmount <= 0.0f || CurrentHealth <= 0.0f) return; const float OldHealth = CurrentHealth; CurrentHealth = FMath::Clamp(CurrentHealth - DamageAmount, 0.0f, MaxHealth); const float Delta = CurrentHealth - OldHealth; OnHealthChanged.Broadcast(CurrentHealth, Delta); if (bCanDie && CurrentHealth <= 0.0f) { // 这里不直接Destroy,交给外部监听者决定 OnHealthChanged.Broadcast(0.0f, Delta); } }注意:不要在Component里直接调用
GetOwner()->Destroy()。Component应该只负责状态管理,死亡后的表现(掉落物品、播放动画、通知GameMode)应该由监听OnHealthChanged的外部逻辑处理。这是单一职责原则在UE里的具体体现。
2.3 网络同步的坑:Replicated属性的正确写法
如果你的项目涉及多人,UPROPERTY需要加Replicated标记,并且要在GetLifetimeReplicatedProps里注册。很多人漏了这一步,导致客户端血量不更新。
// 在头文件里 UPROPERTY(ReplicatedUsing = OnRep_CurrentHealth) float CurrentHealth; UFUNCTION() void OnRep_CurrentHealth(); // 在cpp里 void UHealthComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(UHealthComponent, CurrentHealth); }ReplicatedUsing比单纯的Replicated多了一个回调,让你能在客户端收到新值时做表现层更新(比如刷新血条UI)。这个回调只在客户端触发,服务器端不会调用,所以不要在里面写游戏逻辑。
3. 渲染管线的介入点:从SceneProxy到自定义Pass
3.1 UE渲染管线的分层结构
UE的渲染管线可以粗略分成三层:Game Thread(游戏逻辑)、Render Thread(渲染指令生成)、RHI Thread(GPU指令提交)。你在C++里写的渲染相关代码,大部分运行在Render Thread上,通过FSceneProxy和FPrimitiveSceneProxy与场景交互。
为什么要理解这个分层?因为很多人在Tick里改材质参数,发现延迟一帧才生效,或者在打包后表现和编辑器不一致。根源就是Game Thread和Render Thread的同步问题。
一个典型的自定义渲染需求是:给某个Actor加一个描边效果。最直接的做法是改材质,但如果你要做“只在被遮挡时显示描边”,就需要自定义FPrimitiveSceneProxy,在渲染线程里判断深度。
3.2 自定义SceneProxy的基本骨架
下面是一个简化版的描边SceneProxy结构。实际项目中你还需要处理材质加载、顶点工厂等,但核心逻辑在这里。
class FOutlineSceneProxy : public FPrimitiveSceneProxy { public: FOutlineSceneProxy(const UOutlineComponent* InComponent) : FPrimitiveSceneProxy(InComponent) , OutlineColor(InComponent->OutlineColor) , OutlineThickness(InComponent->OutlineThickness) { bWillEverBeLit = false; } virtual void GetDynamicMeshElements( const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const override { for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ++ViewIndex) { if (!(VisibilityMap & (1 << ViewIndex))) continue; // 这里构建描边Mesh FMeshBatch& Mesh = Collector.AllocateMesh(); // ... 设置顶点、材质、渲染状态 Collector.AddMesh(ViewIndex, Mesh); } } virtual SIZE_T GetTypeHash() const override { static size_t UniquePointer; return reinterpret_cast<size_t>(&UniquePointer); } virtual uint32 GetMemoryFootprint() const override { return sizeof(*this) + GetAllocatedSize(); } private: FLinearColor OutlineColor; float OutlineThickness; };GetDynamicMeshElements是每帧调用的,所以不要在这里做昂贵的计算。如果你需要缓存数据,在构造函数里做完。GetTypeHash用于渲染线程的排序和去重,返回一个稳定的指针地址即可。
实操心得:自定义SceneProxy最容易出的问题是崩溃在
Collector.AllocateMesh()之后忘记设置Mesh.bUseAsOccluder或者Mesh.CastShadow,导致渲染结果和预期不符。建议先在简单场景里跑通,再逐步加复杂度。
3.3 材质参数集与全局Shader
如果你不想动SceneProxy,还有一个更轻量的介入方式:Material Parameter Collection(MPC)。它是一组全局材质参数,可以在C++里通过UKismetMaterialLibrary::SetScalarParameterValue修改,所有引用该MPC的材质都会更新。
这个方案适合做全局效果,比如“时间暂停时所有材质变灰”、“水下场景的全局雾效”。缺点是粒度粗,不能针对单个Actor。选择哪种方案,取决于你的效果是“全局”还是“个体”。
4. C++与蓝图的混合架构:谁该写什么
4.1 划分原则:数据与逻辑分离
我见过很多团队在“用C++还是蓝图”上争论不休。其实答案很简单:C++写逻辑和性能敏感部分,蓝图写数据配置和表现层。
具体来说:
- C++负责:算法、网络同步、性能关键路径、第三方库集成
- 蓝图负责:数值配置、动画状态机、UI逻辑、关卡脚本
举个例子,一个技能系统。C++里定义USkillComponent,包含冷却计算、伤害公式、目标筛选。蓝图里继承这个Component,配置每个技能的具体数值和特效。这样策划改数值不用重新编译C++,程序员也不用为每个技能写重复代码。
4.2 BlueprintImplementableEvent与BlueprintNativeEvent
这两个宏是C++和蓝图沟通的桥梁,但用法有区别。
BlueprintImplementableEvent:C++里只声明,不实现,完全由蓝图实现。适合“表现层回调”。
UFUNCTION(BlueprintImplementableEvent, Category="Skill") void OnSkillActivated(AActor* Target);BlueprintNativeEvent:C++里提供默认实现,蓝图可以覆盖。适合“有默认行为但允许扩展”的场景。
UFUNCTION(BlueprintNativeEvent, Category="Skill") void OnSkillHit(AActor* Target); virtual void OnSkillHit_Implementation(AActor* Target);注意:
BlueprintNativeEvent的C++实现函数名必须加_Implementation后缀,这是UHT的硬性要求。忘了加会编译报错,而且报错信息不太直观,新手容易卡在这里。
4.3 数据驱动的配置表
UE里读Excel或CSV配置,常见做法是用UDataTable。C++里定义行结构:
USTRUCT(BlueprintType) struct FSkillConfigRow : public FTableRowBase { GENERATED_BODY() UPROPERTY(EditAnywhere) float Cooldown = 1.0f; UPROPERTY(EditAnywhere) float Damage = 10.0f; UPROPERTY(EditAnywhere) TSoftObjectPtr<UTexture2D> Icon; };然后在蓝图里用GetDataTableRow节点读取。TSoftObjectPtr是软引用,不会在加载配置表时把所有图标都加载进内存,适合大量资源配置。
这个方案的好处是,策划可以直接在Excel里改数值,导出CSV后导入UE,不需要程序员介入。坏处是CSV的格式校验很弱,建议在导入后加一个校验步骤,检查数值范围是否合理。
5. 性能排查实录:从卡顿到流畅的完整过程
5.1 先用Unreal Insights定位瓶颈
遇到卡顿,第一步不是猜,是用工具。UE自带的Unreal Insights可以录制CPU和GPU的耗时。打开方式:命令行加-trace=cpu,gpu启动,然后用Unreal Insights打开生成的.utrace文件。
看什么?重点看Game Thread和Render Thread的耗时分布。如果Game Thread某帧超过16ms(60帧目标),找到耗时最高的函数。常见元凶:
Tick里遍历大量Actor- 频繁的
SpawnActor和Destroy - 蓝图的
ForEachLoop处理大数组 - 同步加载资源(
LoadObject)
5.2 一个真实的优化案例
我之前做一个RTS项目,单位数量到200时帧率掉到30。用Insights一看,UUnitManager::Tick占了12ms。代码逻辑是每帧遍历所有单位,找最近的敌人。
优化分三步:
- 空间分区:把地图分成网格,每个单位只和同网格及相邻网格的单位比较。这一步把O(n²)降到接近O(n)。
- 降低频率:目标搜索不需要每帧做,改成每0.2秒一次,用定时器触发。
- 缓存结果:找到目标后缓存引用,每帧只做距离校验,超过阈值才重新搜索。
优化后Tick耗时降到1.5ms,帧率回到75+。这个案例说明,大部分性能问题不是代码写得不够底层,而是算法复杂度选错了。
5.3 常见性能问题速查表
| 现象 | 可能原因 | 排查工具 | 解决方向 |
|---|---|---|---|
| Game Thread耗时高 | Tick逻辑过重 | Unreal Insights | 降频、缓存、空间分区 |
| Render Thread耗时高 | Draw Call过多 | stat render | 合并Mesh、用Instancing |
| 内存持续增长 | 资源未释放 | stat memory | 检查硬引用、用软引用 |
| 打包后卡顿 | 编辑器优化未生效 | 打包版Insights | 检查LOD、剔除距离 |
| 网络延迟高 | 属性同步频繁 | net stat | 降低同步频率、用RPC替代 |
实操心得:
stat命令是UE里最实用的调试工具之一。stat fps看帧率,stat unit看Game/Draw/GPU耗时,stat game看Gameplay相关。养成习惯,每做一个新功能就跑一下stat unit,比等到卡顿再查效率高得多。
6. 工具链与开发环境:那些教程不说的细节
6.1 Visual Studio配置的隐藏选项
UE的C++项目在VS里编译,有几个设置直接影响开发体验。在项目属性里:
- C++语言标准:UE 5.x默认用C++20,但部分第三方库可能只支持C++17。如果遇到链接错误,先检查这里。
- 调试信息格式:用
/Zi而不是/Z7,后者会让每个obj文件都带调试信息,编译变慢。 - 多处理器编译:
/MP开启,编译速度能快30%以上。
另外,Visual C++ Redistributable是运行UE打包程序的前提。如果你把打包后的游戏发给别人,对方没装对应的Redistributable,会报缺少DLL。建议在安装包里附带vc_redist.x64.exe。
6.2 热重载的局限与Live Coding
UE的热重载(Hot Reload)在改头文件时经常出问题,尤其是改UCLASS的继承关系或UPROPERTY类型时,容易导致编辑器崩溃。UE 5引入了Live Coding,比热重载稳定,但也不是万能的。
我的建议是:改.cpp文件用Live Coding,改.h文件直接关编辑器重新编译。虽然慢一点,但省去了排查热重载bug的时间。这个习惯让我少踩了很多坑。
6.3 源码阅读的方法
UE的源码有几十万行,不可能全看。我的方法是按需阅读:遇到哪个类不熟悉,就在IDE里跳转到定义,看头文件的注释和成员变量。UE的头文件注释质量很高,很多问题看注释就能解决。
另外,Engine/Source/Runtime下的代码比Engine/Source/Editor更值得读。Runtime是运行时逻辑,Editor是编辑器工具,前者对项目开发更有参考价值。
7. 高级主题的延伸方向
7.1 GAS(Gameplay Ability System)的接入时机
GAS是UE官方推荐的技能系统框架,但它的学习曲线很陡。我的建议是:小项目不要用GAS。它适合技能数量多、需要复杂Buff/Debuff交互、有多人同步需求的项目。如果只是几个简单技能,自己写Component更轻量。
接入GAS的入口是UAbilitySystemComponent和UAttributeSet。前者管理技能激活,后者管理属性(血量、蓝量、攻击力)。GAS的核心概念是GameplayEffect,它描述“对属性做什么修改”,支持持续时间和周期触发。
7.2 自定义资产类型的创建
当你的项目需要一种新的资产类型(比如“关卡配置”、“对话树”),可以继承UDataAsset或UPrimaryDataAsset。前者是简单数据容器,后者支持资产引用和异步加载。
创建步骤:
- 继承
UPrimaryDataAsset - 加
UCLASS(BlueprintType)标记 - 在内容浏览器里右键创建该类型的资产
- 用
UAssetManager管理加载
这个方案比用UDataTable更灵活,因为你可以嵌套结构体、引用其他资产、加自定义编辑器面板。
7.3 插件化架构的收益
当项目变大,把功能拆成Plugin是必要的。Plugin可以独立编译、独立开关,减少主工程的编译时间。创建Plugin用编辑器里的“New Plugin”向导,选“Blank”模板,然后把相关代码移进去。
Plugin的.uplugin文件里可以配置加载阶段(LoadingPhase),比如PreDefault在引擎初始化前加载,Default在正常阶段加载。大部分游戏功能用Default即可。
8. 一些零散但重要的经验
关于UPROPERTY的Transient标记:如果你的变量不需要保存到磁盘(比如运行时缓存),加Transient。否则序列化时会写入,增加存档大小。
关于TArray和TMap的选择:需要顺序访问用TArray,需要快速查找用TMap。TMap的底层是哈希表,插入和查找是O(1),但遍历顺序不保证。如果既要顺序又要查找,用TArray加手动排序,或者TSortedMap。
关于FString和FName:FName是大小写不敏感的字符串,用于标识符(比如骨骼名、材质参数名),比较速度快。FString是可变字符串,用于显示和拼接。不要用FString做频繁的比较,性能差很多。
关于Cast的使用:Cast<AType>(Object)在失败时返回nullptr,不会崩溃。但频繁Cast有性能开销,尤其是在Tick里。如果确定类型,用static_cast;如果不确定,用Cast并检查返回值。
关于内存泄漏的排查:UE有垃圾回收,但UPROPERTY之外的裸指针不会被GC管理。如果你new了一个UObject但没有用UPROPERTY持有,它会被回收,导致悬空指针。反过来,如果你用UPROPERTY持有但忘了置空,对象永远不会被回收。这两个方向都会出问题,建议用TWeakObjectPtr做非拥有引用。
关于打包后的日志:打包版默认不输出UE_LOG到屏幕,但会写到Saved/Logs目录。排查打包版问题时,先看日志文件,比在编辑器里猜快得多。
关于版本控制:UE项目的.gitignore要排除Binaries、Intermediate、Saved、DerivedDataCache。.uasset和.umap是二进制文件,不能合并,所以团队协作时要约定“同一时间只有一个人改同一个蓝图”。这个规则听起来简单,但实际执行时经常被忽略,导致冲突后只能二选一。
关于C++标准库的使用:UE有自己的容器(TArray、TMap)和字符串(FString),尽量用UE的,不要混用std::vector和std::string。混用会导致内存分配器不一致,轻则性能下降,重则崩溃。如果必须用第三方库,在边界处做转换。
关于编译速度:UE项目编译慢是常态。除了/MP,还可以用Unity Build(把多个cpp合并编译),在BuildConfiguration.xml里配置。另外,减少头文件包含,用前向声明替代#include,能显著减少编译时间。
关于调试:UE的UE_LOG是最常用的调试手段,但输出太多会拖慢性能。用UE_LOG(LogTemp, Warning, ...)而不是Log级别,因为Log在打包版会被编译掉。另外,DrawDebugLine、DrawDebugSphere这些调试绘制函数在打包版也会被编译掉,不用担心性能。
关于蓝图性能:蓝图的Event Tick比C++的Tick慢一个数量级。如果某个蓝图Actor数量超过50个,考虑把Tick逻辑移到C++。另外,蓝图的ForEachLoop在大数组上很慢,用ForEachLoopWithBreak或者改成C++的for循环。
关于资源加载:LoadObject是同步加载,会阻塞Game Thread。大资源用StreamableManager做异步加载,加载完成后回调。TSoftObjectPtr和TSoftClassPtr是异步加载的基础,配置表里存软引用,运行时按需加载。
关于网络同步的粒度:不要同步所有属性。只同步“其他玩家需要知道”的属性。比如,玩家的输入不需要同步,但位置和朝向需要。同步频率也可以调,NetUpdateFrequency默认是100Hz,改成10Hz能大幅降低带宽。
关于UI:UE的UMG用蓝图做很方便,但复杂UI(比如大量列表项)用C++的Slate框架性能更好。Slate的学习曲线陡,但如果你要做编辑器工具或者高性能UI,值得投入时间。
关于音频:UE的音频系统支持C++和蓝图。简单的音效播放用蓝图,复杂的音频逻辑(比如动态混音、音频遮挡)用C++。UAudioComponent是C++里的入口。
关于物理:UE默认用Chaos物理引擎。如果你需要自定义物理行为,可以继承UPhysicsConstraintComponent或者用FBodyInstance直接操作。物理相关的代码要注意线程安全,大部分物理计算在物理线程上。
关于动画:动画蓝图适合做状态机和混合,但复杂的IK或程序化动画用C++的FAnimNode更灵活。FAnimNode需要注册到动画图,写法比较模板化,但一旦跑通就很稳定。
关于AI:UE的AI系统基于行为树和黑板。C++里可以自定义UBTService和UBTTask,比蓝图节点性能好。导航系统用UNavigationSystemV1,动态障碍用NavModifierVolume。
关于序列化:自定义的USTRUCT如果要保存到存档,需要加SaveGame标记,并且用FArchive读写。UE的序列化系统支持版本控制,用Ar.UEVer()判断引擎版本,做兼容处理。
关于多平台:UE支持Windows、Mac、Linux、主机、移动端。不同平台的渲染和输入API不同,用PLATFORM_WINDOWS、PLATFORM_ANDROID这些宏做条件编译。移动端要注意性能预算,Draw Call控制在100以内,三角形数量控制在10万以内。
关于插件市场:UE的Fab市场有很多现成插件,但引入第三方插件前要评估:是否还在维护、是否支持你的引擎版本、是否有源码。没有源码的插件出问题很难排查,建议优先选有源码的。
关于学习路径:如果你刚接触UE的C++,建议从FPS模板的C++版本开始读,它包含了Gameplay框架的基本用法。然后读Lyra示例项目,它是UE官方的高级示例,涵盖了GAS、模块化Gameplay、异步加载等高级主题。读源码比看教程慢,但理解更深。
关于社区资源:UE的官方文档质量参差不齐,但API Reference很全。遇到不熟悉的类,直接查API文档。另外,Unreal Slackers社区和官方论坛有很多实战讨论,搜索问题时加上“UE5”和具体类名,命中率更高。
关于版本升级:UE每年出一个大版本,升级时要注意API变化。用Deprecated标记的函数会在下一个版本移除,编译时会警告。建议在升级前先看官方的Upgrade Notes,列出所有破坏性变更。
关于性能预算:一个60帧的游戏,每帧16.6ms。Game Thread、Render Thread、GPU各占一部分。建议Game Thread控制在8ms以内,Render Thread控制在6ms以内,GPU控制在12ms以内。留出余量应对峰值。
关于内存预算:主机平台通常4-8GB可用,移动端1-2GB。纹理是内存大户,用压缩格式(BC/DXT)和Mipmap。音频用流式加载,不要全部常驻。
关于打包配置:Shipping配置会去掉所有调试代码和日志,性能最好。Development配置保留调试功能,用于测试。Debug配置最慢,只在排查底层问题时用。
关于自动化测试:UE支持Automation Test,可以写C++测试用例,在命令行运行。对于核心逻辑(比如伤害计算、背包系统),写自动化测试能省去大量手动回归的时间。
关于代码规范:UE有自己的代码规范(Epic C++ Coding Standard),变量前缀(b表示bool,U表示UObject,A表示Actor,F表示结构体)要遵守。团队协作时,统一规范比个人风格重要。
关于重构:UE的C++重构比蓝图容易,因为IDE支持重命名和查找引用。但改UCLASS的名字要小心,因为蓝图里引用了旧名字会丢失。改名前先在蓝图里搜索引用,或者用Core Redirects做重定向。
关于调试崩溃:UE崩溃时会生成CrashReport,包含调用栈。用Debug配置重现崩溃,在VS里附加到进程,看调用栈。常见的崩溃原因:空指针解引用、数组越界、GC回收了还在使用的对象。
关于多线程:UE的Task Graph系统可以并行执行任务。用AsyncTask或ParallelFor做并行计算。注意,UObject不是线程安全的,只能在Game Thread上操作。Render Thread和RHI Thread有各自的规则。
关于Shader编译:UE的Shader编译很慢,尤其是第一次打开项目。用ShaderCompileWorker可以并行编译。如果Shader编译卡住,检查DerivedDataCache是否损坏,删掉重建。
关于Lumen和Nanite:UE5的Lumen是全局光照,Nanite是虚拟几何体。它们对性能影响很大,移动端不支持。如果项目要上移动端,关掉这两个功能,用传统的光照和LOD。
关于World Partition:UE5的World Partition用于大世界,自动做流式加载。用Data Layer管理不同状态的世界内容(比如“白天”和“夜晚”)。这个系统比传统的Level Streaming更自动化,但学习成本也更高。
关于MetaSound:UE5的MetaSound是程序化音频系统,用节点图做音频逻辑。比传统的Sound Cue更灵活,支持实时参数控制。适合做动态音乐和交互音效。
关于Niagara:UE的粒子系统Niagara支持GPU模拟,性能比Cascade好。C++里可以自定义NiagaraDataInterface,把游戏数据传给粒子系统。这个方向比较深,但做高级特效时很有用。
关于Control Rig:UE的Control Rig用于程序化动画,可以在运行时用C++驱动骨骼。适合做IK、物理动画、程序化步态。和动画蓝图配合使用,效果更好。
关于Chaos Destruction:UE的破坏系统,支持实时碎裂。C++里可以控制碎裂参数和触发条件。性能开销较大,适合做关键场景的破坏效果。
关于Pixel Streaming:UE支持把渲染结果流式传输到浏览器。适合做云游戏和远程演示。C++里可以自定义信令和输入处理。
关于USD导入:UE支持USD格式的资产导入,适合和DCC工具(Maya、Blender)协作。C++里可以用USDStageActor做程序化导入。
关于Python脚本:UE集成了Python,可以用Python做编辑器自动化。比如批量导入资产、生成关卡、跑测试。C++里可以暴露接口给Python调用。
关于编辑器扩展:UE的编辑器支持C++扩展,可以自定义细节面板、资产编辑器、菜单项。用FPropertyEditorModule注册自定义面板,用FExtender扩展菜单。
关于插件打包:Plugin可以打包成独立的二进制,分发给其他团队。在.uplugin里配置PlatformAllowList,指定支持的平台。
关于DLC和补丁:UE支持Pak文件做DLC和补丁。用UnrealPak工具打包,运行时用FPakPlatformFile挂载。补丁要注意版本兼容,用Chunk系统管理。
关于反作弊:UE有EasyAntiCheat集成,但反作弊是攻防战,没有一劳永逸的方案。核心逻辑放服务器,客户端只做表现,能减少大部分作弊。
关于本地化:UE的本地化系统支持多语言。用FText而不是FString做UI文本,用LOCTEXT宏标记。翻译文件用Portable Object格式,和标准工具链兼容。
关于无障碍:UE支持字幕、色盲模式、按键重映射。这些功能在项目初期就要考虑,后期加会很麻烦。
关于存档:UE的SaveGame系统支持序列化。用USaveGame子类定义存档结构,用UGameplayStatics::SaveGameToSlot保存。注意存档版本兼容,加版本号做迁移。
关于成就和统计:UE支持平台成就系统。用OnlineSubsystem接口,不同平台实现不同。C++里用IOnlineAchievements接口。
关于多人游戏:UE的多人基于客户端-服务器模型。用Replication同步状态,用RPC做远程调用。GameMode只在服务器存在,GameState同步到所有客户端,PlayerState同步到所有客户端,PlayerController只属于本地玩家。
关于网络预测:客户端预测用于减少延迟感。用CharacterMovementComponent的内置预测,或者自己实现ServerMove和ClientAdjustPosition。这个方向比较复杂,建议先用引擎自带的。
关于专用服务器:UE支持编译专用服务器。用Server配置编译,没有渲染和音频。部署到云服务器,用Steam或EOS做匹配。
关于跨平台联机:不同平台的联机需要平台方的SDK支持。用OnlineSubsystem抽象层,不同平台用不同的实现。测试时要注意各平台的网络环境差异。
关于性能分析工具:除了Unreal Insights,还有RenderDoc(GPU调试)、PIX(Windows GPU)、Superluminal(CPU采样)。不同工具适合不同场景,组合使用。
关于代码审查:UE项目的代码审查要关注:UPROPERTY是否正确标记、是否有内存泄漏风险、是否线程安全、是否影响网络同步。这些是UE特有的检查点,普通C++审查容易漏掉。
关于技术债务:UE项目容易积累技术债务,因为蓝图改起来快,但重构难。建议定期做“蓝图转C++”的重构,把性能关键和逻辑复杂的部分移到C++。
关于团队协作:程序和策划的协作界面是蓝图和配置表。程序提供C++基类和蓝图可调参数,策划在蓝图里配置。这个分工要明确,否则程序会被策划的“改个数值”需求淹没。
关于版本管理:UE项目的版本管理要区分“代码”和“资产”。代码用Git,资产用Perforce或Git LFS。资产冲突很难合并,所以要用“锁定”机制,同一时间只有一个人改同一个资产。
关于持续集成:UE项目可以配CI,自动编译、跑测试、打包。用RunUAT命令行工具,配合Jenkins或GitHub Actions。CI能提前发现编译错误和测试失败,比人工检查可靠。
关于发布流程:发布前要跑Shipping配置的打包,测试所有平台。检查日志里有没有Warning和Error,检查性能是否达标,检查内存是否泄漏。发布后监控崩溃率和玩家反馈。
关于学习心态:UE的C++生态很庞大,不可能全部掌握。遇到问题先查文档和社区,再读源码。保持耐心,每个卡住的问题都是理解引擎的机会。我做了这么多年,还是经常遇到新问题,但排查速度比以前快多了,因为知道该去哪里找答案。