组件插槽主要是提供除了组件自身位置为锚点的其他可供子组件 Attach 的锚点。
通过 FName 索引到组件上的插槽,组件变换到 WorldSpace 时使用所属组件插槽的 Transform 来进行变换。
插槽管理
从 SceneComponent 中有关插槽管理的有
/**
* Gets the names of all the sockets on the component.
* @return Get the names of all the sockets on the component.
*/
UFUNCTION(BlueprintCallable, Category="Utilities|Transformation", meta=(Keywords="Bone"))
TArray<FName> GetAllSocketNames() const;
/**
* Get world-space socket transform.
* @param InSocketName Name of the socket or the bone to get the transform
* @return Socket transform in world space if socket if found. Otherwise it will return component's transform in world space.
*/
UFUNCTION(BlueprintCallable, Category="Utilities|Transformation", meta=(Keywords="Bone"))
virtual FTransform GetSocketTransform(FName InSocketName, ERelativeTransformSpace TransformSpace = RTS_World) const;
virtual FVector GetSocketLocation(FName InSocketName) const;
virtual FRotator GetSocketRotation(FName InSocketName) const;
virtual FQuat GetSocketQuaternion(FName InSocketName) const;
/**
* Return true if socket with the given name exists
* @param InSocketName Name of the socket or the bone to get the transform
*/
UFUNCTION(BlueprintCallable, Category="Utilities|Transformation", meta=(Keywords="Bone"))
virtual bool DoesSocketExist(FName InSocketName) const;
/**
* Returns true if this component has any sockets
*/
virtual bool HasAnySockets() const;
/**
* Get a list of sockets this component contains
*/
virtual void QuerySupportedSockets(TArray<FComponentSocketDescription>& OutSockets) const;
SpringArmComponent 重写了以下三个函数。
// USceneComponent interface
virtual bool HasAnySockets() const override;
virtual FTransform GetSocketTransform(FName InSocketName, ERelativeTransformSpace TransformSpace = RTS_World) const override;
virtual void QuerySupportedSockets(TArray<FComponentSocketDescription>& OutSockets) const override;
// End of USceneComponent interface
bool USpringArmComponent::HasAnySockets() const
{
return true;
}
void USpringArmComponent::QuerySupportedSockets(TArray<FComponentSocketDescription>& OutSockets) const
{
new (OutSockets) FComponentSocketDescription(SocketName, EComponentSocketType::Socket);
}
如果更规范一点还应该重写 virtual bool DoesSocketExist(FName InSocketName) const;
变换
SpringArmComponent 使用 RelativeSocketRotation, RelativeSocketLocation 两个成员变量存储摇臂尾端相对于组件原点的旋转和位置。
从 GetSocketTransform 中可以看到三个坐标系的转换。
FTransform USpringArmComponent::GetSocketTransform(FName InSocketName, ERelativeTransformSpace TransformSpace) const
{
FTransform RelativeTransform(RelativeSocketRotation, RelativeSocketLocation);
switch(TransformSpace)
{
case RTS_World:
{
return RelativeTransform * GetComponentTransform();
break;
}
case RTS_Actor:
{
if( const AActor* Actor = GetOwner() )
{
FTransform SocketTransform = RelativeTransform * GetComponentTransform();
return SocketTransform.GetRelativeTransform(Actor->GetTransform());
}
break;
}
case RTS_Component:
{
return RelativeTransform;
}
}
return RelativeTransform;
}
Attach 组件到 Socket
通过以上发现 SpringArmComponent 确实构建了一个名叫“SpringEndpoint”的插槽。从挂载到其身上的蓝图也可以看到选项,但是无论选择哪一个,坐标原点都处于末端,原因是 GetSocketTransform 函数中统一返回了 RelativeTransform 没有针对 NONE 和其他插槽做区分。这体现了 SceneComponent 对于组件管理的方式,所有的组件都是通过 Socket 挂载到父组件上的,都通过 GetSocketTransform 来更新自身的世界坐标,对于没有 Socket 的父组件,GetSocketTransform 会返回组件自身的坐标,同时以 None 的形式对外显示。
USceneComponent 中默认的 GetSocketTransform。
FTransform USceneComponent::GetSocketTransform(FName SocketName, ERelativeTransformSpace TransformSpace) const
{
switch(TransformSpace)
{
case RTS_Actor:
{
return GetComponentTransform().GetRelativeTransform( GetOwner()->GetTransform() );
break;
}
case RTS_Component:
case RTS_ParentBoneSpace:
{
return FTransform::Identity;
}
default:
{
return GetComponentTransform();
}
}
}
组件之间的关联是通过 SceneComponent 中的 AttachToComponent 函数完成的。
/**
* Attach this component to another scene component, optionally at a named socket. It is valid to call this on components whether or not they have been Registered, however from
* constructor or when not registered it is preferable to use SetupAttachment.
* @param Parent Parent to attach to.
* @param AttachmentRules How to handle transforms & welding when attaching.
* @param SocketName Optional socket to attach to on the parent.
* @return True if attachment is successful (or already attached to requested parent/socket), false if attachment is rejected and there is no change in AttachParent.
*/
bool AttachToComponent(USceneComponent* InParent, const FAttachmentTransformRules& AttachmentRules, FName InSocketName = NAME_None ){
...
// Now apply attachment rules
FTransform SocketTransform = GetAttachParent()->GetSocketTransform(GetAttachSocketName());
FTransform RelativeTM = GetComponentTransform().GetRelativeTransform(SocketTransform);
switch (AttachmentRules.LocationRule)
{
case EAttachmentRule::KeepRelative:
// dont do anything, keep relative position the same
break;
case EAttachmentRule::KeepWorld:
if (IsUsingAbsoluteLocation())
{
SetRelativeLocation_Direct(GetComponentTransform().GetTranslation());
}
else
{
SetRelativeLocation_Direct(RelativeTM.GetTranslation());
}
break;
case EAttachmentRule::SnapToTarget:
SetRelativeLocation_Direct(FVector::ZeroVector);
break;
}
...
UpdateComponentToWorld(EUpdateTransformFlags::None, ETeleportType::TeleportPhysics);
...
}
UpdateChildTransforms
组件的空间位置是通过 UpdateComponentToWorld 更新的,在 AttachToComponent 的末尾,组件刚被附加时会更新一次,以确定其初始位置。
UpdateComponentToWorld 将自身的 RelativeTransform 通过 Parent 的 Socket 更新到 WorldTransform。
通过 SetRelativeXXX、SetWorldLocationAndRotationNoPhysics、SetAbsolute 等方法时也会调用来更新位置。
SpringArmComponent 在每帧 Tick 时计算碰撞、Lag 等因素来确定末端位置,确定以后必须通过 UpdateChildTransforms 来更新子组件的变换。
void USpringArmComponent::UpdateDesiredArmLocation(bool bDoTrace, bool bDoLocationLag, bool bDoRotationLag, float DeltaTime)
{
FRotator DesiredRot = GetTargetRotation();
// Apply 'lag' to rotation if desired
if(bDoRotationLag)
{
if (bUseCameraLagSubstepping && DeltaTime > CameraLagMaxTimeStep && CameraRotationLagSpeed > 0.f)
{
const FRotator ArmRotStep = (DesiredRot - PreviousDesiredRot).GetNormalized() * (1.f / DeltaTime);
FRotator LerpTarget = PreviousDesiredRot;
float RemainingTime = DeltaTime;
while (RemainingTime > KINDA_SMALL_NUMBER)
{
const float LerpAmount = FMath::Min(CameraLagMaxTimeStep, RemainingTime);
LerpTarget += ArmRotStep * LerpAmount;
RemainingTime -= LerpAmount;
DesiredRot = FRotator(FMath::QInterpTo(FQuat(PreviousDesiredRot), FQuat(LerpTarget), LerpAmount, CameraRotationLagSpeed));
PreviousDesiredRot = DesiredRot;
}
}
else
{
DesiredRot = FRotator(FMath::QInterpTo(FQuat(PreviousDesiredRot), FQuat(DesiredRot), DeltaTime, CameraRotationLagSpeed));
}
}
PreviousDesiredRot = DesiredRot;
// Get the spring arm 'origin', the target we want to look at
FVector ArmOrigin = GetComponentLocation() + TargetOffset;
// We lag the target, not the actual camera position, so rotating the camera around does not have lag
FVector DesiredLoc = ArmOrigin;
...
...
// Form a transform for new world transform for camera
FTransform WorldCamTM(DesiredRot, ResultLoc);
// Convert to relative to component
FTransform RelCamTM = WorldCamTM.GetRelativeTransform(GetComponentTransform());
// Update socket location/rotation
RelativeSocketLocation = RelCamTM.GetLocation();
RelativeSocketRotation = RelCamTM.GetRotation();
UpdateChildTransforms();
}
由于每个组件的 WorldTransform 是自己维护的,而非通过层级树等方式实时计算的,因此当维护组件中某个插槽的位置时必须要 UpdateChildTransforms();而在 SetRelativeXXX 等方法中,去设定某个组件的位置时也会去执行 UpdateChildTransforms 详细参考 USceneComponent::PropagateTransformUpdate。