초보 코린이의 성장 일지

UE4 Weapon Plugin (6) 본문

언리얼

UE4 Weapon Plugin (6)

코오린이 2023. 6. 9. 14:49

체크박스를 생성하고, 체크한 목록에 관한 내용이 나오도록 만들어 나갈 것이다.

1. Grid Panel을 사용할때, 범위가 줄어들게 된다면 줄어든 범위만큼 Text가 보정처리가 안된다.

2. Uniform Grid Panel을 사용하게되면, 범위가 줄어들어도 균등하게 보정처리가 이뤄지기 때문에 이걸 사용할 것이다.


더보기
#pragma once

#include "CoreMinimal.h"

class WEAPON_API SWeaponCheckBoxes
{
public:
	// 하나의 프로퍼티에 대한 식별자 역할, IPropertyHandle
	void AddProperties(TSharedPtr<IPropertyHandle> InHandle);

	// SNew에 최상의 부모가 SWidget이 된다. 레퍼런스를 사용하기 때문에 동일하게 쓰기위함
	TSharedRef<SWidget> Draw(bool bBackground = false);

private:
	// 내부 구조체 생성, 관리를 위해 사용
	struct FInternalData
	{
		bool bChecked;
		FString Name;
		TSharedPtr<IPropertyHandle> Handle;

		FInternalData(TSharedPtr<IPropertyHandle> InHandle)
		{
			bChecked = false;
			Handle = InHandle;

			// 출력할 이름
			Name = Handle->GetPropertyDisplayName().ToString();
		}
	};
	TArray<FInternalData> InternalDatas;

};
#include "SWeaponCheckBoxes.h"
#include "Widgets/Layout/SUniformGridPanel.h"
#include "IPropertyUtilities.h"
#include "IDetailPropertyRow.h"
#include "IDetailChildrenBuilder.h"
#include "DetailWidgetRow.h"

void SWeaponCheckBoxes::AddProperties(TSharedPtr<IPropertyHandle> InHandle)
{
	// 자식 Property가 몇개인지 체크
	uint32 number = 0;
	InHandle->GetNumChildren(number);

	for (uint32 i = 0; i < number; i++)
		InternalDatas.Add(FInternalData(InHandle->GetChildHandle(i)));

}

TSharedRef<SWidget> SWeaponCheckBoxes::Draw(bool bBackground)
{
	TSharedPtr<SUniformGridPanel> panel;
	SAssignNew(panel, SUniformGridPanel); // SNew와 다르게 변수를 선언해 놓는 SAssignNew
	panel->SetMinDesiredSlotWidth(150); // 간격 크기 고정

	for (int32 i = 0; i < InternalDatas.Num(); i++)
	{
		panel->AddSlot(i, 0) // 한줄만 사용, 여러줄사용하려면 ex) i % 5
		[
			SNew(SCheckBox)
			[
				SNew(STextBlock)
				.Text(FText::FromString(InternalDatas[i].Name)) // 이름 출력
			]
		];
	}

	return panel.ToSharedRef(); // 추가를 하게되면, 추가된 그자체를 return

}

1. 구조체를 내부적으로 선언하여 사용.

2. UniformGridPanel를 사용하여 간격을 균등하게 조절 및 크기 150 으로 고정


더보기
#pragma once

#include "CoreMinimal.h"
#include "IPropertyTypeCustomization.h"

class WEAPON_API SWeaponEquipmentData
	: public IPropertyTypeCustomization
{
public:
	// 외부에서 불러 사용하도록 함수 선언
	static TSharedPtr<class SWeaponCheckBoxes> CreateCheckBoxes();

	void CustomizeHeader(TSharedRef<IPropertyHandle> InPropertyHandle, FDetailWidgetRow& InHeaderRow, IPropertyTypeCustomizationUtils& InCustomizationUtils) override;
	void CustomizeChildren(TSharedRef<IPropertyHandle> InPropertyHandle, IDetailChildrenBuilder& InChildBuilder, IPropertyTypeCustomizationUtils& InCustomizationUtils) override;

private:
	// 열릴때마다 객체가 생성되므로, static로 선언해서 소유하고 있게 만들어준다.
	static TSharedPtr<class SWeaponCheckBoxes> CheckBoxes;
};
#include "SweaponEquipmentData.h"
#include "IPropertyUtilities.h"
#include "IDetailPropertyRow.h"
#include "IDetailChildrenBuilder.h"
#include "DetailWidgetRow.h"
#include "IDetailCustomization.h"
#include "SWeaponCheckBoxes.h"

TSharedPtr<SWeaponCheckBoxes> SWeaponEquipmentData::CheckBoxes; // static로 선언했기 때문에 초기화 필요.

TSharedRef<IPropertyTypeCustomization> SWeaponEquipmentData::MakeInstance()
{
    return MakeShareable(new SWeaponEquipmentData());
}

TSharedPtr<SWeaponCheckBoxes> SWeaponEquipmentData::CreateCheckBoxes()
{
    if (CheckBoxes.IsValid())
    {
        CheckBoxes.Reset(); 
        CheckBoxes = nullptr;
    }

    return CheckBoxes = MakeShareable(new SWeaponCheckBoxes()); // 생성하고 대입한 상태로 return
}

// 매개변수 InPropertyHandle가 해당 변수에 식별자를 나타낸다.
void SWeaponEquipmentData::CustomizeHeader(TSharedRef<IPropertyHandle> InPropertyHandle, FDetailWidgetRow& InHeaderRow, IPropertyTypeCustomizationUtils& InCustomizationUtils)
{
   // GLog->Log("CustomizeHeader");

    // 접히는 라인
    InHeaderRow
        .NameContent()
        [
            // Handle에는 기본 모양이 존재하지만, 커스텀 한 모양대로 만들어주기
            InPropertyHandle->CreatePropertyValueWidget()
        ]
		.ValueContent() // 슬레이트 UI 다루는 문법과 동일
		// 사이즈 조절, 마우스로 끌어서 크기를 변경할때 적정선을 못넘어 가도록 고정 (최대, 최소 사이즈)
        .MinDesiredWidth(FEditorStyle::GetFloat("StandardDialog.MinDesiredSlotWidth"))
        .MaxDesiredWidth(FEditorStyle::GetFloat("StandardDialog.MaxDesiredSlotWidth"))
        [
            // InPropertyHandle->CreatePropertyValueWidget() // 접히는 부분
            CheckBoxes->Draw() // 바로 그려준다.
        ];
}

1. 체크 박스를 생성해 주기전에, 목록 카테고리를 만들어주고, 마우스로 드래그에서 창을 높이고 줄여도, 알맞게 위치가 균등하게 조정된다.


더보기
#include "SWeaponDetailsView.h"
#include "SWeaponCheckBoxes.h"
#include "SWeaponEquipmentData.h"
#include "DetailLayoutBuilder.h"
#include "DetailCategoryBuilder.h"
#include "IDetailPropertyRow.h"
#include "Weapons/CWeaponAsset.h"

void SWeaponDetailsView::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
	// EquipmentData
	{
 	    IDetailCategoryBuilder& category = DetailBuilder.EditCategory("EquipmentData", FText::FromString("Equipment Data"));
		IDetailPropertyRow& row = category.AddProperty("EquipmentData", type);
        
		// 처음에 만들어질때 CheckBox 생성
		TSharedPtr<SWeaponCheckBoxes> checkBoxes = SWeaponEquipmentData::CreateCheckBoxes();
		checkBoxes->AddProperties(row.GetPropertyHandle());  // 실제로 가진 Handle 추가해준다.
	}

}

1. 실제로 부르게되는 Handle를 추가해줘서 보이게 만들어 주기.

1. 체크박스 카테고리만 보인다.


더보기
TSharedRef<SWidget> SWeaponCheckBoxes::Draw(bool bBackground)
{
	TSharedPtr<SUniformGridPanel> panel;
	SAssignNew(panel, SUniformGridPanel); // SNew와 다르게 변수를 선언해 놓는 SAssignNew
	panel->SetMinDesiredSlotWidth(150); // 간격 크기 고정

	for (int32 i = 0; i < InternalDatas.Num(); i++)
	{
		panel->AddSlot(i, 0) // 한줄만 사용, 여러줄사용하려면 ex) i % 5
		[
			SNew(SCheckBox) // 체크박스는 컨텐츠 영역을 가지고 있다.
			.IsChecked(InternalDatas[i].bChecked) // 체크 여부 출력, 0과 1로만 판단
			[
				SNew(STextBlock)
				.Text(FText::FromString(InternalDatas[i].Name)) // 이름 출력
			]
		];
	}

	return panel.ToSharedRef(); // 추가를 하게되면, 추가된 그자체를 return

1. 체크 박스를 이제 보이도록 만들어준다.

1. 체크 박스 생성.


이 문법에서 this로 자기자신을 연결시킬때 오류가 발생한다.  그전까지는 자신을 연결해도 정상적으로 작동했는데 이 오류에 대해 알아보려고 한다.

 

CheckBox를 할당할때 MakeShareable를 사용했다. this가 SharedPtr로 공유 포인터이다.

우선 공유 포인터가 this 포인터인지 체크부터 해줘야하는데 여기서 문제가 발생한다. 왜냐 다른 주소가 먼저 들어와있게 된다면 공유하고 this에 주소가 달라지기 때문에 다른이가 공유하면서 사용하게 됐을때 정상적으로 원하는 주소에 접근을 할 수 없게된다.

위에서 오류가 난 원인은 침범을 했기 때문이다. 해결하는 방법은 언리얼 공식문서에 정리되어있다. 

AsShared는 공유될 수 있는 포인터인지,  SharedThis는 그 안에 있는 this 포인터를 공유해서 사용하기 위해 존재한다.

코드를 보면 더 빠르게 이해하기 편할 것이다.

class FRegistryObject;
class FMyBaseClass: public TSharedFromThis<FMyBaseClass>
{
    virtual void RegisterAsBaseClass(FRegistryObject* RegistryObject)
    {
        // 'this'의 쉐어드 레퍼런스에 접근합니다.
        // <TSharedFromThis>로부터 직접 상속되어 AsShared()와 SharedThis(this)는 동일한 타입을 반환합니다.
        TSharedRef<FMyBaseClass> ThisAsSharedRef = AsShared();
        // RegistryObject는 TSharedRef<FMyBaseClass> 또는 TSharedPtr<FMyBaseClass>를 요구합니다. TsharedRef는 묵시적으로 TsharedPtr로 변환될 수 있습니다.
        RegistryObject->Register(ThisAsSharedRef);
    }
};
class FMyDerivedClass : public FMyBaseClass
{
    virtual void Register(FRegistryObject* RegistryObject) override
    {
        // TSharedFromThis<>로부터 직접 상속되지 않아서 AsShared()와 SharedThis(this)는 각기 다른 타입을 반환합니다.
        // AsShared()는 해당 예제 내 TSharedFromThis<> - TSharedRef<FMyBaseClass>에서 정의된 본래 타입을 반환하게 됩니다.
        // SharedThis(this)는 해당 예제 내 'this' - TSharedRef<FMyDerivedClass>의 타입과 함께 TsharedRef를 반환하게 됩니다.
        // SharedThis() 함수는 ‘this' 포인터와 동일한 범위 내에서만 가능합니다.
        TSharedRef<FMyDerivedClass> AsSharedRef = SharedThis(this);
        // FmyDerivedClass는 FmyBaseClass 타입의 일종이기 때문에 RegistryObject가 TSharedRef<FMyDerivedClass>를 허용합니다.
        RegistryObject->Register(ThisAsSharedRef);
    }
};
class FRegistryObject
{
    // 이 함수는 FmyBaseClass나 그 자녀 클래스에 TsharedRef나 TsharedPtr를 허용합니다.
    void Register(TSharedRef<FMyBaseClass>);
};

언리얼 문서에 정리되어있는 예제 코드이다.

중요한 핵심은 "<TSharedFromThis>"가 부모에는 직접 상속되어 있지만, 자식에는 상속이 되어 있지 않다는 점이다.

TSharedFromThis가 붙은 객체는 주소를 반환할때 똑같은 주소를 반환하게된다.

하지만 TSharedFromThis가 붙지 않은 객체는 주소를 반환할때 다른 주소를 반환하게 된다.

 위 코드에 TSharedRef<FMyDerivedClass> AsSharedRef = SharedThis(this); 부분에 this와 Shared에 포인터가 가지고 있는 주소는 다르다. 그 이유는 this 포인터가 다시한번 레핑된 주소가 return이 되기 때문에 주소가 달라지게 되는 것이다. 이 다른 주소를 일치시키는 키워드가 존재한다 "public TSharedFromThis<>" 를 상속받으면 된다.


더보기
#pragma once

#include "CoreMinimal.h"

class WEAPON_API SWeaponCheckBoxes
	: public TSharedFromThis<SWeaponCheckBoxes> // 직접적으로 상속받으면 주소가 일치하게된다.
{
public:
	// 하나의 프로퍼티에 대한 식별자 역할, IPropertyHandle
	void AddProperties(TSharedPtr<IPropertyHandle> InHandle);

	// SNew에 최상의 부모가 SWidget이 된다. 레퍼런스를 사용하기 때문에 동일하게 쓰기위함
	TSharedRef<SWidget> Draw(bool bBackground = false);

private:
	// 체크가 완료되었는지 
	void OnCheckStateChanged(ECheckBoxState InState, int32 InIndex);

private:
	// 내부 구조체 생성, 관리를 위해 사용
	struct FInternalData
	{
		bool bChecked;
		FString Name;
		TSharedPtr<IPropertyHandle> Handle;

		FInternalData(TSharedPtr<IPropertyHandle> InHandle)
		{
			bChecked = false;
			Handle = InHandle;

			// 출력할 이름
			Name = Handle->GetPropertyDisplayName().ToString();
		}
	};
	TArray<FInternalData> InternalDatas;

};
#include "SWeaponCheckBoxes.h"
#include "Widgets/Layout/SUniformGridPanel.h"
#include "IPropertyUtilities.h"
#include "IDetailPropertyRow.h"
#include "IDetailChildrenBuilder.h"
#include "DetailWidgetRow.h"

void SWeaponCheckBoxes::AddProperties(TSharedPtr<IPropertyHandle> InHandle)
{
	// 자식 Property가 몇개인지 체크
	uint32 number = 0;
	InHandle->GetNumChildren(number);

	for (uint32 i = 0; i < number; i++)
		InternalDatas.Add(FInternalData(InHandle->GetChildHandle(i)));

}

TSharedRef<SWidget> SWeaponCheckBoxes::Draw(bool bBackground)
{
	TSharedPtr<SUniformGridPanel> panel;
	SAssignNew(panel, SUniformGridPanel); // SNew와 다르게 변수를 선언해 놓는 SAssignNew
	panel->SetMinDesiredSlotWidth(150); // 간격 크기 고정

	for (int32 i = 0; i < InternalDatas.Num(); i++)
	{
		panel->AddSlot(i, 0) // 한줄만 사용, 여러줄사용하려면 ex) i % 5
		[
			SNew(SCheckBox) // 체크박스는 컨텐츠 영역을 가지고 있다.
			.IsChecked(InternalDatas[i].bChecked) // 체크 여부 출력, 0과 1로만 판단
			.OnCheckStateChanged(this, &SWeaponCheckBoxes::OnCheckStateChanged, i) // 체크 했는지 판단
			[
				SNew(STextBlock)
				.Text(FText::FromString(InternalDatas[i].Name)) // 이름 출력
			]
		];
	}

	return panel.ToSharedRef(); // 추가를 하게되면, 추가된 그자체를 return

}

void SWeaponCheckBoxes::OnCheckStateChanged(ECheckBoxState InState, int32 InIndex)
{
	// 로그로 출력 확인
	GLog->Log(StaticEnum<ECheckBoxState>()->GetValueAsString(InState));

}

1.  public TSharedFromThis<SWeaponCheckBoxes>를 상속 받으면 위에 설명한 원인을 해결할 수 있다.

2. 체크박스 체크가 되었는지 확인하도록 만들어준다.

3. 체크박스 체크시 출력되는지 Log로 확인

1. 체크박스 클릭시 로그가 출력된다.

2. 근데 어떤걸 클릭해서 호출했는지 구분이 불가능하기 때문에 구분을 할 수 있도록 만들어 줄 것이다.

 

더보기
#pragma once

#include "CoreMinimal.h"

class WEAPON_API SWeaponCheckBoxes
	: public TSharedFromThis<SWeaponCheckBoxes> // 직접적으로 상속받으면 주소가 일치하게된다.
{
private:
	// 체크가 완료되었는지 
    // void OnCheckStateChanged(ECheckBoxState InState);
	void OnCheckStateChanged(ECheckBoxState InState, int32 InIndex);
};
#include "SWeaponCheckBoxes.h"
#include "Widgets/Layout/SUniformGridPanel.h"
#include "IPropertyUtilities.h"
#include "IDetailPropertyRow.h"
#include "IDetailChildrenBuilder.h"
#include

TSharedRef<SWidget> SWeaponCheckBoxes::Draw(bool bBackground)
{
	TSharedPtr<SUniformGridPanel> panel;
	SAssignNew(panel, SUniformGridPanel); // SNew와 다르게 변수를 선언해 놓는 SAssignNew
	panel->SetMinDesiredSlotWidth(150); // 간격 크기 고정

	for (int32 i = 0; i < InternalDatas.Num(); i++)
	{
		panel->AddSlot(i, 0) // 한줄만 사용, 여러줄사용하려면 ex) i % 5
		[
			SNew(SCheckBox) // 체크박스는 컨텐츠 영역을 가지고 있다.
			.IsChecked(InternalDatas[i].bChecked) // 체크 여부 출력, 0과 1로만 판단
			.OnCheckStateChanged(this, &SWeaponCheckBoxes::OnCheckStateChanged, i) // 체크 했는지 판단
			[
				SNew(STextBlock)
				.Text(FText::FromString(InternalDatas[i].Name)) // 이름 출력
			]
		];
	}

	return panel.ToSharedRef(); // 추가를 하게되면, 추가된 그자체를 return

}

void SWeaponCheckBoxes::OnCheckStateChanged(ECheckBoxState InState, int32 InIndex)
{
	// 로그로 출력 확인
	GLog->Log(FString::FromInt(InIndex));
	GLog->Log(StaticEnum<ECheckBoxState>()->GetValueAsString(InState));

}

1. 슬레이트 이벤트 특징이있는데, 매개변수에 추가적인 자료형을 넣어주면 확인할 수 있다.

2. void OnCheckStateChanged 뒤에 매개변수 자료형을 설정하고 추가해준다.

3. OnCheckStateChanged(this, &SWeaponCheckBoxes::OnCheckStateChanged, i) 뒤에 i를 넣어줘서 인덱스를 확인.

1. 이제 클릭시 인덱스가 같이 호출되면서, 무엇을 클릭했는지 알 수 있게 된다.


https://www.youtube.com/watch?v=_Pd3_8FEYkY 

 

'언리얼' 카테고리의 다른 글

UE4 Weapon Plugin (8)  (2) 2023.06.13
UE4 Weapon Plugin (7)  (0) 2023.06.12
UE4 Weapon Plugin (5)  (2) 2023.06.07
UE4 Weapon Plugin (4)  (0) 2023.06.01
UE4 Weapon Plugin (3)  (0) 2023.05.31
Comments