본문으로 건너뛰기

안동민 개발노트

본문 시작

액터 생성과 관리

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

이전 절에서 레벨 블루프린트와 C++ 스크립트의 역할과 사용처를 비교하며, 언리얼 엔진에서 게임 로직을 구현하는 다양한 방법을 이해했습니다.

어떤 방식으로 로직을 구현하든, 그 로직이 동작하는 대상은 결국 액터(Actor)입니다.

이번 절에서는 게임 월드 내에서 액터를 생성하고, 찾고, 참조하고, 파괴하는 등 액터를 효과적으로 관리하는 방법을 C++ 관점에서 심층적으로 다룰 것입니다.

월드의 규모가 커지고 액터의 수가 많아질수록 효율적인 액터 관리는 게임의 성능과 안정성에 직접적인 영향을 미칩니다.

액터 관리는 생성 위치보다 수명과 참조 책임을 먼저 본다

레벨에 배치하든 런타임에 스폰하든, 이후 찾기와 파괴 비용까지 한 흐름으로 설계해야 합니다.

  1. 1
    생성 방식 미리 둘지, 이벤트

    만들지 결정합니다.

  2. 2
    초기 정보 Class, Transform

    Owner, Instigator를 묶습니다.

  3. 3
    참조 연결 UPROPERTY, 태그, 등록

    목록 중 하나로 추적합니다.

  4. 4
    수명 종료 Destroy

    EndPlay에서 정리 책임을 나눕니다.

  5. 5
    비용 관리 반복 검색, Tick, 스폰 빈도

    제한합니다.


액터 생성하기 (Spawning Actors)

게임 월드에 액터를 추가하는 방법은 크게 두 가지입니다.

에디터에서 배치 (Manual Placement)

가장 기본적인 방법으로, 언리얼 에디터의 콘텐츠 브라우저에서 원하는 액터 블루프린트(또는 C++ 액터 클래스를 기반으로 만든 블루프린트)를 뷰포트(Viewport)로 드래그 앤 드롭하는 것입니다.

이 경우 액터는 레벨에 영구적으로 배치되며, 레벨이 로드될 때 함께 생성됩니다.

런타임에 동적 생성 (Dynamic Spawning)

게임 플레이 중 특정 이벤트(예: 플레이어가 총을 발사하면 총알이 생성, 적이 죽으면 아이템 생성, 스폰 지점에서 플레이어 캐릭터 생성)에 따라 액터를 동적으로 생성해야 할 때 사용합니다.

C++에서는 주로 UWorld::SpawnActor() 함수를 사용합니다.

MyGameModeBase.cpp 또는 AMyCharacter.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;
    }

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

    // 액터 생성 파라미터 구조체 설정
    FActorSpawnParameters SpawnParams;
    SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
    // 액터 생성 시 콜리전 처리 방식 (예: 충돌 발생 시 위치 조정 후 생성, 또는 항상 생성 등)
    SpawnParams.Owner = this; // 생성하는 액터의 소유자 설정 (가비지 컬렉션 및 네트워크 관련)
    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(), *SpawnLocation.ToString());
    }
}
  • TSubclassOf<AActor>: 이 템플릿 타입은 언리얼 클래스(UClass*)에 대한 참조를 저장하며, 해당 클래스가 특정 부모 클래스(여기서는 AActor)를 상속받아야 함을 강제합니다. 에디터에서 UPROPERTY로 노출하면 드롭다운 목록에서 원하는 블루프린트 클래스를 쉽게 선택할 수 있습니다.
  • FActorSpawnParameters: 액터 생성 시의 다양한 추가 옵션을 설정할 수 있는 구조체입니다. 특히 SpawnCollisionHandlingOverride는 매우 중요하며, 액터가 생성될 위치에 다른 오브젝트가 있을 때 어떻게 처리할지를 결정합니다.
  • World->SpawnActor<T>(): 실제로 액터를 생성하는 함수입니다. T는 생성할 액터의 C++ 타입입니다. (예: <AMyBullet> 또는 <AActor>).

액터 찾기 및 참조하기

월드에 이미 존재하는 액터를 찾거나 다른 액터로부터 참조를 얻는 방법은 여러 가지가 있습니다.

태그(Tags)를 이용한 찾기

액터에 고유한 태그(Tag)를 부여하여 월드 내에서 해당 태그를 가진 액터들을 찾을 수 있습니다.

AMyGameModeBase.cpp
// AMyGameModeBase.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());
        // 찾은 액터에 대한 추가 로직 수행
    }

    // 특정 태그를 가진 액터 중 가장 첫 번째 액터 찾기
    AActor* FirstFoundActor = UGameplayStatics::GetActorWithTag(World, TEXT("PlayerStart"));
    if (FirstFoundActor)
    {
        UE_LOG(LogTemp, Warning, TEXT("First PlayerStart actor: %s"), *FirstFoundActor->GetName());
    }
}
  • AActor::Tags: 액터 클래스의 Tags 배열에 태그를 추가할 수 있습니다. 에디터의 디테일 패널에서도 설정 가능합니다.
  • UGameplayStatics::GetAllActorsWithTag(): 주어진 태그를 가진 모든 액터를 TArray에 담아 반환합니다.
  • UGameplayStatics::GetActorWithTag(): 주어진 태그를 가진 첫 번째 액터를 반환합니다.

클래스(Class)를 이용한 찾기

특정 클래스 타입의 모든 액터를 찾을 때 사용합니다.

AMyGameModeBase.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(): UClass* 타입으로 해당 C++ 클래스에 대한 메타데이터를 반환합니다.
  • UGameplayStatics::GetAllActorsOfClass(): 주어진 클래스 타입의 모든 액터를 찾습니다.
  • UGameplayStatics::GetActorOfClass(): 주어진 클래스 타입의 첫 번째 액터를 찾습니다.

직접 참조 (UPROPERTY)

가장 권장되는 방법 중 하나는 직접적인 참조를 UPROPERTY로 저장하는 것입니다.

이는 디자이너가 에디터에서 수동으로 액터를 할당하거나, 스폰 시 참조를 직접 넘겨주는 방식으로 사용됩니다.

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

이 방법은 런타임에 액터를 검색할 필요 없이 이미 연결된 참조를 사용하므로 성능상 이점이 있습니다.


액터 수명 주기 관리와 파괴

Actor는 생성, 초기화, 플레이, 파괴, 유효성 검사로 관리한다

Destroy 뒤에도 외부 포인터가 남을 수 있으므로 수명과 참조 정리를 함께 봐야 한다.

단계일어나는 일주의할 점
Spawn / Placed코드 생성 또는 레벨 배치로 월드에 들어온다생성 위치와 소유권을 정한다
Initialize컴포넌트와 기본 상태가 준비된다BeginPlay 전 참조와 초기값을 안정화한다
BeginPlay / Tick게임 중 동작과 프레임 업데이트가 진행된다Tick은 필요한 로직만 켠다
Destroy / EndPlay파괴 요청 뒤 종료 이벤트와 참조 정리가 이어진다IsValid와 약한 참조로 접근을 막는다

액터는 생성되면 월드 내에서 수명 주기를 가집니다.

특정 시점에 액터를 월드에서 제거해야 할 때가 오는데, 이를 액터 파괴(Destruction)라고 합니다.

액터의 수명 주기 함수 (Recall)

액터는 다음과 같은 주요 수명 주기 함수들을 가집니다.

이 함수들을 오버라이드하여 액터의 특정 단계에서 원하는 로직을 구현합니다.

  • AActor::PostInitializeComponents(): 액터의 모든 컴포넌트가 초기화된 후 호출됩니다.
  • AActor::BeginPlay(): 액터가 월드에 스폰되거나 레벨이 시작될 때 단 한 번 호출됩니다. 게임플레이 로직의 주요 초기화 지점입니다.
  • AActor::Tick(float DeltaTime): 게임 루프의 매 프레임마다 호출됩니다. 시간의 흐름에 따라 지속적으로 업데이트되어야 하는 로직(움직임, 애니메이션 업데이트 등)에 사용됩니다. PrimaryActorTick.bCanEverTickfalse로 설정하여 비활성화할 수 있습니다.
  • AActor::EndPlay(const EEndPlayReason::Type EndPlayReason): 액터가 월드에서 제거되거나 게임이 종료될 때 호출됩니다. 리소스 해제, 연결 끊기 등의 정리 작업에 사용됩니다.

액터 파괴하기 (Destroying Actors)

액터를 월드에서 제거하려면 AActor::Destroy() 함수를 호출합니다.

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

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

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

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

    // 자신 (아이템 액터)을 파괴
    Destroy();
}
  • Destroy(): 이 함수를 호출하면 액터는 더 이상 렌더링되지 않고, 틱 업데이트를 받지 않으며, 콜리전도 비활성화됩니다. 실제 메모리 해제는 언리얼 엔진의 가비지 컬렉터가 다음 사이클에서 수행합니다. EndPlay() 함수는 Destroy() 호출 시 자동으로 호출됩니다.
  • 주의사항: 액터가 파괴되면 해당 액터에 대한 유효하지 않은 포인터가 남을 수 있습니다. 유효하지 않은 포인터에 접근하면 크래시가 발생할 수 있으므로, 항상 IsValid()nullptr 체크를 통해 포인터의 유효성을 확인해야 합니다.
// 안전한 포인터 사용 예시
if (MyActorPointer && MyActorPointer->IsValidLowLevel()) // 또는 MyActorPointer.IsValid() (TWeakObjectPtr, TSharedPtr 사용 시)
{
    // MyActorPointer가 유효할 때만 작업 수행
    MyActorPointer->DoSomething();
}

생성 방식, 참조 방법, 파괴 시점, 최적화 판단은 따로 떨어진 주제가 아니라 실제 게임 루프 안에서 계속 이어지는 선택입니다.

아래 다이어그램은 액터를 배치하거나 스폰한 뒤, 어떻게 찾고 보관하며, 언제 제거하거나 재사용할지 결정하는 흐름을 한 번에 연결해 보여줍니다.

Actor 관리는 배치, 스폰, 참조, 파괴, 비용 판단까지 이어진다

만드는 방법보다 만든 뒤 누가 참조하고 언제 사라지는지까지 정해야 안정적이다.

구간사용 상황판단 기준
Placed문, 레버, 시작 지점처럼 항상 필요한 액터레벨에 고정된 관계가 분명하다
SpawnActor총알, 아이템, 적처럼 이벤트로 생긴다클래스와 초기 파라미터가 필요하다
Reference태그/클래스 검색은 초기 수집에 쓰고 반복 사용은 저장한다UPROPERTY 직접 참조가 더 안전한지 본다
Destroy / Pool역할이 끝난 액터는 파괴하거나 재사용한다반복 생성 비용과 Tick 비용을 줄인다

액터 관리의 최적화

월드에 수백, 수천 개의 액터가 존재할 수 있는 대규모 게임에서는 액터의 생성, 찾기, 파괴, 그리고 지속적인 업데이트(Tick)에 대한 최적화가 필수적입니다.

  • 불필요한 Tick 비활성화: 액터가 매 프레임 업데이트될 필요가 없다면 PrimaryActorTick.bCanEverTick = false;로 설정하여 성능을 절약합니다. 필요한 순간에만 SetActorTickEnabled(true);로 활성화할 수 있습니다.
  • 액터 풀링(Actor Pooling): 총알이나 파티클 효과처럼 자주 생성되고 파괴되는 액터의 경우, 생성/파괴 비용이 높으므로 액터 풀링 기법을 사용하여 미리 액터를 생성해두고 재활용하는 것이 효율적입니다.
  • 오브젝트 계층 구조 활용: USceneComponent를 통해 액터 내의 컴포넌트들을 계층적으로 구성하거나, 액터 자체를 다른 액터에 부착(AttachToActor 또는 AttachToComponent)하여 월드 아웃라이너를 정리하고 변환 관리를 용이하게 할 수 있습니다.
  • 오브젝트 리스너/이벤트 사용: 특정 액터를 지속적으로 Tick에서 찾거나 모든 액터를 순회하는 대신, DelegateInterface를 사용하여 액터 간의 통신을 이벤트 기반으로 처리하면 불필요한 연산을 줄일 수 있습니다.

아래 다이어그램은 액터 생성, 참조, Tick 비용, 파괴 시점을 한 번에 점검하는 관리 루프입니다.

액터 관리는 생성보다 참조와 종료 기준이 중요하다

SpawnActor로 만든 액터도, 에디터에 배치한 액터도 결국 안전한 참조, 필요한 Tick, 명확한 Destroy 시점으로 관리된다.

  1. 생성 기준

    SpawnActor<T>() 런타임 이벤트라면 클래스, 위치, 충돌 처리 옵션을 함께 넘긴다.

  2. 참조 확보

    UPROPERTY / Tags 반복 검색보다 직접 참조와 이벤트 연결을 우선한다.

  3. 실행 비용

    Tick Enabled 매 프레임 필요하지 않으면 Tick을 끄고 이벤트로 깨운다.

  4. 종료 처리

    Destroy() 파괴 후 포인터 접근을 막고 EndPlay에서 정리한다.

  5. direct
    직접 참조

    direct 에디터에서 할당하거나 스폰 직후 저장한다. 성능과 명확성이 가장 좋다.

  6. tag
    태그 검색

    tag PlayerStart, TargetEnemy처럼 그룹 의미가 분명할 때만 사용한다.

  7. class
    클래스 검색

    class 월드 전체 순회가 필요하므로 BeginPlay 초기화나 드문 검사에 제한한다.

  8. 유효성 확인

    Destroy 이후에는 nullptr, IsValid, 약한 참조 상태를 확인한 뒤 접근한다.

  9. 재사용 판단

    총알과 이펙트처럼 짧게 반복 생성되는 액터는 풀링 비용을 비교한다.


액터 관리는 생성, 조회, 참조 유지, 파괴 시점을 명확히 나누어 다뤄야 합니다.

참조 수명과 스폰 비용을 함께 관리하면 월드 복잡도와 성능 문제를 줄일 수 있습니다.

마지막으로 액터를 스폰하기 전후에 확인해야 할 입력값, 초기화, 수명 관리 흐름을 정리해 보세요.

SpawnActor() 는 생성 호출이 아니라 수명 계약의 시작이다

성공 포인터를 얻는 데서 끝내지 않는다. 입력을 검증하고 월드에 배치한 뒤, 초기화·참조 보관·제거까지 한 흐름으로 추적해야 한다.

  1. 1
    월드와 클래스 확인

    · VALIDATE GetWorld() , TSubclassOf , 서버 권한이 유효하지 않으면 호출 전에 중단해 실패 원인을 고정한다.

  2. 2
    · VALIDATE
  3. 3
    좌표와 충돌 결정

    · PLACE 소켓·타겟·월드 중 Transform 기준을 정하고, SpawnCollisionHandlingOverride 로 겹침 정책을 명시한다.

  4. 4
    · PLACE
  5. 5
    생성 직후 상태 연결

    · INITIALIZE 반환값을 검사한 뒤 Owner, Instigator, 데미지와 목표 같은 필수 값을 초기화하고 게임 흐름에 연결한다.

  6. 6
    · INITIALIZE
  7. 7
    보관과 종료 책임 지정

    · RELEASE 다시 쓸 참조의 소유자를 정하고, 일회성은 Destroy() , 반복 생성 대상은 풀링으로 회수한다.

  8. 8
    · RELEASE