UE4组件中的插槽(Socket)--以SpringArmComponent为例

UE4组件中的插槽(Socket)--以SpringArmComponent为例

组件插槽主要是提供除了组件自身位置为锚点的其他可供子组件 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 两个成员变量存储摇臂尾端相对于组件原点的旋转和位置。

ZxTBb03VZoHRbIxZtTJc4HtynLh.png

从 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 的形式对外显示。

RUeZbZEpMoD3GDxPlmicwnpLnag.png

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。

CC BY-NC-SA 4.0 Deed | 署名-非商业性使用-相同方式共享
最后更新时间:2024-12-28 01:01:01