본문 바로가기

액터 생성과 관리

SpawnActor로 액터를 동적 생성하고 태그·클래스로 탐색한 뒤 참조와 파괴 시점을 안전하게 관리합니다.

총을 쏘면 총알이 생기고, 적이 쓰러지면 아이템이 떨어지고, 아이템을 주우면 그 아이템이 사라집니다. 게임 속 대상의 상당수는 레벨에 미리 놓여 있지 않고 실행 중에 생겼다가 사라지며, 이런 대상은 대부분 액터(Actor)로 만듭니다. 액터를 만드는 일 자체는 함수 하나로 끝나지만, 다른 액터가 그 액터를 찾아 쓰고, 그 액터가 사라진 뒤에도 남은 참조를 안전하게 다루는 일은 따로 신경 써야 합니다.

Level Blueprint vs C++ 스크립트에서 로직을 어디에 둘지 정했다면, 이 글에서는 C++에서 액터를 실행 중에 생성하고, 찾고, 참조하고, 파괴하는 방법을 다룹니다. 월드가 커지고 액터가 많아질수록 이 관리 방식이 게임의 성능과 안정성에 직접 영향을 주므로, 각 단계에서 무엇이 보장되고 무엇이 보장되지 않는지도 함께 살펴봅니다.


액터를 월드에 추가하는 두 가지 방법

가장 기본적인 방법은 에디터에서 직접 배치하는 것입니다. 콘텐츠 브라우저에서 액터 블루프린트나 C++ 액터 클래스를 바탕으로 만든 블루프린트를 뷰포트로 끌어다 놓습니다. 배치한 상태로 레벨을 저장하면 레벨을 불러올 때 그 액터가 준비됩니다. 다만 이것이 게임 중의 파괴나 스트리밍 언로드까지 막아 영원히 살아 있게 해 주는 것은 아닙니다.

다른 방법은 게임 도중 특정 이벤트에 맞춰 액터를 동적으로 생성하는 것입니다. 플레이어가 총을 쏘면 총알을 만들고, 적이 죽으면 아이템을 만들고, 스폰 지점에서 플레이어 캐릭터를 만드는 경우가 여기에 해당합니다. 액터를 실행 중에 만드는 것을 스폰(spawn)이라고 하며, C++에서는 주로 UWorld::SpawnActor()를 씁니다.

SpawnActor로 생성하기

아래 코드는 헤더의 UCLASS 본문에 둘 멤버 선언과 .cpp의 함수 정의를 함께 보여 주는 발췌입니다. ActorToSpawnClass에는 실제로 만들 클래스를 지정하고, SpawnMyActor의 선언도 헤더에 둡니다.

헤더 멤버와 MyGameModeBase.cpp 발췌
#include "Engine/World.h"
#include "Kismet/GameplayStatics.h" // UGameplayStatics::GetPlayerCharacter 등을 위해

// 생성할 액터 클래스 (블루프린트 또는 C++ 클래스)를 UPROPERTY로 노출
// 블루프린트에서 설정할 수 있도록 EditDefaultsOnly 또는 EditAnywhere 사용
UPROPERTY(EditDefaultsOnly, Category = "Spawning")
TSubclassOf<AActor> ActorToSpawnClass; // AActor를 상속받는 모든 클래스 타입 지정

void AMyGameModeBase::SpawnMyActor()
{
    // 월드 포인터는 GetWorld() 함수로 얻을 수 있습니다.
    UWorld* World = GetWorld();
    if (!World)
    {
        UE_LOG(LogTemp, Error, TEXT("World is null. Cannot spawn actor."));
        return;
    }

    if (!ActorToSpawnClass)
    {
        UE_LOG(LogTemp, Error, TEXT("ActorToSpawnClass is not set."));
        return;
    }

    // 액터를 생성할 위치와 회전 정보를 정의
    FVector SpawnLocation = FVector(0.0f, 0.0f, 100.0f); // 월드 원점에서 Z축으로 100cm 위
    FRotator SpawnRotation = FRotator::ZeroRotator;     // 회전 없음

    // 액터 생성 파라미터 구조체 설정
    FActorSpawnParameters SpawnParams;
    SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
    // 액터 생성 시 콜리전 처리 방식 (예: 충돌 발생 시 위치 조정 후 생성, 또는 항상 생성 등)
    SpawnParams.Owner = this; // 네트워크 소유권 등의 Owner 관계; GC 수명 소유와 다름
    SpawnParams.Instigator = GetInstigator(); // 액터의 인스티게이터 설정 (예: 데미지를 준 주체)

    // 액터 생성!
    AActor* SpawnedActor = World->SpawnActor<AActor>(ActorToSpawnClass, SpawnLocation, SpawnRotation, SpawnParams);

    if (SpawnedActor)
    {
        UE_LOG(LogTemp, Warning, TEXT("Actor %s spawned successfully at %s"), *SpawnedActor->GetName(), *SpawnedActor->GetActorLocation().ToString());
    }
}

이 발췌를 따라 할 때는 먼저 프로젝트의 실제 게임 모드 클래스에 ActorToSpawnClass 멤버와 SpawnMyActor() 선언을 넣고, 위 함수를 같은 클래스의 .cpp 정의로 연결합니다. AMyGameModeBase나 MYPROJECT_API 같은 이름은 실제 프로젝트 이름에 맞춥니다. 그다음 기본 게임 모드나 맵의 게임 모드 설정이 이 클래스를 쓰게 하고, 그 블루프린트의 기본값에 만들 액터 클래스를 지정한 뒤, BeginPlay처럼 정한 이벤트에서 한 번 호출합니다. 게임 모드를 쓰는 이 예는 독립 실행이나 서버 쪽 게임플레이를 전제로 하며, 네트워크 클라이언트에도 같은 게임 모드 인스턴스가 있다고 가정하지 않습니다.

코드에 쓰인 타입과 함수를 하나씩 보겠습니다. TSubclassOf<AActor>는 언리얼 클래스(UClass*)를 가리키는 템플릿 타입으로, 그 클래스가 지정한 부모, 여기서는 AActor를 상속해야 한다는 조건을 겁니다. UPROPERTY로 에디터에 공개하면 드롭다운 목록에서 원하는 블루프린트 클래스를 고를 수 있습니다. FActorSpawnParameters는 생성할 때의 추가 옵션을 담는 구조체이고, 그중 SpawnCollisionHandlingOverride는 생성할 위치에 다른 물체가 있을 때 어떻게 처리할지를 정합니다. World->SpawnActor<T>()가 실제로 액터를 만드는 함수이며, T는 만들 액터의 C++ 타입입니다.

요청 위치의 Z=100은 월드 원점에서 100cm 위를 뜻합니다. 이 예제의 충돌 옵션 AdjustIfPossibleButAlwaysSpawn은 겹침을 피하도록 위치를 조정할 수 있으면 조정하고, 조정하지 못해도 생성하라는 선택입니다. 그래서 생성에 성공했더라도 최종 위치와 충돌 상태는 다시 확인해야 합니다. 반대로 반환값이 nullptr이면 뒤의 성공 로그가 나오지 않습니다. 결과가 예상과 다를 때는 원점 근처에 메시가 보이는지, 생성 함수의 반환값이 무엇인지, 실제로 쓰인 게임 모드와 클래스 기본값이 무엇인지를 따로따로 확인합니다.

더 깊이: BeginPlay 전에 값을 넣는 지연 스폰

SpawnActor 한 번의 호출 안에서 엔진은 여러 단계를 차례로 거칩니다. 액터가 월드에 만들어지면 PostActorCreated가 불리고, 이어 ExecuteConstruction과 OnConstruction에서 블루프린트 액터의 컴포넌트가 만들어집니다. 그다음 PostActorConstruction이 PreInitializeComponents, 컴포넌트마다의 InitializeComponent, PostInitializeComponents를 부르고, 월드가 OnActorSpawned를 알린 뒤 마지막으로 BeginPlay가 실행됩니다. 그래서 게임이 진행 중인 월드에서 SpawnActor가 돌려준 포인터로 값을 넣으면, 그 값은 구성 스크립트와 BeginPlay가 이미 실행된 뒤에 들어갑니다.

팀 번호나 초기 탄약처럼 구성 스크립트나 BeginPlay가 읽기 전에 정해져 있어야 하는 값이 있다면 UWorld::SpawnActorDeferred를 씁니다. 지연 스폰은 PostActorCreated까지만 진행하고 멈추므로, 유효하지만 아직 완성되지 않은 액터에 필요한 값을 먼저 넣을 수 있습니다. 값을 모두 넣은 뒤 FinishSpawning을 부르면 ExecuteConstruction부터 나머지 단계가 이어집니다. 블루프린트의 변수에 Expose on Spawn을 켜 스폰 노드의 입력 핀으로 값을 넘길 때도 액터는 이 지연 스폰 경로로 만들어집니다. 각 단계는 Actor Lifecycle 문서에 정리되어 있습니다.


월드에서 액터 찾기

이미 월드에 있는 액터를 찾거나 다른 액터에게서 참조를 얻는 방법도 여러 가지입니다. 크게 보면 필요할 때마다 검색하는 방법과, 참조를 미리 저장해 두고 쓰는 방법으로 나뉩니다. 아래의 태그 검색과 클래스 검색은 부를 때마다 월드의 액터를 훑어 결과를 만들고, UPROPERTY 참조는 한 번 연결해 둔 대상을 바로 씁니다. 그래서 세 방법을 비교할 때는 무엇을 기준으로 찾는지와 함께, 얼마나 자주 불러도 되는지를 봅니다.

태그로 찾기

액터에 태그(tag)를 붙여 두면, 월드에서 그 태그를 가진 액터들을 찾을 수 있습니다. 아래 함수는 두 가지 쓰임을 보여 줍니다. 앞부분은 TargetEnemy 태그를 가진 액터를 모두 찾아 이름을 로그로 남기고, 뒷부분은 PlayerStart 태그의 검색 결과에서 첫 항목 하나만 꺼냅니다. 검색 결과는 반환값이 아니라 세 번째 인자로 넘긴 TArray에 채워진다는 점을 보면 됩니다.

MyGameModeBase.cpp
// MyGameModeBase.cpp
#include "Kismet/GameplayStatics.h"

void AMyGameModeBase::FindActorsByTag()
{
    UWorld* World = GetWorld();
    if (!World) return;

    // 특정 태그를 가진 모든 액터 찾기
    TArray<AActor*> FoundActors;
    UGameplayStatics::GetAllActorsWithTag(World, TEXT("TargetEnemy"), FoundActors);

    for (AActor* Actor : FoundActors)
    {
        UE_LOG(LogTemp, Warning, TEXT("Found actor with tag 'TargetEnemy': %s"), *Actor->GetName());
        // 찾은 액터에 대한 추가 로직 수행
    }

    // 특정 태그를 가진 액터 중 가장 첫 번째 액터 찾기
    TArray<AActor*> PlayerStarts;
    UGameplayStatics::GetAllActorsWithTag(World, TEXT("PlayerStart"), PlayerStarts);
    AActor* FirstFoundActor = PlayerStarts.IsEmpty() ? nullptr : PlayerStarts[0];
    if (FirstFoundActor)
    {
        UE_LOG(LogTemp, Warning, TEXT("First PlayerStart actor: %s"), *FirstFoundActor->GetName());
    }
}

태그는 AActor::Tags 배열에 추가하며, 에디터의 디테일 패널에서도 설정할 수 있습니다. UGameplayStatics::GetAllActorsWithTag()는 주어진 태그를 가진 모든 액터를 TArray에 담아 돌려줍니다. 코드 뒷부분은 검색 결과가 비어 있지 않을 때 첫 항목 하나를 고르는 예입니다. 태그는 유일한 키가 아니고 결과 배열의 순서도 어떤 기준으로 정해진 것이 아니므로, 첫 항목이 특별한 의미를 갖지는 않습니다. 또 PlayerStart라는 이름의 태그를 실제로 붙여 두어야 찾을 수 있으며, APlayerStart 클래스의 이름과 태그는 서로 관계가 없습니다.

클래스로 찾기

특정 클래스 타입의 액터를 모두 찾을 때는 클래스로 검색합니다. 코드의 모양은 태그 검색과 같지만, 검색 조건으로 문자열 대신 StaticClass()가 돌려준 클래스를 넘깁니다. 결과 배열의 원소 타입은 여전히 AActor*이므로, AMyTargetActor의 멤버를 쓰려면 반복문 안에서처럼 Cast로 바꿔야 합니다. 함수의 마지막 부분은 액터가 하나만 필요할 때 쓰는 GetActorOfClass의 예입니다.

MyGameModeBase.cpp
#include "Kismet/GameplayStatics.h"
#include "MyTargetActor.h" // 찾고자 하는 액터 클래스의 헤더 포함

void AMyGameModeBase::FindActorsByClass()
{
    UWorld* World = GetWorld();
    if (!World) return;

    // 특정 클래스 타입의 모든 액터 찾기
    TArray<AActor*> FoundActors;
    UGameplayStatics::GetAllActorsOfClass(World, AMyTargetActor::StaticClass(), FoundActors);

    for (AActor* Actor : FoundActors)
    {
        AMyTargetActor* TargetActor = Cast<AMyTargetActor>(Actor);
        if (TargetActor)
        {
            UE_LOG(LogTemp, Warning, TEXT("Found MyTargetActor: %s"), *TargetActor->GetName());
        }
    }

    // 특정 클래스 타입의 첫 번째 액터 찾기
    AMyTargetActor* FirstTargetActor = Cast<AMyTargetActor>(UGameplayStatics::GetActorOfClass(World, AMyTargetActor::StaticClass()));
    if (FirstTargetActor)
    {
        UE_LOG(LogTemp, Warning, TEXT("First MyTargetActor: %s"), *FirstTargetActor->GetName());
    }
}

AMyTargetActor::StaticClass()는 그 C++ 클래스의 메타데이터를 담은 UClass*를 돌려줍니다. UGameplayStatics::GetAllActorsOfClass()는 주어진 클래스의 모든 액터를 찾고, UGameplayStatics::GetActorOfClass()는 주어진 클래스의 액터 가운데 처음 찾은 하나를 돌려줍니다. 두 함수 모두 월드의 액터를 훑으므로, 매 프레임처럼 자주 부르면 비용이 커집니다.

UPROPERTY로 직접 참조하기

가장 권장되는 방법 가운데 하나는 참조를 UPROPERTY 멤버에 직접 저장해 두는 것입니다. 기획자가 에디터에서 대상을 직접 지정하거나, 액터를 스폰할 때 참조를 넘겨주는 방식으로 씁니다.

MyLever.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyLever.generated.h"

class AMyDoor; // MyDoor 액터 클래스 미리 선언

UCLASS()
class MYPROJECT_API AMyLever : public AActor
{
    GENERATED_BODY()

public:
    AMyLever();

protected:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
    AMyDoor* ConnectedDoor; // 이 레버가 제어할 문 액터에 대한 직접 참조

    // 레버가 당겨졌을 때 호출될 함수 (예시)
    void OnLeverPulled();
};

이 방법은 실행 중에 액터를 검색할 필요 없이 이미 연결된 참조를 쓰므로 성능에서 유리합니다. 레버에 연결할 문을 미리 ConnectedDoor에 지정해 두면, 레버를 당길 때마다 월드 전체에서 문을 찾지 않아도 됩니다. 다만 헤더의 선언만으로 OnLeverPulled()가 구현되거나 블루프린트에서 부를 수 있는 함수가 되지는 않으므로, 실제 .cpp 정의와 공개할 함수의 지정자를 함께 준비합니다.


액터의 수명 주기와 파괴

액터는 생성된 뒤 월드 안에서 수명 주기를 가지며, 그 단계마다 불리는 함수를 재정의해 원하는 로직을 넣습니다. AActor::PostInitializeComponents()는 액터의 모든 컴포넌트가 초기화된 뒤에 불리고, AActor::BeginPlay()는 액터가 게임플레이를 시작할 때 불리는 초기화 지점입니다. AActor::Tick(float DeltaTime)은 틱이 활성화되어 있고 설정한 실행 간격에 이르렀을 때 불리며, 움직임처럼 시간에 따라 계속 갱신할 로직에 씁니다. 생성자에서 PrimaryActorTick.bCanEverTick을 false로 두면 틱을 끌 수 있습니다. AActor::EndPlay(const EEndPlayReason::Type EndPlayReason)는 액터가 월드에서 제거되거나 게임이 끝날 때 불리며, 자원을 해제하고 연결을 끊는 정리 작업에 씁니다.

Destroy로 파괴하기

어떤 시점에 액터를 월드에서 없애야 할 때는 AActor::Destroy()를 부릅니다. 다음은 총알이 목표물에 맞았을 때와 아이템을 주웠을 때 자신을 파괴하는 예입니다.

MyBullet.cpp (총알이 목표물에 맞았을 때 자신을 파괴하는 예시)
void AMyBullet::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
    // ... 데미지 처리 로직 ...

    // 자신 (총알 액터)을 파괴
    Destroy();
}

// MyItem.cpp (플레이어가 아이템을 주웠을 때 아이템을 파괴하는 예시)

void AMyItem::OnCollected()
{
    // ... 아이템 획득 로직 ...

    // 자신 (아이템 액터)을 파괴
    Destroy();
}

Destroy()의 반환값이 false이면 파괴되지 않은 것입니다. true는 이번에 새로 파괴 처리를 시작했거나 이미 파괴 대상으로 표시되어 있었다는 뜻입니다. 파괴는 바로 끝나지 않고 처리 과정을 거치며, 월드에서 제거되는 과정과 실제 메모리 회수는 서로 다른 단계입니다. 또 EndPlay는 파괴할 때뿐 아니라 레벨 전환, 언로드, 게임 종료 때에도 불릴 수 있습니다.

파괴된 액터를 가리키던 포인터는 그대로 남아 있을 수 있고, 그런 포인터에 접근하면 크래시가 날 수 있습니다. 그래서 GC가 추적하는 참조나 TWeakObjectPtr를 쓰고, 접근할 때마다 IsValid로 상태를 확인합니다. nullptr 검사만으로는 파괴 대기 중인 액터를 걸러 낼 수 없고, 반대로 IsValid도 이미 해제된 임의의 원시 포인터를 안전하게 되살려 주지는 못합니다.

// 안전한 포인터 사용 예시
if (IsValid(MyActorPointer)) // GC가 추적하는 참조를 전제로 함; 약한 참조는 TWeakObjectPtr::Get()/IsValid() 사용
{
    // MyActorPointer가 유효할 때만 작업 수행
    MyActorPointer->DoSomething();
}

찾은 참조가 보장하는 것

조회 성공과 참조 보관은 생존 보장이 아닙니다

현재 대상 조회/보관→사용 직전 유효성 확인→호출 또는 생략/재연결로 액터 종료 전후 상태를 보여 준다. 참조가 Destroy/언로드를 막지 않는 점을 유지한다.

UNREAL C++ · CH4-3

조회 성공과 참조 보관은 생존 보장이 아닙니다

현재 월드에서 액터를 찾은 결과, GC가 추적하는 참조, 약한 참조, Destroy 반환값의 의미를 각각 확인합니다.

보관한 액터를 사용하기 전에 유효성을 확인한다현재 조회하거나 할당받은 대상을 적절히 추적한 참조에 보관한다. 사용 직전에 유효한 대상을 얻었으면 동작을 호출하고, 아니면 호출을 건너뛰고 다시 연결할지 결정한다. 참조 보관은 Destroy나언로드를 막는 보장이 아니며 이미 해제된 임의의 raw 포인터를 복구하는 경로가 아니다.조회 · 할당현재 대상 얻기추적한 참조UPROPERTY / Weak유효한 대상?사용 직전 확인예아니요대상 동작 호출확인한 대상으로 작업호출 건너뜀다시 연결할지 결정
보관한 액터를 사용하기 전에 유효성을 확인한다현재 조회하거나 할당받은 대상을 적절히 추적한 참조에 보관한다. 사용 직전에 유효한 대상을 얻었으면 동작을 호출하고, 아니면 호출을 건너뛰고 다시 연결할지 결정한다. 참조 보관은 Destroy나언로드를 막는 보장이 아니며 이미 해제된 임의의 raw 포인터를 복구하는 경로가 아니다.조회 · 할당현재 대상 얻기추적한 참조UPROPERTY / Weak유효한 대상?사용 직전 확인예아니요대상 동작 호출확인한 대상으로 작업호출 건너뜀다시 연결할지 결정

UPROPERTY는 도달 가능한 객체의 추적 멤버, 약한 참조는 TWeakObjectPtr의 유효한 대상 얻기를 전제로 합니다. 종료·언로드 뒤에도 보관한 대상의 상태를 다시 확인하며, Destroy 반환값과 즉시 메모리 회수는 같은 판정이 아닙니다.

액터를 찾은 뒤 참조가 보장하는 범위
상황얻는 것추가로 확인할 것
태그·클래스 조회현재 월드에서 찾은 액터 목록태그 유일성·순서·나중 생존을 보장하지 않음
직접 참조 보관반복 검색 없이 지정한 대상 접근UPROPERTY 참조라도 Actor의 Destroy를 막지 않음
약한 참조 보관대상을 살려 두지 않고 유효성 확인TWeakObjectPtr에서 유효한 대상을 얻은 뒤 사용
Destroy 요청파괴 처리 요청 또는 이미 파괴 중임을 나타내는 반환값호출 즉시 모든 포인터가 사라지거나 메모리가 회수되는 것은 아님
태그·클래스 조회
얻는 것: 현재 월드에서 찾은 액터 목록
추가로 확인할 것: 태그 유일성·순서·나중 생존을 보장하지 않음
직접 참조 보관
얻는 것: 반복 검색 없이 지정한 대상 접근
추가로 확인할 것: UPROPERTY 참조라도 Actor의 Destroy를 막지 않음
약한 참조 보관
얻는 것: 대상을 살려 두지 않고 유효성 확인
추가로 확인할 것: TWeakObjectPtr에서 유효한 대상을 얻은 뒤 사용
Destroy 요청
얻는 것: 파괴 처리 요청 또는 이미 파괴 중임을 나타내는 반환값
추가로 확인할 것: 호출 즉시 모든 포인터가 사라지거나 메모리가 회수되는 것은 아님

풀링은 살아 있는 액터의 재사용입니다. 숨김만으로 Tick·충돌·타이머가 모두 정지하지 않습니다. 접근 시 유효성을 확인하며 이미 해제된 임의의 raw 포인터를 IsValid로 복구할 수는 없습니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명현재 조회하거나 할당받은 대상을 적절히 추적한 참조에 보관한다. 사용 직전에 유효한 대상을 얻었으면 동작을 호출하고, 아니면 호출을 건너뛰고 다시 연결할지 결정한다. 참조 보관은 Destroy나 언로드를 막는 보장이 아니며 이미 해제된 임의의 raw 포인터를 복구하는 경로가 아니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
대상 보관

현재 대상 Actor를 조회해 참조에 저장한다.

  • 원천: 현재 월드 대상
  • 필드: 보관 참조
  • 참조를 보관해도 Actor 종료를 금지하지 않는다.
사용 직전

대상이 아직 유효한지 확인한다.

  • 검사: 현재 유효성
  • 실패: 호출 생략
  • 조회 시점의 유효성을 후속 사용까지 자동 보장하지 않는다.
종료·재연결

Destroy/언로드 후 대상이 없으면 재조회 정책을 쓴다.

  • 기존 대상: 종료 가능
  • 후속: 생략 또는 재연결
  • 강한 참조와 Actor 게임 수명의 차이를 유지한다.

조회 성공과 참조 보관은 생존 보장이 아닙니다

현재 대상 조회/보관→사용 직전 유효성 확인→호출 또는 생략/재연결로 액터 종료 전후 상태를 보여 준다. 참조가 Destroy/언로드를 막지 않는 점을 유지한다.

UNREAL C++ · CH4-3

조회 성공과 참조 보관은 생존 보장이 아닙니다

현재 월드에서 액터를 찾은 결과, GC가 추적하는 참조, 약한 참조, Destroy 반환값의 의미를 각각 확인합니다.

보관한 액터를 사용하기 전에 유효성을 확인한다현재 조회하거나 할당받은 대상을 적절히 추적한 참조에 보관한다. 사용 직전에 유효한 대상을 얻었으면 동작을 호출하고, 아니면 호출을 건너뛰고 다시 연결할지 결정한다. 참조 보관은 Destroy나언로드를 막는 보장이 아니며 이미 해제된 임의의 raw 포인터를 복구하는 경로가 아니다.조회 · 할당현재 대상 얻기추적한 참조UPROPERTY / Weak유효한 대상?사용 직전 확인예아니요대상 동작 호출확인한 대상으로 작업호출 건너뜀다시 연결할지 결정
보관한 액터를 사용하기 전에 유효성을 확인한다현재 조회하거나 할당받은 대상을 적절히 추적한 참조에 보관한다. 사용 직전에 유효한 대상을 얻었으면 동작을 호출하고, 아니면 호출을 건너뛰고 다시 연결할지 결정한다. 참조 보관은 Destroy나언로드를 막는 보장이 아니며 이미 해제된 임의의 raw 포인터를 복구하는 경로가 아니다.조회 · 할당현재 대상 얻기추적한 참조UPROPERTY / Weak유효한 대상?사용 직전 확인예아니요대상 동작 호출확인한 대상으로 작업호출 건너뜀다시 연결할지 결정

UPROPERTY는 도달 가능한 객체의 추적 멤버, 약한 참조는 TWeakObjectPtr의 유효한 대상 얻기를 전제로 합니다. 종료·언로드 뒤에도 보관한 대상의 상태를 다시 확인하며, Destroy 반환값과 즉시 메모리 회수는 같은 판정이 아닙니다.

액터를 찾은 뒤 참조가 보장하는 범위
상황얻는 것추가로 확인할 것
태그·클래스 조회현재 월드에서 찾은 액터 목록태그 유일성·순서·나중 생존을 보장하지 않음
직접 참조 보관반복 검색 없이 지정한 대상 접근UPROPERTY 참조라도 Actor의 Destroy를 막지 않음
약한 참조 보관대상을 살려 두지 않고 유효성 확인TWeakObjectPtr에서 유효한 대상을 얻은 뒤 사용
Destroy 요청파괴 처리 요청 또는 이미 파괴 중임을 나타내는 반환값호출 즉시 모든 포인터가 사라지거나 메모리가 회수되는 것은 아님
태그·클래스 조회
얻는 것: 현재 월드에서 찾은 액터 목록
추가로 확인할 것: 태그 유일성·순서·나중 생존을 보장하지 않음
직접 참조 보관
얻는 것: 반복 검색 없이 지정한 대상 접근
추가로 확인할 것: UPROPERTY 참조라도 Actor의 Destroy를 막지 않음
약한 참조 보관
얻는 것: 대상을 살려 두지 않고 유효성 확인
추가로 확인할 것: TWeakObjectPtr에서 유효한 대상을 얻은 뒤 사용
Destroy 요청
얻는 것: 파괴 처리 요청 또는 이미 파괴 중임을 나타내는 반환값
추가로 확인할 것: 호출 즉시 모든 포인터가 사라지거나 메모리가 회수되는 것은 아님

풀링은 살아 있는 액터의 재사용입니다. 숨김만으로 Tick·충돌·타이머가 모두 정지하지 않습니다. 접근 시 유효성을 확인하며 이미 해제된 임의의 raw 포인터를 IsValid로 복구할 수는 없습니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명현재 조회하거나 할당받은 대상을 적절히 추적한 참조에 보관한다. 사용 직전에 유효한 대상을 얻었으면 동작을 호출하고, 아니면 호출을 건너뛰고 다시 연결할지 결정한다. 참조 보관은 Destroy나 언로드를 막는 보장이 아니며 이미 해제된 임의의 raw 포인터를 복구하는 경로가 아니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
대상 보관

현재 대상 Actor를 조회해 참조에 저장한다.

  • 원천: 현재 월드 대상
  • 필드: 보관 참조
  • 참조를 보관해도 Actor 종료를 금지하지 않는다.
사용 직전

대상이 아직 유효한지 확인한다.

  • 검사: 현재 유효성
  • 실패: 호출 생략
  • 조회 시점의 유효성을 후속 사용까지 자동 보장하지 않는다.
종료·재연결

Destroy/언로드 후 대상이 없으면 재조회 정책을 쓴다.

  • 기존 대상: 종료 가능
  • 후속: 생략 또는 재연결
  • 강한 참조와 Actor 게임 수명의 차이를 유지한다.

위 그림의 위쪽은 보관한 액터를 쓰기 전에 유효성을 확인하는 흐름입니다. 먼저 현재 월드에서 조회하거나 할당받은 대상을 UPROPERTY나 약한 참조처럼 적절히 추적되는 참조에 보관합니다. 그 대상을 쓰려는 직전에 유효한 대상을 얻을 수 있는지 확인하고, 유효하면 그 대상의 동작을 부르며, 유효하지 않으면 호출을 건너뛰고 다시 연결할지를 정합니다. 그림의 설명처럼 UPROPERTY는 도달 가능한 객체 안의 추적 멤버를, 약한 참조는 TWeakObjectPtr에서 유효한 대상을 얻는 경우를 전제로 합니다.

그림의 표는 액터를 찾은 뒤 각 참조가 어디까지 보장하는지 정리합니다. 태그나 클래스로 조회하면 현재 월드에서 찾은 액터 목록을 얻지만, 태그가 유일한지, 순서가 어떤지, 나중에도 살아 있을지는 보장되지 않습니다. 직접 참조를 보관하면 반복 검색 없이 지정한 대상에 접근할 수 있지만, UPROPERTY 참조라도 액터의 Destroy를 막지는 않습니다. 약한 참조는 대상을 살려 두지 않고 유효성만 확인하게 해 주므로, TWeakObjectPtr에서 유효한 대상을 얻은 뒤에 씁니다. Destroy 요청의 반환값은 파괴 처리 요청이 받아들여졌거나 이미 파괴 중임을 나타낼 뿐, 호출 즉시 모든 포인터가 사라지거나 메모리가 회수된다는 뜻이 아닙니다.

단계 그림은 이 과정을 시간 순서로 따라갑니다. 대상을 보관한 시점에 유효했다는 사실이 나중에 쓸 때까지 이어지지는 않으므로 사용 직전에 다시 확인하고, Destroy나 언로드로 대상이 사라졌다면 다시 조회하는 정책을 씁니다. 강한 참조를 가지고 있다는 것과 액터가 게임에서 살아 있다는 것은 다른 이야기라는 점이 핵심입니다.


액터가 많아질 때 관리하기

수백, 수천 개의 액터가 있는 큰 게임에서는 액터의 생성, 검색, 파괴, 그리고 매 프레임 갱신(Tick)을 최적화해야 합니다.

먼저 필요 없는 Tick을 끕니다. 틱을 전혀 쓰지 않는 액터는 bCanEverTick=false로 둡니다. 나중에 켤 수도 있는 액터는 bCanEverTick=true를 유지하고, 시작할 때의 활성 상태나 SetActorTickEnabled로 켜고 끕니다.

총알이나 이펙트처럼 자주 만들어지고 사라지는 액터는 생성과 파괴 비용이 크므로 액터 풀링(actor pooling)을 검토할 수 있습니다. 풀링은 살아 있는 액터를 숨겨 두었다가 다시 쓰는 방식입니다. 그림 아래의 주의처럼 숨기기만 해서는 Tick, 충돌, 타이머가 모두 멈추지 않으므로, 다시 쓸 때 이것들과 게임 상태를 초기화하는 비용과 메모리 비용까지 함께 비교해야 합니다.

USceneComponent로 액터 안의 컴포넌트를 계층적으로 구성하거나, AttachToActor, AttachToComponent로 액터를 다른 액터에 붙이면 아웃라이너가 정리되고 트랜스폼을 다루기도 쉬워집니다. 그리고 Tick에서 특정 액터를 계속 찾거나 모든 액터를 순회하는 대신, 델리게이트(delegate)나 인터페이스(interface)로 액터 사이의 통신을 이벤트 방식으로 처리하면 불필요한 연산을 줄일 수 있습니다.

스폰과 반복 검색의 비용은 실제 대상 기기에서 측정한 뒤 조정합니다. 생성에 성공했다는 사실과 올바르게 초기화되고, 참조되고, 정리되었다는 사실은 각각 따로 확인해야 합니다.


정리

  • 액터는 에디터에서 배치하거나 UWorld::SpawnActor()로 실행 중에 스폰하며, TSubclassOf로 만들 클래스를 고르고 FActorSpawnParameters로 충돌 처리 같은 옵션을 정합니다.
  • 생성에 성공해도 충돌 옵션에 따라 위치가 조정될 수 있으므로 최종 위치를 다시 확인하고, 반환값이 nullptr인 경우를 처리합니다.
  • 태그나 클래스로 찾은 결과는 유일성이나 순서를 보장하지 않으며, 자주 부르면 비용이 큽니다. 미리 UPROPERTY로 연결한 직접 참조가 더 효율적입니다.
  • Destroy()는 파괴 처리를 요청할 뿐 즉시 메모리를 회수하지 않으며, UPROPERTY 참조가 있어도 액터의 파괴를 막지 않습니다.
  • 보관한 액터는 사용 직전에 IsValid나 TWeakObjectPtr로 유효성을 확인하고, 사라졌다면 다시 연결할지 정합니다.
  • 필요 없는 Tick은 끄고, 자주 생성되는 액터는 풀링을 검토하며, 반복 검색 대신 이벤트 방식의 통신을 씁니다.

연습 문제

1. 같은 태그의 첫 번째 문

문 세 개에 같은 태그를 붙였고, 검색 결과의 첫 항목을 레버의 대상으로 골랐습니다. 이 선택이 항상 플레이어에게 가장 가까운 문을 뜻할까요?

해설 보기

그렇지 않습니다. 태그는 유일한 키가 아니고, 검색 결과 배열의 순서도 거리 같은 기준으로 정해진 것이 아닙니다. 가장 가까운 문이 필요하다면 유효한 후보들의 거리를 비교하는 것처럼 선택 기준을 직접 정해야 합니다. 대상이 하나로 정해져 있다면 처음부터 UPROPERTY 참조로 연결해 두는 편이 낫습니다.

2. 파괴된 문의 포인터

GC가 추적하는 멤버에 저장해 둔 문이 다른 로직에 의해 파괴 처리되었습니다. 포인터가 nullptr이 아니라는 이유만으로 그 문의 열기 동작을 불러도 될까요? 또 Destroy()가 true를 돌려준 횟수를 세면 문이 몇 번 파괴되었는지 알 수 있을까요?

해설 보기

부르면 안 됩니다. 파괴나 언로드가 일어난 뒤에는 nullptr 검사로는 파괴 대기 상태를 걸러 낼 수 없으므로, 적절히 추적한 참조의 유효성을 IsValid로 확인한 뒤 쓸지 결정합니다. 참조를 가지고 있다고 해서 Destroy가 막히거나, 반대로 메모리가 즉시 회수되었다고 판단할 수도 없습니다.

Destroy()의 true는 새로 파괴 처리를 시작한 경우뿐 아니라 이미 파괴 대상으로 표시된 경우에도 돌아오므로, 그 횟수를 새로 파괴한 횟수로 셀 수 없습니다.

액터를 스폰하는 함수와 매개변수는 Epic의 Spawning Actors 문서에서, 액터가 만들어지고 사라지는 순서는 Actor Lifecycle 문서에서 확인할 수 있습니다.

다음 글 레벨 스트리밍과 World Partition에서는 큰 월드를 필요한 부분만 불러오는 레벨 스트리밍과 월드 파티션을 살펴봅니다.