ue-actor-component-architecture

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

UE 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
.agents/ue-project-context.md
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层次结构或组件模式。这会告诉你已确立的基类以及适用的命名规范。

Information 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
AActor
is a container that owns components. Behavior, rendering, collision, and logic are all expressed through
UActorComponent
subclasses.
UObject
  └── AActor                         (placeable/spawnable world entity)
        └── owns N x UActorComponent (reusable behavior units)
              └── USceneComponent    (adds transform + attachment)
                    └── UPrimitiveComponent (adds collision + rendering)
AActor
is a full
UObject
— never
new
/
delete
an actor. Always use
SpawnActor
and
Destroy
.

Unreal的Actor-Component系统遵循组合优于继承的原则。
AActor
是一个拥有组件的容器。行为、渲染、碰撞和逻辑都通过
UActorComponent
子类来实现。
UObject
  └── AActor                         (可放置/可生成的世界实体)
        └── owns N x UActorComponent (可复用的行为单元)
              └── USceneComponent    (添加变换 + 附着功能)
                    └── UPrimitiveComponent (添加碰撞 + 渲染功能)
AActor
是完整的
UObject
——永远不要用
new
/
delete
来创建或销毁Actor。请始终使用
SpawnActor
Destroy

Actor Lifecycle

Actor生命周期

Full event order and safety rules are in
references/actor-lifecycle.md
. Key sequence:
Constructor                  → 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.md
。关键流程:
Constructor                  → 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.
GetWorld()
returns
nullptr
on the CDO. Never access the world or other actors in the constructor.
cpp
// 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上调用
GetWorld()
会返回
nullptr
。永远不要在构造函数中访问世界或其他Actor。
cpp
// 正确——仅在构造函数中执行
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——原因很重要

ReasonWhen
Destroyed
Actor->Destroy()
called explicitly
LevelTransition
Map change
EndPlayInEditor
PIE session ended
RemovedFromWorld
Level streaming unloaded the sublevel
Quit
Application shutdown
cpp
void AMyActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    GetWorld()->GetTimerManager().ClearAllTimersForObject(this);
    Super::EndPlay(EndPlayReason);
}
原因触发时机
Destroyed
显式调用
Actor->Destroy()
LevelTransition
地图切换
EndPlayInEditor
PIE会话结束
RemovedFromWorld
关卡流加载卸载子关卡
Quit
应用程序关闭
cpp
void AMyActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    GetWorld()->GetTimerManager().ClearAllTimersForObject(this);
    Super::EndPlay(EndPlayReason);
}

Network lifecycle note

网络生命周期注意事项

Replicated actors: on clients,
BeginPlay
may fire before all replicated properties arrive. Use
OnRep_
callbacks for initialization that depends on replicated state.
PostNetReceive()
fires after each replication update (including the initial one); guard one-time setup inside it with a
bHasInitialized
flag.
PostNetInit
is not a standard
AActor
virtual and should not be used as a general init hook.

可复制Actor:在客户端上,
BeginPlay
可能在所有复制属性到达之前触发。对于依赖复制状态的初始化,请使用
OnRep_
回调。
PostNetReceive()
会在每次复制更新(包括初始更新)后触发;可使用
bHasInitialized
标志在其中进行一次性设置。
PostNetInit
不是标准的
AActor
虚函数,不应作为通用初始化钩子使用。

Component System

组件系统

The three layers

三层结构

ClassTransformRendering/CollisionUse for
UActorComponent
NoNoPure logic — health, inventory, AI data
USceneComponent
YesNoTransform anchors, grouping, pivot points
UPrimitiveComponent
YesYesMeshes, shapes, anything visible or collidable
Notable subclasses:
UStaticMeshComponent
,
USkeletalMeshComponent
, shape primitives (
UCapsuleComponent
,
UBoxComponent
,
USphereComponent
),
UWidgetComponent
(3D UI in world space — requires
"UMG"
module),
USpringArmComponent
+
UCameraComponent
,
UChildActorComponent
. See
references/component-types.md
.
变换功能渲染/碰撞功能适用场景
UActorComponent
纯逻辑——生命值、背包、AI数据
USceneComponent
变换锚点、分组、枢轴点
UPrimitiveComponent
网格体、形状、任何可见或可碰撞的对象
值得注意的子类
UStaticMeshComponent
USkeletalMeshComponent
、形状基元(
UCapsuleComponent
UBoxComponent
USphereComponent
)、
UWidgetComponent
(世界空间中的3D UI——需要
"UMG"
模块)、
USpringArmComponent
+
UCameraComponent
UChildActorComponent
。详情请参考
references/component-types.md

Component 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
NewObject
are not automatically serialized unless you add them to a
UPROPERTY
array.
在构造函数中(用于在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的垃圾回收根。通过
NewObject
创建的运行时组件不会自动序列化,除非你将它们添加到
UPROPERTY
数组中。

Attachment

附着

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
BeginPlay
reads data that must be set before it runs.
cpp
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的
BeginPlay
会读取必须在其运行前设置的数据时,请使用此方式。
cpp
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
SpawnActor
/
Destroy
creates GC pressure. Pool them: pre-spawn, hide + disable collision to "return," re-enable to "reuse."
cpp
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(投射物、弹壳),重复调用
SpawnActor
/
Destroy
会造成垃圾回收压力。请使用对象池:预先生成,隐藏并禁用碰撞来“回收”,重新启用以“复用”。
cpp
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:
TG_PrePhysics
(default, input/movement) →
TG_DuringPhysics
(physics-coupled logic, runs during physics step) →
TG_PostPhysics
(camera, IK) →
TG_PostUpdateWork
(final reads).
Component tick: Set
PrimaryComponentTick.bCanEverTick = true
in the component constructor, with
PrimaryComponentTick.TickGroup
for ordering — same API as actor tick.
cpp
AMyActor::AMyActor()
{
    PrimaryActorTick.bCanEverTick = true;
    PrimaryActorTick.bStartWithTickEnabled = false; // 在BeginPlay中启用
    PrimaryActorTick.TickInterval = 0.1f;           // ~10 Hz 节流
    PrimaryActorTick.TickGroup = TG_PostPhysics;    // 物理模拟完成后执行
}
Tick组:
TG_PrePhysics
(默认,输入/移动)→
TG_DuringPhysics
(与物理耦合的逻辑,在物理步骤中运行)→
TG_PostPhysics
(相机、反向运动学)→
TG_PostUpdateWork
(最终读取)。
组件Tick:在组件构造函数中设置
PrimaryComponentTick.bCanEverTick = true
,并通过
PrimaryComponentTick.TickGroup
指定顺序——API与Actor Tick相同。

Tick 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 / OnActorBeginOverlap
Only 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
Cast<ASpecificType>
scattered across your codebase.
接口允许不相关的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 BeginPlay
cpp
// 错误——每个变体对应一个类
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
Super::
. Skipping it breaks replication, GC, and Blueprint event forwarding.
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

相关技能

  • ue-cpp-foundations
    — UCLASS, UPROPERTY, UFUNCTION macros underpinning all patterns above
  • ue-gameplay-framework
    — GameMode, PlayerController, Pawn layered on top of this system
  • ue-physics-collision
    — UPrimitiveComponent channels, sweeps, overlap events

  • ue-cpp-foundations
    —— 支撑上述所有模式的UCLASS、UPROPERTY、UFUNCTION宏
  • ue-gameplay-framework
    —— 构建在此系统之上的GameMode、PlayerController、Pawn
  • ue-physics-collision
    —— UPrimitiveComponent通道、扫描、重叠事件

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 fires
Constructor          CreateDefaultSubobject、SetRootComponent、Tick配置
PostInitialize       将委托绑定到自身组件;可访问世界
BeginPlay            完整游戏逻辑;SpawnActor;定时器设置
Tick                 仅用于每帧需求;优先使用定时器/事件
EndPlay              ClearAllTimers;必须调用Super
Destroyed            垃圾回收前;最小化逻辑

CreateDefaultSubobject<T>()          构造函数中使用——拥有、序列化、可编辑
NewObject<T>() + RegisterComponent() 运行时使用——动态、不自动序列化
SetupAttachment()                    构造函数中声明父组件
AttachToComponent()                  运行时使用变换规则附着

SpawnActor<T>()                      标准生成
SpawnActorDeferred<T>() + Finish     在BeginPlay触发前配置