BLOGブログ

2021.10.06UE4その他

[UE4]データテーブルとデータアセットのテキスト化

執筆バージョン: Unreal Engine 4.27

こんにちは。エンジニアの森です。

今回は、 UE4 上のデータテーブルとデータアセットを、テキスト出力する手法について考えます。

データテーブルを CSV エクスポートする

UE4 のデータテーブルは CSV 形式と Json 形式でのエクスポート/インポートをサポートしています。
また、データテーブルの右クリックメニューから「Export as CSV」が用意されているので、これを使えば、データテーブルのエクスポートはできます。
でも、データテーブルを手動でエクスポートするのって、面倒ですよね?
できれば自動でエクスポートを行う機能が欲しいです。
というわけで早速作っていきます。

データテーブルを CSV テキスト化して出力するために、実際に行うことは以下の 3 ステップです。

  1. テキスト保存先をあらかじめ決めておく
  2. データテーブルを CSV テキスト化
  3. FFileHelper::SaveStringToFile を使用してテキストファイルを出力

出力先フォルダはプロジェクト設定で決められるようにして、出力ファイル名はアセット名と同じにしておけば良いでしょう。
データテーブルから CSV テキストへの変換は、 UDataTable::GetTableAsCSV を使用します。
独自のプロジェクト設定を追加する方法はこちらの記事(UDeveloperSettingsでプロジェクト設定に項目を追加する)を参照。
データテーブルの編集時には FTableRowBase::OnDataTableChanged が呼ばれますので、実装には UBlueprintFunctionLibrary を使うとすると、以下のようになります。

#include “CoreMinimal.h”
#include “Kismet/BlueprintFunctionLibrary.h”
#include “MyBlueprintFunctionLibrary.generated.h”

class UDataTable;

UCLASS()
class MYPROJECT_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()

public:

UFUNCTION()
static FString GetExportFileName(const UObject *Object, const FString &Extension);

UFUNCTION()
static void SaveStringToFile(const FString &String, const FString &FilePath);

UFUNCTION()
static void ExportDataTableAsCSV(const UDataTable *Table);
};

FString UMyBlueprintFunctionLibrary::GetExportFileName(const UObject *Object, const FString &Extension)
{
// 指定の拡張子で、UObject と同じ名前のファイル名を生成する
// ファイルパスは、プロジェクト設定に依存
static const FString PathPrefix = TEXT(“/Game”);
const UMySettings *Settings = UMySettings::GetMySettings();
if (Settings != nullptr) {
const FString ProjectDir = FPaths::ProjectDir();
FString FileName = Object->GetPackage()->GetFName().ToString() + TEXT(“.”) + Extension;
if (FileName.StartsWith(PathPrefix))
{
FileName = FileName.RightChop(PathPrefix.Len());
}
const FString OutputPath = FPaths::Combine(ProjectDir, Settings->ResourceOutputFolder, FileName);
return OutputPath;
}

return FString();
}

void UMyBlueprintFunctionLibrary::SaveStringToFile(const FString &String, const FString &FilePath)
{
// UTF-8 設定でテキストファイルを出力
if (USourceControlHelpers::IsEnabled())
{
USourceControlHelpers::CheckOutFile(FilePath);
}
FFileHelper::SaveStringToFile(String, *FilePath, FFileHelper::EEncodingOptions::ForceUTF8);
if (USourceControlHelpers::IsEnabled())
{
USourceControlHelpers::MarkFileForAdd(FilePath);
}
}

void UMyBlueprintFunctionLibrary::ExportDataTableAsCSV(const UDataTable *Table)
{
// CSV 形式でデータテーブルを出力
if (!Table)
{
return;
}

const FString CsvString = Table->GetTableAsCSV(EDataTableExportFlags::None);
const FString OutputPath = GetExportFileName(Table, TEXT(“csv”));
SaveStringToFile(CsvString, OutputPath);
}

#include “CoreMinimal.h”
#include “Engine/DeveloperSettings.h”
#include “MySettings.generated.h”

UCLASS(config = Game, defaultconfig)
class MYPROJECT_API UMySettings : public UDeveloperSettings
{
GENERATED_BODY()

public:

UMySettings();

#if WITH_EDITORONLY_DATA
// ファイルの出力先フォルダー
UPROPERTY(BlueprintReadWrite, EditAnywhere, Config, Category = “Data”)
FString ResourceOutputFolder;

// 自動エクスポート機能の ON/OFF
UPROPERTY(BlueprintReadWrite, EditAnywhere, Config, Category = “Data”)
bool AutoExportOnEdit;
#endif

UFUNCTION(BlueprintPure)
static UMySettings *GetMySettings();

virtual FName GetCategoryName() const override;

#if WITH_EDITOR
virtual FText GetSectionText() const override;
virtual FText GetSectionDescription() const override;
#endif

};

#define LOCTEXT_NAMESPACE “MySettings”

UMySettings::UMySettings()
: Super()
#if WITH_EDITOR
, ResourceOutputFolder(TEXT(“Output”))
, AutoExportOnEdit(true)
#endif
{

}

UMySettings *UMySettings::GetMySettings()
{
return GetMutableDefault();
}

FName UMySettings::GetCategoryName() const
{
return “MySettings”;
}

#if WITH_EDITOR
FText UMySettings::GetSectionText() const
{
return LOCTEXT(“UMySettings::GetSectionText”, “Import/Export”);
}

FText UMySettings::GetSectionDescription() const
{
return LOCTEXT(“UMySettings::GetSectionDescription”, “Configuring the export and import of data assets”);
}
#endif

#undef LOCTEXT_NAMESPACE

#include “CoreMinimal.h”
#include “Engine/DataTable.h”
#include “MyTableRowBase.generated.h”

USTRUCT()
struct MYPROJECT_API FMyTableRowBase : public FTableRowBase
{
GENERATED_BODY()

public:

#if WITH_EDITOR
virtual void OnDataTableChanged(const UDataTable * InDataTable, const FName InRowName) override;
#endif
};

 

#if WITH_EDITOR
#include “MyProject/MyBlueprintFunctionLibrary/MyBlueprintFunctionLibrary.h”
#include “MyProject/MySettings/MySettings.h”
#endif

#if WITH_EDITOR
void FMyTableRowBase::OnDataTableChanged(const UDataTable * InDataTable, const FName InRowName)
{
const UMySettings *Settings = UMySettings::GetMySettings();
if ((Settings != nullptr) && (Settings->AutoExportOnEdit))
{
UMyBlueprintFunctionLibrary::ExportDataTableAsCSV(InDataTable);
}
}
#endif

#include “CoreMinimal.h”
#include “MyProject/MyDataTable/MyTableRowBase.h”
#include “MyTestDataTable.generated.h”

USTRUCT()
struct MYPROJECT_API FMyTestDataTable : public FMyTableRowBase
{
GENERATED_BODY()

public:

UPROPERTY(EditAnywhere, Category = “Data”)
int IntValue;

UPROPERTY(EditAnywhere, Category = “Data”)
TArray IntArray;

UPROPERTY(EditAnywhere, Category = “Data”)
TMap<FString, int> IntMap;

};

自動でエクスポートする様子

自動エクスポート機能ができました。

データアセットを Json エクスポートする

データアセットについての過去記事はこちら↓
独自に用意したデータアセットをより便利に活用する方法

データテーブルと同じように、データアセットもエクスポートをサポートしたいです。
しかし、データテーブルと違って UDataTable::GetTableAsCSV のような便利メソッドは存在しません。
標準の Json モジュールと JsonUtilities モジュールを使用すれば Json テキスト化が可能になります。
FJsonObjectConverter::UStructToJsonObject でデータアセットを JsonObject に変換し、シリアライズする、という流れです。
それ以外のステップは、データテーブルのときと同じ。
ただし、データテーブルと区別するためにファイル拡張子は .myjson にしておきます

UFUNCTION()
static void ExportObjectAsJson(const UObject *Object, const FString &OutputPath);

};

void UMyBlueprintFunctionLibrary::ExportObjectAsJson(const UObject *Object, const FString &OutputPath)
{
// JSON 形式で UObject を出力
if (!Object)
{
return;
}

FString JsonString;
if (UObjectToJsonString(Object, JsonString))
{
SaveStringToFile(JsonString, OutputPath);
}
else
{
UE_LOG(LogTemp, Error, TEXT(“UMyBlueprintFunctionLibrary::ExportObjectAsJson: Failed to Export %s”), *Object->GetName());
}
}

#include “CoreMinimal.h”
#include “Engine/DataAsset.h”
#include “MyDataAsset.generated.h”

class UAssetImportData;

UCLASS()
class MYPROJECT_API UMyDataAsset : public UDataAsset
{
GENERATED_BODY()

public:

#if WITH_EDITORONLY_DATA
static FString FileExtension;
#endif

#if WITH_EDITOR
UFUNCTION(Category = “Import/Export”, meta = (CallInEditor = true))
void Export();

UFUNCTION(Category = “Import/Export”, meta = (CallInEditor = true))
void OpenSourceLocation() const;

void PostEditChangeProperty(FPropertyChangedEvent & PropertyChangedEvent);
#endif

};

#if WITH_EDITOR
#include “MyProject/MyBlueprintFunctionLibrary/MyBlueprintFunctionLibrary.h”
#include “MyProject/MySettings/MySettings.h”
#include “UObject/UnrealType.h”
#include “Windows/WindowsPlatformProcess.h”
#endif

#if WITH_EDITOR
FString UMyDataAsset::FileExtension = TEXT(“myjson”);

void UMyDataAsset::Export()
{
FString FilePath = UMyBlueprintFunctionLibrary::GetExportFileName(this, FileExtension);
UMyBlueprintFunctionLibrary::ExportObjectAsJson(this, FilePath);
}

void UMyDataAsset::OpenSourceLocation() const
{
if (AssetImportData != nullptr)
{
FPlatformProcess::ExploreFolder(*AssetImportData->GetFirstFilename());
}
}

void UMyDataAsset::PostEditChangeProperty(FPropertyChangedEvent &PropertyChangedEvent)
{
if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ArrayAdd)
{
// 配列要素を追加した瞬間は自動エクスポートしない(クラッシュ回避)
return;
}

const UMySettings *Settings = UMySettings::GetMySettings();
if ((Settings != nullptr) && (Settings->AutoExportOnEdit))
{
Export();
}
}
#endif

#include “CoreMinimal.h”
#include “MyProject/MyDataAsset/MyDataAsset.h”
#include “MyTestDataAsset.generated.h”

UCLASS()
class MYPROJECT_API UMyTestDataAsset : public UMyDataAsset
{
GENERATED_BODY()

public:

UPROPERTY(EditAnywhere, Category = “Data”)
int IntValue;

UPROPERTY(EditAnywhere, Category = “Data”)
TArray IntArray;

UPROPERTY(EditAnywhere, Category = “Data”)
TMap<FString, int> StringIntMap;

UPROPERTY(EditAnywhere, Category = “Data”)
TArray StringArray;

UPROPERTY(EditAnywhere, Category = “Data”)
TMap<int, int> IntIntMap;
};

自動でエクスポートする様子

自動エクスポート機能ができました。

Json をインポートしてデータアセット化する

さて、データアセットをエクスポートできたのですが、今度はインポートもできるようにしたいと思います。
インポートを実装する際には、こちらの記事(独自のアセットを実装する方法(2) インポートの実装)が参考になります。
MyDataTable にコードを書き加えて、試してみましょう。

#if WITH_EDITOR
UFUNCTION(Category = “Import/Export”, meta = (CallInEditor = true))
void Import();

void UpdateAssetImportData(FString Filename);
#endif
};

 

#if WITH_EDITOR
FString UMyDataAsset::FileExtension = TEXT(“myjson”);

void UMyDataAsset::Import()
{
FString FilePath = UMyBlueprintFunctionLibrary::GetExportFileName(this, FileExtension);
UMyBlueprintFunctionLibrary::ImportJsonToObject(FilePath, this);
UpdateAssetImportData(FilePath);
}

void UMyDataAsset::Export()
{
FString FilePath = UMyBlueprintFunctionLibrary::GetExportFileName(this, FileExtension);
UMyBlueprintFunctionLibrary::ExportObjectAsJson(this, FilePath);
UpdateAssetImportData(FilePath);
}

void UMyDataAsset::UpdateAssetImportData(FString Filename)
{
if (AssetImportData == nullptr)
{
AssetImportData = NewObject(this);
}
AssetImportData->Update(Filename);
}
#endif

class UMyDataAsset;

class FAssetTypeActions_MyDataAsset : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override
{
return NSLOCTEXT(“AssetTypeActions”, “AssetTypeActions_MyDataAsset”, “MyDataAsset”);
}
virtual FColor GetTypeColor() const override { return FColor::Red; }
virtual uint32 GetCategories() override { return EAssetTypeCategories::Misc; }
virtual void GetResolvedSourceFilePaths(const TArray<UObject*>& TypeAssets, TArray& OutSourceFilePaths) const override;
virtual UClass* GetSupportedClass() const override;
virtual bool IsImportedAsset() const override { return true; }
virtual bool HasActions(const TArray<UObject*>& InObjects) const override { return true; }
virtual void GetActions(const TArray<UObject*>& InObjects, FMenuBuilder& MenuBuilder) override;
void Export(TArray<TWeakObjectPtr> Objects);
};

#define LOCTEXT_NAMESPACE “AssetTypeActions”

UClass* FAssetTypeActions_MyDataAsset::GetSupportedClass() const
{
return UMyDataAsset::StaticClass();
}

void FAssetTypeActions_MyDataAsset::GetResolvedSourceFilePaths(const TArray<UObject*>& TypeAssets, TArray& OutSourceFilePaths) const
{
for (UObject *TypeAsset : TypeAssets)
{
const UMyDataAsset *MyDataAsset = CastChecked(TypeAsset);
if(MyDataAsset != nullptr && MyDataAsset->AssetImportData != nullptr)
{
MyDataAsset->AssetImportData->ExtractFilenames(OutSourceFilePaths);
}
}
}

void FAssetTypeActions_MyDataAsset::GetActions(const TArray<UObject*>& InObjects, FMenuBuilder& MenuBuilder)
{
TArray<TWeakObjectPtr> MyAssetImports = GetTypedWeakObjectPtrs(InObjects);
MenuBuilder.AddMenuEntry(
LOCTEXT(“MyDataAsset_Export”, “Export”),
LOCTEXT(“MyDataAsset_ExportTooltip”, “Export this MyDataAsset”),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(this, &FAssetTypeActions_MyDataAsset::Export, MyAssetImports), FCanExecuteAction())
);
}

void FAssetTypeActions_MyDataAsset::Export(TArray<TWeakObjectPtr> Objects)
{
for (auto It = Objects.CreateIterator(); It; ++It)
{
UMyDataAsset *MyDataAsset = (*It).Get();
if (MyDataAsset != nullptr)
{
MyDataAsset->Export();
}
}
}
#undef LOCTEXT_NAMESPACE

#include “CoreMinimal.h”
#include “Factories/Factory.h”
#include “EditorReimportHandler.h”
#include “MyDataAssetFactory.generated.h”

UCLASS()
class MYPROJECT_API UMyDataAssetFactory : public UFactory, public FReimportHandler
{
GENERATED_BODY()

public:

UMyDataAssetFactory(const FObjectInitializer& ObjectInitializer);

virtual bool DoesSupportClass(UClass* Class) override;
virtual UClass* ResolveSupportedClass() override;
virtual UObject* FactoryCreateText(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, const TCHAR* Type, const TCHAR*& Buffer, const TCHAR* BuferEnd, FFeedbackContext* Warn) override;

virtual bool CanReimport(UObject* Obj, TArray& OutFilenames) override;
virtual void SetReimportPaths(UObject* Obj, const TArray& NewReimportPaths) override;
virtual EReimportResult::Type Reimport(UObject* Obj) override;
};

UMyDataAssetFactory::UMyDataAssetFactory(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
SupportedClass = UMyDataAsset::StaticClass();
bCreateNew = false;
bEditorImport = true;
bText = true;
Formats.Add(UMyDataAsset::FileExtension + TEXT(“;My Data Asset”));
}

bool UMyDataAssetFactory::DoesSupportClass(UClass* Class)
{
return Class->IsChildOf(UMyDataAsset::StaticClass());
}

UClass* UMyDataAssetFactory::ResolveSupportedClass()
{
return UMyDataAsset::StaticClass();
}

UObject* UMyDataAssetFactory::FactoryCreateText(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, const TCHAR* Type, const TCHAR*& Buffer, const TCHAR* BuferEnd, FFeedbackContext* Warn)
{
UClass *Class = InClass;
TSharedRef<TJsonReader> JsonReader = TJsonReaderFactory::Create(Buffer);
TSharedPtr JsonObject = MakeShareable(new FJsonObject());
FJsonSerializer::Deserialize(JsonReader, JsonObject);
TSharedPtr *ClassName = (*JsonObject).Values.Find(TEXT(“nativeClass”));
if (ClassName != nullptr)
{
UClass *FoundClass = FindObject(ANY_PACKAGE, *(*ClassName).Get()->AsString(), false);
if (FoundClass != nullptr)
{
Class = FoundClass;
}
FStaticConstructObjectParameters Params(Class);
Params.Outer = InParent;
Params.Name = InName;
Params.SetFlags = Flags;
UMyDataAsset* NewMyAsset = CastChecked(StaticConstructObject_Internal(Params));
FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), Class, (void*)(NewMyAsset), 0, 0);

return NewMyAsset;
}

UE_LOG(LogTemp, Error, TEXT(“UMyDataAssetFactory::FactoryCreateText: nativeClass was not found”));
return nullptr;
}

bool UMyDataAssetFactory::CanReimport(UObject* Obj, TArray& OutFilenames)
{
UMyDataAsset* MyAsset = Cast(Obj);
if (MyAsset != nullptr && MyAsset->AssetImportData != nullptr)
{
MyAsset->AssetImportData->ExtractFilenames(OutFilenames);
return true;
}
return false;
}

void UMyDataAssetFactory::SetReimportPaths(UObject* Obj, const TArray& NewReimportPaths)
{
UMyDataAsset* MyAsset = Cast(Obj);
if (MyAsset && ensure(NewReimportPaths.Num() == 1))
{
MyAsset->AssetImportData->UpdateFilenameOnly(NewReimportPaths[0]);
}
}

EReimportResult::Type UMyDataAssetFactory::Reimport(UObject* Obj)
{
UMyDataAsset* MyAsset = Cast(Obj);
if (!MyAsset)
{
return EReimportResult::Failed;
}

const FString Filename = MyAsset->AssetImportData->GetFirstFilename();
if (!Filename.Len() || IFileManager::Get().FileSize(*Filename) == INDEX_NONE)
{
return EReimportResult::Failed;
}

EReimportResult::Type Result = EReimportResult::Failed;
if (UFactory::StaticImportObject(MyAsset->GetClass(), MyAsset->GetOuter(), *MyAsset->GetName(), RF_Public | RF_Standalone, *Filename, NULL, this))
{
if (MyAsset->GetOuter())
{
MyAsset->GetOuter()->MarkPackageDirty();
}
else
{
MyAsset->MarkPackageDirty();
}
return EReimportResult::Succeeded;
}

return EReimportResult::Failed;
}

#include “CoreMinimal.h”
#include “Modules/ModuleManager.h”
#include “MyProject/MyDataAsset/AssetTypeActions_MyDataAsset.h”

class FAssetTypeActions_MyDataAsset;

class FMyProjectModule : public IModuleInterface
{
public:

virtual void StartupModule() override;
virtual void ShutdownModule() override;
TSharedPtr MyDataAsset_AssetTypeActions;
};

#define LOCTEXT_NAMESPACE “FMyProjectModule”

void FMyProjectModule::StartupModule()
{
MyDataAsset_AssetTypeActions = MakeShareable(new FAssetTypeActions_MyDataAsset);
FModuleManager::LoadModuleChecked(“AssetTools”).Get().RegisterAssetTypeActions(MyDataAsset_AssetTypeActions.ToSharedRef());
}

void FMyProjectModule::ShutdownModule()
{
if (MyDataAsset_AssetTypeActions.IsValid())
{
if (FModuleManager::Get().IsModuleLoaded(“AssetTools”))
{
FModuleManager::GetModuleChecked(“AssetTools”).Get().UnregisterAssetTypeActions(MyDataAsset_AssetTypeActions.ToSharedRef());
}
MyDataAsset_AssetTypeActions.Reset();
}
}

#undef LOCTEXT_NAMESPACE

IMPLEMENT_MODULE(FMyProjectModule, MyProject)

これで、インポートができるようになりました。

メニューから「Reimport」が選べるようになった

自動エクスポートには事故もつきものですので、導入には慎重な判断が必要ですが、やってみる価値はあると思います。

ソースコードが多くなってしまったので、制作物をこちらからダウンロードできるようにしておきます。
UE4.27.0 です。

ありがとうございました。