[UE] ContentBrowser Menu: 사용하지 않는 에셋/빈 폴더 제거
Content Browser Menu에 사용하지 않는 에셋과 폴더를 제거하는 기능을 추가하였다. 폴더를 우클릭하여 해당 기능을 눌러 실행한다. 해당 에셋이 사용하고 있는지 체크하고 사용하지 않을 경우에만 삭제를 진행한다. 폴더 역시 빈 폴더인지 체크 후에 빈 폴더이면 삭제한다.
목차
사용하지 않는 에셋 제거
Delegate 선언 정리
DECLARE_DELEGATE( DelegateName ) | void Function() |
ex. DECLARE_DELEGATE( OnDelegateButtonClicked ) | |
DECLARE_DELEGATE_OneParam( DelegateName, Param1 Type ) | void Function( Param1 ) |
ex. DECLARE_DELEGATE_OneParam( OnDelegateButtonClicked, int32 ) | ex. void Function ( int 32) |
DECLARE_DELEGATE_ReturnValue_OneParam ( ReturnValueType, DelegateName, Param1 Type ) |
<ReturnValueType> Function( Param1 ) |
ex. DECLARE_DELEGATE_OneParam( bool, OnDelegateButtonClicked, int32 ) | ex. bool Function ( int 32) |
ContentBrowserDelegate.h에 있는 DECLARE_DELEGATE

Custom Menu Entry 만들기 과정
1. StartupModule에 Content Browser Module를 Load 한다.
2. PathViewContextMenuExtenders
3. Delegate를 선언한다.
4. Delegate과 멤버함수를 Bind 시킨다.
SWMananger
SWMananger.h
#pragma once #include "CoreMinimal.h" #include "Modules/ModuleManager.h" class FSWManagerModule : public IModuleInterface { public: /** IModuleInterface implementation */ virtual void StartupModule() override; virtual void ShutdownModule() override; private: #pragma region ContentBrowserMenuExtention void InitCBMenuExtention(); TSharedRef<FExtender> CustomCBMenuExtender(const TArray<FString>& SelectedPaths); void AddCBMenuEntry(class FMenuBuilder& MenuBuilder); void OnDeleteUnsuedAssetButtonClicked(); #pragma endregion };
함수 추가
- TSharedRef<FExtender> CustomCBMenuExtender(const TArray<FString>& SelectedPaths);
- void AddCBMenuEntry(class FMenuBuilder& MenuBuilder);
- void OnDeleteUnsuedAssetButtonClicked();
SWMananger.cpp
#include "SWManager.h" #include "ContentBrowserModule.h" #define LOCTEXT_NAMESPACE "FSWManagerModule" void FSWManagerModule::StartupModule() { // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module // SWManager.uplugin에서 설정한 PreDefault 이후 실행됨 InitCBMenuExtention(); } #pragma region ContentBrowserMenuExtention void FSWManagerModule::InitCBMenuExtention() { FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser")); // ContentBrowser 모듈 로드하여 변수에 담음 TArray<FContentBrowserMenuExtender_SelectedPaths>& ContentBrowserModuleMenuExtenders = ContentBrowserModule.GetAllPathViewContextMenuExtenders(); // ContentBrowserModule의 모든ContentMenuExtenders를 TArray변수에 담음 /*FContentBrowserMenuExtender_SelectedPaths CustomCBMenuDelegate; CustomCBMenuDelegate.BindRaw(this, &FSWManagerModule::CustomCBMenuExtender); ContentBrowserModuleMenuExtenders.Add(CustomCBMenuDelegate); //아래의 한줄과 같은 의미 */ // ContentBrowserModuleMenuExtenders에 항목을 추가하고 FContentBrowserMenuExtender_SelectedPaths에 this(=FSWManagerModule)를 생성하고 CustomCBMenuExtender함수를 Bind 한다. ContentBrowserModuleMenuExtenders.Add(FContentBrowserMenuExtender_SelectedPaths::CreateRaw(this, &FSWManagerModule::CustomCBMenuExtender)); } TSharedRef<FExtender> FSWManagerModule::CustomCBMenuExtender(const TArray<FString>& SelectedPaths) { TSharedRef<FExtender> MenuExtender(new FExtender()); if (SelectedPaths.Num() > 0) { // MenuExtender의 "Delete"위치 뒤에 UICommandList를 추가하고 AddCBMenuEntry함수를 Bind 한다. MenuExtender->AddMenuExtension(FName("Delete"), EExtensionHook::After, TSharedPtr<FUICommandList>(), FMenuExtensionDelegate::CreateRaw(this, &FSWManagerModule::AddCBMenuEntry)); } return MenuExtender; } void FSWManagerModule::AddCBMenuEntry(FMenuBuilder& MenuBuilder) { MenuBuilder.AddMenuEntry ( FText::FromString(TEXT("사용하지 않은 에셋 제거하기")), FText::FromString(TEXT("Safely delete all unused assets under folder")), FSlateIcon(), FExecuteAction::CreateRaw(this, &FSWManagerModule::OnDeleteUnsuedAssetButtonClicked) ); } void FSWManagerModule::OnDeleteUnsuedAssetButtonClicked() { } #pragma endregion void FSWManagerModule::ShutdownModule() { // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, // we call this function before unloading the module. } #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FSWManagerModule, SWManager)
함수 정의
- TSharedRef<FExtender> CustomCBMenuExtender(const TArray<FString>& SelectedPaths);
- void AddCBMenuEntry(class FMenuBuilder& MenuBuilder);
- void OnDeleteUnsuedAssetButtonClicked();
첫번째 Binding | 삽입되는 Menu Entry의 위치를 정의 TSharedRef CustomCBMenuExtender(const TArray& SelectedPaths); |
두번째 Binding | Menu Entry 관련 세부사항을 정의 ex. Title, Tooltip, Function void AddCBMenuEntry(class FMenuBuilder& MenuB uilder); |
세번째 Binding | 실제로 실행될 함수 void OnDeleteUnsuedAssetButtonClicked(); |
void InitCBMenuExtention();
↑
TSharedRef<FExtender> CustomCBMenuExtender(const TArray<FString>& SelectedPaths);
↑
void AddCBMenuEntry(class FMenuBuilder& MenuBuilder);
↑
void OnDeleteUnsuedAssetButtonClicked();
한글을 사용할 수 있도록 SWMananger.cpp를 유니코드 65001 형식으로 다시 저장

Editor Preferences - Developer Tools - Display UI Extension Points 체크
Editor Preferences - Developer Tools - Display UI Extension Points 체크할 시
아래의 이미지와 같이 각각의 위치의 extension hook 이름을 알 수 있다.


실행화면

우클릭 시 해당 기능을 보여주게 해주었다. 아직 버튼이 눌렸을 때 실행되는 것이 없다. 그래서 눌렀을 시 아무일도 일어나지 않는다.
에셋 삭제 기능 구현하기
참고사항
EditorAssetLibrary.h에 정의된 static TArray<FString> ListAssets() 함수 활용

SWManager
SWMananger.h
#pragma once #include "CoreMinimal.h" #include "Modules/ModuleManager.h" class FSWManagerModule : public IModuleInterface { public: /** IModuleInterface implementation */ virtual void StartupModule() override; virtual void ShutdownModule() override; private: #pragma region ContentBrowserMenuExtention void InitCBMenuExtention(); TArray<FString> FolderPathsSelected; TSharedRef<FExtender> CustomCBMenuExtender(const TArray<FString>& SelectedPaths); void AddCBMenuEntry(class FMenuBuilder& MenuBuilder); void OnDeleteUnsuedAssetButtonClicked(); // 에셋 삭제 #pragma endregion };
변수 추가
- TArray<FString> FolderPathsSelected;
SWMananger.cpp
#include "SWManager.h" #include "ContentBrowserModule.h" #include "DebugHeader.h" #include "EditorAssetLibrary.h" #include "ObjectTools.h" #define LOCTEXT_NAMESPACE "FSWManagerModule" void FSWManagerModule::StartupModule() { // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module // SWManager.uplugin에서 설정한 PreDefault 이후 실행됨 InitCBMenuExtention(); } #pragma region ContentBrowserMenuExtention void FSWManagerModule::InitCBMenuExtention() { FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser")); // ContentBrowser 모듈 로드하여 변수에 담음 TArray<FContentBrowserMenuExtender_SelectedPaths>& ContentBrowserModuleMenuExtenders = ContentBrowserModule.GetAllPathViewContextMenuExtenders(); // ContentBrowserModule의 모든ContentMenuExtenders를 TArray변수에 담음 /*FContentBrowserMenuExtender_SelectedPaths CustomCBMenuDelegate; CustomCBMenuDelegate.BindRaw(this, &FSWManagerModule::CustomCBMenuExtender); ContentBrowserModuleMenuExtenders.Add(CustomCBMenuDelegate); //아래의 한줄과 같은 의미 */ // ContentBrowserModuleMenuExtenders에 항목을 추가하고 FContentBrowserMenuExtender_SelectedPaths에 this(=FSWManagerModule)를 생성하고 CustomCBMenuExtender함수를 Bind 한다. ContentBrowserModuleMenuExtenders.Add(FContentBrowserMenuExtender_SelectedPaths::CreateRaw(this, &FSWManagerModule::CustomCBMenuExtender)); } TSharedRef<FExtender> FSWManagerModule::CustomCBMenuExtender(const TArray<FString>& SelectedPaths) // 우클릭 시 ContentBrowserMenu에 노출시켜질 위치 { TSharedRef<FExtender> MenuExtender(new FExtender()); if (SelectedPaths.Num() > 0) { // MenuExtender의 "Delete"위치 뒤에 UICommandList를 추가하고 AddCBMenuEntry함수를 Bind 한다. MenuExtender->AddMenuExtension(FName("Delete"), EExtensionHook::After, TSharedPtr<FUICommandList>(), FMenuExtensionDelegate::CreateRaw(this, &FSWManagerModule::AddCBMenuEntry)); FolderPathsSelected = SelectedPaths; // 유저가 현재 선택한 폴더에 접근할 수 있도록 SelectedPaths를 FolderPathsSelected변수에 담는다 } return MenuExtender; } void FSWManagerModule::AddCBMenuEntry(FMenuBuilder& MenuBuilder) // ContentBrowserMenu에 노출되는 버튼에 함수 바인딩 { MenuBuilder.AddMenuEntry ( FText::FromString(TEXT("사용하지 않은 에셋 제거하기")), FText::FromString(TEXT("폴더에서 사용하지 않는 에셋들을 안전하게 지우는 기능")), FSlateIcon(), FExecuteAction::CreateRaw(this, &FSWManagerModule::OnDeleteUnsuedAssetButtonClicked) ); } void FSWManagerModule::OnDeleteUnsuedAssetButtonClicked() // 에셋 삭제 { if (FolderPathsSelected.Num() > 1) // 2개 이상의 폴더가 선택된 경우 { DebugHeader::ShowMsgDialog(EAppMsgType::Ok, TEXT("한 개의 폴더에서만 진행할 수 있습니다")); return; } TArray<FString> AssetsPathNames = UEditorAssetLibrary::ListAssets(FolderPathsSelected[0]); // 선택된 폴더(FolderPathsSelected[0]) 내에 있는 모든 에셋들을 AssetsPathNames변수에 담는다 if (AssetsPathNames.Num() == 0) // 폴더를 선택하지 않은 경우 { DebugHeader::ShowMsgDialog(EAppMsgType::Ok, TEXT("선택한 폴더에서 아무런 에셋을 찾을 수 없습니다.")); return; } EAppReturnType::Type ConfirmResult =DebugHeader::ShowMsgDialog(EAppMsgType::YesNo, TEXT("총 ") + FString::FromInt(AssetsPathNames.Num()) + TEXT(" 개의 에셋을 찾았습니다.\n삭제를 진행하시겠습니까?")); if (ConfirmResult == EAppReturnType::No) return; TArray<FAssetData> UnusedAssetsDataArray; // 사용되지 않는 에셋들을 담는 TArray변수 for (const FString& AssetPathName : AssetsPathNames) { // Root 폴더는 건드리면 안 된다. 프로젝트 파일 Content 밑의 Developer, Collection 폴더일 경우 continue if (AssetPathName.Contains(TEXT("Developers")) || AssetPathName.Contains(TEXT("Collections"))) { continue; } // 에셋이 없다면 continue if (false == UEditorAssetLibrary::DoesAssetExist(AssetPathName)) continue; // AssetReferencer변수에 referencers들의 package path를 담는다. 에셋 데이터가 아닌 path를 담는것이다. TArray<FString> AssetReferencers = UEditorAssetLibrary::FindPackageReferencersForAsset(AssetPathName); if (AssetReferencers.Num() == 0) // 해당 에셋이 사용되고 있지 않다면 { // 에셋 데이터를 UnusedAssetsDataArray(=사용되지 않는 에셋들을 담는 TArray변수)에 추가 const FAssetData UnusedAssetData = UEditorAssetLibrary::FindAssetData(AssetPathName); UnusedAssetsDataArray.Add(UnusedAssetData); } } if (UnusedAssetsDataArray.Num() > 0) // 사용되지 않는 에셋이 0이상이라면 { ObjectTools::DeleteAssets(UnusedAssetsDataArray); // UnusedAssetsDataArray를 삭제 } else { DebugHeader::ShowMsgDialog(EAppMsgType::Ok, TEXT("선택한 폴더에서 사용하지 않는 에셋을 찾을 수 없습니다. 삭제할 수 없습니다.\n 에셋이 없거나 사용중입니다.")); } } #pragma endregion void FSWManagerModule::ShutdownModule() { // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, // we call this function before unloading the module. } #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FSWManagerModule, SWManager)
함수 정의
- void OnDeleteUnsuedAssetButtonClicked();
- 에셋을 삭제하는 역할을 수행하는 함수
실행화면
- YouTube
www.youtube.com
사용하지 않는 에셋 제거 - Redirectors 문제 해결하기
파일을 다른 폴더에 옮겼을 때 생길 수 있는 경로문제를 해결하기 위해 Redirectors 관련해서 코드를 추가하였다.
SWManager
SWMananger.h
#pragma once #include "CoreMinimal.h" #include "Modules/ModuleManager.h" class FSWManagerModule : public IModuleInterface { public: /** IModuleInterface implementation */ virtual void StartupModule() override; virtual void ShutdownModule() override; private: #pragma region ContentBrowserMenuExtention void InitCBMenuExtention(); TArray<FString> FolderPathsSelected; TSharedRef<FExtender> CustomCBMenuExtender(const TArray<FString>& SelectedPaths); void AddCBMenuEntry(class FMenuBuilder& MenuBuilder); void OnDeleteUnsuedAssetButtonClicked(); // 사용하지 않는 Asset 제거 void FixUpRedirectors(); // Fix Up Redirectors #pragma endregion };
함 추가
- void FixUpRedirectors();
SWMananger.cpp
#include "SWManager.h" #include "ContentBrowserModule.h" #include "DebugHeader.h" #include "EditorAssetLibrary.h" #include "ObjectTools.h" #include "AssetRegistryModule.h" #include "AssetToolsModule.h" #define LOCTEXT_NAMESPACE "FSWManagerModule" void FSWManagerModule::StartupModule() { // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module // SWManager.uplugin에서 설정한 PreDefault 이후 실행됨 InitCBMenuExtention(); } #pragma region ContentBrowserMenuExtention void FSWManagerModule::InitCBMenuExtention() { FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser")); // ContentBrowser 모듈 로드하여 변수에 담음 TArray<FContentBrowserMenuExtender_SelectedPaths>& ContentBrowserModuleMenuExtenders = ContentBrowserModule.GetAllPathViewContextMenuExtenders(); // ContentBrowserModule의 모든ContentMenuExtenders를 TArray변수에 담음 /*FContentBrowserMenuExtender_SelectedPaths CustomCBMenuDelegate; CustomCBMenuDelegate.BindRaw(this, &FSWManagerModule::CustomCBMenuExtender); ContentBrowserModuleMenuExtenders.Add(CustomCBMenuDelegate); //아래의 한줄과 같은 의미 */ // ContentBrowserModuleMenuExtenders에 항목을 추가하고 FContentBrowserMenuExtender_SelectedPaths에 this(=FSWManagerModule)를 생성하고 CustomCBMenuExtender함수를 Bind 한다. ContentBrowserModuleMenuExtenders.Add(FContentBrowserMenuExtender_SelectedPaths::CreateRaw(this, &FSWManagerModule::CustomCBMenuExtender)); } TSharedRef<FExtender> FSWManagerModule::CustomCBMenuExtender(const TArray<FString>& SelectedPaths) // 우클릭 시 ContentBrowserMenu에 노출시켜질 위치 { TSharedRef<FExtender> MenuExtender(new FExtender()); if (SelectedPaths.Num() > 0) { // MenuExtender의 "Delete"위치 뒤에 UICommandList를 추가하고 AddCBMenuEntry함수를 Bind 한다. MenuExtender->AddMenuExtension(FName("Delete"), EExtensionHook::After, TSharedPtr<FUICommandList>(), FMenuExtensionDelegate::CreateRaw(this, &FSWManagerModule::AddCBMenuEntry)); FolderPathsSelected = SelectedPaths; // 유저가 현재 선택한 폴더에 접근할 수 있도록 SelectedPaths를 FolderPathsSelected변수에 담는다 } return MenuExtender; } void FSWManagerModule::AddCBMenuEntry(FMenuBuilder& MenuBuilder) // ContentBrowserMenu에 노출되는 버튼에 함수 바인딩 { MenuBuilder.AddMenuEntry ( FText::FromString(TEXT("사용하지 않은 에셋 제거하기")), FText::FromString(TEXT("폴더에서 사용하지 않는 에셋들을 안전하게 지우는 기능")), FSlateIcon(), FExecuteAction::CreateRaw(this, &FSWManagerModule::OnDeleteUnsuedAssetButtonClicked) ); } void FSWManagerModule::OnDeleteUnsuedAssetButtonClicked() // 에셋 삭제 { if (FolderPathsSelected.Num() > 1) // 2개 이상의 폴더가 선택된 경우 { DebugHeader::ShowMsgDialog(EAppMsgType::Ok, TEXT("한 개의 폴더에서만 진행할 수 있습니다")); return; } TArray<FString> AssetsPathNames = UEditorAssetLibrary::ListAssets(FolderPathsSelected[0]); // 선택된 폴더(FolderPathsSelected[0]) 내에 있는 모든 에셋들을 AssetsPathNames변수에 담는다 if (AssetsPathNames.Num() == 0) // 폴더를 선택하지 않은 경우 { DebugHeader::ShowMsgDialog(EAppMsgType::Ok, TEXT("선택한 폴더에서 아무런 에셋을 찾을 수 없습니다.")); return; } EAppReturnType::Type ConfirmResult =DebugHeader::ShowMsgDialog(EAppMsgType::YesNo, TEXT("총 ") + FString::FromInt(AssetsPathNames.Num()) + TEXT(" 개의 에셋을 찾았습니다.\n삭제를 진행하시겠습니까?")); if (ConfirmResult == EAppReturnType::No) return; FixUpRedirectors(); TArray<FAssetData> UnusedAssetsDataArray; // 사용되지 않는 에셋들을 담는 TArray변수 for (const FString& AssetPathName : AssetsPathNames) { // Root 폴더는 건드리면 안 된다. 프로젝트 파일 Content 밑의 Developer, Collection 폴더일 경우 continue if (AssetPathName.Contains(TEXT("Developers")) || AssetPathName.Contains(TEXT("Collections"))) { continue; } // 에셋이 없다면 continue if (false == UEditorAssetLibrary::DoesAssetExist(AssetPathName)) continue; // AssetReferencer변수에 referencers들의 package path를 담는다. 에셋 데이터가 아닌 path를 담는것이다. TArray<FString> AssetReferencers = UEditorAssetLibrary::FindPackageReferencersForAsset(AssetPathName); if (AssetReferencers.Num() == 0) // 해당 에셋이 사용되고 있지 않다면 { // 에셋 데이터를 UnusedAssetsDataArray(=사용되지 않는 에셋들을 담는 TArray변수)에 추가 const FAssetData UnusedAssetData = UEditorAssetLibrary::FindAssetData(AssetPathName); UnusedAssetsDataArray.Add(UnusedAssetData); } } if (UnusedAssetsDataArray.Num() > 0) // 사용되지 않는 에셋이 0이상이라면 { ObjectTools::DeleteAssets(UnusedAssetsDataArray); // UnusedAssetsDataArray를 삭제 } else { DebugHeader::ShowMsgDialog(EAppMsgType::Ok, TEXT("선택한 폴더에서 사용하지 않는 에셋을 찾을 수 없습니다. 삭제할 수 없습니다.\n 에셋이 없거나 사용중입니다.")); } } void FSWManagerModule::FixUpRedirectors() { TArray<UObjectRedirector*> RedirectorsToFixArray; FAssetRegistryModule& AssetRegistryModule = FModuleManager::Get().LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")); FARFilter Filter; Filter.bRecursivePaths = true; // subfolder에 접근이 가능하도록 true 설정 Filter.PackagePaths.Emplace("/Game"); // 어떤 폴더에 접근할 지 경로 설정 Filter.ClassNames.Emplace("ObjectRedirector"); // 필터 하기를 희망하는 클래스의 이름 TArray<FAssetData> OutRedirectors; // 결과를 저장할 TArry 변수 AssetRegistryModule.Get().GetAssets(Filter, OutRedirectors); // Filter한 결과를 OutRedirectors에 담는다 for (const FAssetData& RedirectorData : OutRedirectors) { if (UObjectRedirector* RedirectorToFix = Cast<UObjectRedirector>(RedirectorData.GetAsset())) // Redirect 할게 있다면 { RedirectorsToFixArray.Add(RedirectorToFix); // RedirectorsToFixArray에 RedirectorToFix 담음 } } FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>(TEXT("AssetTools")); AssetToolsModule.Get().FixupReferencers(RedirectorsToFixArray); } #pragma endregion void FSWManagerModule::ShutdownModule() { // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, // we call this function before unloading the module. } #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FSWManagerModule, SWManager)
헤더 추가
- #include "AssetRegistryModule.h"
- #include "AssetToolsModule.h"
함수 정의
- void FixUpRedirectors();
- 구현한 원리는 일전에 QuickAssetAction.cpp에서 구현한 void UQuickAssetAction::FixUpRedirectors()와 똑같다.
수정사항
- void FSWManagerModule::OnDeleteUnsuedAssetButtonClicked() 함수 내에서 void FixUpRedirectors();함수를 콜해준다.
사용하지 않는 폴더 제거
사용하지 않는 에셋 제거와 같은 기능으로 사용하지 않는 폴더 제거하기 기능을 만든다.
참고사항
EditorAssetLibray.h

EditorAssetLibray.h에 정의된 static TArray<FString> ListAssets() 함수
- bRecursive = true면, 하위 폴더들도 검사한다.
- bIncludeFolder = true면,
SWManager
SWMananger.h
#pragma once #include "CoreMinimal.h" #include "Modules/ModuleManager.h" class FSWManagerModule : public IModuleInterface { public: /** IModuleInterface implementation */ virtual void StartupModule() override; virtual void ShutdownModule() override; private: #pragma region ContentBrowserMenuExtention void InitCBMenuExtention(); TArray<FString> FolderPathsSelected; TSharedRef<FExtender> CustomCBMenuExtender(const TArray<FString>& SelectedPaths); void AddCBMenuEntry(class FMenuBuilder& MenuBuilder); void OnDeleteUnsuedAssetButtonClicked(); // 사용하지 않는 Asset 제거 void OnDeleteEmptyFoldersButtonClicked(); // 사용하지 않는 Folder 제거 void FixUpRedirectors(); // Fix Up Redirectors #pragma endregion };
함수 추가
- void OnDeleteEmptyFoldersButtonClicked(); // 사용하지 않는 Folder 제거
SWMananger.cpp
#include "SWManager.h" #include "ContentBrowserModule.h" #include "DebugHeader.h" #include "EditorAssetLibrary.h" #include "ObjectTools.h" #include "AssetRegistryModule.h" #include "AssetToolsModule.h" #define LOCTEXT_NAMESPACE "FSWManagerModule" void FSWManagerModule::StartupModule() { // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module // SWManager.uplugin에서 설정한 PreDefault 이후 실행됨 InitCBMenuExtention(); } #pragma region ContentBrowserMenuExtention void FSWManagerModule::InitCBMenuExtention() { FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser")); // ContentBrowser 모듈 로드하여 변수에 담음 TArray<FContentBrowserMenuExtender_SelectedPaths>& ContentBrowserModuleMenuExtenders = ContentBrowserModule.GetAllPathViewContextMenuExtenders(); // ContentBrowserModule의 모든ContentMenuExtenders를 TArray변수에 담음 /*FContentBrowserMenuExtender_SelectedPaths CustomCBMenuDelegate; CustomCBMenuDelegate.BindRaw(this, &FSWManagerModule::CustomCBMenuExtender); ContentBrowserModuleMenuExtenders.Add(CustomCBMenuDelegate); //아래의 한줄과 같은 의미 */ // ContentBrowserModuleMenuExtenders에 항목을 추가하고 FContentBrowserMenuExtender_SelectedPaths에 this(=FSWManagerModule)를 생성하고 CustomCBMenuExtender함수를 Bind 한다. 다른말로 표현하면, 현재 존재하는 모든 delegates들에 내가 만든 CustomCBMenuExtender 델리게이트를 추가한다. ContentBrowserModuleMenuExtenders.Add(FContentBrowserMenuExtender_SelectedPaths::CreateRaw(this, &FSWManagerModule::CustomCBMenuExtender)); } TSharedRef<FExtender> FSWManagerModule::CustomCBMenuExtender(const TArray<FString>& SelectedPaths) // 우클릭 시 ContentBrowserMenu에 노출시켜질 위치를 정의해주기 { TSharedRef<FExtender> MenuExtender(new FExtender()); if (SelectedPaths.Num() > 0) { // MenuExtender의 "Delete"위치 뒤에 UICommandList를 추가하고 AddCBMenuEntry함수를 Bind 한다. MenuExtender->AddMenuExtension(FName("Delete"), // Extension hook, 삽입할 위치 EExtensionHook::After, // Delete 뒤에 들어가게 After 설정 TSharedPtr<FUICommandList>(), // Custom hot keys FMenuExtensionDelegate::CreateRaw(this, &FSWManagerModule::AddCBMenuEntry)); // Second binding, Menu Entry에 노출될 디테일을 정의해줄 AddCBMenuEntry를 바인딩 FolderPathsSelected = SelectedPaths; // 유저가 현재 선택한 폴더에 접근할 수 있도록 SelectedPaths를 FolderPathsSelected변수에 담는다 } return MenuExtender; } void FSWManagerModule::AddCBMenuEntry(FMenuBuilder& MenuBuilder) // ContentBrowserMenu에 노출되는 버튼에 함수 바인딩 { MenuBuilder.AddMenuEntry ( FText::FromString(TEXT("사용하지 않은 에셋 제거하기")), // Menu Entry의 이름 FText::FromString(TEXT("폴더에서 사용하지 않는 에셋들을 안전하게 지우는 기능")), // Tooltip 설명 FSlateIcon(), // Custom Icon FExecuteAction::CreateRaw(this, &FSWManagerModule::OnDeleteUnsuedAssetButtonClicked) // 실행될 함수 ); MenuBuilder.AddMenuEntry ( FText::FromString(TEXT("사용하지 않은 폴더 제거하기")), // Menu Entry의 이름 FText::FromString(TEXT("빈 폴더를 안전하게 지우는 기능")), // Tooltip 설명 FSlateIcon(), // Custom Icon FExecuteAction::CreateRaw(this, &FSWManagerModule::OnDeleteEmptyFoldersButtonClicked) // 실행될 함수 ); } void FSWManagerModule::OnDeleteUnsuedAssetButtonClicked() // 에셋 삭제 { if (FolderPathsSelected.Num() > 1) // 2개 이상의 폴더가 선택된 경우 { DebugHeader::ShowMsgDialog(EAppMsgType::Ok, TEXT("한 개의 폴더에서만 진행할 수 있습니다")); return; } TArray<FString> AssetsPathNames = UEditorAssetLibrary::ListAssets(FolderPathsSelected[0]); // 선택된 폴더(FolderPathsSelected[0]) 내에 있는 모든 에셋들을 AssetsPathNames변수에 담는다 if (AssetsPathNames.Num() == 0) // 폴더 내에서 에셋을 찾지 못한 경우 { DebugHeader::ShowMsgDialog(EAppMsgType::Ok, TEXT("선택한 폴더에서 아무런 에셋을 찾을 수 없습니다."), false); return; } EAppReturnType::Type ConfirmResult =DebugHeader::ShowMsgDialog(EAppMsgType::YesNo, TEXT("총 ") + FString::FromInt(AssetsPathNames.Num()) + TEXT(" 개의 에셋을 찾았습니다.\n사용하지 않는지 체크 후 삭제를 진행하시겠습니까?"), false); if (ConfirmResult == EAppReturnType::No) return; // 유저가 No를 누르면 삭제를 진행하지 않고 바로 종료 FixUpRedirectors(); // 경로가 문제되지 않도록 체크 후 업데이트 TArray<FAssetData> UnusedAssetsDataArray; // 사용되지 않는 에셋들을 담는 TArray변수 for (const FString& AssetPathName : AssetsPathNames) { // Root 폴더는 건드리면 안 된다. 프로젝트 파일 Content 밑의 Developer, Collection 폴더일 경우 continue if (AssetPathName.Contains(TEXT("Developers")) || AssetPathName.Contains(TEXT("Collections")) || AssetPathName.Contains(TEXT("__ExternalActors__")) || AssetPathName.Contains(TEXT("__ExternalObjects__"))) { continue; } // 에셋이 없다면 continue if (false == UEditorAssetLibrary::DoesAssetExist(AssetPathName)) continue; // AssetReferencer변수에 referencers들의 package path를 담는다. 에셋 데이터가 아닌 path를 담는것이다. TArray<FString> AssetReferencers = UEditorAssetLibrary::FindPackageReferencersForAsset(AssetPathName); if (AssetReferencers.Num() == 0) // 해당 에셋이 사용되고 있지 않다면 { // 에셋 데이터를 UnusedAssetsDataArray(=사용되지 않는 에셋들을 담는 TArray변수)에 추가 const FAssetData UnusedAssetData = UEditorAssetLibrary::FindAssetData(AssetPathName); UnusedAssetsDataArray.Add(UnusedAssetData); } } if (UnusedAssetsDataArray.Num() > 0) // 사용되지 않는 에셋이 0이상이라면 { ObjectTools::DeleteAssets(UnusedAssetsDataArray); // UnusedAssetsDataArray를 삭제 } else { DebugHeader::ShowMsgDialog(EAppMsgType::Ok, TEXT("선택한 폴더에서 사용하지 않는 에셋을 찾을 수 없습니다. 삭제할 수 없습니다.\n 에셋이 없거나 사용중입니다."), false); } } void FSWManagerModule::OnDeleteEmptyFoldersButtonClicked() // 사용하지 않는 Folder 제거 { FixUpRedirectors(); // 경로가 문제되지 않도록 체크 후 업데이트 TArray<FString> FolderPathsArray = UEditorAssetLibrary::ListAssets(FolderPathsSelected[0], true, true); // 선택된 폴더(FolderPathsSelected[0]) 내에 있는 모든 에셋+폴더들을 FolderPathsArray변수에 담는다 uint32 Counter = 0; FString EmptyFolderPathsNames; TArray<FString> EmptyFoldersPathsArray; // 빈 폴더들을 담을 TArray변수 for (const FString& FolderPath : FolderPathsArray) { // Root 폴더는 건드리면 안 된다. 프로젝트 파일 Content 밑의 Developer, Collection, __ExternalActors__, __ExternalObjects__폴더일 경우 continue if (FolderPath.Contains(TEXT("Developers")) || FolderPath.Contains(TEXT("Collections")) || FolderPath.Contains(TEXT("__ExternalActors__")) || FolderPath.Contains(TEXT("__ExternalObjects__"))) { continue; } // 폴더가 없다면 continue if (false == UEditorAssetLibrary::DoesDirectoryExist(FolderPath)) continue; // FolderPath에 에셋이 없다면 if (false == UEditorAssetLibrary::DoesDirectoryHaveAssets(FolderPath)) { EmptyFolderPathsNames.Append(FolderPath); EmptyFolderPathsNames.Append(TEXT("\n")); EmptyFoldersPathsArray.Add(FolderPath); // EmptyFoldersPathsArray배열에 빈 폴더의 FolderPath를 추가 } } if (EmptyFoldersPathsArray.Num() == 0) // 빈 폴더가 없다면 { DebugHeader::ShowMsgDialog(EAppMsgType::Ok, TEXT("선택한 폴더들 중 빈 폴더를 찾지 못 했습니다."), false); return; } EAppReturnType::Type ConfirmResult = DebugHeader::ShowMsgDialog(EAppMsgType::OkCancel, TEXT("빈 폴더 ") + EmptyFolderPathsNames + TEXT(" 를 찾았습니다. \n 모두 삭제 하시겠습니까?"), false); if (ConfirmResult == EAppReturnType::Cancel) return; // Cancel를 누르면 삭제하지 않고 종료 for (const FString& EmptyFolderPath : EmptyFoldersPathsArray) { UEditorAssetLibrary::DeleteDirectory(EmptyFolderPath) ? ++Counter : DebugHeader::Print(TEXT("폴더 " + EmptyFolderPath + " 삭제를 실패했습니다."), FColor::Red); // 디버깅 메시지 띄우기 } if (Counter > 0) { DebugHeader::ShowNotifyInfo(TEXT("폴더 ") + FString::FromInt(Counter) + TEXT("개를 성공적으로 삭제했습니다.")); } } void FSWManagerModule::FixUpRedirectors() { TArray<UObjectRedirector*> RedirectorsToFixArray; FAssetRegistryModule& AssetRegistryModule = FModuleManager::Get().LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")); FARFilter Filter; Filter.bRecursivePaths = true; // subfolder에 접근이 가능하도록 true 설정 Filter.PackagePaths.Emplace("/Game"); // 어떤 폴더에 접근할 지 경로 설정 Filter.ClassNames.Emplace("ObjectRedirector"); // 필터 하기를 희망하는 클래스의 이름 TArray<FAssetData> OutRedirectors; // 결과를 저장할 TArry 변수 AssetRegistryModule.Get().GetAssets(Filter, OutRedirectors); // Filter한 결과를 OutRedirectors에 담는다 for (const FAssetData& RedirectorData : OutRedirectors) { if (UObjectRedirector* RedirectorToFix = Cast<UObjectRedirector>(RedirectorData.GetAsset())) // Redirect 할게 있다면 { RedirectorsToFixArray.Add(RedirectorToFix); // RedirectorsToFixArray에 RedirectorToFix 담음 } } FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>(TEXT("AssetTools")); AssetToolsModule.Get().FixupReferencers(RedirectorsToFixArray); } #pragma endregion void FSWManagerModule::ShutdownModule() { // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, // we call this function before unloading the module. } #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FSWManagerModule, SWManager)
함수 정의
- void OnDeleteEmptyFoldersButtonClicked();
실행화면
https://youtu.be/MicvxYclXkE?si=3WI4ltsyKRJvsQO3
'⭐ Unreal Engine > UE Plugin - Custom Editor Tool' 카테고리의 다른 글
[UE] Slate UI 2: 제거 마법사 - 에셋 리스트 띄우기 (0) | 2023.12.01 |
---|---|
[UE] Slate UI 1: Tab 띄우기 (0) | 2023.12.01 |
[UE] Asset Editor Tool 3: 사용하지 않는 Asset만 제거하기 (0) | 2023.11.25 |
[UE] Asset Editor Tool 2: Asset 접두어 넣기 (0) | 2023.11.25 |
[UE] Asset Editor Tool 1: Asset 복제 기능의 Editor 만들기 (0) | 2023.11.23 |
댓글
이 글 공유하기
다른 글
-
[UE] Slate UI 2: 제거 마법사 - 에셋 리스트 띄우기
[UE] Slate UI 2: 제거 마법사 - 에셋 리스트 띄우기
2023.12.01Content Menu Extender에 제거 마법사를 만들어 선택한 폴더에 들어있는 에셋들을 한 눈에 볼 수 있도록 만들것이다. 이번시간에는 Slate UI 내에 에셋리스트를 띄울 것이다. 다음 시간에는 띄운 에셋 리스트를 삭제할 수 있는 기능을 추가할 것이다. 목차 Widget을 생성하여 탭에 텍스트 띄우기 AdvanceDeletionWidget 생성 AdvanceDeletionWidget.h 더보기 #pragma once #include "Widgets/SCompoundWidget.h" /** Slate Widget을 위해 빈 클래스가 필요 * */ class SAdvanceDeletionTab : public SCompoundWidget { SLATE_BEGIN_ARGS(SAdvanceDeleti… -
[UE] Slate UI 1: Tab 띄우기
[UE] Slate UI 1: Tab 띄우기
2023.12.01Slate UI를 사용하여 탭을 생성하고 폴더 내에 있는 에셋 목록을 한번에 보여주게 만들 것이다. 에셋 목록을 조작하여 필요시 선택한 에셋들을 지우는 기능을 추가할 것이다. 일단, 탭을 생성하고 띄워주는 것부터 하겠다. 목차 Smart Pointer Smart Pointer 스마트 포인터 객체를 소유 삭제 예방 Unique Property TSharedPtr O O Reference Counting 메서드 사용 NULL을 가리킬 수 있음 TSharedRef O O NULL을 가리킬 수 없음 무조건유효한 객체를 가르켜야 함 TWeakPtr X X Reference cycle을 부순다 TSharedRef타입의 함수는 return 값이 무조건 NULL이 아닌 유효한 값이다. 이 점을 알고 있으면 코드 파악이… -
[UE] Asset Editor Tool 3: 사용하지 않는 Asset만 제거하기
[UE] Asset Editor Tool 3: 사용하지 않는 Asset만 제거하기
2023.11.25선택한 에셋들 중 사용하지 않는 에셋만 삭제하는 기능을 추가하였다. 이 기능으로 에셋을 삭제하면 의도치않게 에셋을 삭제하는 불상사를 방지할 수 있다. 목차 1. 사용되지 않는 Asset만 제거하기 .Build.cs 에 "UMG", "Niagara" 추가하기 SWManager.Build.cs 더보기 using UnrealBuildTool; public class SWManager : ModuleRules { public SWManager(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; PrivateIncludePaths.AddRange( new string[] { S… -
[UE] Asset Editor Tool 2: Asset 접두어 넣기
[UE] Asset Editor Tool 2: Asset 접두어 넣기
2023.11.25언리얼에서 작업을 할때 Asset을 보기좋게 찾기 위해 접두어를 넣는 경우는 흔하다. Material의 경우 M_, Blueprint는 BP_와 같은식이다. 매번 이 과정을 하는것은 귀찮은 일이기 때문에 자동으로 접두어를 붙여주는 기능을 만들었다. 목차 1. Asset 접두어 넣기 언리얼 공식문서에서 제시한 추천 접두어 모듬 https://docs.unrealengine.com/4.27/en-US/ProductionPipelines/AssetNaming/ Recommended Asset Naming Conventions A recommended naming convention to help organize your Assets. docs.unrealengine.com .Build.cs 에 "UMG", "…
댓글을 사용할 수 없습니다.