ue-actor-component-architecture
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseUE Actor-Component Architecture
UE Actor-Component 架构
You are an expert in Unreal Engine's Actor-Component architecture.
你是Unreal Engine的Actor-Component架构专家。
Project Context
项目上下文
Before responding, read for the project's subsystem inventory, coding conventions, and any existing actor hierarchies or component patterns. This tells you which base classes are established and what naming conventions apply.
.agents/ue-project-context.md在回复之前,请阅读,了解项目的子系统清单、编码规范以及任何现有的Actor层次结构或组件模式。这会告诉你已确立的基类以及适用的命名规范。
.agents/ue-project-context.mdInformation Gathering
信息收集
Clarify the developer's specific need before diving in:
- New actor from scratch, or adding behavior to an existing one?
- Logic-only (UActorComponent) or needs world position (USceneComponent)?
- Spawning requirement (deferred init, pooling, net-spawned)?
- Lifecycle bug (BeginPlay/Constructor confusion, component not initialized)?
- Cross-actor behavior via interfaces?
在深入之前,请明确开发者的具体需求:
- 是从头创建新Actor,还是为现有Actor添加行为?
- 仅需逻辑(UActorComponent)还是需要世界位置(USceneComponent)?
- 生成需求(延迟初始化、对象池、网络生成)?
- 生命周期问题(BeginPlay/构造函数混淆、组件未初始化)?
- 通过接口实现跨Actor行为?
Core Architecture Mental Model
核心架构思维模型
Unreal's Actor-Component system is composition over inheritance. An is a container that owns components. Behavior, rendering, collision, and logic are all expressed through subclasses.
AActorUActorComponentUObject
└── AActor (placeable/spawnable world entity)
└── owns N x UActorComponent (reusable behavior units)
└── USceneComponent (adds transform + attachment)
└── UPrimitiveComponent (adds collision + rendering)AActorUObjectnewdeleteSpawnActorDestroyUnreal的Actor-Component系统遵循组合优于继承的原则。是一个拥有组件的容器。行为、渲染、碰撞和逻辑都通过子类来实现。
AActorUActorComponentUObject
└── AActor (可放置/可生成的世界实体)
└── owns N x UActorComponent (可复用的行为单元)
└── USceneComponent (添加变换 + 附着功能)
└── UPrimitiveComponent (添加碰撞 + 渲染功能)AActorUObjectnewdeleteSpawnActorDestroyActor Lifecycle
Actor生命周期
Full event order and safety rules are in . Key sequence:
references/actor-lifecycle.mdConstructor → CreateDefaultSubobject, tick config, default values
PostActorCreated → spawned actors only; before construction script
PostInitializeComponents → all components initialized; world accessible
BeginPlay → game running; full logic OK; components BeginPlay fires here
Tick(DeltaTime) → per-frame; each ticking component's TickComponent fires
EndPlay(EEndPlayReason) → cleanup; ClearAllTimers; call Super
Destroyed → pre-GC; avoid complex logic完整的事件顺序和安全规则请参考。关键流程:
references/actor-lifecycle.mdConstructor → CreateDefaultSubobject、Tick配置、默认值设置
PostActorCreated → 仅针对生成的Actor;在构造脚本之前执行
PostInitializeComponents → 所有组件初始化完成;可访问世界
BeginPlay → 游戏运行中;可执行完整逻辑;组件的BeginPlay在此触发
Tick(DeltaTime) → 每帧执行;每个启用Tick的组件的TickComponent会触发
EndPlay(EEndPlayReason) → 清理操作;ClearAllTimers;调用Super
Destroyed → 垃圾回收前;避免复杂逻辑Constructor vs BeginPlay
Constructor vs BeginPlay
Constructor runs first on the Class Default Object (CDO) — an archetype used for default values. returns on the CDO. Never access the world or other actors in the constructor.
GetWorld()nullptrcpp
// CORRECT — constructor-time only
AMyActor::AMyActor()
{
MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
SetRootComponent(MeshComp);
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.TickInterval = 0.1f;
}
// CORRECT — world-dependent code belongs in BeginPlay
void AMyActor::BeginPlay()
{
Super::BeginPlay(); // Required — always call Super
GetWorld()->SpawnActor<AProjectile>(...);
}Constructor首先在**类默认对象(CDO)**上运行——CDO是用于设置默认值的原型。在CDO上调用会返回。永远不要在构造函数中访问世界或其他Actor。
GetWorld()nullptrcpp
// 正确——仅在构造函数中执行
AMyActor::AMyActor()
{
MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
SetRootComponent(MeshComp);
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.TickInterval = 0.1f;
}
// 正确——依赖世界的代码应放在BeginPlay中
void AMyActor::BeginPlay()
{
Super::BeginPlay(); // 必须调用——始终调用Super
GetWorld()->SpawnActor<AProjectile>(...);
}PostInitializeComponents
PostInitializeComponents
Called before BeginPlay; components are initialized; world exists. Use it to bind delegates to own components.
cpp
void AMyCharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
HealthComponent->OnDeath.AddDynamic(this, &AMyCharacter::HandleDeath);
}在BeginPlay之前调用;组件已初始化;世界已存在。可用于将委托绑定到自身组件。
cpp
void AMyCharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
HealthComponent->OnDeath.AddDynamic(this, &AMyCharacter::HandleDeath);
}EndPlay — reasons matter
EndPlay——原因很重要
| Reason | When |
|---|---|
| |
| Map change |
| PIE session ended |
| Level streaming unloaded the sublevel |
| Application shutdown |
cpp
void AMyActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
GetWorld()->GetTimerManager().ClearAllTimersForObject(this);
Super::EndPlay(EndPlayReason);
}| 原因 | 触发时机 |
|---|---|
| 显式调用 |
| 地图切换 |
| PIE会话结束 |
| 关卡流加载卸载子关卡 |
| 应用程序关闭 |
cpp
void AMyActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
GetWorld()->GetTimerManager().ClearAllTimersForObject(this);
Super::EndPlay(EndPlayReason);
}Network lifecycle note
网络生命周期注意事项
Replicated actors: on clients, may fire before all replicated properties arrive. Use callbacks for initialization that depends on replicated state. fires after each replication update (including the initial one); guard one-time setup inside it with a flag. is not a standard virtual and should not be used as a general init hook.
BeginPlayOnRep_PostNetReceive()bHasInitializedPostNetInitAActor可复制Actor:在客户端上,可能在所有复制属性到达之前触发。对于依赖复制状态的初始化,请使用回调。会在每次复制更新(包括初始更新)后触发;可使用标志在其中进行一次性设置。不是标准的虚函数,不应作为通用初始化钩子使用。
BeginPlayOnRep_PostNetReceive()bHasInitializedPostNetInitAActorComponent System
组件系统
The three layers
三层结构
| Class | Transform | Rendering/Collision | Use for |
|---|---|---|---|
| No | No | Pure logic — health, inventory, AI data |
| Yes | No | Transform anchors, grouping, pivot points |
| Yes | Yes | Meshes, shapes, anything visible or collidable |
Notable subclasses: , , shape primitives (, , ), (3D UI in world space — requires module), + , . See .
UStaticMeshComponentUSkeletalMeshComponentUCapsuleComponentUBoxComponentUSphereComponentUWidgetComponent"UMG"USpringArmComponentUCameraComponentUChildActorComponentreferences/component-types.md| 类 | 变换功能 | 渲染/碰撞功能 | 适用场景 |
|---|---|---|---|
| 无 | 无 | 纯逻辑——生命值、背包、AI数据 |
| 有 | 无 | 变换锚点、分组、枢轴点 |
| 有 | 有 | 网格体、形状、任何可见或可碰撞的对象 |
值得注意的子类:、、形状基元(、、)、(世界空间中的3D UI——需要模块)、 + 、。详情请参考。
UStaticMeshComponentUSkeletalMeshComponentUCapsuleComponentUBoxComponentUSphereComponentUWidgetComponent"UMG"USpringArmComponentUCameraComponentUChildActorComponentreferences/component-types.mdComponent creation
组件创建
In the constructor (for default components that appear in the Details panel):
cpp
AMyActor::AMyActor()
{
// CreateDefaultSubobject registers the component as a subobject —
// it is serialized with the actor and visible in Blueprint editors.
MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
SetRootComponent(MeshComp);
ArrowComp = CreateDefaultSubobject<UArrowComponent>(TEXT("Arrow"));
ArrowComp->SetupAttachment(MeshComp); // Parent set here; no world needed
HealthComp = CreateDefaultSubobject<UHealthComponent>(TEXT("Health"));
// Logic-only components need no attachment
}At runtime (dynamic addition):
cpp
void AMyActor::AddLight()
{
// NewObject creates but does NOT register with the world
UPointLightComponent* Light = NewObject<UPointLightComponent>(this,
UPointLightComponent::StaticClass(), TEXT("DynamicLight"));
Light->SetupAttachment(GetRootComponent());
Light->RegisterComponent(); // Gives it world presence (render proxy, physics)
Light->SetIntensity(5000.f);
}
void AMyActor::RemoveLight(UActorComponent* Comp)
{
Comp->DestroyComponent(); // Unregisters and marks for GC
}
// UnregisterComponent() removes a component from the world without destroying it (reversible).
// DestroyComponent() marks it for GC — irreversible. Use Unregister when you may re-enable it later.Why this distinction matters: constructor-created components are owned subobjects and participate in the actor's GC root. Runtime components via are not automatically serialized unless you add them to a array.
NewObjectUPROPERTY在构造函数中(用于在Details面板中显示的默认组件):
cpp
AMyActor::AMyActor()
{
// CreateDefaultSubobject会将组件注册为子对象——
// 它会随Actor一起序列化,并在蓝图编辑器中可见。
MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
SetRootComponent(MeshComp);
ArrowComp = CreateDefaultSubobject<UArrowComponent>(TEXT("Arrow"));
ArrowComp->SetupAttachment(MeshComp); // 在此设置父组件;无需访问世界
HealthComp = CreateDefaultSubobject<UHealthComponent>(TEXT("Health"));
// 纯逻辑组件无需附着
}在运行时(动态添加):
cpp
void AMyActor::AddLight()
{
// NewObject创建组件但不会向世界注册
UPointLightComponent* Light = NewObject<UPointLightComponent>(this,
UPointLightComponent::StaticClass(), TEXT("DynamicLight"));
Light->SetupAttachment(GetRootComponent());
Light->RegisterComponent(); // 使其在世界中生效(渲染代理、物理)
Light->SetIntensity(5000.f);
}
void AMyActor::RemoveLight(UActorComponent* Comp)
{
Comp->DestroyComponent(); // 注销并标记为待垃圾回收
}
// UnregisterComponent()会将组件从世界中移除但不销毁(可恢复)。
// DestroyComponent()会将其标记为待垃圾回收——不可恢复。当你之后可能重新启用组件时,请使用Unregister。为什么要区分这两种方式:构造函数创建的组件是拥有的子对象,属于Actor的垃圾回收根。通过创建的运行时组件不会自动序列化,除非你将它们添加到数组中。
NewObjectUPROPERTYAttachment
附着
cpp
// Constructor (SetupAttachment — no world required)
SpringArmComp->SetupAttachment(RootComponent);
CameraComp->SetupAttachment(SpringArmComp);
// Runtime (AttachToComponent — world must exist)
WeaponMesh->AttachToComponent(
CharMesh,
FAttachmentTransformRules::SnapToTargetNotIncludingScale,
TEXT("WeaponSocket") // Named socket on the skeletal mesh
);
WeaponMesh->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);cpp
// 构造函数中(SetupAttachment——无需访问世界)
SpringArmComp->SetupAttachment(RootComponent);
CameraComp->SetupAttachment(SpringArmComp);
// 运行时(AttachToComponent——必须存在世界)
WeaponMesh->AttachToComponent(
CharMesh,
FAttachmentTransformRules::SnapToTargetNotIncludingScale,
TEXT("WeaponSocket") // 骨骼网格体上的命名插槽
);
WeaponMesh->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);Activation
激活
cpp
// In constructor — opt out of auto-activation for optional components
SoundComp->bAutoActivate = false;
// Runtime — Activate() checks ShouldActivate() internally
SoundComp->Activate();
SoundComp->Deactivate();
SoundComp->SetActive(true, /*bReset=*/false);cpp
// 在构造函数中——为可选组件取消自动激活
SoundComp->bAutoActivate = false;
// 运行时——Activate()内部会检查ShouldActivate()
SoundComp->Activate();
SoundComp->Deactivate();
SoundComp->SetActive(true, /*bReset=*/false);Spawning
生成Actor
Standard spawn
标准生成
cpp
FActorSpawnParameters Params;
Params.Owner = this;
Params.Instigator = GetInstigator();
Params.SpawnCollisionHandlingOverride =
ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
Params.Name = FName("Enemy_Boss"); // deterministic name for replication (must be unique)
AEnemy* Enemy = GetWorld()->SpawnActor<AEnemy>(
AEnemy::StaticClass(), Location, Rotation, Params);cpp
FActorSpawnParameters Params;
Params.Owner = this;
Params.Instigator = GetInstigator();
Params.SpawnCollisionHandlingOverride =
ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
Params.Name = FName("Enemy_Boss"); // 用于复制的确定性名称(必须唯一)
AEnemy* Enemy = GetWorld()->SpawnActor<AEnemy>(
AEnemy::StaticClass(), Location, Rotation, Params);Deferred spawning — configure before BeginPlay
延迟生成——在BeginPlay前配置
Use when the actor's reads data that must be set before it runs.
BeginPlaycpp
AEnemy* Enemy = GetWorld()->SpawnActorDeferred<AEnemy>(
AEnemy::StaticClass(), SpawnTransform, Owner, Instigator,
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
if (Enemy)
{
Enemy->SetEnemyData(EnemyDataAsset); // Set BEFORE BeginPlay
Enemy->FinishSpawning(SpawnTransform);
// FinishSpawning triggers PostInitializeComponents then BeginPlay
}当Actor的会读取必须在其运行前设置的数据时,请使用此方式。
BeginPlaycpp
AEnemy* Enemy = GetWorld()->SpawnActorDeferred<AEnemy>(
AEnemy::StaticClass(), SpawnTransform, Owner, Instigator,
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
if (Enemy)
{
Enemy->SetEnemyData(EnemyDataAsset); // 在BeginPlay前设置
Enemy->FinishSpawning(SpawnTransform);
// FinishSpawning会触发PostInitializeComponents,然后是BeginPlay
}Object pooling
对象池
For high-frequency actors (projectiles, shell casings), repeated / creates GC pressure. Pool them: pre-spawn, hide + disable collision to "return," re-enable to "reuse."
SpawnActorDestroycpp
AProjectile* AProjectilePool::Get()
{
for (AProjectile* P : Pool)
{
// IsHidden() reflects the pool's "inactive" state set on return.
// IsActive() exists only on UActorComponent, not on AActor.
if (P->IsHidden())
{
P->SetActorHiddenInGame(false);
P->SetActorEnableCollision(true);
return P;
}
}
AProjectile* New = GetWorld()->SpawnActor<AProjectile>(ProjectileClass, ...);
Pool.Add(New);
return New;
}对于高频生成的Actor(投射物、弹壳),重复调用/会造成垃圾回收压力。请使用对象池:预先生成,隐藏并禁用碰撞来“回收”,重新启用以“复用”。
SpawnActorDestroycpp
AProjectile* AProjectilePool::Get()
{
for (AProjectile* P : Pool)
{
// IsHidden()反映了回收时设置的“非活跃”状态。
// IsActive()仅存在于UActorComponent,而非AActor。
if (P->IsHidden())
{
P->SetActorHiddenInGame(false);
P->SetActorEnableCollision(true);
return P;
}
}
AProjectile* New = GetWorld()->SpawnActor<AProjectile>(ProjectileClass, ...);
Pool.Add(New);
return New;
}Ticking
Tick机制
Setup
设置
cpp
AMyActor::AMyActor()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = false; // Enable in BeginPlay
PrimaryActorTick.TickInterval = 0.1f; // ~10 Hz throttle
PrimaryActorTick.TickGroup = TG_PostPhysics; // After physics settles
}Tick groups: (default, input/movement) → (physics-coupled logic, runs during physics step) → (camera, IK) → (final reads).
TG_PrePhysicsTG_DuringPhysicsTG_PostPhysicsTG_PostUpdateWorkComponent tick: Set in the component constructor, with for ordering — same API as actor tick.
PrimaryComponentTick.bCanEverTick = truePrimaryComponentTick.TickGroupcpp
AMyActor::AMyActor()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = false; // 在BeginPlay中启用
PrimaryActorTick.TickInterval = 0.1f; // ~10 Hz 节流
PrimaryActorTick.TickGroup = TG_PostPhysics; // 物理模拟完成后执行
}Tick组:(默认,输入/移动)→ (与物理耦合的逻辑,在物理步骤中运行)→ (相机、反向运动学)→ (最终读取)。
TG_PrePhysicsTG_DuringPhysicsTG_PostPhysicsTG_PostUpdateWork组件Tick:在组件构造函数中设置,并通过指定顺序——API与Actor Tick相同。
PrimaryComponentTick.bCanEverTick = truePrimaryComponentTick.TickGroupTick dependencies
Tick依赖
cpp
// ActorA ticks after ActorB completes
ActorA->AddTickPrerequisiteActor(ActorB);
ComponentA->AddTickPrerequisiteComponent(ComponentB);cpp
// ActorA在ActorB完成Tick后执行
ActorA->AddTickPrerequisiteActor(ActorB);
ComponentA->AddTickPrerequisiteComponent(ComponentB);When NOT to tick
何时不应使用Tick
Tick has per-frame cost even when nothing changes. Prefer:
cpp
// Delayed/repeating events → FTimerHandle
GetWorld()->GetTimerManager().SetTimer(TimerHandle, this,
&AMyActor::OnTimerFired, 2.0f, /*bLoop=*/true);
// State changes → delegates / multicast delegates
HealthComp->OnDeath.AddDynamic(this, &AMyActor::HandleDeath);
// Collision events → OnComponentBeginOverlap / OnActorBeginOverlapOnly tick for true per-frame needs: smooth interpolation, physics sub-stepping, streaming queries.
即使没有任何变化,Tick也会产生每帧开销。优先选择:
cpp
// 延迟/重复事件 → FTimerHandle
GetWorld()->GetTimerManager().SetTimer(TimerHandle, this,
&AMyActor::OnTimerFired, 2.0f, /*bLoop=*/true);
// 状态变化 → 委托/多播委托
HealthComp->OnDeath.AddDynamic(this, &AMyActor::HandleDeath);
// 碰撞事件 → OnComponentBeginOverlap / OnActorBeginOverlap仅在真正需要每帧执行的场景中使用Tick:平滑插值、物理子步进、流查询。
Interfaces (UINTERFACE Pattern)
接口(UINTERFACE模式)
Interfaces let unrelated actor types respond to the same message without coupling through inheritance. This replaces scattered across your codebase.
Cast<ASpecificType>接口允许不相关的Actor类型响应相同的消息,而无需通过继承耦合。这可以避免代码中到处使用。
Cast<ASpecificType>Declaration
声明
cpp
// IInteractable.h
UINTERFACE(MinimalAPI, Blueprintable)
class UInteractable : public UInterface { GENERATED_BODY() };
class MYGAME_API IInteractable
{
GENERATED_BODY()
public:
// BlueprintNativeEvent: C++ default + Blueprint can override
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Interaction")
void OnInteract(AActor* Instigator);
};cpp
// IInteractable.h
UINTERFACE(MinimalAPI, Blueprintable)
class UInteractable : public UInterface { GENERATED_BODY() };
class MYGAME_API IInteractable
{
GENERATED_BODY()
public:
// BlueprintNativeEvent: C++默认实现 + 蓝图可重写
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Interaction")
void OnInteract(AActor* Instigator);
};Implementation
实现
cpp
// AChest.h
UCLASS()
class AChest : public AActor, public IInteractable
{
GENERATED_BODY()
public:
virtual void OnInteract_Implementation(AActor* Instigator) override;
};cpp
// AChest.h
UCLASS()
class AChest : public AActor, public IInteractable
{
GENERATED_BODY()
public:
virtual void OnInteract_Implementation(AActor* Instigator) override;
};Calling through the interface
通过接口调用
cpp
// No cast needed — works on any actor or component
if (Target->Implements<UInteractable>())
{
// Execute_ prefix required for Blueprint-callable interface functions
IInteractable::Execute_OnInteract(Target, GetPawn());
}Interface vs component: use an interface for a capability declaration ("this can be interacted with") especially when Blueprint classes need to implement it. Use a component when the behavior has its own state, needs ticking, or is reused identically by many actor types.
cpp
// 无需转换——适用于任何Actor或组件
if (Target->Implements<UInteractable>())
{
// 可蓝图调用的接口函数需要加Execute_前缀
IInteractable::Execute_OnInteract(Target, GetPawn());
}接口vs组件:当需要声明一个能力(“此对象可交互”)时使用接口,尤其是当蓝图类需要实现该能力时。当行为有自己的状态、需要Tick或被许多Actor类型重复使用时,使用组件。
Composition Patterns
组合模式
Favor components over deep inheritance
优先使用组件而非深层继承
cpp
// Wrong: inheritance hierarchy collapses under varied requirements
ACharacter → AHero → ASwordHero → AFireSwordHero
// Right: flat base + composed components
ABaseCharacter
+ UHealthComponent (HP, damage, death event)
+ UInventoryComponent (items, equipment)
+ UAbilityComponent (skill execution)
+ UStatusComponent (buffs/debuffs)cpp
// 错误:继承层次结构在多样化需求下会崩溃
ACharacter → AHero → ASwordHero → AFireSwordHero
// 正确:扁平基类 + 组合组件
ABaseCharacter
+ UHealthComponent (生命值、伤害、死亡事件)
+ UInventoryComponent (物品、装备)
+ UAbilityComponent (技能执行)
+ UStatusComponent (增益/减益效果)Component-to-component communication
组件间通信
Components should not hold raw pointers to siblings. Query through the owner or use delegates:
cpp
// Query approach
UHealthComponent* Health = GetOwner()->FindComponentByClass<UHealthComponent>();
// Delegate approach — total decoupling
HealthComp->OnDeath.AddDynamic(AbilityComp, &UAbilityComponent::OnOwnerDied);组件不应持有兄弟组件的原始指针。请通过所有者查询或使用委托:
cpp
// 查询方式
UHealthComponent* Health = GetOwner()->FindComponentByClass<UHealthComponent>();
// 委托方式——完全解耦
HealthComp->OnDeath.AddDynamic(AbilityComp, &UAbilityComponent::OnOwnerDied);Data-driven composition
数据驱动的组合
cpp
// UEnemyData (UDataAsset) — varies per enemy type
// AEnemy reads configuration at BeginPlay or via SpawnActorDeferred
void AEnemy::Initialize(UEnemyData* Data)
{
HealthComp->SetMaxHealth(Data->MaxHealth);
for (TSubclassOf<UActorComponent> CompClass : Data->AdditionalComponents)
{
UActorComponent* Comp = NewObject<UActorComponent>(this, CompClass);
Comp->RegisterComponent();
}
}cpp
// UEnemyData (UDataAsset)——每个敌人类型各不相同
// AEnemy在BeginPlay或通过SpawnActorDeferred读取配置
void AEnemy::Initialize(UEnemyData* Data)
{
HealthComp->SetMaxHealth(Data->MaxHealth);
for (TSubclassOf<UActorComponent> CompClass : Data->AdditionalComponents)
{
UActorComponent* Comp = NewObject<UActorComponent>(this, CompClass);
Comp->RegisterComponent();
}
}Common Mistakes and Anti-Patterns
常见错误与反模式
Inheritance abuse
滥用继承
cpp
// Wrong — one class per variant
UCLASS() class AFireEnemy : public AEnemy { };
UCLASS() class AIceEnemy : public AEnemy { };
// Right — one class, multiple DataAssets
// UEnemyData_Fire.uasset, UEnemyData_Ice.uasset → AEnemy reads at BeginPlaycpp
// 错误——每个变体对应一个类
UCLASS() class AFireEnemy : public AEnemy { };
UCLASS() class AIceEnemy : public AEnemy { };
// 正确——一个类,多个DataAsset
// UEnemyData_Fire.uasset, UEnemyData_Ice.uasset → AEnemy在BeginPlay时读取Tick polling instead of events
使用Tick轮询而非事件
cpp
// Wrong — checked every frame
void AMyActor::Tick(float DeltaTime)
{
if (HealthComp->IsDead()) { HandleDeath(); }
}
// Right — event-driven, zero per-frame cost
void AMyActor::BeginPlay()
{
Super::BeginPlay();
HealthComp->OnDeath.AddDynamic(this, &AMyActor::HandleDeath);
SetActorTickEnabled(false);
}cpp
// 错误——每帧检查
void AMyActor::Tick(float DeltaTime)
{
if (HealthComp->IsDead()) { HandleDeath(); }
}
// 正确——事件驱动,无每帧开销
void AMyActor::BeginPlay()
{
Super::BeginPlay();
HealthComp->OnDeath.AddDynamic(this, &AMyActor::HandleDeath);
SetActorTickEnabled(false);
}Forgetting Super in lifecycle overrides
在生命周期重写中忘记调用Super
Every lifecycle override must call . Skipping it breaks replication, GC, and Blueprint event forwarding.
Super::cpp
// Always
void AMyActor::BeginPlay() { Super::BeginPlay(); ... }
void AMyActor::EndPlay(...) { ...; Super::EndPlay(EndPlayReason); }
void AMyActor::PostInitializeComponents() { Super::PostInitializeComponents(); ... }每个生命周期重写都必须调用。跳过它会破坏复制、垃圾回收和蓝图事件转发。
Super::cpp
// 务必调用
void AMyActor::BeginPlay() { Super::BeginPlay(); ... }
void AMyActor::EndPlay(...) { ...; Super::EndPlay(EndPlayReason); }
void AMyActor::PostInitializeComponents() { Super::PostInitializeComponents(); ... }Storing raw actor pointers
存储原始Actor指针
cpp
// Wrong — crashes when the actor is destroyed
AActor* CachedTarget;
// Right — use TWeakObjectPtr and check IsValid before use
TWeakObjectPtr<AActor> CachedTarget;
if (CachedTarget.IsValid()) { CachedTarget->DoSomething(); }cpp
// 错误——Actor销毁时会崩溃
AActor* CachedTarget;
// 正确——使用TWeakObjectPtr,使用前检查IsValid
TWeakObjectPtr<AActor> CachedTarget;
if (CachedTarget.IsValid()) { CachedTarget->DoSomething(); }Related Skills
相关技能
- — UCLASS, UPROPERTY, UFUNCTION macros underpinning all patterns above
ue-cpp-foundations - — GameMode, PlayerController, Pawn layered on top of this system
ue-gameplay-framework - — UPrimitiveComponent channels, sweeps, overlap events
ue-physics-collision
- —— 支撑上述所有模式的UCLASS、UPROPERTY、UFUNCTION宏
ue-cpp-foundations - —— 构建在此系统之上的GameMode、PlayerController、Pawn
ue-gameplay-framework - —— UPrimitiveComponent通道、扫描、重叠事件
ue-physics-collision
Quick Reference
快速参考
Constructor CreateDefaultSubobject, SetRootComponent, tick config
PostInitialize Bind delegates to own components; world accessible
BeginPlay Full game logic; SpawnActor; timer setup
Tick Per-frame only; prefer timers/events
EndPlay ClearAllTimers; Super required
Destroyed Pre-GC; minimal logic
CreateDefaultSubobject<T>() Constructor — owned, serialized, editable
NewObject<T>() + RegisterComponent() Runtime — dynamic, not auto-serialized
SetupAttachment() Constructor parent declaration
AttachToComponent() Runtime attachment with transform rules
SpawnActor<T>() Standard spawn
SpawnActorDeferred<T>() + Finish Configure before BeginPlay firesConstructor CreateDefaultSubobject、SetRootComponent、Tick配置
PostInitialize 将委托绑定到自身组件;可访问世界
BeginPlay 完整游戏逻辑;SpawnActor;定时器设置
Tick 仅用于每帧需求;优先使用定时器/事件
EndPlay ClearAllTimers;必须调用Super
Destroyed 垃圾回收前;最小化逻辑
CreateDefaultSubobject<T>() 构造函数中使用——拥有、序列化、可编辑
NewObject<T>() + RegisterComponent() 运行时使用——动态、不自动序列化
SetupAttachment() 构造函数中声明父组件
AttachToComponent() 运行时使用变换规则附着
SpawnActor<T>() 标准生成
SpawnActorDeferred<T>() + Finish 在BeginPlay触发前配置