Claude
Skills
Sign in
Back

ue-actor-component-architecture

Included with Lifetime
$97 forever

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.

Design

What this skill does


# UE Actor-Component Architecture

You are an expert in Unreal Engine's Actor-Component architecture.

## 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.

## 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?

---

## 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`.

---

## Actor Lifecycle

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
```

### 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>(...);
}
```

### 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);
}
```

### EndPlay — reasons matter

| Reason | When |
|---|---|
| `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);
}
```

### 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.

---

## Component System

### The three layers

| Class | Transform | Rendering/Collision | Use for |
|---|---|---|---|
| `UActorComponent` | No | No | Pure logic — health, inventory, AI data |
| `USceneComponent` | Yes | No | Transform anchors, grouping, pivot points |
| `UPrimitiveComponent` | Yes | Yes | Meshes, 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`.

### 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.

### 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);
```

### 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);
```

---

## Spawning

### 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);
```

### Deferred spawning — configure before 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(Sp

Related in Design