本文使用 UE 4.26,引擎自带 ARPG 项目,从代码和编辑器两个方面记录一下 UE4 中的 DataTable,也就是数据表的用法。
一、DataTable 是什么
DataTable 就是数据表(以下简称 DT),也就是二维的,M 行 N 列的矩阵,如下图所示:
是一个 5 行(Row),三列(Col)的数据表。程序可以通过策划配置的数据表找到对应关系做相应的逻辑,对策划很友好。
二、编辑器中使用
2.1 创建一个数据表
在编辑器中,右键,Miscellaneous -> DataTable
即可创建一个 DT:
需要选择 Row (即列)的数据结构,比如选了 GameplayTagTableRow,就会创建一个如下所示的 DT:
新创建的 DT 默认是空的, 点击 Add 按钮可以创建一个默认行,如果 DT 的列结构是代码里的,则默认值在代码中设置;如果 DT 的列结构是资源,则在资源中设置。这个例子中 GameplayTagTableRow 是代码里写的,所以默认值在代码中设置(没有设置默认值,所以 Tag 默认是 None,DevComment 默认是空):
/** Simple struct for a table row in the gameplay tag table and element in the ini list */
USTRUCT()
struct FGameplayTagTableRow : public FTableRowBase
{
GENERATED_USTRUCT_BODY()
/** Tag specified in the table */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=GameplayTag)
FName Tag;
/** Developer comment clarifying the usage of a particular tag, not user facing */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=GameplayTag)
FString DevComment;
/** Constructors */
FGameplayTagTableRow() {}
FGameplayTagTableRow(FName InTag, const FString& InDevComment = TEXT("")) : Tag(InTag), DevComment(InDevComment) {}
GAMEPLAYTAGS_API FGameplayTagTableRow(FGameplayTagTableRow const& Other);
/** Assignment/Equality operators */
GAMEPLAYTAGS_API FGameplayTagTableRow& operator=(FGameplayTagTableRow const& Other);
GAMEPLAYTAGS_API bool operator==(FGameplayTagTableRow const& Other) const;
GAMEPLAYTAGS_API bool operator!=(FGameplayTagTableRow const& Other) const;
GAMEPLAYTAGS_API bool operator<(FGameplayTagTableRow const& Other) const;
};
点击 DT 中的一行,这一行会高亮,且可以在 Row Editor 中修改这一行的值(注意:代表行数的第一列,以及列名称是不能改的)
2.2 自定义数据表的列
DT 的列在创建出来之后就固定了,不能随便加一列,删一列,如果需要自定义列的数据表,可以在 Content 里右键,创建一个 “Structure”(即结构):
在 Structure 中可以新增变量,调整变量位置(越靠上,DT 中就越靠左),Default Values
里可以设置每一列的默认值(注意:变量和字符串值,都可以是中文):
然后右键创建一个 DT,使用刚刚创建的自定义列结构,点击 Add,就能看到默认值:
三、代码中使用
3.1 使用代码创建一个列结构
在代码中可以仿照 FGameplayTagTableRow 写一个:
USTRUCT(BlueprintType)
struct FTableRowTest : public FTableRowBase
{
GENERATED_USTRUCT_BODY()
public:
FTableRowTest() {}
FTableRowTest(bool InLoop, int32 InCurrentCount, int32 InMaxNum, float InLifeTime)
: bLoop(InLoop)
, CurrentCount(InCurrentCount)
, MaxNum(InMaxNum)
, LifeTime(InLifeTime)
{
}
UPROPERTY(EditAnywhere, BlueprintReadOnly, DisplayName = "是否循环")
bool bLoop = false;
UPROPERTY(EditAnywhere, BlueprintReadOnly, DisplayName = "当前数量")
int64 CurrentCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadOnly, DisplayName = "最大数量")
int64 MaxNum = 0;
UPROPERTY(EditAnywhere, BlueprintReadOnly, DisplayName = "生命周期")
float LifeTime = 0.f;
};
这样就可以在编辑器中创建一个这个列结构的 DT 了(且由于是代码创建的列结构,在 Row Structure 里是跳转不过去的,如果是资源,能直接跳到结构资源):
3.2 通过代码读/写 DT
在代码中可以直接通过资源路径和名称加载 DT,比如在 Content/TestForDT 目录下的 “MyTestDT”,可以这样加载:
UDataTable* const TestTable = LoadObject<UDataTable>(nullptr, TEXT("/Game/TestForDT/MyTestDT.MyTestDT"));
然后就可以通过 void UDataTable::AddRow(FName RowName, const FTableRowBase& RowData)
来新增行了。比如:
TestTable->AddRow(FName("Bob"), FTableRowTest(true, 3, 8, 2.7f));
3.3 CSV、JSON
DT 创建之后可以右键,导出成 CSV 或者 JSON 文件:
编辑器中可以通过 Reimport 通过 CSV 或者 JSON 导入,具体格式试一试就知道了:
在代码中(或者蓝图中)也可以直接通过 CSV 或者 JSON 创建一个 DT:
/**
* 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);
四、注意事项
4.1 复制内容
选中一行之后,如果想复制其中的内容是分两种情况的:
-
如果是第一列,即 Key,那么选中一行之后按
F2
即可
-
如果是其他列,即 Value,那么选中一行之后在 Row Editor 中可以复制
4.2 排序
DT 是可以按照某一列的值排序的,但是很扯淡的是:不管是什么类型,都是 TEXT 转成 FString 然后比较,源码如下(在 FDataTableEditor::OnColumnSortModeChanged
函数中):
if (InSortMode == EColumnSortMode::Ascending)
{
VisibleRows.Sort([ColumnIndex](
const FDataTableEditorRowListViewDataPtr& first,
const FDataTableEditorRowListViewDataPtr& second)
{
// 返回值:大于 0 表示 A > B; 0 表示相等; 小于 0 表示 A < B
int32 Result = (first->CellData[ColumnIndex].ToString()).Compare(second->CellData[ColumnIndex].ToString());
if (!Result)
{
return first->RowNum < second->RowNum;
}
return Result < 0;
});
}
而 FString 的比较,是 字典排序,所以 99
> 963
:
以及 2.87
> 11.09
:
我自己的改法是直接把引擎改了,如果是 Numeric,用数值类型排序,而不是字符串:
VisibleRows.Sort([ColumnIndex](
const FDataTableEditorRowListViewDataPtr& first,
const FDataTableEditorRowListViewDataPtr& second)
{
int32 Result = (first->CellData[ColumnIndex].ToString()).Compare(second->CellData[ColumnIndex].ToString());
FString const FirstColumnStr = first->CellData[ColumnIndex].ToString();
FString const SecondColumnStr = second->CellData[ColumnIndex].ToString();
bool const bNumeric = FCString::IsNumeric(*FirstColumnStr) && FCString::IsNumeric(*SecondColumnStr);
if (bNumeric)
{
double const FirstNum = FCString::Atod(*FirstColumnStr);
double const SecondNum = FCString::Atod(*SecondColumnStr);
Result = (FirstNum > SecondNum) ? 1 : ((FirstNum < SecondNum) ? -1 : 0);
}
if (!Result)
{
return first->RowNum < second->RowNum;
}
return Result < 0;
});