Loading...
Loading...
Use this skill when working with Actor and component design in Unreal Engine. Triggers on: Actor, component, BeginPlay, Tick, SpawnActor, lifecycle, CreateDefaultSubobject, composition, EndPlay, PostInitializeComponents, UActorComponent, USceneComponent, UINTERFACE, attachment, spawn, interface. See references/actor-lifecycle.md and references/component-types.md for detailed tables.
npx skill4agent add quodsoler/unreal-engine-skills ue-actor-component-architecture.agents/ue-project-context.mdAActorUActorComponentUObject
└── AActor (placeable/spawnable world entity)
└── owns N x UActorComponent (reusable behavior units)
└── USceneComponent (adds transform + attachment)
└── UPrimitiveComponent (adds collision + rendering)AActorUObjectnewdeleteSpawnActorDestroyreferences/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 logicGetWorld()nullptr// 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>(...);
}void AMyCharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
HealthComponent->OnDeath.AddDynamic(this, &AMyCharacter::HandleDeath);
}| Reason | When |
|---|---|
| |
| Map change |
| PIE session ended |
| Level streaming unloaded the sublevel |
| Application shutdown |
void AMyActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
GetWorld()->GetTimerManager().ClearAllTimersForObject(this);
Super::EndPlay(EndPlayReason);
}BeginPlayOnRep_PostNetReceive()bHasInitializedPostNetInitAActor| 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 |
UStaticMeshComponentUSkeletalMeshComponentUCapsuleComponentUBoxComponentUSphereComponentUWidgetComponent"UMG"USpringArmComponentUCameraComponentUChildActorComponentreferences/component-types.mdAMyActor::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
}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.NewObjectUPROPERTY// 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);// 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);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);BeginPlayAEnemy* 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
}SpawnActorDestroyAProjectile* 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;
}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
}TG_PrePhysicsTG_DuringPhysicsTG_PostPhysicsTG_PostUpdateWorkPrimaryComponentTick.bCanEverTick = truePrimaryComponentTick.TickGroup// ActorA ticks after ActorB completes
ActorA->AddTickPrerequisiteActor(ActorB);
ComponentA->AddTickPrerequisiteComponent(ComponentB);// 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 / OnActorBeginOverlapCast<ASpecificType>// 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);
};// AChest.h
UCLASS()
class AChest : public AActor, public IInteractable
{
GENERATED_BODY()
public:
virtual void OnInteract_Implementation(AActor* Instigator) override;
};// 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());
}// 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)// Query approach
UHealthComponent* Health = GetOwner()->FindComponentByClass<UHealthComponent>();
// Delegate approach — total decoupling
HealthComp->OnDeath.AddDynamic(AbilityComp, &UAbilityComponent::OnOwnerDied);// 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();
}
}// 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// 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);
}Super::// Always
void AMyActor::BeginPlay() { Super::BeginPlay(); ... }
void AMyActor::EndPlay(...) { ...; Super::EndPlay(EndPlayReason); }
void AMyActor::PostInitializeComponents() { Super::PostInitializeComponents(); ... }// 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(); }ue-cpp-foundationsue-gameplay-frameworkue-physics-collisionConstructor 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