浅析Unreal Engine 3中的FName

时间:2021-03-01 18:03:50

近来新到一个使用Unreal Engine 3的项目,本着熟悉代码的目的,看了一些代码,简单记录一下。

本文主要分析Unreal Engine 3中对于字符串封装后的结构FName,内容主要包含以下3点:

1.FName的作用;

2.FName的具体实现;

3.FName的一些特殊处理


1.FName的作用

— FName 利用 hash table 来存贮字符串。
— FName 对象进行比较时,实际上只需对两个整数进行比较。
— 查找操作类似于在 std::map 中根据主键(整数)查找对应的值。效率上有了很明显的改善。

2.FName的具体实现
FName中的关键数据结构
struct  FNameEntry{
INT Index;
FNameEntry* HashNext;
}
FNameEntry是一个全局的结构,后面会说到的存储结构中都是以FNameEntry为元素存储的。
class  FName{		INT 	Index;		INT	Number;		static 	TArrayNoInit<FNameEntry*>	Names;		static	FNameEntry*		NameHash[65536];}

FName就是今天要分析的重点,这里是几个关键的变量及存储字符串的容器。

Index是Names数组中的索引,用于快速的查找字符串的字符部分,Number是字符串中的数字部分。 


接着,来看FName的Init()函数,Init()函数会在FName的构造函数中调用,一般情况下,使用FName只需传入第一个参数,其他参数在FName的构造函数中填入默认值。FName的Init()函数传入了4个参数,InName字符串中的字符部分,InNumber字符串中的数字部分,FindType表示操作类型,bSplitName分割标示符。传入的字符串在InName中,分割后会将字符保留在InName内,而将数字部分保存在InNumber中。

void FName::Init(const TCHAR* InName, INT InNumber, EFindName FindType, UBOOL bSplitName)
{
StaticInit();
if (InNumber == NAME_NO_NUMBER_INTERNAL && bSplitName == TRUE)
{
if (SplitNameWithCheck(InName,...))
{}
}
INT iHash;
iHash = appStrhash( InName ) & ( ARRAY_COUNT(NameHash)-1 );
for (FNameEntry* Hash = NameHash[iHash]; Hash; Hash=Hash->HashNext)
{
if( Hash->IsEqual(InName))
{
Index = Hash->GetIndex();
...
}
}
Index = Names.Add();
Names(Index) = NameHash[iHash] = AllocateNameEntry( NewName, Index, NameHash[iHash], bIsPureAnsi );
}

FName::Init()函数主要做了以下的工作:

1)— 做一些必要的初始化工作,初始化 hashtable ,注册关键字
2) 分割传入字符串中的字符和数字
3) 根据传入的字符串( InName )计算得到 hash 值,在 hash table 中查找 hash 值,如果不存在对应的值,会将该 hash 值加入 hash table 。(重点)
其中NameHash的结构可以参照下图

根据FindType的不同,Fname::Init()函数有两个作用,第一个作用是作为FName的初始化函数,第二个作用是作为FName的查找函数。

浅析Unreal Engine 3中的FName

Init()中具体的查找步骤为通过计算字符串的hash值,在hash table中定位到到对应链表的头结点,接着遍历链表依次比较当前元素与传入的InName。

Enum  EFindName
{
FNAME_Find,
FNAME_Add,
FNAME_Replace
}

当Init中的FindName变量值为FNAME_Find时,当没有找到对应的字符串会将Index = NAME_None,找到对应的字符串会将Index置为hash table中的hash值。

当FindName为FName_Add时,没有找到对应的字符串就会将该字符串经过hash加入到hash table对应的位置。

当FindName为FName_Replace时,会将查找到的字符串替换为传入的字符串。


— FName 重载 == 运算符用于比较操作
FORCEINLINE  UBOOL  operator==(const FNAME& other) const
{
return Index == Other.Index && Number == Other.Number;
}

对FName对象的比较有两种方式,第一种是通过重载关系运算符==,首先比较索引(这个索引是保存在Names数组中的索引值),第二种实现了一个compare函数,不同之处在于两个FName对象不等时compare函数的返回值将根据字母表的升序来返回小于0或者大于0。

INT  FName::Compare(const  FName& other) const
{
if (GetIndex() == Other.GetIndex())
{
return GetNumber() – Other.GetNumber();
}
else

}

可以看到比较两个FName对象时,先比较二者的Index,如果不等则退化为调用传统的字符比较函数(正常情况下经由FName的构造函数调用FName::Init()都会生成对应的Index)。


3.FName的一些特殊处理
static 	TArrayNoInit<FNameEntry*>	Names;
static FNameEntry* NameHash[65536];

在前面FName的定义中,看到除了定义NameHash数组之外,还定义了一个Names数组,这是为什么?两个数组在功能上有什么不同之处?

其原因在于hash table可以快速查找,却不能随机存取一个元素,当需要根据Index来获取一个FNameEntry对象时,在Names数组中通过下标直接存取效率会更高,这样做弥补了hash table无法随机存取的缺点。


在FName::Init()中,有提到最后一个参数bSplitName是分割标示符,而在实际的使用中,假设我们将其置为TRUE,传入“test1”,发现调用分割函数FName::SplitNameWithCheck()并没有成功。查看FName::SplitNameWithCheck()的代码后,发现分割函数只是针对类似“test_1”这样格式的字符串进行分割,经过与同事的交流得知,Unreal编辑生成的资源文件多是以这样格式命名(或者在内部处理时将对象的命名统一格式)以加快处理速度。