什么是组件化
组件化是把具有明确状态、行为和生命周期的一项能力交给可挂在宿主 Actor 上的组件。
以门为例,若需求是“普通门、上锁门、计时门、可破坏门、需要钥匙的计时可破坏门”,靠子类逐项排列,很容易导致子类爆炸。因此可引入组件化思想:
ADoor表示世界中的门及其网格/动画ULockComponent管理锁的判定与状态UHealthComponent管理受伤与死亡
源码角度查看组件工作原理
1. 默认组件与运行时组件
UObject::CreateDefaultSubobject 是构造函数内声明默认子对象的入口。
Lock = CreateDefaultSubobject<ULockComponent>(TEXT("Lock"));
运行时动态添加与之不同:NewObject 先创建 UObject,之后还要考虑 Actor 的实例组件记录、注册、移除及网络。一个常见流程是:
// Owner 是已经存在于 World 的 Actor。
ULockComponent* Lock = NewObject<ULockComponent>(Owner);
Owner->AddInstanceComponent(Lock);
Lock->RegisterComponent();
// 不再需要时由拥有者调用 Lock->DestroyComponent(),并清理外部订阅。
AActor::AddInstanceComponent 设置 CreationMethod::Instance 并添加进 InstanceComponents;注册阶段会先检查Owner所在的 UWorld 是否有效,再调用 RegisterComponentWithWorld;RegisterComponentWithWorld检查对象有效、是否已经注册和 World 是否存在。
2. BeginPlay 的实际门槛
AActor::BeginPlay 遍历组件,只对 IsRegistered() 且尚未 HasBegunPlay() 的组件调用 BeginPlay,可见:
for (UActorComponent* Component : Components)
{
// bHasBegunPlay will be true for the component if the component was renamed and moved to a new outer during initialization
if (Component->IsRegistered() && !Component->HasBegunPlay())
{
Component->RegisterAllComponentTickFunctions(true);
Component->BeginPlay();
ensureMsgf(Component->HasBegunPlay(), TEXT("Failed to route BeginPlay (%s)"), *Component->GetFullName());
}
}
若 Actor 已开始 Play,后来注册的组件则走UActorComponent::RegisterComponentWithWorld-> AActor::HandleRegisterComponentWithWorld 的初始化与 BeginPlay 路径。
void AActor::HandleRegisterComponentWithWorld(UActorComponent* Component)
{
const bool bOwnerBeginPlayStarted = HasActorBegunPlay() || IsActorBeginningPlay();
if (!Component->HasBeenInitialized() && Component->bWantsInitializeComponent && IsActorInitialized())
{
Component->InitializeComponent();
// The component was finally initialized, it can now be replicated
// Note that if this component does not ask to be initialized, it would have started to be replicated inside AddOwnedComponent.
if (bOwnerBeginPlayStarted && Component->GetIsReplicated())
{
AddComponentForReplication(Component);
}
}
if (bOwnerBeginPlayStarted)
{
Component->RegisterAllComponentTickFunctions(true);
if (!Component->HasBegunPlay())
{
Component->BeginPlay();
ensureMsgf(Component->HasBegunPlay(), TEXT("Failed to route BeginPlay (%s)"), *Component->GetFullName());
}
}
}
3. 生命周期
Actor 会维护它所持有的组件。在 Actor 的 BeginDestroy 方法中,它会调用 UnregisterAllComponents。在这个方法里面,它会逐个遍历 Actor 所持有的这个组件,并调用每个组件的 UnregisterComponent ,这个方法会解除注册事件/Tick;。
void UActorComponent::UnregisterComponent()
{
SCOPE_CYCLE_COUNTER(STAT_UnregisterComponent);
FScopeCycleCounterUObject ComponentScope(this);
// Do nothing if not registered
if(!IsRegistered())
{
UE_LOG(LogActorComponent, Log, TEXT("UnregisterComponent: (%s) Not registered. Aborting."), *GetPathName());
return;
}
// If registered, should have a world
checkf(WorldPrivate != nullptr, TEXT("%s"), *GetFullName());
RegisterAllComponentTickFunctions(false);
ExecuteUnregisterEvents();
WorldPrivate = nullptr;
}
DestroyComponent 是结束该组件的语义入口。外部如果订阅了它的委托,仍应按业务协议解绑;GC 和组件注销不会替 UI 决定下一个数据源。
数据驱动:DataAsset 与类默认值各管什么
UDataAsset 是存放某系统数据的资产,可由原生派生类在内容浏览器创建。需要 AssetManager 主资产 ID、资产包或明确加载/卸载管理时,才进一步考虑 UPrimaryDataAsset。
这里补充一下二者区别:
UDataAsset用于存储配置数据;UPrimaryDataAsset在此基础上提供了接入UAssetManager主资产管理体系的默认实现。1. 主资产 ID(Primary Asset ID)
UPrimaryDataAsset默认提供FPrimaryAssetId,由资产类型(Type)和名称(Name)组成。例如
Equipment:DA_Sword,AssetManager 可以通过该 ID 查找和加载已注册的资产,而不需要调用方直接持有资产引用或路径。2. 资产包(Asset Bundle)
Asset Bundle 用于将主资产引用的资源按用途分组,实现按需加载。
UPROPERTY(EditDefaultsOnly, meta=(AssetBundles="UI")) TSoftObjectPtr<UTexture2D> Icon; UPROPERTY(EditDefaultsOnly, meta=(AssetBundles="Game")) TSoftObjectPtr<USkeletalMesh> WeaponMesh;例如打开背包时只加载
UI组的图标,实际装备时再加载Game组的模型,减少不必要的资源加载。3. 加载与卸载管理
AssetManager 提供统一的资源生命周期管理接口:
LoadPrimaryAsset():加载主资产及指定 Bundle。ChangeBundleStateForPrimaryAssets():调整已加载的资源组。UnloadPrimaryAsset():释放 AssetManager 对主资产的加载保持。注意:卸载不代表立即销毁资源。如果仍存在硬引用,资源可能继续驻留内存。
总结
UDataAsset:适合移动参数、攻击配置等普通静态数据。UPrimaryDataAsset:适合装备、物品、角色配置等需要按 ID 查找、分组加载或统一管理生命周期的资产。
类默认对象(CDO)是另一种配置来源。UClass::GetDefaultObject 返回类默认对象;蓝图子类可覆盖默认属性。
对于“每种门一个稳定的类及默认网格/组件结构”,蓝图子类默认值已经合适。对于“同一种门逻辑有许多可复用配置,并希望配置作为独立资产引用、审查或替换”,DataAsset 更合适。
UCLASS(BlueprintType)
class UDoorDefinition : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
float InteractionRange = 200.f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
bool bStartsLocked = false;
};
// ADoor 内:配置引用和每个门自己的运行状态分开。
UPROPERTY(EditDefaultsOnly, Category="Door")
TObjectPtr<UDoorDefinition> Definition;
UPROPERTY(VisibleInstanceOnly, Category="Door")
bool bIsOpen = false;
何时不适合组件化?
判断组件是否合适,可问四个问题:
- 组件对外能否用 2~5 个稳定操作解释?还是外部必须反复读写它的内部字段?
- 它能否独立维护一个不变量,如血量范围、背包容量、锁状态?
- 换一个宿主仍能成立吗?若到处
Cast<ADoor>(GetOwner()),很可能只是把门代码搬到了组件里。
职责分明的示例
flowchart LR
subgraph Character["① 角色层 · Character"]
direction TB
Player["玩家角色<br/>SwPlayerCharacter"]
Enemy["敌人角色<br/>SwEnemyCharacter"]
end
subgraph Components["② 组件层 · Components"]
direction TB
Attack["攻击组件<br/>AttackComponent"]
Combo["连招组件<br/>ComboComponent"]
HitReact["受击组件<br/>HitReactComponent"]
Dodge["闪避组件<br/>DodgeComponent"]
end
subgraph Systems["③ 系统层 · Gameplay"]
direction TB
GAS["技能系统<br/>AbilitySystemComponent"]
Event["事件分发<br/>Gameplay Events"]
end
subgraph Data["④ 配置层 · Data Assets"]
direction TB
AttackDA[("攻击配置<br/>AttackDataAsset")]
ComboDA[("连招配置<br/>ComboDataAsset")]
CharacterDA[("角色配置<br/>CharacterDataAsset")]
end
Player --> Attack
Player --> Combo
Player --> HitReact
Player --> Dodge
Enemy --> Attack
Enemy --> HitReact
Combo -->|"请求攻击"| Attack
Attack -->|"激活技能"| GAS
GAS -->|"发送事件"| Event
Event -.->|"受击事件"| HitReact
Attack -.->|"读取"| AttackDA
Combo -.->|"读取"| ComboDA
HitReact -.->|"读取"| CharacterDA
classDef character fill:#172554,stroke:#60A5FA,color:#EFF6FF,stroke-width:2px
classDef component fill:#0F766E,stroke:#5EEAD4,color:#FFFFFF,stroke-width:1.5px
classDef system fill:#581C87,stroke:#C084FC,color:#FFFFFF,stroke-width:1.5px
classDef data fill:#78350F,stroke:#FBBF24,color:#FFFFFF,stroke-width:1.5px
class Player,Enemy character
class Attack,Combo,HitReact,Dodge component
class GAS,Event system
class AttackDA,ComboDA,CharacterDA data
style Character fill:#EFF6FF,stroke:#93C5FD,color:#1E3A8A
style Components fill:#F0FDFA,stroke:#5EEAD4,color:#115E59
style Systems fill:#FAF5FF,stroke:#D8B4FE,color:#6B21A8
style Data fill:#FFFBEB,stroke:#FCD34D,color:#92400E