[UE4] WeaponAssetEditor 만들기 (FAssetEditorToolkit, SDockTab, ListView, SCompoundWidget, Construct, FArguments, SLATE_ARGS, SAssignNew, SMultiColumnTableRow, GenerateWidgetForColumn)
💡FAssetEditorToolkit
FAssetEditorToolkit은 애셋을 편집하기 위한 필수 기능들로 애셋을 저장하고, 해당 애셋이 콘텐츠 브라우저에서 어디에 위치하는지를 찾는 작업을 제공하는 언리얼 엔진의 클래스이다.
이 클래스를 상속받고 추가적인 기능을 가진 클래스를 설계하면 새로운 유형의 애셋 에디터를 만들 수 있다.
FAssetEditorToolkit을 상속받은 클래스는 부모 클래스에서 지정된 몇 가지 기본적인 기능들을 스스로 구현해주어야 한다.
애셋 에디터의 기본 레이아웃은 FTabManager의 inner 클래스인 FLayout을 사용해 제작한다.
💻 결과물 먼저 보기
Weapon의 DataAsset을 편리하게 설정할 수 있는 Weapon 애셋 에디터를 제작해보았다.
레이아웃을 짜고, 커스텀 Window 창을 띄우기 위한 과정이 복잡하기 때문에 우선 현재까지 제작한 결과물을 먼저 확인해보자.
툴바에 Weapon이라는 커스텀 버튼을 추가하여 Command 입력이 들어오면 Weapon Asset Editor 창의 열리도록 구현하였다.
툴바에 버튼을 추가하는 방법은 이전 게시글에 정리해두었다.
위 창이 내가 힘겹게 만든 Window 창이다..!
1️⃣ FWeaponAssetEditor : FAssetEditorToolkit 클래스 만들기
FAssetEditorToolkit을 상속받는 WeaponAssetEditor 클래스를 제작하였다.
🚩 순수가상함수
먼저, FAssetEditorToolkit 클래스의 순수가상함수를 재정의하였다.
GetToolkitFName 함수는 편집할 창의 이름을 반환하고,
GetBaseToolkitName 함수는 언리얼에서 식별할 에디터의 이름을 반환한다.
GetWorldCentricTabPrefix 함수는 창의 Tab 이름을 반환하고,
GetWorldCentricTabColorScale 함수는 창에서 편집할 애셋의 색상을 반환한다.
에디터 이름은 static 변수로 저장해두었다.
🚩OpenWindow / Shutdown 함수
WeaponAssetEditor 클래스를 싱글톤 형식으로 사용하기 위해 자기 자신과 같은 자료형의 Instance 변수를 선언해두었다.
따라서 Window을 열 때 Instance가 존재한다면, 즉, 이미 열린 창이 있다면 창을 닫은 후 Instance를 다시 생성하여 열 수 있도록 구현하였다.
CloseWindow와 Reset 함수는 부모의 함수이다. (Open은 내가 만들 함수)
Shutdown 함수는 Instance가 존재한다면 창을 닫고 NULL로 초기화해주었다.
🚩 Open 함수
에디터 창의 Layout을 생성한 뒤, InitAssetEditor 함수를 통해 에디터 창을 초기화해주었다.
InitAssetEditor 함수에 관한 자세한 내용은 이전 게시글에 정리해두었다.
레이아웃은 TabManager의 NewLayout 함수를 이용해 제작하였고 하나의 ToolBar 영역과 Tab 영역만 우선 만들어두었다.
Tab의 영역에는 ListView를 하나 추가하였다. 이런 ListView를 위해 SWeaponListView 클래스를 제작하였다.
🚩 RegisterTabSpawners 함수
Open 함수에서는 Tab의 영역만 만들어준것이고, 실제 Tab의 생성은 RegisterTabSpawners 함수에서 진행된다.
FOnSpawnTab 객체로 Tab 객체를 만들고, TabManager의 RegisterTabSpawner 함수를 통해 현재 에디터에서 사용하는 탭을 등록해주었다.
Spawn_ListViewTab 함수를 통해 사용하는 탭 객체를 전달받았다.
🚩 Spawn_ListViewTab 함수
SDockTab은 Tab 하나에 들어갈 공간으로, 도킹 가능한 탭을 나타내는 Slate 위젯이다.
이 위젯은 일반적으로 SDockTabStack 위젯에 자식으로 추가되어 사용된다.
각 SDockTab은 자체적인 레이아웃 및 컨텐츠를 가질 수 있는데, 예를 들어, 탭 내부에 여러 위젯이나 뷰포트를 배치할 수 있다.
또한 탭의 타이틀, 아이콘, 닫기 버튼 등을 사용자 정의하여 사용할 수 있다.
Spawn_ListViewTab 함수는 SNew로 SDockTab의 공간을 생성하고, ListView를 Tab 공간에 추가하여 반환해준다.
📦 ListView의 구조
월드 아웃라이너처럼 객체같은 목록들을 쭉 보여주는 ListView 형태를 제작해보았다.
내가 제작할 ListView는 Weapon의 데이터들을 출력하기 위함이다.
우선, 표의 여러 항목들을 지정하는 표의 형식으로 Weapon Row Data라는 구조체를 만들었다.
이 구조체를 TSharedPtr 포인터로 다루기 위해 typedef로 이름을 FWeaponRowDataPtr로 지정해두었다.
이러한 Row Data를 가지고 있는 표의 한 줄 한 줄을 출력하기 위한 형식과, 데이터는 SMultiColumnTableRow 클래스에 존재한다.
따라서 SMultiColumnTableRow 클래스를 상속받는 SWeaponTableRow 클래스를 생성해주었다.
MultiColumnTableRow 클래스가 표의 실제 데이터를 가지는 것이기 때문에 ListView에 들어갈 전체 데이터를 담기 위해 Tarray로 MultiColumnTableRow 클래스(SWeaponTableRow)를 담아두었다.
SCompoundWidget 클래스는 ListView의 전체, 즉 표 전체를 반환하는 클래스이다.
따라서 SCompoundWidget를 상속받는 SWeaponListView 클래스를 생성해주었다.
SWeaponListView 클래스에서 ListView의 데이터로 사용되기 위해 SListView<FWeaponRowDataPtr> SWeaponListViewRow라는 변수도 선언해주었다.
구조가 복잡하지만, 결과적으로 ListView를 출력하기 위한 데이터는 두가지라고 보면 된다.
하나는, 실제로 보여줄 데이터 TArray<FWeaponRowDataPtr> RowDatas 와,
또 하나는, ListView를 통해 실제로 보여주기 위한 데이터 TSharedPtr<SWeaponListViewRow> ListViewDatas 이다.
2️⃣ SWeaponListView : SCompoundWidget 클래스 만들기
🚩FWeaponRowData 구조체
ListView의 내용으로 들어갈 데이터를 WeaponRowData 구조체로 생성해주었다.
Make 함수를 만들어 새로운 구조체 데이터를 생성할 수 있도록 만들었다.
이러한 WeaponRowData를 스마트 포인터인 TSharedPtr로 다루는 자료형을 FWeaponRowDataPtr 이라는 이름으로 타입 재정의 하였고,
이러한 FWeaponRowDataPtr 자료형을 SListView 객체로 다루는 자료형을 SWeaponListViewRow 라고 타입 재정의 하였다. SListView는 Compound에 들어가면서 에디터에 출력된다.
🚩SWeaponListView 클래스
SCompoundWidget을 상속받는 SWeaponListView는 Construct 함수를 갖는다.
클래스 명 앞에 S가 붙은 클래스는 FArguments를 가져야하는데, 이는 Slate를 생성할 때 사용하는 SNew 함수의 매개변수로 FArguments가 필요하기 때문이다.
또한 SNew라는 매크로가 Construct 함수를 Call 해준다.
현재 WeaponListView 클래스에서의 Construct 함수에서는 ListView가 보여질 모양을 정의한다.
💡 SLATE_ARGS 명령어
SLATE_BEGIN_ARGS( class ){ } 명령어와
SLATE_END_ARGS() 명령어는 Slate 문법에서 외부로 Argument를 넘겨줄 때 사용된다.
BEGIN과 END 사이에 SLATE_ARGUMENT 명령어와 함께 Argument의 자료형, 변수명을 설정하면 외부에서 " ._변수명 " 을 통해 해당 Slate의 Argument에 접근할 수 있다.
🚩Construct 함수
Construct 함수에서는 ChildSlot을 이용해 ListView가 나타날 영역을 지정해주었다. ChildSlot의 내부에는 또 다른 위젯들을 배치할 수 있기 때문에 SNew 함수를 통해 Contents 내용을 채우기 시작했다.
SVerticalBox로 공간을 생성하고, + SVerticalBox::Slot() 키워드로 수직의 공간을 생성하였다. 이때 + 는 연산자 오버로딩 된 것이다.
🪄 SAssignNew
SVerticalBox의 Slot 내용으로 ListView 데이터를 추가하였는데, 이때 데이터를 매개변수로 받아야 하기 때문에 SNew가 아닌 SAssignNew 함수를 사용하였다.
SNew는 Return 전용 함수로 어떠한 인자를 받아 동적할당 할 수 없다.
그러나 SAssignNew를 사용하면 외부로부터 매개변수를 받아 사용할 수 있다.
SAssignNew로 ListView 데이터를 보냈다면 ListView에는 헤더가 필수적으로 들어가야 한다.
.HeaderRow 명령어로 헤더를 생성하고, .ListItemsSource 명령어로 헤더 아래에 들어갈 리스트의 내용들을 전달해주면 된다.
🪄 OnGenerateRow
OnGenerateRow 함수는 On 키워드가 붙었기 때문에 이벤트라는 것을 알 수 있다.
FOnGenerateRow는 TSlateDelegates로, ListView를 만들때 각 행에 대응하는 데이터 항목의 형식을 사용자 지정 위젯으로 생성할 수 있게 도와준다.
델리게이트와 같은 이름과 같은 매개변수의 형식으로 OnGenerateRow 함수를 생성하고, ListView의 행으로 들어갈 실제 데이터들을 반환해주었다.
이때 행은 제작한 SWeaponTableRow 클래스를 InTable의 Row로 넘겨주었다.
먼저, Layout에 ListView가 정상적으로 만들어지는지를 테스트하기 위해 RowDatas에 임의의 데이터 4개를 추가시켜두었다.
ListView의 데이터가 추가되고 난 후에는 RequestListRefresh 함수로 List를 갱신해주어야 한다.
🚩 SWeaponTableRow 클래스
SWeaponTableRow 클래스는 SMultiColumnTableRow 클래스를 상속받는다.
SCompoundWidget을 상속받는 WeaponListView와 마찬가지로, SWeaponTableRow 클래스도 Construct 함수를 선언해야 한다.
Construct 함수에는 첫번째 매개변수로 무조건 FArguments가 들어가고, 이후에는 상황에 따라 필요한 매개변수를 전달하여 사용할 수 있다.
🪄 Construct 함수
SWeaponTableRow의 Construct에서는 FWeaponRowDataPtr을 FArguments로 초기화하고,
부모의 Construct 함수를 호출하여 ListView의 스타일을 지정해주었다.
이때 사용한 스타일은 FEditorStyle 클래스에 기본으로 만들어져있는 "TableView.DarkRow" 를 사용하였다.
🪄 GenerateWidgetForColumn 함수
GenerateWidgetForColumn 함수에서는 각 컬럼의 셀에 대한 위젯을 생성하고 반환하는 역할을 수행한다.
이 함수는 각 열에 대한 위젯을 생성하고 반환하는 콜백 함수를 구현해야 하고, 반환된 위젯은 해당 열의 셀에 배치되어 나타나게 된다.
매개변수로 각 열의 셀 위젯을 생성하는 데 필요한 데이터를 인수받는다. 이 데이터는 SMultiColumnTableRow의 행 데이터와 해당 열의 데이터를 나타낸다.
InColumnName에 열의 이름이 들어오는데, 해당 이름이 Index라면 Row 데이터의 Index를 출력하고, InColumnName이 Name이라면 Row 데이터의 Name을 출력한다.
(Row가 바로 실제 출력할 데이터)
컴파일 후 프로그램을 실행시킨 결과는 아래와 같다.
참조
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=destiny9720&logNo=221382207008
에디터 확장 기초 #4 - 애셋 에디터 제작
지난 강좌에서는 언리얼 엔진이 제공하는 레벨 에디터에 메뉴와 툴바를 추가하는 방법에 대해 알아보았습니...
blog.naver.com
https://m.blog.naver.com/destiny9720/221383661821
에디터 확장 기초 마지막 - 슬레이트의 활용
에디터 확장 관련해서 다룰 내용이 많지만 이번 강좌를 끝으로 마무리를 하려고 합니다. ( 사실 준비한 예...
blog.naver.com