UE4底层剖析之命名系统

引言

UE4的命名系统是整个UE4对象系统的核心部分之一,在引擎中每一个UObject都必须有名字,引擎为我们保证了不同的对象能够拥有相同的StringName。我们在UE4开发的过程中,对象的创建以及命名往往是比较随意的,如果对UE4底层这套命名机制不够熟悉的话,非常容易踩入深坑无法自拔,本文的目的是从UE底层剖析一下引擎在对象命名这一块到底是怎么做的以及分享一下作者本人在开发过程中踩过的坑,为其他开发者节省踩坑成本,同时希望也起到抛砖引玉的作用。


FName和FNamePool

UE为了保证名字的唯一性以及生成和查询的效率,引入了FName和FNamePool这两个东西,FName与FString不同的是,FName不仅仅有PlainString还有一个Number和ComparisonIndex。

比较两个FName是否相等是这么比较的:

简单来说只需要ComparisonIndex和Number相等就可以了,那么这两个东西是如何来的呢?

可以看到,FName通过这个MakeDetectNumber来生成。

MakeDetectNumber里面又调用了ParseNumber来生成了FName的Number,然后调用MakeWithNumber来生成ComparisonIndex。
最后我们可以在Make函数里看到ComparisonIndex是如何生成的。

这里ComparisonIndex通过FNamePool中取到,实际上FNamePool维护了一个Name的Hash池,每一个Name实际上都拥有一个唯一的HashValue,这个ComparisonIndex实际上就是这个Name在Hash池对应的Index。FNamePool在运行时还会动态Compact,以保证生成的Index是紧凑的,具体的实现细节可以在UnrealNames.cpp这里看到。
FNamePool在引擎初始化的时候初始化,并会先创建出引擎预先定义的Name,并加入到池中。

后续任何新加入和分配的Name都会被Hash并且加入到池中。
我们同样可以看到引擎预先定义的Name(比如NAME_None)是如何定义并创建的。

UObject的类型

在UE中,UObject一共有三种,分别是:

  1. CDO(Class Default Object)
    UE中每一个UClass都会拥有一个DefaultObject,我们在编辑器中修改一个蓝图的Property实际上就是修改的这个UClass对应的CDO的Property,然后通过序列化的方式存入磁盘。
  2. DSO(Default Subobject) DSO是一个UClass的默认Subobject,比如我在一个蓝图Actor类中创建了一个SkeletalMeshComponent,那么这个SkeletalMeshComponent就是一个DSO。当然我们也能在C++中创建DSO,通过带FObjectInitilizer的构造函数来创建,调用FObjectInitializer::CreateDefaultSubobject。
  3. 通过NewObject创建的UObject NewObject有两个比较重要的参数,一个是Outer指针一个是Name。NewObject可以直接在运行时创建出UObject。

下面说一说Class Default Object。

在引擎中CDO的创建是Lazy Allocation的方式,只有当GetDefaultObject的时候才创建CDO。

引擎在加载的时候会主动对每一个Module下所依赖的UClass都调用一遍GetDefaultObject来创建其CDO。

事实上,我们这三种不同的UObject在创建的时候都是通过引擎的StaticAllocateObject函数来创建该UObject实例。

UObject如何命名

对于DSO来说,由于是使用者主动创建,那么DSO是使用者来命名,CreateDefauleSubobject会首先检查是否有其他的DSO与当前正在创建的DSO重名,如果重名了就直接报Fatal error,所以DSO的命名在同一个Owner下始终是唯一的。

对于CDO来说,CDO有着不同于普通UObject的特殊命名,它的名字是在UClass名字前面加上Default__前缀。

对于NewObject创建的UObject来说,情况稍微复杂一些,由于Name是调用者传入的那么这里就会有两种情况:

  • 使用缺省值(NAME_None)
    如果使用缺省值NAME_None,那么引擎会帮你生成一个唯一的Name。

MakeUniqueObjectName中简单来说有两种UniqueName生成方式。

如果开启了GFastPathUniqueNameGeneration,则Index从大往小减,反之则从0开始往上加。MakeUniqueObjectName的目的是为了保证在同一个Outer下的子节点拥有不同的名字,但是不同的Outer下是可以有相同名字的Object,比如两个不同的Actor是可以分别拥有名为SkeletalMeshComponent_0这个UObject。

  • 显示指定Name
    直接把Name作为显示参数传入,则UObject会直接用这个名字,当然引擎会检查命名的唯一性,这个地方需要注意的是如果同一个Outer下已经有相同名字的UObject,那么将不会走UObject的创建流程而是直接将已经存在的UObject覆盖掉,那么这里需要考虑一种情况,假设已经存在一个StaticMeshComponent,我再NewObject一个名字一样的SkeletalMeshComponent,假设都叫Mesh,这种情况下引擎是不允许的,重名的UObject必须是不同的UClass。

有可能会踩坑的地方

上面讲了UObject的命名方式,在实际开发中相信大部分的开发者在创建一个UObject的时候还是会使用缺省Name的方式,因为这种方式最快并且不用考虑为这个UObject取什么名字,也不用考虑是否重名,因为引擎会为你分配好唯一的名字。如果开发者采用自己手动命名的方式,那么一定要注意在逻辑层就要确保不会重名,否则你会发现NewObject会覆盖掉那个被重名的Object。
缺省命名方式在绝大多数情况下都works fine,但是笔者在有一个情况下踩了一个大坑。

Sequencer的坑
首先我们了解一下Sequence的工作流程,Sequence在制作的时候会记录下绑定Object的层次关系,比如:

ATestActor
    |__RootComponent
            |__Component_0
            |__Component_1
            |__Component_2

那么,在运行时播放的时候,如果我们需要重新绑定Actor到这个Sequence,Sequence会从根节点开始依次匹配每一个子节点到每一个Track上,匹配的Key就是这个UObject的名字。
考虑以下情况,假设有一个Actor,我们在运行时动态为这个Actor创建了1个StaticMeshComponent,代码如下:

void ATestActor::CreateMeshComponents() 
{     
      StaticMeshComponent1 = NewObject<UStaticMeshComponent>(this);     
      StaticMeshComponent1->RegisterComponent();     
      StaticMeshComponent1->AttachToComponent(RootComponent, FAttachmentRules::KeepRelativeTransform); 
}

那么它的层次结构就是这样的:

ATestActor
    |__RootComponent
            |__StaticMeshComponent_0

上面我们介绍过,引擎会为缺省名字的UObject调用MakeUniqueObjectName来生成独一无二的名字,我们再来回顾一下MakeUniqueObjectName的实现,在生成名字后缀的时候有两种不同的方式,一种是从小到大,一种是重大到小,如果开启了GFastPathUniqueNameGeneration,则引擎会采用FastGeneration的方式由大到小生成Index。

然后我们会在CoreGlobals.cpp里面找到它的初始化。


我们发现开了编辑器和没有开编辑器的情况下生成后缀的方式是不同的,这就导致了在移动设备上和在PC带Editor的版本上播这个Sequence动画,由于StaticMeshComponent的名字不一样,导致不带Editor的版本匹配不上导致动画无法正常播放,ATestActor的层次结构变成了这样:

ATestActor
    |__RootComponent
            |__StaticMeshComponent_(MAX_int32 - 1000)

总结

对于临时使用的UObject来说,直接使用缺省命名方式创建对开发者来说肯定是最方便最快捷的方式,一般也不会出问题;但是对于结构比较固定,且需要对结构有依赖的情况下,是否需要在逻辑层手动来维护命名的唯一性是一个需要思考的问题。

编辑于 05-11

文章被以下专栏收录