액터 생성과 관리
SpawnActor로 액터를 동적 생성하고 태그·클래스로 탐색한 뒤 참조와 파괴 시점을 안전하게 관리합니다.
이전 절에서 레벨 블루프린트와 C++ 스크립트의 역할과 사용처를 비교하며, 언리얼 엔진에서 게임 로직을 구현하는 다양한 방법을 이해했습니다.
월드에 배치하는 게임플레이 대상은 흔히 액터(Actor)로 구현합니다.
이번 절에서는 게임 월드 내에서 액터를 생성하고, 찾고, 참조하고, 파괴하는 등 액터를 효과적으로 관리하는 방법을 C++ 관점에서 심층적으로 다룰 것입니다.
월드의 규모가 커지고 액터의 수가 많아질수록 효율적인 액터 관리는 게임의 성능과 안정성에 직접적인 영향을 미칩니다.
액터 생성하기 (Spawning Actors)
게임 월드에 액터를 추가하는 방법은 크게 두 가지입니다.
에디터에서 배치 (Manual Placement)
가장 기본적인 방법으로, 언리얼 에디터의 콘텐츠 브라우저에서 원하는 액터 블루프린트(또는 C++ 액터 클래스를 기반으로 만든 블루프린트)를 뷰포트(Viewport)로 드래그 앤 드롭하는 것입니다.
배치를 레벨에 저장하면 로드할 때 해당 액터가 준비됩니다. 게임 중 파괴나 스트리밍 언로드까지 막는 영구 생존 보장은 아닙니다.
런타임에 동적 생성 (Dynamic Spawning)
게임 플레이 중 특정 이벤트(예: 플레이어가 총을 발사하면 총알이 생성, 적이 죽으면 아이템 생성, 스폰 지점에서 플레이어 캐릭터 생성)에 따라 액터를 동적으로 생성해야 할 때 사용합니다.
C++에서는 주로 UWorld::SpawnActor()를 사용합니다. 아래는 헤더의 UCLASS 본문에 둘 멤버 선언과 .cpp의 함수 정의를 함께 보여 주는 발췌입니다. ActorToSpawnClass를 실제 클래스에 할당하고, SpawnMyActor 선언도 헤더에 둡니다.
#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());
}
}TSubclassOf<AActor>: 이 템플릿 타입은 언리얼 클래스(UClass*)에 대한 참조를 저장하며, 해당 클래스가 특정 부모 클래스(여기서는AActor)를 상속받아야 함을 강제합니다. 에디터에서UPROPERTY로 노출하면 드롭다운 목록에서 원하는 블루프린트 클래스를 쉽게 선택할 수 있습니다.FActorSpawnParameters: 액터 생성 시의 다양한 추가 옵션을 설정할 수 있는 구조체입니다. 특히SpawnCollisionHandlingOverride는 매우 중요하며, 액터가 생성될 위치에 다른 오브젝트가 있을 때 어떻게 처리할지를 결정합니다.World->SpawnActor<T>(): 실제로 액터를 생성하는 함수입니다.T는 생성할 액터의 C++ 타입입니다. (예:<AMyBullet>또는<AActor>).
액터 찾기 및 참조하기
월드에 이미 존재하는 액터를 찾거나 다른 액터로부터 참조를 얻는 방법은 여러 가지가 있습니다.
태그(Tags)를 이용한 찾기
액터에 태그(Tag)를 부여하여 월드 내에서 해당 태그를 가진 액터들을 찾을 수 있습니다.
// 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: 액터 클래스의Tags배열에 태그를 추가할 수 있습니다. 에디터의 디테일 패널에서도 설정 가능합니다.UGameplayStatics::GetAllActorsWithTag(): 주어진 태그를 가진 모든 액터를TArray에 담아 반환합니다.- 배열의 첫 항목: 태그 검색 결과가 비어 있지 않을 때 한 항목을 고른 예시입니다. 태그는 유일 키가 아니고 결과 순서도 선택 정책이 아닙니다. PlayerStart라는 이름의 태그를 실제로 설정해야 하며, 클래스 이름과 태그는 별개입니다.
클래스(Class)를 이용한 찾기
특정 클래스 타입의 모든 액터를 찾을 때 사용합니다.
#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로 저장하는 것입니다.
이는 디자이너가 에디터에서 수동으로 액터를 할당하거나, 스폰 시 참조를 직접 넘겨주는 방식으로 사용됩니다.
#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();
};이 방법은 런타임에 액터를 검색할 필요 없이 이미 연결된 참조를 사용하므로 성능상 이점이 있습니다.
액터 수명 주기 관리와 파괴
액터는 생성되면 월드 내에서 수명 주기를 가집니다.
특정 시점에 액터를 월드에서 제거해야 할 때가 오는데, 이를 액터 파괴(Destruction)라고 합니다.
액터의 수명 주기 함수 (Recall)
액터는 다음과 같은 주요 수명 주기 함수들을 가집니다.
이 함수들을 오버라이드하여 액터의 특정 단계에서 원하는 로직을 구현합니다.
AActor::PostInitializeComponents(): 액터의 모든 컴포넌트가 초기화된 후 호출됩니다.AActor::BeginPlay(): 액터가 월드에 스폰되거나 레벨이 시작될 때 단 한 번 호출됩니다. 게임플레이 로직의 주요 초기화 지점입니다.AActor::Tick(float DeltaTime): 틱이 활성화되어 있고 설정한 실행 간격에 도달했을 때 호출됩니다. 시간의 흐름에 따라 지속적으로 업데이트되어야 하는 로직(움직임, 애니메이션 업데이트 등)에 사용됩니다.PrimaryActorTick.bCanEverTick을false로 설정하여 비활성화할 수 있습니다.AActor::EndPlay(const EEndPlayReason::Type EndPlayReason): 액터가 월드에서 제거되거나 게임이 종료될 때 호출됩니다. 리소스 해제, 연결 끊기 등의 정리 작업에 사용됩니다.
액터 파괴하기 (Destroying Actors)
액터를 월드에서 제거하려면 AActor::Destroy() 함수를 호출합니다.
void AMyBullet::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
// ... 데미지 처리 로직 ...
// 자신 (총알 액터)을 파괴
Destroy();
}
// MyItem.cpp (플레이어가 아이템을 주웠을 때 아이템을 파괴하는 예시)
void AMyItem::OnCollected()
{
// ... 아이템 획득 로직 ...
// 자신 (아이템 액터)을 파괴
Destroy();
}- Destroy(): 반환값이 false이면 파괴되지 않은 경우입니다. 파괴는 지연 처리되며, 월드에서 제거되는 과정과 실제 메모리 회수는 구분됩니다. EndPlay는 파괴뿐 아니라 레벨 전환·언로드나 게임 종료로도 발생할 수 있습니다.
- 주의사항: 액터가 파괴되면 해당 액터에 대한 유효하지 않은 포인터가 남을 수 있습니다. 유효하지 않은 포인터에 접근하면 크래시가 발생할 수 있으므로, GC가 추적하는 참조나 TWeakObjectPtr를 사용하고 접근 시 IsValid로 상태를 확인합니다. nullptr 검사만으로 파괴 대기 상태를 거르지는 못하며, IsValid도 이미 해제된 임의의 raw 포인터를 안전하게 복구하지는 않습니다.
// 안전한 포인터 사용 예시
if (IsValid(MyActorPointer)) // GC가 추적하는 참조를 전제로 함; 약한 참조는 TWeakObjectPtr::Get()/IsValid() 사용
{
// MyActorPointer가 유효할 때만 작업 수행
MyActorPointer->DoSomething();
}조회 성공, 참조 보관, 파괴 요청은 다른 상태입니다.
| 상황 | 얻는 것 | 추가로 확인할 것 |
|---|---|---|
| 태그·클래스 조회 | 현재 월드에서 찾은 액터 목록 | 태그 유일성·순서·나중 생존을 보장하지 않음 |
| 직접 참조 보관 | 반복 검색 없이 지정한 대상 접근 | UPROPERTY 참조라도 Actor의 Destroy를 막지 않음 |
| 약한 참조 보관 | 대상을 살려 두지 않고 유효성 확인 | TWeakObjectPtr에서 유효한 대상을 얻은 뒤 사용 |
| Destroy 요청 | 파괴 처리 요청 또는 이미 파괴 중임을 나타내는 반환값 | 호출 즉시 모든 포인터가 사라지거나 메모리가 회수되는 것은 아님 |
- 태그·클래스 조회
- 얻는 것: 현재 월드에서 찾은 액터 목록추가로 확인할 것: 태그 유일성·순서·나중 생존을 보장하지 않음
- 직접 참조 보관
- 얻는 것: 반복 검색 없이 지정한 대상 접근추가로 확인할 것: UPROPERTY 참조라도 Actor의 Destroy를 막지 않음
- 약한 참조 보관
- 얻는 것: 대상을 살려 두지 않고 유효성 확인추가로 확인할 것: TWeakObjectPtr에서 유효한 대상을 얻은 뒤 사용
- Destroy 요청
- 얻는 것: 파괴 처리 요청 또는 이미 파괴 중임을 나타내는 반환값추가로 확인할 것: 호출 즉시 모든 포인터가 사라지거나 메모리가 회수되는 것은 아님
풀링은 파괴 후 부활이 아니라 살아 있는 액터의 재사용입니다. 숨김만으로 Tick·충돌·타이머가 모두 정지하지는 않습니다.
액터 관리의 최적화
월드에 수백, 수천 개의 액터가 존재할 수 있는 대규모 게임에서는 액터의 생성, 찾기, 파괴, 그리고 지속적인 업데이트(Tick)에 대한 최적화가 필수적입니다.
- 불필요한 Tick 비활성화: Tick을 전혀 쓰지 않으면 bCanEverTick=false로 둡니다. 나중에 활성화할 액터는 bCanEverTick=true를 유지하고 시작 활성 상태나 SetActorTickEnabled로 켜고 끕니다.
- 액터 풀링(Actor Pooling): 총알이나 파티클 효과처럼 자주 생성되고 파괴되는 액터의 경우, 생성/파괴 비용이 높으므로 풀링을 검토할 수 있습니다. 재활용 시 충돌·Tick·타이머·게임 상태 초기화와 메모리 비용까지 비교해야 합니다.
- 오브젝트 계층 구조 활용:
USceneComponent를 통해 액터 내의 컴포넌트들을 계층적으로 구성하거나, 액터 자체를 다른 액터에 부착(AttachToActor또는AttachToComponent)하여 월드 아웃라이너를 정리하고 변환 관리를 용이하게 할 수 있습니다. - 오브젝트 리스너/이벤트 사용: 특정 액터를 지속적으로
Tick에서 찾거나 모든 액터를 순회하는 대신,Delegate나Interface를 사용하여 액터 간의 통신을 이벤트 기반으로 처리하면 불필요한 연산을 줄일 수 있습니다.
스폰과 반복 검색의 비용은 실제 대상 기기에서 측정한 뒤 조정합니다. 생성 성공과 올바른 초기화·참조·종료 처리는 각각 확인해야 합니다.