Unreal Engine 中的 Actor/Component 系统

UE中也有一套自己的Actor/ActorComponent系统,这里的Actor基本等价于前文中描述的entity,而ActorComponent也基本对应上了component。不过我们在之前提到的actor/component系统在UEActor/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的内存布局

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

actorcomponent的继承关系

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

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