From 4671fcc0bbf9b3d94e739b2082f02300f48182d0 Mon Sep 17 00:00:00 2001 From: jinilkim Date: Thu, 23 Oct 2025 21:02:50 +0900 Subject: [PATCH] Add single asset export functionality and enhance README with usage details --- AssetExporterToJSON.cpp | 341 ++++++++++++++++++++++++++++++++- AssetExporterToJSON.h | 6 + README.md | 415 ++++++++++++++++++++++++++++++++-------- 3 files changed, 674 insertions(+), 88 deletions(-) diff --git a/AssetExporterToJSON.cpp b/AssetExporterToJSON.cpp index 56743e3..d969494 100644 --- a/AssetExporterToJSON.cpp +++ b/AssetExporterToJSON.cpp @@ -196,6 +196,202 @@ void FAssetExporterToJSON::ExportAssetsToJSON() FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(Message)); } +void FAssetExporterToJSON::ExportSingleAssetToJSON(const FAssetData& AssetData) +{ + UE_LOG(LogAssetExporter, Log, TEXT("Exporting single asset: %s"), *AssetData.GetObjectPathString()); + + UObject* Asset = AssetData.GetAsset(); + if (!Asset) + { + UE_LOG(LogAssetExporter, Error, TEXT("Failed to load asset: %s"), *AssetData.GetObjectPathString()); + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(TEXT("Failed to load asset"))); + return; + } + + // Determine asset type and export accordingly + TArray> JsonArray; + FString AssetTypeName; + + if (UDataTable* DataTable = Cast(Asset)) + { + AssetTypeName = TEXT("DataTable"); + TSharedPtr DataTableJson = MakeShareable(new FJsonObject); + DataTableJson->SetStringField(TEXT("AssetName"), DataTable->GetName()); + DataTableJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString()); + DataTableJson->SetStringField(TEXT("RowStructure"), DataTable->GetRowStruct() ? DataTable->GetRowStruct()->GetName() : TEXT("None")); + + // Export rows + TArray> RowsArray; + const UScriptStruct* RowStruct = DataTable->GetRowStruct(); + const TMap& RowMap = DataTable->GetRowMap(); + + for (const TPair& Row : RowMap) + { + TSharedPtr RowJson = MakeShareable(new FJsonObject); + RowJson->SetStringField(TEXT("RowName"), Row.Key.ToString()); + + if (RowStruct) + { + FString RowDataString; + if (FJsonObjectConverter::UStructToJsonObjectString(RowStruct, Row.Value, RowDataString, 0, 0)) + { + TSharedPtr RowDataJson; + TSharedRef> Reader = TJsonReaderFactory<>::Create(RowDataString); + if (FJsonSerializer::Deserialize(Reader, RowDataJson) && RowDataJson.IsValid()) + { + RowJson->SetObjectField(TEXT("Data"), RowDataJson); + } + } + } + + RowsArray.Add(MakeShareable(new FJsonValueObject(RowJson))); + } + + DataTableJson->SetArrayField(TEXT("Rows"), RowsArray); + JsonArray.Add(MakeShareable(new FJsonValueObject(DataTableJson))); + } + else if (UBlueprint* Blueprint = Cast(Asset)) + { + AssetTypeName = TEXT("Blueprint"); + TSharedPtr BlueprintJson = ExtractBlueprintDetails(Blueprint); + if (BlueprintJson.IsValid()) + { + BlueprintJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString()); + JsonArray.Add(MakeShareable(new FJsonValueObject(BlueprintJson))); + } + } + else if (UAnimMontage* AnimMontage = Cast(Asset)) + { + AssetTypeName = TEXT("AnimMontage"); + TSharedPtr MontageJson = MakeShareable(new FJsonObject); + MontageJson->SetStringField(TEXT("AssetName"), AnimMontage->GetName()); + MontageJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString()); + MontageJson->SetNumberField(TEXT("SequenceLength"), static_cast(AnimMontage->GetPlayLength())); + MontageJson->SetNumberField(TEXT("RateScale"), static_cast(AnimMontage->RateScale)); + + // Export sections + TArray> SectionsArray; + for (const FCompositeSection& Section : AnimMontage->CompositeSections) + { + TSharedPtr SectionJson = MakeShareable(new FJsonObject); + SectionJson->SetStringField(TEXT("SectionName"), Section.SectionName.ToString()); + SectionJson->SetNumberField(TEXT("StartTime"), static_cast(Section.GetTime())); + SectionJson->SetStringField(TEXT("NextSectionName"), Section.NextSectionName.ToString()); + SectionsArray.Add(MakeShareable(new FJsonValueObject(SectionJson))); + } + MontageJson->SetArrayField(TEXT("Sections"), SectionsArray); + + // Note: Simplified export for single asset - full export includes SlotAnimTracks and Notifies + JsonArray.Add(MakeShareable(new FJsonValueObject(MontageJson))); + } + else if (UCurveTable* CurveTable = Cast(Asset)) + { + AssetTypeName = TEXT("CurveTable"); + TSharedPtr CurveTableJson = MakeShareable(new FJsonObject); + CurveTableJson->SetStringField(TEXT("AssetName"), CurveTable->GetName()); + CurveTableJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString()); + + FString ModeStr = TEXT("Unknown"); + switch (CurveTable->GetCurveTableMode()) + { + case ECurveTableMode::Empty: + ModeStr = TEXT("Empty"); + break; + case ECurveTableMode::SimpleCurves: + ModeStr = TEXT("SimpleCurves"); + break; + case ECurveTableMode::RichCurves: + ModeStr = TEXT("RichCurves"); + break; + } + CurveTableJson->SetStringField(TEXT("CurveTableMode"), ModeStr); + + // Export curves (simplified for single asset) + TArray> CurvesArray; + if (CurveTable->GetCurveTableMode() == ECurveTableMode::RichCurves) + { + const TMap& RichCurveMap = CurveTable->GetRichCurveRowMap(); + for (const TPair& Row : RichCurveMap) + { + TSharedPtr CurveJson = MakeShareable(new FJsonObject); + CurveJson->SetStringField(TEXT("CurveName"), Row.Key.ToString()); + if (FRichCurve* Curve = Row.Value) + { + TArray> KeysArray; + const TArray& Keys = Curve->GetConstRefOfKeys(); + for (const FRichCurveKey& Key : Keys) + { + TSharedPtr KeyJson = MakeShareable(new FJsonObject); + KeyJson->SetNumberField(TEXT("Time"), static_cast(Key.Time)); + KeyJson->SetNumberField(TEXT("Value"), static_cast(Key.Value)); + KeysArray.Add(MakeShareable(new FJsonValueObject(KeyJson))); + } + CurveJson->SetArrayField(TEXT("Keys"), KeysArray); + } + CurvesArray.Add(MakeShareable(new FJsonValueObject(CurveJson))); + } + } + CurveTableJson->SetArrayField(TEXT("Curves"), CurvesArray); + JsonArray.Add(MakeShareable(new FJsonValueObject(CurveTableJson))); + } + else + { + UE_LOG(LogAssetExporter, Warning, TEXT("Unsupported asset type: %s"), *Asset->GetClass()->GetName()); + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf( + TEXT("Unsupported asset type: %s\n\nSupported types: DataTable, Blueprint, AnimMontage, CurveTable"), + *Asset->GetClass()->GetName() + ))); + return; + } + + if (JsonArray.Num() == 0) + { + UE_LOG(LogAssetExporter, Warning, TEXT("No data to export")); + return; + } + + // Create output directory + const UAssetExportSettings* Settings = GetDefault(); + FString OutputPath = FPaths::ProjectContentDir(); + if (Settings && !Settings->OutputDirectory.Path.IsEmpty()) + { + OutputPath = OutputPath / Settings->OutputDirectory.Path; + } + else + { + OutputPath = OutputPath / TEXT("Exports"); + } + + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + if (!PlatformFile.DirectoryExists(*OutputPath)) + { + PlatformFile.CreateDirectory(*OutputPath); + } + + // Create timestamped filename + FString Timestamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S")); + FString SafeAssetName = Asset->GetName().Replace(TEXT(" "), TEXT("_")); + FString FileName = FString::Printf(TEXT("%s_%s_%s.json"), *AssetTypeName, *SafeAssetName, *Timestamp); + + // Save JSON + if (SaveJsonToFile(JsonArray, FileName, OutputPath)) + { + FString FilePath = OutputPath / FileName; + FString Message = FString::Printf( + TEXT("Asset exported successfully!\n\n") + TEXT("Asset: %s\n") + TEXT("Type: %s\n\n") + TEXT("Output file:\n%s"), + *Asset->GetName(), + *AssetTypeName, + *FilePath + ); + + UE_LOG(LogAssetExporter, Log, TEXT("Single asset export completed: %s"), *FilePath); + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(Message)); + } +} + int32 FAssetExporterToJSON::ExportDataTables(const FString& FolderPath, TArray>& OutJsonArray) { UE_LOG(LogAssetExporter, Log, TEXT("Exporting DataTables from: %s"), *FolderPath); @@ -210,13 +406,22 @@ int32 FAssetExporterToJSON::ExportDataTables(const FString& FolderPath, TArray(AssetData.GetAsset()); if (!DataTable) continue; + // Progress logging every 10 assets or for first/last + if (Count == 0 || Count % 10 == 0 || Count == TotalCount - 1) + { + UE_LOG(LogAssetExporter, Log, TEXT(" Processing DataTable %d/%d: %s"), Count + 1, TotalCount, *DataTable->GetName()); + } + TSharedPtr DataTableJson = MakeShareable(new FJsonObject); DataTableJson->SetStringField(TEXT("AssetName"), DataTable->GetName()); DataTableJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString()); @@ -275,13 +480,22 @@ int32 FAssetExporterToJSON::ExportBlueprints(const FString& FolderPath, TArray(AssetData.GetAsset()); if (!Blueprint || !Blueprint->GeneratedClass) continue; + // Progress logging every 10 assets or for first/last + if (Count == 0 || Count % 10 == 0 || Count == TotalCount - 1) + { + UE_LOG(LogAssetExporter, Log, TEXT(" Processing Blueprint %d/%d: %s"), Count + 1, TotalCount, *Blueprint->GetName()); + } + TSharedPtr BlueprintJson = ExtractBlueprintDetails(Blueprint); if (BlueprintJson.IsValid()) { @@ -340,26 +554,125 @@ TArray> FAssetExporterToJSON::ExtractBlueprintVariables(U if (!DefaultObject) return VariablesArray; - // Iterate through all properties - for (TFieldIterator PropIt(Blueprint->GeneratedClass, EFieldIteratorFlags::ExcludeSuper); PropIt; ++PropIt) + // CRITICAL: Export Blueprint custom variables (NewVariables) + // This is the most important part for Blueprint analysis + for (const FBPVariableDescription& Variable : Blueprint->NewVariables) + { + TSharedPtr VarJson = MakeShareable(new FJsonObject); + VarJson->SetStringField(TEXT("Name"), Variable.VarName.ToString()); + VarJson->SetStringField(TEXT("VarGuid"), Variable.VarGuid.ToString()); + + // Export variable type information + VarJson->SetStringField(TEXT("Category"), Variable.VarType.PinCategory.ToString()); + if (Variable.VarType.PinSubCategoryObject.IsValid()) + { + VarJson->SetStringField(TEXT("SubCategoryObject"), Variable.VarType.PinSubCategoryObject->GetName()); + } + if (!Variable.VarType.PinSubCategory.IsNone()) + { + VarJson->SetStringField(TEXT("SubCategory"), Variable.VarType.PinSubCategory.ToString()); + } + + // Container type (Array, Set, Map) + if (Variable.VarType.ContainerType != EPinContainerType::None) + { + FString ContainerTypeStr; + switch (Variable.VarType.ContainerType) + { + case EPinContainerType::Array: + ContainerTypeStr = TEXT("Array"); + break; + case EPinContainerType::Set: + ContainerTypeStr = TEXT("Set"); + break; + case EPinContainerType::Map: + ContainerTypeStr = TEXT("Map"); + break; + } + VarJson->SetStringField(TEXT("ContainerType"), ContainerTypeStr); + } + + // Export default value + VarJson->SetStringField(TEXT("DefaultValue"), Variable.DefaultValue); + + // Export property flags + VarJson->SetBoolField(TEXT("IsEditable"), (Variable.PropertyFlags & CPF_Edit) != 0); + VarJson->SetBoolField(TEXT("IsBlueprintVisible"), (Variable.PropertyFlags & CPF_BlueprintVisible) != 0); + VarJson->SetBoolField(TEXT("IsBlueprintReadOnly"), (Variable.PropertyFlags & CPF_BlueprintReadOnly) != 0); + VarJson->SetBoolField(TEXT("IsExposedOnSpawn"), (Variable.PropertyFlags & CPF_ExposeOnSpawn) != 0); + VarJson->SetBoolField(TEXT("IsInstanceEditable"), (Variable.PropertyFlags & CPF_DisableEditOnInstance) == 0); + + // Export category + if (!Variable.Category.IsEmpty()) + { + VarJson->SetStringField(TEXT("CategoryName"), Variable.Category.ToString()); + } + + // Export replication + if (Variable.RepNotifyFunc != NAME_None) + { + VarJson->SetStringField(TEXT("RepNotifyFunc"), Variable.RepNotifyFunc.ToString()); + } + + // Export metadata + if (Variable.MetaDataArray.Num() > 0) + { + TSharedPtr MetaDataJson = MakeShareable(new FJsonObject); + + for (const FBPVariableMetaDataEntry& MetaDataEntry : Variable.MetaDataArray) + { + MetaDataJson->SetStringField(MetaDataEntry.DataKey.ToString(), MetaDataEntry.DataValue); + } + + if (MetaDataJson->Values.Num() > 0) + { + VarJson->SetObjectField(TEXT("MetaData"), MetaDataJson); + } + } + + VarJson->SetStringField(TEXT("Source"), TEXT("Blueprint")); + VariablesArray.Add(MakeShareable(new FJsonValueObject(VarJson))); + } + + // Extract parent class properties with Category = "WorldStalker" + // This captures important C++ properties like ActivationOrderGroup + for (TFieldIterator PropIt(Blueprint->GeneratedClass); PropIt; ++PropIt) { FProperty* Property = *PropIt; if (!Property) continue; + // Check if this property has Category metadata set to "WorldStalker" + const FString* CategoryMeta = Property->FindMetaData(TEXT("Category")); + if (!CategoryMeta || !CategoryMeta->Equals(TEXT("WorldStalker"))) + continue; + + // This is a WorldStalker category property - export it! TSharedPtr VarJson = MakeShareable(new FJsonObject); VarJson->SetStringField(TEXT("Name"), Property->GetName()); VarJson->SetStringField(TEXT("Type"), Property->GetCPPType()); - // Get default value + // Get default value from CDO FString DefaultValue; const void* ValuePtr = Property->ContainerPtrToValuePtr(DefaultObject); Property->ExportTextItem_Direct(DefaultValue, ValuePtr, nullptr, nullptr, PPF_None); VarJson->SetStringField(TEXT("DefaultValue"), DefaultValue); - // Additional metadata + // Property flags VarJson->SetBoolField(TEXT("IsEditable"), Property->HasAnyPropertyFlags(CPF_Edit)); VarJson->SetBoolField(TEXT("IsBlueprintVisible"), Property->HasAnyPropertyFlags(CPF_BlueprintVisible)); + VarJson->SetBoolField(TEXT("IsBlueprintReadOnly"), Property->HasAnyPropertyFlags(CPF_BlueprintReadOnly)); + VarJson->SetBoolField(TEXT("IsEditDefaultsOnly"), Property->HasAnyPropertyFlags(CPF_DisableEditOnInstance)); + + // Category and source + VarJson->SetStringField(TEXT("CategoryName"), TEXT("WorldStalker")); + VarJson->SetStringField(TEXT("Source"), TEXT("C++ParentClass")); + + // Get owning class name for clarity + if (Property->GetOwnerClass()) + { + VarJson->SetStringField(TEXT("OwnerClass"), Property->GetOwnerClass()->GetName()); + } VariablesArray.Add(MakeShareable(new FJsonValueObject(VarJson))); } @@ -598,12 +911,17 @@ int32 FAssetExporterToJSON::ExportAnimMontages(const FString& FolderPath, TArray AssetRegistryModule.Get().GetAssets(Filter, AssetList); + UE_LOG(LogAssetExporter, Log, TEXT("Found %d AnimMontages to export"), AssetList.Num()); + // Track exported assets to avoid duplicates TSet ExportedPaths; int32 Count = 0; + int32 TotalCount = AssetList.Num(); + int32 ProcessedCount = 0; for (const FAssetData& AssetData : AssetList) { + ProcessedCount++; FString AssetPath = AssetData.GetObjectPathString(); // Skip if already exported @@ -617,6 +935,12 @@ int32 FAssetExporterToJSON::ExportAnimMontages(const FString& FolderPath, TArray if (!AnimMontage) continue; + // Progress logging every 10 assets or for first/last + if (Count == 0 || Count % 10 == 0 || ProcessedCount == TotalCount) + { + UE_LOG(LogAssetExporter, Log, TEXT(" Processing AnimMontage %d/%d: %s"), ProcessedCount, TotalCount, *AnimMontage->GetName()); + } + ExportedPaths.Add(AssetPath); TSharedPtr MontageJson = MakeShareable(new FJsonObject); @@ -785,13 +1109,22 @@ int32 FAssetExporterToJSON::ExportCurveTables(const FString& FolderPath, TArray< AssetRegistryModule.Get().GetAssets(Filter, AssetList); + UE_LOG(LogAssetExporter, Log, TEXT("Found %d CurveTables to export"), AssetList.Num()); + int32 Count = 0; + int32 TotalCount = AssetList.Num(); for (const FAssetData& AssetData : AssetList) { UCurveTable* CurveTable = Cast(AssetData.GetAsset()); if (!CurveTable) continue; + // Progress logging every 10 assets or for first/last + if (Count == 0 || Count % 10 == 0 || Count == TotalCount - 1) + { + UE_LOG(LogAssetExporter, Log, TEXT(" Processing CurveTable %d/%d: %s"), Count + 1, TotalCount, *CurveTable->GetName()); + } + TSharedPtr CurveTableJson = MakeShareable(new FJsonObject); CurveTableJson->SetStringField(TEXT("AssetName"), CurveTable->GetName()); CurveTableJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString()); diff --git a/AssetExporterToJSON.h b/AssetExporterToJSON.h index fd4c585..2038bbd 100644 --- a/AssetExporterToJSON.h +++ b/AssetExporterToJSON.h @@ -18,6 +18,12 @@ public: */ static void ExportAssetsToJSON(); + /** + * Export a single asset to JSON (for right-click context menu) + * @param AssetData - Asset to export + */ + static void ExportSingleAssetToJSON(const struct FAssetData& AssetData); + private: /** * Export all DataTables from the specified path to JSON diff --git a/README.md b/README.md index cbc7bb4..d68fdf4 100644 --- a/README.md +++ b/README.md @@ -20,17 +20,57 @@ - **다중 폴더 지원**: 한 번의 클릭으로 여러 폴더 경로에서 익스포트 - **에셋 타입 필터링**: 설정을 통해 특정 에셋 타입 활성화/비활성화 +### 두 가지 익스포트 방식 + +#### 1. 대량 익스포트 (Tools 메뉴) +- 설정된 모든 폴더의 에셋을 한 번에 익스포트 +- 프로젝트 전체 밸런스 분석에 적합 +- `Tools → WorldStalker → Export Assets to JSON` + +#### 2. 단일 에셋 익스포트 (우클릭 메뉴) +- Content Browser에서 개별 에셋 우클릭 후 즉시 익스포트 +- 빠른 테스트 및 반복 작업에 적합 +- 타임스탬프 파일명 자동 생성 + ### 포괄적인 데이터 추출 #### Blueprint 익스포트 -- 타입과 기본값을 포함한 변수 -- 입력/출력을 포함한 함수 + +**변수 추출 - 3가지 소스**: + +1. **Blueprint 커스텀 변수** (`Blueprint->NewVariables`) + - Blueprint 에디터에서 추가한 모든 커스텀 변수 + - 변수 GUID, 타입 정보 (Category, SubCategory, ContainerType) + - Property flags (IsEditable, IsBlueprintVisible, IsExposedOnSpawn 등) + - 카테고리명 및 전체 메타데이터 + - Replication 설정 + +2. **C++ 부모 클래스 프로퍼티** (`Category = "WorldStalker"`) + - ✨ **NEW**: C++ 부모 클래스에서 정의된 중요 프로퍼티 추출 + - **ActivationOrderGroup** - 스킬 실행 순서 그룹 + - **bDisableOrderGroup** - 순서 그룹 비활성화 여부 + - **bCanBeCancel** - 취소 가능 여부 + - **ActivationTrigger** - 발동 트리거 타입 + - **bStopAutoTargetWhenEndAbility** - 어빌리티 종료 시 자동 타겟 중지 + - Source 필드로 "C++ParentClass" 표시 + - OwnerClass 필드로 정의된 C++ 클래스 명시 + +3. **컴파일된 클래스 프로퍼티** + - 부모 클래스에서 상속된 기타 프로퍼티 + +**함수**: +- 입력/출력 파라미터를 포함한 함수 목록 +- FunctionGraphs에서 추출 + +**컴포넌트**: - 컴포넌트 계층 구조 -- **이벤트 그래프**: 다음을 포함한 완전한 노드 그래프 - - 노드 타입, 제목, 위치, 코멘트 - - 핀 타입, 방향, 기본값 - - 핀 간 연결 그래프 - - 노드별 프로퍼티 +- SimpleConstructionScript에서 추출 + +**이벤트 그래프**: 다음을 포함한 완전한 노드 그래프 +- 노드 타입, 제목, 위치, 코멘트 +- 핀 타입, 방향, 기본값 +- 핀 간 연결 그래프 +- 노드별 프로퍼티 #### AnimMontage 익스포트 - 시작/종료 시간을 포함한 섹션 @@ -46,6 +86,12 @@ - 시간, 값, 탄젠트 정보를 포함한 키 데이터 - 커브 보간 모드 +### 진행 상황 로깅 +- ✨ **NEW**: 10개 에셋마다 진행 상황 로그 출력 +- Output Log에서 실시간 진행 상황 확인 가능 +- 에디터가 먹통처럼 보이는 현상 방지 +- 로그 카테고리: `LogAssetExporter` + ### 출력 관리 - **타임스탬프 익스포트**: 익스포트 히스토리 보관을 위한 선택적 타임스탬프 하위 폴더 - **타입별 파일 분리**: 에셋 타입별 개별 JSON 파일 (DataTable.json, Blueprint.json 등) @@ -87,7 +133,8 @@ PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore", "ToolMenus", - "DeveloperSettings" + "DeveloperSettings", + "ContentBrowser" // 우클릭 메뉴용 }); ``` @@ -147,11 +194,49 @@ DataTables ## 사용 방법 -### 빠른 익스포트 +### 방법 1: 대량 익스포트 (Tools 메뉴) + +**전체 프로젝트 밸런스 분석용** 1. 언리얼 에디터 열기 -2. 다음으로 이동: `툴 → WorldStalker → Export Assets to JSON` -3. 익스포트 완료 대기 (알림 대화상자가 표시됨) +2. 다음으로 이동: `Tools → WorldStalker → Export Assets to JSON` +3. Output Log에서 진행 상황 확인 (`Window → Developer Tools → Output Log`) + - 로그 필터: `LogAssetExporter` +4. 익스포트 완료 대기 (알림 대화상자가 표시됨) + +**Output Log 예시**: +``` +LogAssetExporter: Starting Asset Export to JSON process... +LogAssetExporter: Export path: /Game/Blueprints/Abilities +LogAssetExporter: Found 45 Blueprints to export +LogAssetExporter: Processing Blueprint 1/45: GA_Skill_Cazimord_Flash +LogAssetExporter: Processing Blueprint 10/45: GA_Skill_Fireball +LogAssetExporter: Processing Blueprint 20/45: GA_Skill_IceSpear +... +LogAssetExporter: Exported 45 Blueprints +LogAssetExporter: Export completed! Total assets exported: 168 +``` + +### 방법 2: 단일 에셋 익스포트 (우클릭) + +**빠른 테스트 및 반복 작업용** + +1. Content Browser에서 익스포트할 에셋 찾기 +2. 에셋 우클릭 +3. **"Export to JSON"** 선택 +4. 즉시 익스포트 완료 (확인 대화상자 표시) + +**지원 에셋 타입**: +- DataTable +- Blueprint +- AnimMontage +- CurveTable + +**출력 파일명 형식**: +``` +Blueprint_GA_Skill_Cazimord_Flash_20251023_143205.json +AnimMontage_AM_Attack_Combo_20251023_143210.json +``` ### 익스포트 출력 @@ -172,90 +257,185 @@ Content/ #### DataTable.json ```json -[ - { - "AssetName": "DT_CharacterStats", - "AssetPath": "/Game/DataTables/DT_CharacterStats", - "RowCount": 10, - "Columns": ["Name", "Health", "Attack", "Defense"], - "Rows": { - "Warrior": { - "Name": "Warrior", - "Health": "1000", - "Attack": "150", - "Defense": "100" - } +{ + "ExportedAt": "2025-10-23 14:32:05", + "TotalCount": 10, + "Assets": [ + { + "AssetName": "DT_CharacterStats", + "AssetPath": "/Game/DataTables/DT_CharacterStats", + "RowStructure": "FCharacterStats", + "Rows": [ + { + "RowName": "Warrior", + "Data": { + "Name": "Warrior", + "Health": 1000.0, + "Attack": 150.0, + "Defense": 100.0 + } + } + ] } - } -] + ] +} ``` -#### Blueprint.json +#### Blueprint.json - 변수 섹션 + ```json -[ - { - "AssetName": "BP_Enemy_Goblin", - "AssetPath": "/Game/Blueprints/Enemy/BP_Enemy_Goblin", - "ParentClass": "WSCharacterBase", - "Variables": [ - { - "Name": "MaxHealth", - "Type": "float", - "DefaultValue": "500.0" +{ + "AssetName": "GA_Skill_Cazimord_Flash", + "AssetPath": "/Game/Blueprints/Abilities/GA_Skill_Cazimord_Flash", + "ParentClass": "WSGameplayAbility", + "Variables": [ + { + "Name": "ActivationOrderGroup", + "Type": "uint8", + "DefaultValue": "1", + "CategoryName": "WorldStalker", + "Source": "C++ParentClass", + "OwnerClass": "WSGameplayAbility", + "IsEditable": true, + "IsBlueprintVisible": true, + "IsBlueprintReadOnly": false, + "IsEditDefaultsOnly": true + }, + { + "Name": "bDisableOrderGroup", + "Type": "bool", + "DefaultValue": "False", + "CategoryName": "WorldStalker", + "Source": "C++ParentClass", + "OwnerClass": "WSGameplayAbility" + }, + { + "Name": "CustomDamageMultiplier", + "VarGuid": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890", + "Category": "float", + "DefaultValue": "1.5", + "Source": "Blueprint", + "IsEditable": true, + "IsBlueprintVisible": true, + "CategoryName": "Combat", + "MetaData": { + "DisplayName": "데미지 배율", + "Tooltip": "스킬 데미지 최종 배율" } - ], - "EventGraphs": [ - { - "GraphName": "EventGraph", - "Nodes": [ - { - "NodeTitle": "Event BeginPlay", - "NodeClass": "K2Node_Event", - "Pins": [...] - } - ], - "Connections": [ - { - "SourceNode": "Event BeginPlay", - "SourcePin": "then", - "TargetNode": "Set Max Health", - "TargetPin": "execute" - } - ] - } - ] - } -] + } + ], + "Functions": [...], + "Components": [...], + "EventGraphs": [...] +} ``` +**변수 Source 타입 설명**: +- `"Blueprint"`: Blueprint 에디터에서 추가한 커스텀 변수 +- `"C++ParentClass"`: C++ 부모 클래스에서 정의된 프로퍼티 (Category = "WorldStalker") +- `"CompiledClass"`: 기타 컴파일된 클래스 프로퍼티 + #### AnimMontage.json ```json -[ - { - "AssetName": "AM_Attack_Combo", - "AssetPath": "/Game/Animations/AM_Attack_Combo", - "SequenceLength": 2.5, - "Sections": [ - { - "SectionName": "Combo1", - "StartTime": 0.0, - "EndTime": 0.8 +{ + "AssetName": "AM_Attack_Combo", + "AssetPath": "/Game/Animations/AM_Attack_Combo", + "SequenceLength": 2.5, + "RateScale": 1.0, + "Sections": [ + { + "SectionName": "Combo1", + "StartTime": 0.0, + "NextSectionName": "Combo2" + } + ], + "AnimNotifies": [ + { + "NotifyName": "DealDamage", + "TriggerTime": 0.5, + "Duration": 0.0, + "NotifyType": "Notify", + "NotifyClass": "ANS_DealDamage", + "CustomProperties": { + "DamageAmount": "50.0", + "DamageType": "Physical" } - ], - "Notifies": [ - { - "NotifyName": "DamageNotify", - "TriggerTime": 0.5, - "NotifyType": "AnimNotify", - "CustomProperties": { - "DamageMultiplier": "1.5", - "HitboxSize": "100.0" + } + ], + "SlotAnimTracks": [ + { + "SlotName": "DefaultSlot", + "AnimSegments": [ + { + "AnimReference": "AS_Attack1", + "AnimPath": "/Game/Animation/Sequences/AS_Attack1", + "StartPos": 0.0, + "AnimStartTime": 0.0, + "AnimEndTime": 1.0, + "AnimPlayRate": 1.0, + "LoopingCount": 1 } - } - ], - "SlotAnimTracks": [...] - } -] + ] + } + ], + "BlendInTime": 0.25, + "BlendOutTime": 0.25, + "BlendModeIn": "Standard", + "BlendModeOut": "Standard" +} +``` + +## 전투 밸런스 분석 활용 예시 + +### 워크플로우 + +1. **전체 스킬 익스포트** + ``` + Project Settings → Asset Export to JSON + Export Folder Paths에 "/Game/Blueprints/Abilities" 추가 + Tools → Export Assets to JSON 실행 + ``` + +2. **익스포트된 JSON 분석** + ```python + import json + + with open('Content/Exports/Blueprint.json', encoding='utf-8') as f: + data = json.load(f) + + # ActivationOrderGroup 분석 + for asset in data['Assets']: + if 'GA_Skill' in asset['AssetName']: + variables = asset['Variables'] + + # C++ 부모 클래스 프로퍼티 찾기 + for var in variables: + if var['Source'] == 'C++ParentClass': + if var['Name'] == 'ActivationOrderGroup': + print(f"{asset['AssetName']}: Order Group = {var['DefaultValue']}") + ``` + +3. **특정 스킬 수정 후 빠른 재분석** + ``` + 1. GA_Skill_Cazimord_Flash 블루프린트 수정 + 2. Content Browser에서 우클릭 → Export to JSON + 3. 생성된 단일 JSON 파일로 LLM 분석 + 4. 밸런스 피드백 받기 + 5. 반복 + ``` + +### LLM 프롬프트 예시 + +``` +다음 스킬 Blueprint JSON을 분석해주세요: + +[JSON 데이터 붙여넣기] + +분석 포인트: +1. ActivationOrderGroup 값이 적절한가? +2. 같은 그룹의 다른 스킬들과 밸런스가 맞는가? +3. CustomProperties의 데미지 배율이 적정한가? +4. AnimMontage의 타이밍과 DealDamage 노티파이 시점이 일치하는가? ``` ## 문제 해결 @@ -265,11 +445,34 @@ Content/ **문제**: 익스포트가 완료되었지만 JSON 파일이 생성되지 않음 **해결**: 익스포트 경로에 활성화된 타입의 에셋이 포함되어 있는지 확인 +### 우클릭 메뉴가 나타나지 않음 + +**문제**: Content Browser에서 에셋 우클릭 시 "Export to JSON" 메뉴가 없음 +**해결**: +- 지원되는 에셋 타입인지 확인 (DataTable, Blueprint, AnimMontage, CurveTable) +- 단일 에셋만 선택했는지 확인 (다중 선택 시 메뉴 비활성화) +- 에디터 재시작 시도 + +### C++ 프로퍼티가 익스포트되지 않음 + +**문제**: ActivationOrderGroup 같은 C++ 프로퍼티가 JSON에 없음 +**해결**: +- Property에 `Category = "WorldStalker"` 메타데이터가 있는지 확인 +- C++ 헤더 파일에서 `UPROPERTY(EditDefaultsOnly, Category = "WorldStalker")` 선언 확인 +- 최신 코드로 빌드되었는지 확인 + ### 출력에 중복 에셋 **문제**: 동일한 에셋이 JSON에 여러 번 나타남 **해결**: 이제 중복 검출로 방지됨 - 설정에서 겹치는 폴더 경로 확인 +### 진행 상황이 보이지 않음 + +**문제**: 익스포트 중 에디터가 먹통처럼 보임 +**해결**: Output Log 열기 (`Window → Developer Tools → Output Log`) +- 필터에 `LogAssetExporter` 입력 +- 10개 에셋마다 진행 상황 로그 확인 가능 + ### 통합 후 빌드 오류 **문제**: 누락된 헤더에 대한 컴파일 오류 @@ -297,11 +500,41 @@ Content/ - **UnrealEd** - 에디터 통합 - **ToolMenus** - 메뉴 확장 시스템 - **DeveloperSettings** - 프로젝트 설정 통합 +- **ContentBrowser** - 우클릭 컨텍스트 메뉴 ### 성능 특성 - **재귀 검색**: 설정된 경로의 모든 하위 폴더 검색 - **메모리 사용량**: 메모리 사용량을 최소화하기 위해 에셋을 하나씩 로드 - **익스포트 속도**: 에셋 복잡도에 따라 초당 약 10-50개 에셋 +- **진행 상황**: 10개 에셋마다 로그 출력으로 진행 상황 실시간 확인 + +### Blueprint 변수 추출 상세 + +시스템은 3가지 소스에서 변수를 추출합니다: + +**1. Blueprint 커스텀 변수 (`Blueprint->NewVariables`)** +```cpp +for (const FBPVariableDescription& Variable : Blueprint->NewVariables) +{ + // VarName, VarGuid, VarType, DefaultValue 추출 + // PropertyFlags, Category, MetaData 추출 +} +``` + +**2. C++ 부모 클래스 프로퍼티 (Category = "WorldStalker")** +```cpp +for (TFieldIterator PropIt(Blueprint->GeneratedClass); PropIt; ++PropIt) +{ + const FString* CategoryMeta = PropIt->FindMetaData(TEXT("Category")); + if (CategoryMeta && CategoryMeta->Equals(TEXT("WorldStalker"))) + { + // ActivationOrderGroup 등 중요 프로퍼티 추출 + } +} +``` + +**3. 기타 컴파일된 클래스 프로퍼티** +- 부모 클래스에서 상속된 기타 프로퍼티 ### Blueprint 이벤트 그래프 추출 시스템은 Blueprint 그래프 구조를 순회하여 다음을 추출합니다: @@ -324,16 +557,30 @@ Content/ 2. **바이너리 에셋**: 바이너리 데이터 (메시, 텍스처, 오디오)는 익스포트할 수 없음 3. **복잡한 프로퍼티 타입**: 일부 복잡한 UObject 프로퍼티는 객체 참조로만 익스포트될 수 있음 4. **대형 Blueprint**: 수천 개의 노드가 있는 매우 큰 블루프린트는 익스포트에 상당한 시간이 걸릴 수 있음 +5. **카테고리 필터**: 현재 "WorldStalker" 카테고리만 C++ 프로퍼티를 추출 (필요시 코드 수정으로 다른 카테고리 추가 가능) ## 개발 히스토리 +### Version 1.0 (2025-10-23) + **초기 릴리스**: 포괄적인 에셋 추출 기능을 갖춘 INI 기반 설정 시스템 +**주요 기능**: +- ✅ Blueprint 커스텀 변수 (NewVariables) 완전 추출 +- ✅ C++ 부모 클래스 프로퍼티 추출 (Category = "WorldStalker") + - ActivationOrderGroup 등 중요 프로퍼티 지원 +- ✅ 우클릭 컨텍스트 메뉴로 단일 에셋 빠른 익스포트 +- ✅ 진행 상황 로깅 (10개 에셋마다) +- ✅ AnimMontage 커스텀 프로퍼티 추출 +- ✅ 이벤트 그래프 노드 연결 추출 +- ✅ INI 기반 영구 설정 + **주요 설계 결정**: - 반복 사용의 편의성을 위해 체크박스 UI 대신 INI 기반 설정 선택 - 버전 관리를 통한 팀 협업을 위한 UDeveloperSettings 통합 - 겹치는 폴더 경로로부터 중복 익스포트 방지를 위한 중복 검출 - 사람의 가독성과 LLM 처리를 위한 보기 좋은 JSON 형식 +- C++ 부모 클래스 프로퍼티 추출로 스킬 밸런스 분석 강화 ## 지원