UE4 动态读写DataTable数据表
1. Overview
DataTable数据表在UE4中是一类重要的资产(Asset),截至ue4 v4.25版本,引擎内置的函数并不支持运行时(Runtime)修改DataTable表,所以写下本教程,讲解如何在ue4中实现动态修改DataTable数据表。如下图所示函数,由于其功能和ue4内置的DataTable编辑脚本(EditorScript)相同,所以仿照源码取了相同的函数名称,但以下蓝图节点在打包之后仍然可以调用。
2. Introduction
DataTable数据表在ue4开发中应用广泛,特别是,在进行数据驱动开发时具有举足轻重的地位;但是ue4引擎暴露出来函数中,能够对DataTable表进行编辑的函数却特别少,其中还有一些函数被WITH_EDITOR宏包裹着,属于编辑器脚本 (Editor Script),无法在项目打包之后调用。要想对DataTable进行更多样的操作,只有自己创建c++函数来实现动态读写DataTable功能。
#if WITH_EDITOR
/**
* Empty and fill a Data Table from CSV string.
* @param CSVString The Data that representing the contents of a CSV file.
* @return True if the operation succeeds, check the log for errors if it didn't succeed.
*/
UFUNCTION(BlueprintCallable, Category = "Editor Scripting | DataTable", DisplayName="Fill Data Table from CSV String")
static bool FillDataTableFromCSVString(UDataTable* DataTable, const FString& CSVString);
/**
* Empty and fill a Data Table from CSV file.
* @param CSVFilePath The file path of the CSV file.
* @return True if the operation succeeds, check the log for errors if it didn't succeed.
*/
UFUNCTION(BlueprintCallable, Category = "Editor Scripting | DataTable", DisplayName = "Fill Data Table from CSV File")
static bool FillDataTableFromCSVFile(UDataTable* DataTable, const FString& CSVFilePath);
/**
* Empty and fill a Data Table from JSON string.
* @param JSONString The Data that representing the contents of a JSON file.
* @return True if the operation succeeds, check the log for errors if it didn't succeed.
*/
UFUNCTION(BlueprintCallable, Category = "Editor Scripting | DataTable", DisplayName = "Fill Data Table from JSON String")
static bool FillDataTableFromJSONString(UDataTable* DataTable, const FString& JSONString);
/**
* Empty and fill a Data Table from JSON file.
* @param JSONFilePath The file path of the JSON file.
* @return True if the operation succeeds, check the log for errors if it didn't succeed.
*/
UFUNCTION(BlueprintCallable, Category = "Editor Scripting | DataTable", DisplayName = "Fill Data Table from JSON File")
static bool FillDataTableFromJSONFile(UDataTable* DataTable, const FString& JSONFilePath);
#endif //WITH_EDITOR
3. Why
首先想要简要叙述以下二个问题:
(1) 为什么需要动态读写DataTable?
最大的好处是便于修改,打包之后可修改DataTable数据表中的数据,无需重复打包。目前引擎默认的功能中DataTable只允许在编辑器中编辑,或者在编辑时导入相同格式的CSV文件/JSON文件,这些功能在项目打包之后都是无法使用的;所以一般情况下,在修改DataTable中的数据之后,只能重新打包项目,费时费力。要想避免此类重复打包,实现动态读写DataTable就意义非凡了。
(2) 实现动态读写DataTable的原理?
DataTable写操作过程:
从ue4 源码DataTable.h和DataTableFunctionLibrary.h二个类中不难发现,DataTable支持动态读写操作的,其中UDataTable::CreateTableFromCSVString()和UDataTable :: CreateTableFromJSONString()二个函数尤其值得注意,这二个函数并未被WITH_EDITOR宏包裹,也就是在非编辑器模式下,仍可被调用。而二种函数中的输入参数CSVSting和JSONString既可以通过本地磁盘文件获得,也可以使用HTTP网络传输的数据来获得。所以在获得CSVString/JSONString之后,调用这二个函数,即可实现向DataTable中写入数据。
/**
* Create table from CSV style comma-separated string.
* RowStruct must be defined before calling this function.
* @return Set of problems encountered while processing input
*/
ENGINE_API TArray<FString> CreateTableFromCSVString(const FString& InString);
/**
* Create table from JSON style string.
* RowStruct must be defined before calling this function.
* @return Set of problems encountered while processing input
*/
ENGINE_API TArray<FString> CreateTableFromJSONString(const FString& InString);
DataTable读操作过程:
从DataTable的Editor Script不难看出DataTable可以逆序列化为CSV文件和JSON文件,以下GetTableAsCSVFile方法将DataTable每个数据转换成String类型,按照CSV文件格式保存成CSV文件。
4. Approach
自定义动态读写DataTable的蓝图节点,实现过程大致如下:
(1) 以UBlueprintFunctionLibrary为基类,创建C++类,该类的.h文件,主要代码如下:
包含的头文件:
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Engine/DataTable.h"
#include "GenericArrayLibrary.generated.h"
自定义了一个LogCategory,用于分类LOG
// Declare General Log Category, header file .h
DECLARE_LOG_CATEGORY_EXTERN(LogUtiliesNode, Log, All);
函数声明
/**
* Empty and fill a Data Table from CSV string.
* @param CSVString The Data that representing the contents of a CSV file.
* @return True if the operation succeeds, check the log for errors if it didn't succeed.
*/
UFUNCTION(BlueprintCallable, DisplayName = "Fill Data Table from CSV String", Category = "DataTable")
static bool FillDataTableFromCSVString(UDataTable* DataTable, const FString& CSVString);
/**
* Empty and fill a Data Table from CSV file.
* @param CSVFilePath The file path of the CSV file.
* @return True if the operation succeeds, check the log for errors if it didn't succeed.
*/
UFUNCTION(BlueprintCallable, DisplayName = "Fill Data Table from CSV File", Category = "DataTable")
static bool FillDataTableFromCSVFile(UDataTable* DataTable, const FString& CSVFilePath);
/**
* Empty and fill a Data Table from JSON string.
* @param JSONString The Data that representing the contents of a JSON file.
* @return True if the operation succeeds, check the log for errors if it didn't succeed.
*/
UFUNCTION(BlueprintCallable, DisplayName = "Fill Data Table from JSON String", Category = "DataTable")
static bool FillDataTableFromJSONString(UDataTable* DataTable, const FString& JSONString);
/**
* Empty and fill a Data Table from JSON file.
* @param JSONFilePath The file path of the JSON file.
* @return True if the operation succeeds, check the log for errors if it didn't succeed.
*/
UFUNCTION(BlueprintCallable, DisplayName = "Fill Data Table from JSON File", Category = "DataTable")
static bool FillDataTableFromJSONFile(UDataTable* DataTable, const FString& JSONFilePath);
/** Output entire contents of table as CSV string */
UFUNCTION(BlueprintCallable, DisplayName = "Get Table As CSV String", Category = "DataTable")
static void GetDataTableAsCSVString(UDataTable* DataTable, FString& CSVString);
/** Output entire contents of table as CSV File */
UFUNCTION(BlueprintCallable, DisplayName = "Get Table As CSV File", Category = "DataTable")
static void GetDataTableAsCSVFile(UDataTable* DataTable, const FString& CSVFilePath);
该类.cpp文件中,包含的头文件:
#include "GenericArrayLibrary.h"
#include "HAL/PlatformFilemanager.h"
#include "Misc/FileHelper.h"
#include "DataTableUtils.h"
声明Log Category
//Declare General Log Category, source file .cpp
DEFINE_LOG_CATEGORY(LogUtiliesNode);
函数实现:
bool UGenericMiscLibrary::FillDataTableFromCSVString(UDataTable* DataTable, const FString& CSVString)
{
if (!DataTable || (CSVString.Len() == 0))
{
UE_LOG(LogUtiliesNode, Warning, TEXT("FillDataTableFromCSVString -> Can't fill DataTable with CSVString: %."), *CSVString);
return false;
}
// Call bulit-in function
TArray<FString> Errors = DataTable->CreateTableFromCSVString(CSVString);
if (Errors.Num())
{
// It has some error message
for (const FString& Error : Errors)
{
UE_LOG(LogUtiliesNode, Warning, TEXT("%s"), *Error);
}
return false;
}
return true;
}
bool UGenericMiscLibrary::FillDataTableFromCSVFile(UDataTable* DataTable, const FString& CSVFilePath)
{
FString CSVString;
if (FPlatformFileManager::Get().GetPlatformFile().FileExists(*CSVFilePath))
{
// Supports all combination of ANSI/Unicode files and platforms.
FFileHelper::LoadFileToString(CSVString, *CSVFilePath);
}
else
{
UE_LOG(LogUtiliesNode, Warning, TEXT("FillDataTableFromCSVFile -> Cannot find CSV file %s"), *CSVFilePath);
return false;
}
return UGenericMiscLibrary::FillDataTableFromCSVString(DataTable, CSVString);
}
bool UGenericMiscLibrary::FillDataTableFromJSONString(UDataTable* DataTable, const FString& JSONString)
{
if (!DataTable || (JSONString.Len() == 0))
{
UE_LOG(LogUtiliesNode, Warning, TEXT("FillDataTableFromJSONString -> Can't fill DataTable with JSONString: %."), *JSONString);
return false;
}
// Call bulit-in function
TArray<FString> Errors = DataTable->CreateTableFromJSONString(JSONString);
if (Errors.Num())
{
// It has some error message
for (const FString& Error : Errors)
{
UE_LOG(LogUtiliesNode, Warning, TEXT("%s"), *Error);
}
return false;
}
return true;
}
bool UGenericMiscLibrary::FillDataTableFromJSONFile(UDataTable* DataTable, const FString& JSONFilePath)
{
FString JSONString;
if (FPlatformFileManager::Get().GetPlatformFile().FileExists(*JSONFilePath))
{
// Supports all combination of ANSI/Unicode files and platforms.
FFileHelper::LoadFileToString(JSONString, *JSONFilePath);
}
else
{
UE_LOG(LogUtiliesNode, Warning, TEXT("FillDataTableFromJSONFile -> Cannot find CSV file %s"), *JSONFilePath);
return false;
}
return UGenericMiscLibrary::FillDataTableFromJSONString(DataTable, JSONString);
}
void UGenericMiscLibrary::GetDataTableAsCSVString(UDataTable* DataTable, FString& CSVString)
{
CSVString = FString();
if (!DataTable || (DataTable->RowStruct == nullptr))
{
UE_LOG(LogTemp, Warning, TEXT("UGenericMiscLibrary::GetTableAsCSV : Missing DataTable or RowStruct !"));
return;
}
// First build array of properties
TArray<FProperty*> StructProps;
for (TFieldIterator<FProperty> It(DataTable->RowStruct); It; ++It)
{
FProperty* Prop = *It;
check(Prop != nullptr);
StructProps.Add(Prop);
}
// First row, column titles, taken from properties
CSVString += TEXT("---");
for (int32 PropIdx = 0; PropIdx < StructProps.Num(); PropIdx++)
{
CSVString += TEXT(",");
CSVString += StructProps[PropIdx]->GetName();
}
CSVString += TEXT("\n");
// Now iterate over rows
for (auto RowIt = DataTable->GetRowMap().CreateConstIterator(); RowIt; ++RowIt)
{
FName RowName = RowIt.Key();
CSVString += RowName.ToString();
uint8* RowData = RowIt.Value();
for (int32 PropIdx = 0; PropIdx < StructProps.Num(); PropIdx++)
{
CSVString += TEXT(",");
CSVString += DataTableUtils::GetPropertyValueAsString(StructProps[PropIdx], RowData, EDataTableExportFlags::None);
}
CSVString += TEXT("\n");
}
}
void UGenericMiscLibrary::GetDataTableAsCSVFile(UDataTable* DataTable, const FString& CSVFilePath)
{
FString CSVString;
UGenericMiscLibrary::GetDataTableAsCSVString(DataTable, CSVString);
if (CSVString.Len() == 0)
{
return;
}
FFileHelper::SaveStringToFile(CSVString, *CSVFilePath, FFileHelper::EEncodingOptions::ForceUTF8);
}
编译成功之后,即可获得以上6个节点。
5. Usage
在c++类中自定义结构体,并且继承FTableRowBase类,以下示例使用的结构体如下:
/** Comment */
USTRUCT(BlueprintType)
struct FYourCppStruct : public FTableRowBase
{
GENERATED_USTRUCT_BODY()
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "YourCppStruct")
int32 IntegerValue;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "YourCppStruct")
float FloatValue;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "YourCppStruct")
FString StingValue;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "YourCppStruct")
FTransform TransformValue;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "YourCppStruct")
TArray<int32> ArrayValue;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "YourCppStruct")
TSet<int32> SetValue;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "YourCppStruct")
TMap<int32, FString> MapValue;
};
**特别提醒:此结构体必须在c++中定义,**原因在于Cpp Struct和BP Struct在反射和继承等特征上并非是等价的,。
编译结束之后,创建以该结构体为RowStruct的DataTable表
蓝图中使用示例,以CSV文件读写DataTable为例,(注意:CSV文件/Json文件需和从引擎中DataTable导出的CSV/Json文件,在文件内容采用相同格式)
6. Conclusion
本文主要介绍了ue4中动态读写DataTable表方法,实现了CSV/JSON格式数据直接写入DataTable中功能,同时也支持将DataTable数据表导出成CSV数据,完全可以满足对DataTable数据表的动态读写操作。不足之处:将DataTable导出成可读性更高的JSON文件 (Get Table As Json File) 没有完成。
以上内容其实属于对整个DataTable的操作,还有对DataTable的行(Row),属性(Property)进行操作的方法,如下。