Unreal Engine 中的 Actor/Component 系统
UE中也有一套自己的Actor/ActorComponent系统,这里的Actor基本等价于前文中描述的entity,而ActorComponent也基本对应上了component。不过我们在之前提到的actor/component系统在UE的Actor/ActorComponent系统面前相形见绌,缺少很多的灵活性。例如在Actor中其实并没有字段来存储这个对象的位置信息,而是存储了一个带位置信息的RootComponent,所有的位置查询设置接口都会转发到这个RootComponent:
/** The component that defines the transform (location, rotation, scale) of this Actor in the world, all other components must be attached to this one somehow */
UPROPERTY(BlueprintGetter=K2_GetRootComponent, Category="Utilities|Transformation")
USceneComponent* RootComponent;
/**
* Get the actor-to-world transform.
* @return The transform that transforms from actor space to world space.
*/
UFUNCTION(BlueprintCallable, meta=(DisplayName = "GetActorTransform", ScriptName = "GetActorTransform"), Category="Utilities|Transformation")
const FTransform& GetTransform() const
{
return ActorToWorld();
}
/** Get the local-to-world transform of the RootComponent. Identical to GetTransform(). */
FORCEINLINE const FTransform& ActorToWorld() const
{
return (RootComponent ? RootComponent->GetComponentTransform() : FTransform::Identity);
}
bool AActor::SetActorLocation(const FVector& NewLocation, bool bSweep, FHitResult* OutSweepHitResult, ETeleportType Teleport)
{
if (RootComponent)
{
const FVector Delta = NewLocation - GetActorLocation();
return RootComponent->MoveComponent(Delta, GetActorQuat(), bSweep, OutSweepHitResult, MOVECOMP_NoFlags, Teleport);
}
else if (OutSweepHitResult)
{
*OutSweepHitResult = FHitResult();
}
return false;
}
这里的RootComponent的类型是USceneComponent,而不是其父类UActorComponent。这是因为并不是每个UActorComponent都是带位置信息的,我们平常使用到的还包括UMovementComponent,UInputComponent,UAIComponent等类型就不需要携带位置信息。如果每个UActorComponent都带上transform相关字段,其内存大小会多将几百来个字节。为了节省Actor的整体内存消耗量,这个transform信息被挂载到了UActorComponent的子类USceneComponent上。从下面的USceneComponent内存布局可以看出,UActorComponent占据了0x00C0=192个字节的内存,而USceneComponent则占据了0x0220=544个字节的内存,增加的数据量极其恐怖:

逻辑层对USceneComponent做了进一步的划分,下面是一些常见的子类:

这里SceneComponent情况最特殊,因为它支持组件之间的关联,即Attach操作,下图就是一个Actor的层级关联实例:

一个Actor所拥有的所有ActorComponent都会存到OwnedComponents这个集合中,同时提供了添加或者删除ActorComponent的接口去更新OwnedComponents集合:
/**
* All ActorComponents owned by this Actor. Stored as a Set as actors may have a large number of components
* @see GetComponents()
*/
TSet<UActorComponent*> OwnedComponents;
/**
* Puts a component in to the OwnedComponents array of the Actor.
* The Component must be owned by the Actor or else it will assert
* In general this should not need to be called directly by anything other than UActorComponent functions
*/
void AddOwnedComponent(UActorComponent* Component);
/**
* Removes a component from the OwnedComponents array of the Actor.
* In general this should not need to be called directly by anything other than UActorComponent functions
*/
void RemoveOwnedComponent(UActorComponent* Component);
在ActorComponent的初始化的时候,会自动的调用这个添加接口:
void UActorComponent::PostInitProperties()
{
Super::PostInitProperties();
// Instance components will be added during the owner's initialization
if (OwnerPrivate && CreationMethod != EComponentCreationMethod::Instance)
{
if (!FPlatformProperties::RequiresCookedData() && CreationMethod == EComponentCreationMethod::Native && HasAllFlags(RF_NeedLoad|RF_DefaultSubObject))
{
UObject* MyArchetype = GetArchetype();
if (!MyArchetype->IsPendingKill() && MyArchetype != GetClass()->ClassDefaultObject)
{
OwnerPrivate->AddOwnedComponent(this);
}
// 省略后续代码
}
}
}
同时ActorComponent在销毁的时候会自动的调用到删除接口:
void UActorComponent::BeginDestroy()
{
if (bHasBegunPlay)
{
EndPlay(EEndPlayReason::Destroyed);
}
// Ensure that we call UninitializeComponent before we destroy this component
if (bHasBeenInitialized)
{
UninitializeComponent();
}
ExecuteUnregisterEvents();
// Ensure that we call OnComponentDestroyed before we destroy this component
if (bHasBeenCreated)
{
OnComponentDestroyed(GExitPurge);
}
WorldPrivate = nullptr;
// Remove from the parent's OwnedComponents list
if (AActor* MyOwner = GetOwner())
{
MyOwner->RemoveOwnedComponent(this);
}
Super::BeginDestroy();
}
这样就完成了组件集合维护的操作。除了增删组件接口之外,Actor上还提供了很多组件的查询接口,获取一个Actor上指定类型的ActorComponent其实就是对这个集合做遍历的过程:
UActorComponent* AActor::FindComponentByClass(const TSubclassOf<UActorComponent> ComponentClass) const
{
UActorComponent* FoundComponent = nullptr;
if (UClass* TargetClass = ComponentClass.Get())
{
for (UActorComponent* Component : OwnedComponents)
{
if (Component && Component->IsA(TargetClass))
{
FoundComponent = Component;
break;
}
}
}
return FoundComponent;
}
/** Templatized version of FindComponentByClass that handles casting for you */
template<class T>
T* AActor::FindComponentByClass() const
{
static_assert(TPointerIsConvertibleFromTo<T, const UActorComponent>::Value, "'T' template parameter to FindComponentByClass must be derived from UActorComponent");
return (T*)FindComponentByClass(T::StaticClass());
}
这个操作在遍历的过程中会不断的执行IsA操作,内部会调用到UClass::IsChildOf函数上:
// class UObjectBaseUtility
private:
template <typename ClassType>
static FORCEINLINE bool IsChildOfWorkaround(const ClassType* ObjClass, const ClassType* TestCls)
{
return ObjClass->IsChildOf(TestCls);
}
public:
/** Returns true if this object is of the specified type. */
template <typename OtherClassType>
FORCEINLINE bool IsA( OtherClassType SomeBase ) const
{
// We have a cyclic dependency between UObjectBaseUtility and UClass,
// so we use a template to allow inlining of something we haven't yet seen, because it delays compilation until the function is called.
// 'static_assert' that this thing is actually a UClass pointer or convertible to it.
const UClass* SomeBaseClass = SomeBase;
(void)SomeBaseClass;
checkfSlow(SomeBaseClass, TEXT("IsA(NULL) cannot yield meaningful results"));
const UClass* ThisClass = GetClass();
// Stop the compiler doing some unnecessary branching for nullptr checks
UE_ASSUME(SomeBaseClass);
UE_ASSUME(ThisClass);
return IsChildOfWorkaround(ThisClass, SomeBaseClass);
}
/** Returns true if this object is of the template type. */
template<class T>
bool IsA() const
{
return IsA(T::StaticClass());
}
而这个IsChildOf的实现其实是非常低效的,他会不断的将存储的父类与目标类型做相等比较,不等的时候再递归查询,一次失败的比较会获取完整的继承链:
/**
* @return true if this object is of the specified type.
*/
#if USTRUCT_FAST_ISCHILDOF_COMPARE_WITH_OUTERWALK || USTRUCT_FAST_ISCHILDOF_IMPL == USTRUCT_ISCHILDOF_OUTERWALK
bool UStruct::IsChildOf( const UStruct* SomeBase ) const
{
if (SomeBase == nullptr)
{
return false;
}
bool bOldResult = false;
for ( const UStruct* TempStruct=this; TempStruct; TempStruct=TempStruct->GetSuperStruct() )
{
if ( TempStruct == SomeBase )
{
bOldResult = true;
break;
}
}
#if USTRUCT_FAST_ISCHILDOF_IMPL == USTRUCT_ISCHILDOF_STRUCTARRAY
const bool bNewResult = IsChildOfUsingStructArray(*SomeBase);
#endif
#if USTRUCT_FAST_ISCHILDOF_COMPARE_WITH_OUTERWALK
ensureMsgf(bOldResult == bNewResult, TEXT("New cast code failed"));
#endif
return bOldResult;
}
#endif
后面UE也发现这个IsA实现实在是太慢了,严重的拖慢了FindComponentByClass的整体速度。所以后面在针对打包版本的程序,实现了一个快速判定的版本IsChildOfUsingStructArray:
/** Returns true if this struct either is SomeBase, or is a child of SomeBase. This will not crash on null structs */
#if USTRUCT_FAST_ISCHILDOF_COMPARE_WITH_OUTERWALK || USTRUCT_FAST_ISCHILDOF_IMPL == USTRUCT_ISCHILDOF_OUTERWALK
bool IsChildOf( const UStruct* SomeBase ) const;
#else
bool IsChildOf(const UStruct* SomeBase) const
{
return (SomeBase ? IsChildOfUsingStructArray(*SomeBase) : false);
}
#endif
这个IsChildOfUsingStructArray实现的很巧妙,在打包的时候会将每个类型到UObjectBase的继承深度计算出来,同时利用了UObject不允许菱形继承的规则为每个类型都构造出一个继承链数组。数组大小就是当前类型的继承深度,数组中的每个元素都是在对应深度的父类。在有了这个继承链数组之后,判定类型B是否是类型A的子类只需要获取A的继承深度,同时在B的继承链数组中根据这个深度获取指定元素里存储的指针C是否与传入的指针A相等。这样判定是否子类就只需要常数时间复杂度了:
#if USTRUCT_FAST_ISCHILDOF_IMPL == USTRUCT_ISCHILDOF_STRUCTARRAY
class FStructBaseChain
{
protected:
COREUOBJECT_API FStructBaseChain();
COREUOBJECT_API ~FStructBaseChain();
// Non-copyable
FStructBaseChain(const FStructBaseChain&) = delete;
FStructBaseChain& operator=(const FStructBaseChain&) = delete;
COREUOBJECT_API void ReinitializeBaseChainArray();
FORCEINLINE bool IsChildOfUsingStructArray(const FStructBaseChain& Parent) const
{
int32 NumParentStructBasesInChainMinusOne = Parent.NumStructBasesInChainMinusOne;
return NumParentStructBasesInChainMinusOne <= NumStructBasesInChainMinusOne && StructBaseChainArray[NumParentStructBasesInChainMinusOne] == &Parent;
}
private:
FStructBaseChain** StructBaseChainArray;
int32 NumStructBasesInChainMinusOne;
friend class UStruct;
};
#endif