KoreanFoodie's Study
[언리얼] 동적으로 액터 컴포턴트 생성하기 (Dynamically create ActorComponent) 본문
[언리얼] 동적으로 액터 컴포턴트 생성하기 (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<UStaticMeshComponent>();
if (LodComponent)
{
return LodComponent;
}
// Now create the component
LodComponent = NewObject<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 에서 액터에 동적으로 액터 컴포넌트 붙이기
'Game Dev > Unreal C++ : Dev Log' 카테고리의 다른 글
[언리얼] ENUM_CLASS_FLAGS 사용하기 (언리얼 플래그 사용법) (0) | 2023.03.14 |
---|---|
[언리얼] TimerHandle 배열 사용하기 (TimerHandle Array) (0) | 2023.03.10 |
SetTimer 에 함수 및 람다(Lambda) 연결하기 (0) | 2023.02.20 |
[언리얼] 현재 시간 로그 찍기 (0) | 2023.02.02 |
[언리얼] SafeZone 위젯 (0) | 2023.01.16 |