본문 바로가기

언리얼 개발자

[Unreal] Property Editor : 조건부로 변수 노출하기

* 방법만 확인하실 분은 [ 내가 만든 클래스에 적용해보기 ] 부터 보시면 됩니다.

 

[ 언리얼 BT에서 확인한 에디터 기능 ]

 Unreal에서 제공하는 BehaviorTree[ Blackboard ] Decorator의 디테일 패널을 보면 흥미로운 부분이 있다.

Blackboard Decorator

 

 바로 아래 나오는 이미지들인데,

선택한 [ Blackboard Key ] 자료형에 따라 [ Key Query ] 항목과 [ Key Value ] 가 달라지거나 없어진다.

Actor Value
Float Value
String Value

 

https://docs.unrealengine.com/4.26/ko/ProgrammingAndScripting/GameplayArchitecture/Metadata/

 언리얼에서 제공해주는 [ UPROPERTY ] 를 통해 에디터 상에 노출을 해줄 수 있고, 다양한 메타데이터 지정자를 통해 조작을 할 수 있으나 조건부로 변수를 다르게 하거나, 사라지게 하는 옵션은 찾을 수 없었다.

 

 

그래서 해당 클래스로 가보니, [ Key Value ] 항목은 그냥 아래와 같이 선언되어 있을 뿐이었다.

UBTDecorator_Blackboard Class [ Key Value ]

 

[ Key Query ] 항목도 [ Key Value ]와 다를 바가 없었다.

UBTDecorator_Blackboard Class [ Key Query ]

 

[ 내가 만든 클래스에서 테스트해보기 ]

APropertyVisiblityTestActor Class [ Key Value ]
Editor Result

 테스트로 위와 같이 코드를 작성한 후에 에디터에서 확인하면 결과는 바로 위의 이미지와 같다.

저렇게 Key Value라는 DisplayName을 가진 Property가 선언한 개수만큼 나와야 한다. 하지만 Blackboard Decorator에서 확인했을 때는 적절한 Key Value 변수 외에는 모두 디테일 패널에서 제거되었다.

 

 결과를 먼저 말해보자면, UBTDecorator_Blackboard 클래스에서 friend 선언을 해둔

[ FBlackboardDecoratorDetails ] 라는 클래스가 조건을 충족하는 Key Value만을 에디터 상에 노출시키고 있었다. 저 클래스를 살펴보면 우리가 만든 클래스에도 적용하는 것은 어렵지 않다.

 

[ 내가 만든 클래스에 적용해보기 ]

< 준비 사항 >

 Custom Detail 패널 기능을 사용하기 위해서는 코드가 속해있는 모듈의 Build.cs 파일에 

'PropertyEditor'와 'SlateCore' 모듈을 참조해주어야 한다.

 

< PropertyVisiblityTestActor.h >

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "GameFramework/Actor.h"

#include "IDetailCustomization.h"

#include "PropertyVisiblityTestActor.generated.h"

struct EVisibility;

/*
	선택된 Value type
*/
UENUM(BlueprintType)
enum class ESelectedValueType : uint8
{
	None,
	Int,
	Float,
	String,
};

UCLASS()
class FUNCTESTPROJ_API APropertyVisiblityTestActor : public AActor
{
	GENERATED_BODY()
	
public:	
	APropertyVisiblityTestActor();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

protected:
	UPROPERTY(Category = EditTest, EditAnywhere, meta = (DisplayName = "Key Value"))
		int32 m_IntValue;

	UPROPERTY(Category = EditTest, EditAnywhere, meta = (DisplayName = "Key Value"))
		float m_FloatValue;

	UPROPERTY(Category = EditTest, EditAnywhere, meta = (DisplayName = "Key Value"))
		FString m_StringValue;

	UPROPERTY(Category = EditTest, EditAnywhere, meta = (DisplayName = "Key Query"))
		ESelectedValueType m_SelectedValueType;

	/*
		멤버 변수 Name 등에 접근할 수 있도록 friend 선언
	*/
	friend class FPropertyVisiblityTestActorDetails;
};

class FPropertyVisiblityTestActorDetails : public IDetailCustomization
{
public:
	FPropertyVisiblityTestActorDetails();

	/*
		[APropertyVisiblityTestActor] 클래스를 에디팅할 때 사용할 Detail 객체를 반환
	*/
	static TSharedRef<IDetailCustomization> MakeInstance();

	/* 
		Property Visibility 조건부 설정 기능을 작성할 함수
	*/
	virtual void CustomizeDetails(IDetailLayoutBuilder& InDetailLayout) override;

	/*
		에디팅할 수 있는지 조건
	*/
	bool IsEditingEnabled() const;

private:
	/*
		APropertyVisiblityTestActor::m_SelectedValueType의 값 변화 이벤트를 받을 함수
	*/
	void OnChangeSelectedValueType();

	/*
		APropertyVisiblityTestActor::m_IntValue 변수의 노출 조건
	*/
	EVisibility GetIntValueVisibility() const;
	/*
		APropertyVisiblityTestActor::m_FloatValue 변수의 노출 조건
	*/
	EVisibility GetFloatValueVisibility() const;
	/*
		APropertyVisiblityTestActor::m_StringValue 변수의 노출 조건
	*/
	EVisibility GetStringValueVisibility() const;

private:
	/*
		에디팅할 대상 캐싱
	*/
	TWeakObjectPtr<APropertyVisiblityTestActor> m_CachedPropertyVisiblityTestActor;

	/*
		에디팅할 대상의 현재 APropertyVisiblityTestActor::m_SelectedValueType 캐싱
		: 이 타입에 따라 어떤 [ Key Value ] 를 보여줄 지 지정합니다.
	*/
	ESelectedValueType m_CachedSelectedValueType;
};

< PropertyVisiblityTestActor.cpp >

// Fill out your copyright notice in the Description page of Project Settings.


#include "PropertyVisiblityTestActor.h"

/*
	커스텀 디테일 패널 기능을 적용시키기 위한 헤더
*/
#include "DetailLayoutBuilder.h"
#include "DetailCategoryBuilder.h"
#include "PropertyHandle.h"
#include "Layout/Visibility.h"

/*
	커스텀 디테일 패널을 등록해주기 위한 헤더
*/
#include "Modules/ModuleManager.h"
#include "PropertyEditorModule.h"

// Sets default values
APropertyVisiblityTestActor::APropertyVisiblityTestActor()
	: m_IntValue(0)
	, m_FloatValue(0.0f)
	, m_StringValue(TEXT("Hello"))
	, m_SelectedValueType(ESelectedValueType::None)
{
	PrimaryActorTick.bCanEverTick = true;

	/*
		[주의] 이 코드들이 속한 모듈의 StartupModule() 쪽에서 세팅해주는게 적절하나, 테스트 편의 상 이 곳에 작성합니다.
		: APropertyVisiblityTestActor 클래스의 CustomClassLayout을 새로 만든 FPropertyVisiblityTestActorDetails로 등록
	*/
	FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
	PropertyModule.RegisterCustomClassLayout("PropertyVisiblityTestActor", FOnGetDetailCustomizationInstance::CreateStatic(&FPropertyVisiblityTestActorDetails::MakeInstance));
}

// Called when the game starts or when spawned
void APropertyVisiblityTestActor::BeginPlay()
{
	Super::BeginPlay();
	
}

// Called every frame
void APropertyVisiblityTestActor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

#pragma region FPropertyVisiblityTestActorDetails
FPropertyVisiblityTestActorDetails::FPropertyVisiblityTestActorDetails()
	: m_CachedPropertyVisiblityTestActor(nullptr)
	, m_CachedSelectedValueType(ESelectedValueType::None)
{

}

TSharedRef<IDetailCustomization> FPropertyVisiblityTestActorDetails::MakeInstance()
{
	return MakeShareable(new FPropertyVisiblityTestActorDetails);
}

void FPropertyVisiblityTestActorDetails::CustomizeDetails(IDetailLayoutBuilder& InDetailLayout)
{
	// >> : 에디팅 대상 찾아 캐싱하기
	TArray<TWeakObjectPtr<UObject> > MyOuters;
	InDetailLayout.GetObjectsBeingCustomized(MyOuters);

	m_CachedPropertyVisiblityTestActor.Reset();
	for (int32 i = 0; i < MyOuters.Num(); i++)
	{
		APropertyVisiblityTestActor* EditingActor = Cast<APropertyVisiblityTestActor>(MyOuters[i].Get());
		if (EditingActor != nullptr)
		{
			m_CachedPropertyVisiblityTestActor = EditingActor;
			break;
		}
	}

	if (m_CachedPropertyVisiblityTestActor.IsValid() == false)
	{
		UE_LOG(LogTemp, Error, TEXT("Fail to find m_CachedPropertyVisiblityTestActor[APropertyVisiblityTestActor] in InDetailLayout[IDetailLayoutBuilder] outers"));
		return;
	}
	// << : 에디팅 대상 찾아 캐싱하기

	// Property의 IsEnabled 조건 적용. IsEditingEnabled()가 false를 리턴할 경우, 에디팅하지 못하도록 회색으로 Block 처리 된다.
	TAttribute<bool> PropertyEditCheck = TAttribute<bool>::Create(TAttribute<bool>::FGetter::CreateSP(this, &FPropertyVisiblityTestActorDetails::IsEditingEnabled));

	// 고려할 Property가 속해있는 Category 가져오기
	IDetailCategoryBuilder& EditTestCategory = InDetailLayout.EditCategory("EditTest");

	// 조건이 될 Enum 변수의 Change Event 바인딩하기
	TSharedPtr<IPropertyHandle> CondPropertyHandle = InDetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(APropertyVisiblityTestActor, m_SelectedValueType), APropertyVisiblityTestActor::StaticClass());
	if (CondPropertyHandle.IsValid() == true)
	{
		FSimpleDelegate OnKeyChangedDelegate = FSimpleDelegate::CreateSP(this, &FPropertyVisiblityTestActorDetails::OnChangeSelectedValueType);
		CondPropertyHandle->SetOnPropertyValueChanged(OnKeyChangedDelegate);
		OnChangeSelectedValueType();
	}

	// APropertyVisiblityTestActor::m_IntValue에 대한 조건 설정
	IDetailPropertyRow& IntValueRow = EditTestCategory.AddProperty(InDetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(APropertyVisiblityTestActor, m_IntValue)));
	IntValueRow.Visibility(TAttribute<EVisibility>::Create(TAttribute<EVisibility>::FGetter::CreateSP(this, &FPropertyVisiblityTestActorDetails::GetIntValueVisibility)));
	IntValueRow.IsEnabled(PropertyEditCheck);

	// APropertyVisiblityTestActor::m_FloatValue에 대한 조건 설정
	IDetailPropertyRow& FloatValueRow = EditTestCategory.AddProperty(InDetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(APropertyVisiblityTestActor, m_FloatValue)));
	FloatValueRow.Visibility(TAttribute<EVisibility>::Create(TAttribute<EVisibility>::FGetter::CreateSP(this, &FPropertyVisiblityTestActorDetails::GetFloatValueVisibility)));
	FloatValueRow.IsEnabled(PropertyEditCheck);

	// APropertyVisiblityTestActor::m_StringValue에 대한 조건 설정
	IDetailPropertyRow& StringValueRow = EditTestCategory.AddProperty(InDetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(APropertyVisiblityTestActor, m_StringValue)));
	StringValueRow.Visibility(TAttribute<EVisibility>::Create(TAttribute<EVisibility>::FGetter::CreateSP(this, &FPropertyVisiblityTestActorDetails::GetStringValueVisibility)));
	StringValueRow.IsEnabled(PropertyEditCheck);
}

bool FPropertyVisiblityTestActorDetails::IsEditingEnabled() const
{
	/*
		따로 조건 지정하지 않음
	*/
	return true;
}

void FPropertyVisiblityTestActorDetails::OnChangeSelectedValueType()
{
	m_CachedSelectedValueType = ESelectedValueType::None;

	if (m_CachedPropertyVisiblityTestActor.IsValid() == false)
	{
		UE_LOG(LogTemp, Error, TEXT("Invalid m_CachedPropertyVisiblityTestActor[APropertyVisiblityTestActor]"));
		return;
	}

	// 에디팅 대상의 변경된 SelectedValueType 저장
	m_CachedSelectedValueType = m_CachedPropertyVisiblityTestActor->m_SelectedValueType;
}

EVisibility FPropertyVisiblityTestActorDetails::GetIntValueVisibility() const
{
	return m_CachedSelectedValueType == ESelectedValueType::Int ? EVisibility::Visible : EVisibility::Collapsed;
}

EVisibility FPropertyVisiblityTestActorDetails::GetFloatValueVisibility() const
{
	return m_CachedSelectedValueType == ESelectedValueType::Float ? EVisibility::Visible : EVisibility::Collapsed;
}

EVisibility FPropertyVisiblityTestActorDetails::GetStringValueVisibility() const
{
	return m_CachedSelectedValueType == ESelectedValueType::String ? EVisibility::Visible : EVisibility::Collapsed;
}
#pragma endregion // FPropertyVisiblityTestActorDetails

 

 [ 실행 결과 ]

ESelectedValueType == None
ESelectedValueType == Int
ESelectedValueType == Float
ESelectedValueType == String

 위 이미지들처럼 내가 지정한 Enum 값에 따라 적절한 Key Value만 보여주는 것을 확인할 수 있다.

 

[ 마치며 ]

 위에 올려져 있는 코드들은 디테일 패널에 프로퍼티 노출과 관련된 기능들이 있음을 전달하기 위한 테스트 코드들입니다.

인게임에서 사용할 클래스와 Editor Tool 코드가 함께 있는 것부터, Actor의 생성자에서 특정 Module을 가져와 디테일 패널 클래스를 적용하는 괴랄한 코드들은 무시해주시면 감사하겠습니다.