KoreanFoodie's Study

[언리얼] 동적으로 액터 컴포턴트 생성하기 (Dynamically create ActorComponent) 본문

Game Dev/Unreal C++ : Dev Log

[언리얼] 동적으로 액터 컴포턴트 생성하기 (Dynamically create ActorComponent)

GoldGiver 2023. 3. 10. 16:20

동적으로 컴포턴트 생성하기 (Dynamically create ActorComponent)

핵심 :

1. 일반적으로 ActorComponent 는 생성자에서 CreateDefaultSubobject 함수를 통해 CDO 가 생성된다.
2. 동적으로 액터 컴포넌트를 생성할 때는 두 가지 방법이 있다. 블루프린트에서는 AddComponent 함수를 사용하면 되며,  C++ 에서는 NewObject 로 ActorComponent 를 생성 후, 기존 RootComponent 에 Attach 하는 방식을 활용한다.
3. 커스텀 AddComponent 함수를 만들어서 편하게 활용하거나, 템플릿 함수를 만드는 것도 좋은 방법이다.

블루프린트에서

AActor 클래스에는 AddComponent 라는, 내부적으로 NewObject 를 호출하여 넘겨 받은 Template Object 인자로부터 컴포넌트 객체를 생성하는 함수가 있다. Actor.h 에는 선언, ActorConstruction.cpp 에는 해당 함수가 정의되어 있는데... 한 번 정의를 보자.

/** 
 * Creates a new component and assigns ownership to the Actor this is 
 * called for. Automatic attachment causes the first component created to 
 * become the root, and all subsequent components to be attached under that 
 * root. When bManualAttachment is set, automatic attachment is 
 * skipped and it is up to the user to attach the resulting component (or 
 * set it up as the root) themselves.
 *
 * @see UK2Node_AddComponent	DO NOT CALL MANUALLY - BLUEPRINT INTERNAL USE ONLY (for Add Component nodes)
 *
 * @param TemplateName					The name of the Component Template to use.
 * @param bManualAttachment				Whether manual or automatic attachment is to be used
 * @param RelativeTransform				The relative transform between the new component and its attach parent (automatic only)
 * @param ComponentTemplateContext		Optional UBlueprintGeneratedClass reference to use to find the template in. If null (or not a BPGC), component is sought in this Actor's class
 * @param bDeferredFinish				Whether or not to immediately complete the creation and registration process for this component. Will be false if there are expose on spawn properties being set
 */
UFUNCTION(BlueprintCallable, meta=(ScriptNoExport, BlueprintInternalUseOnly = "true", DefaultToSelf="ComponentTemplateContext", InternalUseParam="ComponentTemplateContext,bDeferredFinish"))
UActorComponent* AddComponent(FName TemplateName, bool bManualAttachment, const FTransform& RelativeTransform, const UObject* ComponentTemplateContext, bool bDeferredFinish = false);

설명을 읽어보면... UK2Node_AddComponent 함수는 블루프린트에서만 사용할 수 있다.

언리얼 문서에도 설명이 나름 잘 되어 있는데, 인자 중 하나인 ComponentTemplateContext 에 무엇을 넣어야 하는지 약간 어렵다... 😅

 

 

C++ 에서

일단, 템플릿 버전으로 액터 컴포넌트를 동적으로 생성하는 코드를 한 번 보자.

template<typename T>
T * AddComponentOnExistingActor(AActor& actor, FName component_name, T * parent_component = nullptr)
{
	T * component = NewObject<T>(&actor, component_name);
	
	if (component != nullptr)
	{
		if (parent_component == nullptr) //root component
		{
			actor.SetRootComponent(component);
		}
		else
		{
			component->SetupAttachment(parent_component);
		}

		component->CreationMethod = EComponentCreationMethod::Instance;
		component->RegisterComponent();
	}
	else
	{
		UE_LOG(LogScript, Warning, TEXT("Can't add component : %s"), *(T::StaticClass()->GetFName().ToString()));
	}

	return component;
}

보면, 원하는 타입(T) 의 컴포넌트를 NewObject 를 통해 생성한 후, 원하는 parent_component 에 붙여주고 있다(없을 경우 해당 컴포넌트를 RootComponent 로 설정).

그리고 생성 타입을 EComponentCreationMethod::Instance 로 설정했는데... 해당 타입은 다음과 같이 나뉜다 (ComponentInstanceDataCache.h 에 정의) :

UENUM()
enum class EComponentCreationMethod : uint8
{
	/** A component that is part of a native class. */
	Native,
	/** A component that is created from a template defined in the Components section of the Blueprint. */
	SimpleConstructionScript,
	/**A dynamically created component, either from the UserConstructionScript or from a Add Component node in a Blueprint event graph. */
	UserConstructionScript,
	/** A component added to a single Actor instance via the Component section of the Actor's details panel. */
	Instance,
};

 

그리고 해당 변수(CreationMethod) 는 Actor.h 에 다음과 같이 정의되어 있다.

public:
	/** Describes how a component instance will be created */
	UPROPERTY()
	EComponentCreationMethod CreationMethod;

만약 C++ 에서만 해당 위젯을 사용할 거라면, Native 로 만드는 것이 제일 빠르지 않을까.. 생각이 든다. 물론 블루프린트에서도 조작해야 한다면 다른 옵션을 써야 하겠지만...

 

아, 참고로 위젯 컴포넌트의 여러 설정값은 다음과 같이 넣어 주면 편하다(UWidgetComponenet 는 UActorComponent 의 파생 클래스이다 😉)

testWidget->SetRelativeLocation(FVector(0.0f, 0.0f, 400));
testWidget->SetWidgetSpace(EWidgetSpace::Screen);
testWidget->SetDrawSize(FVector2D(400, 600.0f));
testWidget->SetCastShadow(false);

testWidget->SetupAttachment(GetMesh());
testWidget->CreationMethod = EComponentCreationMethod::Instance;
testWidget->RegisterComponent();

그런데 신기한 건, RegisterComponent 를 SetUpAttachMent 보다 먼저 쓰면, 다음 구문에서 에러가 발생한다.

void USceneComponent::SetupAttachment(class USceneComponent* InParent, FName InSocketName)
{
	if (ensureMsgf(!bRegistered, TEXT("SetupAttachment should only be used to initialize AttachParent and AttachSocketName for a future AttachToComponent. Once a component is registered you must use AttachToComponent. Owner [%s], InParent [%s], InSocketName [%s]"), *GetPathNameSafe(GetOwner()), *GetNameSafe(InParent), *InSocketName.ToString()))
	{
		if (ensureMsgf(InParent != this, TEXT("Cannot attach a component to itself.")))
		{
			if (ensureMsgf(InParent == nullptr || !InParent->IsAttachedTo(this), TEXT("Setting up attachment would create a cycle.")))
			{
				if (ensureMsgf(AttachParent == nullptr || !AttachParent->AttachChildren.Contains(this), TEXT("SetupAttachment cannot be used once a component has already had AttachTo used to connect it to a parent.")))
				{
					SetAttachParent(InParent);
					SetAttachSocketName(InSocketName);
					bShouldBeAttached = AttachParent != nullptr;
				}
			}
		}
	}
}

바로... "SetupAttachment cannot be used once a component has already had AttachTo used to connect it to a parent." 구문이다. 흠좀무. 아마도 Owner 설정이 안되면 내부적으로 World 를 Owner 로 설정해서 그런듯.

 

다음과 같은 예제도 있다.

UStaticMeshComponent \*UDestructibleComponent::FindOrCreateStaticLodMesh()

{

static const FName NAME_StaticMeshComponent = TEXT("StaticLodMesh");



 if (HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject))

  {

 return nullptr;

  }

// If we have a reference to the component, just return it

if (LodComponent)

{

return LodComponent;

}



 // Try to find the component on the owner actor

 LodComponent = GetOwner()->FindComponentByClass&lt;UStaticMeshComponent>();

 if (LodComponent)

 {

 return LodComponent;

 }


 // Now create the component

 LodComponent = NewObject&lt;UStaticMeshComponent>(GetOwner(), NAME_StaticMeshComponent);

 LodComponent->SetupAttachment(GetOwner()->GetRootComponent());

 LodComponent->CreationMethod = EComponentCreationMethod::Instance;

 LodComponent->RegisterComponent();

 return LodComponent;

 }

해당 예제는 UStaticMeshComponent 를 생성하며, 해당 UStaticMeshComponent 를 참조하는 녀석이 없을 경우 자동으로 메모리를 해제해 줄 것이다. 아, 물론 LodComponent 에 UPROPERTY 가 붙어 있어야 할 것이다. 다음과 같이...

UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Mesh", meta = (AllowPrivateAccess = "true"))
UStaticMeshComponent* LodComponent;

하지만 위 예제는 결국 UStaticMeshComponent 를 선언해야 하므로, '진짜' 동적으로 만들고 싶으면 첫번째 방식을 활용하면 된다!

 

참고 : 언리얼 커뮤니티,  GameDev Guide 커뮤니티, BeginPlay 에서 액터에 동적으로 액터 컴포넌트 붙이기
Comments