UE 的组件系统是"组合优于继承"原则在引擎层的落地:Actor 只负责生命周期和变换,具体功能(渲染、物理、移动、AI)全部由组件实现。一个 Character 类不需要知道怎么渲染网格、怎么模拟物理——它只要挂一个 USkeletalMeshComponent 和一个 UCharacterMovementComponent,功能就齐了。下面基于 UE5.3(Engine/Source/Runtime/Engine/Private/ActorComponent.cpp)把组件系统的核心机制拆开。
组件的三种创建方式
组件挂在 Actor 上的路径有三条,生命周期和序列化行为完全不同:
1. CreateDefaultSubobject(构造函数)
AMyActor::AMyActor()
{
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
RootComponent = Mesh;
}
特点:
- 在 CDO 构造时执行,创建的是"模板"。
- 每个实例 NewObject 时通过
InstanceObjectGraph拿到独立副本。 - 组件作为 Actor 的子对象序列化,编辑器里可以在 Details 面板改属性。
- 组件名固定,实例间共享名字。
2. AddComponent(运行时动态添加)
UMyComponent* NewComp = NewObject<UMyComponent>(this);
NewComp->RegisterComponent();
特点:
- 运行时创建,不进 CDO,实例间独立。
- 需要手动
RegisterComponent才会进入世界。 - 不序列化(除非手动处理),Actor 保存到关卡时动态组件丢失。
3. 蓝图 SCS(SimpleConstructionScript)
蓝图里"添加组件"按钮生成的组件,存在 UBlueprint::SimpleConstructionScript 里。Actor 构造时 SCS 执行,等价于自动化的 CreateDefaultSubobject。特点是支持继承覆盖——子蓝图可以改父蓝图组件的属性。
注册:组件进入世界的唯一入口
组件的"激活"不是创建时发生的,而是 RegisterComponent() 调用时。注册流程(UActorComponent::RegisterComponent):
- 检查
bRegistered,已注册则跳过。 - 调
OnRegister():虚函数钩子,组件在这里创建渲染状态(FSceneProxy)、物理状态(FPhysScene里的 body instance)。 - 把组件加入 World 的组件列表,开始接收 Tick。
- 如果组件是
USceneComponent,建立 Attachment 关系(attach 到父组件)。
UnregisterComponent() 是逆过程:销毁渲染/物理状态,从 World 移除,停止 Tick。
为什么分离"创建"和"注册"?因为组件创建时 World 可能还没准备好(比如 Actor 构造函数里 World 是空的),或者组件需要先配置再进入世界。CreateDefaultSubobject 只创建对象,注册由 Actor 的 RegisterAllComponents 统一触发——这保证了所有组件在同一时机进入世界,避免"组件 A 注册时组件 B 还没创建"的时序问题。
Tick 集成:FTickFunction 的分组
组件的 Tick 不是独立调度的,而是挂在 Actor 的 Tick 上。机制:
- 每个组件有一个
FActorComponentTickFunction(PrimaryComponentTick)。 RegisterComponent时,组件的 TickFunction 注册到 Actor 的 Tick 列表。- Actor Tick 时,按 TickGroup 顺序遍历所有组件的 TickFunction 执行。
TickGroup 的层级(Engine/Classes/Engine/EngineBaseTypes.h 的 ETickingGroup):
TG_PrePhysics → 物理模拟前
TG_StartPhysics → 物理开始
TG_DuringPhysics → 物理进行中(并行)
TG_EndPhysics → 物理结束
TG_PostPhysics → 物理后(动画、相机)
TG_PostUpdateWork → 渲染数据提交前
TG_LastDemotable → 最后
组件可以设 PrimaryComponentTick.TickGroup 控制执行时机。比如相机组件用 TG_PostPhysics,保证在物理模拟完成后才更新位置,避免"相机跟着物理抖动"。
Tick 依赖:组件可以声明"我必须在组件 X 之后 Tick"(AddTickPrerequisiteComponent)。这在 MovementComponent 和 MeshComponent 之间常见——Movement 先算出新位置,Mesh 再按新位置更新渲染状态。
USceneComponent 的 Attachment 树
USceneComponent 引入了变换层级:组件可以 Attach 到另一个组件,形成树。每个节点存相对父节点的变换(RelativeLocation/Rotation/Scale),世界变换由树向上递归计算。
Attachment 的维护在 OnRegister 时完成:USceneComponent::OnRegister 会调 UpdateComponentToWorld,把当前变换提交给渲染线程。父组件移动时,子组件的世界变换自动更新——这是通过 FTransform 的缓存和脏标记实现的,不是每帧全量重算。
Detach 的代价要注意:DetachFromComponent 会触发子树的变换重算,如果子树很深(几十层),这步不便宜。高频 Attach/Detach 的场景(比如武器切换)应该考虑用 Socket 或手动变换同步替代。
组合优于继承的具体体现
组件系统让 UE 的类继承树保持浅而宽:
APawn只有"被 Controller 控制"的能力,移动、渲染、碰撞全由组件提供。ACharacter= APawn +UCharacterMovementComponent+UCapsuleComponent+USkeletalMeshComponent。- 自定义功能优先写组件而不是继承 Actor——比如"可交互"功能写成
UInteractableComponent,任何 Actor 挂上就能被交互,不需要继承自某个AInteractableActor。
这个设计的收益在大型项目里很明显:功能可以跨 Actor 类型复用,不需要为"会飞的敌人"和"会游泳的敌人"分别继承出 AFlyingEnemy 和 ASwimmingEnemy——只要给同一个 AEnemy 挂不同的 MovementComponent。
实战结论
- 组件创建优先用
CreateDefaultSubobject,动态添加是特例不是常态。 - 组件的
OnRegister里做资源创建,OnUnregister里做资源销毁——成对出现,避免泄漏。 - TickGroup 选错会导致"物理抖动"或"渲染延迟",相机和动画用
TG_PostPhysics。 - 深度嵌套的 Attachment 树有变换重算开销,UI 或高频更新的组件尽量扁平。
- 动态添加的组件不参与序列化,存档时需要手动处理状态恢复。