액터 생성과 관리
이전 절에서 레벨 블루프린트와 C++ 스크립트의 역할과 사용처를 비교하며, 언리얼 엔진에서 게임 로직을 구현하는 다양한 방법을 이해했습니다. 어떤 방식으로 로직을 구현하든, 그 로직이 동작하는 대상은 결국 액터(Actor) 입니다. 이번 절에서는 게임 월드 내에서 액터를 생성하고, 찾고, 참조하고, 파괴하는 등 액터를 효과적으로 관리하는 방법을 C++ 관점에서 심층적으로 다룰 것입니다. 월드의 규모가 커지고 액터의 수가 많아질수록 효율적인 액터 관리는 게임의 성능과 안정성에 직접적인 영향을 미칩니다.
액터 생성하기 (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
#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();
};
이 방법은 런타임에 액터를 검색할 필요 없이 이미 연결된 참조를 사용하므로 성능상 이점이 있습니다.
액터 수명 주기 관리와 파괴
액터는 생성되면 월드 내에서 수명 주기를 가집니다. 특정 시점에 액터를 월드에서 제거해야 할 때가 오는데, 이를 액터 파괴(Destruction) 라고 합니다.
액터의 수명 주기 함수 (Recall)
액터는 다음과 같은 주요 수명 주기 함수들을 가집니다. 이 함수들을 오버라이드하여 액터의 특정 단계에서 원하는 로직을 구현합니다.
AActor::PostInitializeComponents()
: 액터의 모든 컴포넌트가 초기화된 후 호출됩니다.AActor::BeginPlay()
: 액터가 월드에 스폰되거나 레벨이 시작될 때 단 한 번 호출됩니다. 게임플레이 로직의 주요 초기화 지점입니다.AActor::Tick(float DeltaTime)
: 게임 루프의 매 프레임마다 호출됩니다. 시간의 흐름에 따라 지속적으로 업데이트되어야 하는 로직(움직임, 애니메이션 업데이트 등)에 사용됩니다.PrimaryActorTick.bCanEverTick
을false
로 설정하여 비활성화할 수 있습니다.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();
}
액터 관리의 최적화
월드에 수백, 수천 개의 액터가 존재할 수 있는 대규모 게임에서는 액터의 생성, 찾기, 파괴, 그리고 지속적인 업데이트(Tick)에 대한 최적화가 필수적입니다.
- 불필요한 Tick 비활성화: 액터가 매 프레임 업데이트될 필요가 없다면
PrimaryActorTick.bCanEverTick = false;
로 설정하여 성능을 절약합니다. 필요한 순간에만SetActorTickEnabled(true);
로 활성화할 수 있습니다. - 액터 풀링(Actor Pooling): 총알이나 파티클 효과처럼 자주 생성되고 파괴되는 액터의 경우, 생성/파괴 비용이 높으므로 액터 풀링 기법을 사용하여 미리 액터를 생성해두고 재활용하는 것이 효율적입니다.
- 오브젝트 계층 구조 활용:
USceneComponent
를 통해 액터 내의 컴포넌트들을 계층적으로 구성하거나, 액터 자체를 다른 액터에 부착(AttachToActor
또는AttachToComponent
)하여 월드 아웃라이너를 정리하고 변환 관리를 용이하게 할 수 있습니다. - 오브젝트 리스너/이벤트 사용: 특정 액터를 지속적으로
Tick
에서 찾거나 모든 액터를 순회하는 대신,Delegate
나Interface
를 사용하여 액터 간의 통신을 이벤트 기반으로 처리하면 불필요한 연산을 줄일 수 있습니다.
이제 언리얼 엔진에서 액터를 생성하고, 월드에서 찾고, 참조하며, 필요에 따라 파괴하는 방법에 대한 깊이 있는 지식을 갖추게 되셨습니다. 액터 관리는 게임의 복잡성을 다루고 성능을 최적화하는 데 핵심적인 부분입니다.