Goal
- The tool should help users to organize the existing assets into their proper subdirectories by type, for example:
Skeleton
andSkeletal Mesh
should reside inMeshes
subdirectory.Physics Asset
,IK Rig
,IK Retargeter
, andControl Rig
, should reside inRigs
subdirectory.Anim Sequence
,Anim Blueprint
, andBlendSpace
, should reside inDemo/Animations
subdirectory.
- If an asset already resides in some matching subdirectory, then the tool shouldn't put it into another subdirectory.
Pitfalls
- We can use a
TMap<UClass*, FString>
to hard code the relationship between an asset type and their matching subdirectory. But the first challenge is to provide the properinclude
directives so that our desiredUClass
es can be found. When we are still new with C++ and with the build process of Unreal overall, this is quite tricky. - To move an asset, we'll use
UEditorAssetLibrary::RenameLoadedAsset
method, but we'll have to make sure the asset is indeed loaded first.
Suggested Solution
Build.cs file
- (1): "
IKRig
" module is needed forUIKRigDefintion
andUIKRetargeter
classes. - (2): "
ControlRig
" module is needed forUControlRig
class, but usually we deal withUControlRigBlueprint
class, so we also need "ControlRigDeveloper
" module. - (3): "
LevelSequence
" module is needed forULevelSequence
class.
using UnrealBuildTool;
public class TruongControlRigs : ModuleRules
{
public TruongControlRigs(ReadOnlyTargetRules Target) : base(Target)
{
// --snip--
PrivateDependencyModuleNames.AddRange(new string[]
{
"CoreUObject",
"Engine"
});
PublicDependencyModuleNames.AddRange(new string[] {
"Core",
"InputCore",
"Blutility",
"EditorScriptingUtilities",
"UnrealEd",
"IKRig", // (1)
"ControlRig", // (2)
"ControlRigDeveloper", // (2)
"LevelSequence" // (3)
});
}
}
Header file
Then, same drill with the previous exercise, we define an inline static const
variable for our mapping (4). Our method doesn't have any parameters (5) as we're allowing the tool to process everything automatically in this exercise, which is not a very flexible design, but we'll accept that for practice purpose.
#pragma once
#include "CoreMinimal.h"
#include "AssetActionUtility.h"
#include "PhysicsEngine/PhysicsAsset.h"
#include "Rig/IKRigDefinition.h" // (1)
#include "Retargeter/IKRetargeter.h" // (1)
#include "ControlRig.h" // (2)
#include "ControlRigBlueprint.h" // (2)
#include "Animation/BlendSpace1D.h"
#include "LevelSequence.h" // (3)
#include "mkCharSetup.generated.h"
UCLASS()
class TRUONGCONTROLRIGS_API UmkCharSetup : public UAssetActionUtility
{
GENERATED_BODY()
private:
inline static const TMap<UClass*, FString> AssetTypeToFolder = {
// `Meshes` folder
{USkeleton::StaticClass(), TEXT("Meshes")},
{USkeletalMesh::StaticClass(), TEXT("Meshes")},
// `Rigs` folder
{UPhysicsAsset::StaticClass(), TEXT("Rigs")},
{UIKRigDefinition::StaticClass(), TEXT("Rigs")},
{UIKRetargeter::StaticClass(), TEXT("Rigs")},
{UControlRig::StaticClass(), TEXT("Rigs")},
{UControlRigBlueprint::StaticClass(), TEXT("Rigs")},
// `Textures` folder
{UTexture::StaticClass(), TEXT("Textures")},
// `Materials` folder
{UMaterial::StaticClass(), TEXT("Materials")},
// `Demo/Animations` folder
{UAnimSequence::StaticClass(), TEXT("Demo/Animations")},
{UBlendSpace::StaticClass(), TEXT("Demo/Animations")},
{UBlendSpace1D::StaticClass(), TEXT("Demo/Animations")},
{UAnimBlueprint::StaticClass(), TEXT("Demo/Animations")},
// `Maps` folder
{UWorld::StaticClass(), TEXT("Maps")},
{ULevel::StaticClass(), TEXT("Maps")},
// `Sequences` folder
{ULevelSequence::StaticClass(), TEXT("Sequences")},
}; // (4)
public:
UFUNCTION(CallInEditor)
static void MoveAssetsToSubDirs(); // (5)
};
cpp file
- A selected asset might not be within our
AssetTypeToFolder
definition, so we'll skip processing it if nothing valid is found in the mapping (6). - We also don't want to create another subdirectory if some other subdirectory with the same name is already in the path of the asset (7).
- Finally we want to load the asset first (8) before moving it with
UEditorAssetLibrary::RenameLoadedAsset
(9).
void UmkCharSetup::MoveAssetsToSubDirs()
{
for (TArray<UObject*> SelectedObjects = UEditorUtilityLibrary::GetSelectedAssets(); const UObject* Object :
SelectedObjects)
{
if (!ensure(Object)) { continue; }
const FString* SubDirName = AssetTypeToFolder.Find(Object->GetClass());
if (!(ensure(SubDirName) && !SubDirName->IsEmpty())) // (6)
{
UE_LOG(LogTemp, Warning, TEXT("No subdirectory defined for class %s"), *Object->GetClass()->GetName());
continue;
}
FString CurrAssetPath = Object->GetPackage()->GetPathName();
if (CurrAssetPath.Contains(*SubDirName)) // (7)
{
UE_LOG(LogTemp, Display, TEXT("Skipping %s as it's already in a correct subdirectory"), *CurrAssetPath);
continue;
}
// We must load the asset first or else we'll have problem moving it.
UObject* LoadedAsset = UEditorAssetLibrary::LoadAsset(CurrAssetPath); // (8)
if (!ensure(LoadedAsset))
{
UE_LOG(LogTemp, Warning, TEXT("Failed to load asset at path: %s"), *CurrAssetPath);
continue;
}
FString NewAssetPath = FPaths::Combine(FPaths::GetPath(CurrAssetPath), *SubDirName, *Object->GetName());
UEditorAssetLibrary::RenameLoadedAsset(LoadedAsset, NewAssetPath); // (9)
}
}
Considerations
- We may provide support for any type of assets -- instead of only the types included in our
TMap
-- by resorting to its class name. - A preview of which goes where would be helpful for the user before they decide to proceed with the operation, but let's settle with this for now 😼.