나만의 작은 도서관

[TIL][C++] 251030 MMO 서버 개발 126일차: [언리얼] UWorld::SpawnActor 함수에 대해 자세히 알아보기, SpawnActor로부터 반환받은 액터는 BeginPlay() 호출 직전인 시점이다, IsA<T>() 본문

Today I Learn

[TIL][C++] 251030 MMO 서버 개발 126일차: [언리얼] UWorld::SpawnActor 함수에 대해 자세히 알아보기, SpawnActor로부터 반환받은 액터는 BeginPlay() 호출 직전인 시점이다, IsA<T>()

pledge24 2025. 10. 30. 23:54
주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다. 

[언리얼] UWorld::SpawnActor 함수에 대해 자세히 알아보기

  • UWorld::SpawnAcotr()는 월드에 액터를 스폰하는 핵심적인 함수이다.
  • SpawnActor()는 단순히 메모리를 할당하는 것이 아니라,
    • 액터의 생성자 호출,
    • 컴포넌트 등록,
    • 초기화(ConstructionScript, OnConstruction 등),
    • BeginPlay 등
    • 액터의 생명주기에 해당하는 각 작업을 트리거한다.
  • SpawnActor()의 형태는 2가지가 있다(UE5.3 기준). 2 형태는 아래와 같이 생겼다.
// 형태 1. 위치와 회전값을 따로 전달.
// @param Class - 스폰할 클래스(필수 인자)
// @param Transform - 월드에 스폰할 Transform(필수 인자)
// @param SpawnParameters - 추가 파라미터(옵션 인자)
AActor* SpawnActor( UClass* Class, FTransform const* Transform, const FActorSpawnParameters& SpawnParameters = FActorSpawnParameters());

// 형태 2. Transform 자체를 인자로 전달
// @param Class - 스폰할 클래스(필수 인자)
// @param Location- 월드에 스폰할 위치(옵션 인자)
// @param Rotation- 월드에 스폰할 회전값(옵션 인자)
// @param SpawnParameters - 추가 파라미터(옵션 인자)
AActor* SpawnActor( UClass* InClass, FVector const* Location=NULL, FRotator const* Rotation=NULL, const FActorSpawnParameters& SpawnParameters = FActorSpawnParameters() );

 

 

FActorSpawnParameters?

  • 액터를 스폰할 때 추가적으로 전달해주고 싶은 파라미터들을 묶어둔 “구조체”. 생긴건 아래와 같이 생겼다.
struct ENGINE_API FActorSpawnParameters
{
	FActorSpawnParameters();

	/* A name to assign as the Name of the Actor being spawned. If no value is specified, the name of the spawned Actor will be automatically generated using the form [Class]_[Number]. */
	FName Name;

	/* An Actor to use as a template when spawning the new Actor. The spawned Actor will be initialized using the property values of the template Actor. If left NULL the class default object (CDO) will be used to initialize the spawned Actor. */
	AActor* Template;

	/* The Actor that spawned this Actor. (Can be left as NULL). */
	AActor* Owner;

	/* The APawn that is responsible for damage done by the spawned Actor. (Can be left as NULL). */
	APawn*	Instigator;

	/* The ULevel to spawn the Actor in, i.e. the Outer of the Actor. If left as NULL the Outer of the Owner is used. If the Owner is NULL the persistent level is used. */
	class	ULevel* OverrideLevel;
	
	// ... 그 뒤로 길게 있다.
}
  • FActorSpawnParameters를 설정해야 하는 상황은 딱히 마주한 적은 없었기에 추가적인 설명은 하지 않겠다.

 

 

다운캐스팅 패턴

  • SpawnActor는 반환값이 AActor*이므로, 내가 스폰한 액터 클래스 타입으로 다운캐스팅을 해야 한다.
  • 다운캐스팅 패턴은 크게 두 가지가 있다. 첫 번째는 Cast <T>를 사용하는 것이고, 두 번째는 SpawnActor <T>를 사용하는 것이다.
// 첫번째 Cast<T>
AMyActor* NewActor = Cast<AMyActor>(GetWorld()->SpawnActor(Class));

// 두번째 SpawnActor<T>
AMyActor* NewActor = GetWorld()->SpawnActor<AMyActor>(Class);
  • 여기서 SpawnActor <T>가 조금 불편한 수 있는 부분이 있는데, 위치와 회전값 중 하나만 넘겨줘야 하는 경우가 있을 때, 일반 형태는 하나만 넘겨줄 수 있는 반면, 템플릿 형태는 이 둘에 대한 변수를 인자로 전달해줘야 한다는 것이다.
// 이미 준비되어 있는 Location 값
FVector SpawnLocation(10.f, 10.f, 0.f);

// 첫번째 Cast<T> - Location 넘겨주는 버전
AMyActor* NewActor = Cast<AMyActor>(GetWorld()->SpawnActor(Class, &SpawnLocation));

// 두번째 SpawnActor<T> - Location 넘겨주는 버전
FRotator SpawnRotator = FRotator::ZeroRotator; // 반드시 필요
AMyActor* NewActor = GetWorld()->SpawnActor<AMyActor>(Class, SpawnLocation, SpawnRotator);

 

 


SpawnActor로부터 반환받은 액터는 BeginPlay() 호출 직전인 시점이다.

  • SpawnActor는 동기 함수이기 때문에, 액터 포인터를 반환받는 시점은 항상 같은 시점임을 보장받을 수 있다. 그 시점이 바로 BeginPlay() 호출 직전인 시점이 된다.

 

SpawnActor가 반환될 때까지 진행된 과정들은?

  • 앞에서 잠깐 언급한 것처럼 액터가 스폰될 때 굉장히 많은 파이프라인을 거쳐간다. 반환된 시점에서 SpawnActor가 지나온 파이프라인 중 중요한 시점은 아래와 같다.
    • OnConstruction()
      • PostSpawnInitialize, PostActorCreated, ExecuteConstruction을 지나온 시점에 트리거.
      • C++에서 설정해야 할 모든 프로퍼티와 구조가 완료된 상태
      • 에디터의 편집 상태에서도 이미 호출된 상태인 시점이다.
    • InitializeComponents()
      • 컴포넌트들을 초기화하는 단계.
      • C++ 전용 함수. (블루프린트에서 오버라이드 불가능)
      • 런타임에 실행해야 하는 초기화 로직들을 넣는 시점이며, 플레이(Play)가 시작되어 액터가 실제 월드(World)에 등록된 시점 이후에만 호출된다.
        • 즉, 게임 실행 중에만 필요한 로직들이 들어있다.

 

 

BeginPlay()는 다음 틱에 호출된다.

  • 현재 틱에 SpawnActor를 호출했다면, 적어도 현재 틱에서는 BeginPlay()가 호출되지 않을 거라는 보장을 받을 수 있다. 따라서, 아래와 같이 초기화 코드를 넣어도 안전하다.
AActor* NewActor = GetWorld()->SpawnActor<AActor>(SomeClass, Transform);
// 여기서 NewActor는 이미 생성 완료, BeginPlay는 아직 호출되지 않음.
NewActor->ManualInitialize(); // <-- 항상 BeginPlay보다 먼저 실행됨이 보장됨.

  • 그런데 이 방법에서 안전하지 않은 경우가 존재한다. SpawnActor가 액터를 반환하는 시점에서 해당 액터는 모든 컴포넌트의 초기화가 끝난 상태일 것이다. 그래서 컴포넌트 초기화 결과에 영향을 주는 함수를 호출한다면, 원하는 결과가 나오지 않을 수 있다.
// 잘못된 방법 - 컴포넌트가 이미 초기화됨
ACharacter* Character = SpawnActor<ACharacter>();
Character->bUseControllerRotationYaw = true; // 너무 늦음!

 

 

그래서 사용하는 것이 SpawnActorDeferred()!

  • 그래서 사용하는 함수가 바로 SpawnActorDeferred() 함수이다. 이 함수는 FinishSpawningActor를 호출하기 전까지 OnConstruction이 호출되지 않으므로 OnConstruction이 먼저 실행되지 않음을 보장받을 수 있다. 따라서, OnConstruction보다 뒤에 있는 InitializeComponents 함수도 자연스레 실행되지 않음을 보장할 수 있다.
// 올바른 방법
ACharacter* Character = SpawnActorDeferred<ACharacter>();
Character->bUseControllerRotationYaw = true; // 컴포넌트 초기화 전 설정
Character->FinishSpawning(Transform);

 

 

사용 예제?

// AI 난이도에 따라 스탯이 달라지는 경우
AEnemy* Enemy = World->SpawnActorDeferred<AEnemy>(
    EnemyClass, 
    SpawnTransform
);

// PostInitializeComponents에서 이 값들을 참조함
Enemy->Difficulty = HardMode;
Enemy->MaxHealth = 200;
Enemy->MovementSpeed = 500;

Enemy->FinishSpawning(SpawnTransform);
// 이제 PostInitializeComponents에서 설정된 값으로 초기화됨

 

 

 


[언리얼] 포인터는 필요 없고 타입 체크만 하고 싶은 경우 - IsA <T>()

  • Cast <T>() 함수를 사용하면 반환되는 값을 통해 타입 체크가 가능하다.
if(AMyActor* MyActor = Cast<AMyActor>(SomeActor))
{
	// 캐스팅이 가능할때만 진입
}
  • 그런데, 반환되는 포인터 변수는 필요 없을 때가 있다. 타입 체크만 하고 싶은 경우에 Cast <T>()는 과하다.
  • 이럴 때 사용할 수 있는 것이 IsA <T>이다. 이 함수는 타입 체크의 결과를 bool타입으로 반환한다.
if(SomeActor->IsA<AMyActor>())
{
	// 캐스팅이 가능할때만 진입
}