1. Overview

DataTable数据表在UE4中是一类重要的资产(Asset),截至ue4 v4.25版本,引擎内置的函数并不支持运行时(Runtime)修改DataTable表,所以写下本教程,讲解如何在ue4中实现动态修改DataTable数据表。如下图所示函数,由于其功能和ue4内置的DataTable编辑脚本(EditorScript)相同,所以仿照源码取了相同的函数名称,但以下蓝图节点在打包之后仍然可以调用。

图1. 动态读写DataTable的自定义蓝图节点;

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
DataTableFunctionLibrary类的Editor Script写入DataTable;

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.h中的函数;

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表

图2. 使用在Cpp中定义的结构体,创建DataTable;

蓝图中使用示例,以CSV文件读写DataTable为例,(注意:CSV文件/Json文件需和从引擎中DataTable导出的CSV/Json文件,在文件内容采用相同格式)

图3. 使Fill Data Table From CSV File和Get Table As CSV File节点使用示例;

6. Conclusion

本文主要介绍了ue4中动态读写DataTable表方法,实现了CSV/JSON格式数据直接写入DataTable中功能,同时也支持将DataTable数据表导出成CSV数据,完全可以满足对DataTable数据表的动态读写操作。不足之处:将DataTable导出成可读性更高的JSON文件 (Get Table As Json File) 没有完成。

以上内容其实属于对整个DataTable的操作,还有对DataTable的行(Row),属性(Property)进行操作的方法,如下。