📋 MoveAlongFloor 函数功能详解

🎯 核心功能

MoveAlongFloor 是 UE 角色移动组件中负责在地面上移动角色的核心函数。它处理角色沿着地板(包括斜坡)移动时的各种复杂情况。


🔍 函数签名

void UCharacterMovementComponent::MoveAlongFloor(
    const FVector& InVelocity,        // 输入速度向量
    float DeltaSeconds,                // 时间增量(帧时间)
    FStepDownResult* OutStepDownResult // 输出:台阶下降结果(可选)
)

📊 执行流程图

graph TD
    A[开始 MoveAlongFloor] --> B{当前地板可行走?}
    B -->|否| C[直接返回]
    B -->|是| D[计算移动增量Delta]
    D --> E[计算斜坡移动向量RampVector]
    E --> F[执行安全移动]
    F --> G{碰撞检测结果}
    
    G -->|开始时穿透| H[处理穿透]
    H --> I[沿表面滑动]
    I --> J{仍在穿透?}
    J -->|是| K[触发卡住事件]
    J -->|否| L[结束]
    
    G -->|有效阻挡碰撞| M{碰撞物可行走?}
    M -->|是,另一个斜坡| N[计算剩余移动]
    N --> O[再次移动]
    O --> P{仍有碰撞?}
    
    P -->|是| Q{可以踩上去?}
    Q -->|是| R[尝试StepUp]
    R --> S{StepUp成功?}
    S -->|是| T[调整速度]
    S -->|否| U[滑动处理]
    
    Q -->|否| V[滑动处理]
    
    G -->|无碰撞| L

🔧 详细步骤解析

步骤 1:前置检查

if (!CurrentFloor.IsWalkableFloor())
{
    return;
}
  • 检查当前地板是否可行走
  • 如果不可行走(如太陡的斜坡、空中),直接返回

步骤 2:计算移动增量

const FVector Delta = RotateGravityToWorld(
    RotateWorldToGravity(InVelocity) * FVector(1.0, 1.0, 0.0)
) * DeltaSeconds;

关键点:

  • 将速度转换到重力坐标系
  • 消除垂直分量(* FVector(1.0, 1.0, 0.0))- 只保留水平移动
  • 转换回世界坐标系
  • 乘以时间增量得到位移

为什么要消除垂直分量?

  • 在地面移动时,垂直速度应该由地面约束,而不是自由移动
  • 防止角色”陷入”或”飘离”地面

步骤 3:计算斜坡移动向量

FVector RampVector = ComputeGroundMovementDelta(Delta, CurrentFloor.HitResult, CurrentFloor.bLineTrace);
  • 调用之前我们分析过的 ComputeGroundMovementDelta 函数
  • 将水平移动投影到斜坡表面
  • 确保移动沿着地面而不是穿透地面

步骤 4:执行安全移动

SafeMoveUpdatedComponent(RampVector, UpdatedComponent->GetComponentQuat(), true, Hit);
  • 尝试移动角色
  • true 参数表示进行碰撞检测
  • 结果存储在 Hit 中

步骤 5:处理碰撞结果

情况 A:开始时就穿透(Start Penetrating)

if (Hit.bStartPenetrating)
{
    HandleImpact(Hit);
    SlideAlongSurface(Delta, 1.f, Hit.Normal, Hit, true);
    
    if (Hit.bStartPenetrating)
    {
        OnCharacterStuckInGeometry(&Hit);
    }
}

处理逻辑:

  1. 处理碰撞影响
  2. 尝试沿表面滑动
  3. 如果仍然穿透,触发”卡住”事件

应用场景:

  • 角色被挤压到墙角
  • 物体突然出现在角色位置

情况 B:有效的阻挡碰撞

else if (Hit.IsValidBlockingHit())
{
    // 处理碰撞到障碍物的情况
}
子情况 B1:碰到另一个可行走的斜坡
if ((Hit.Time > 0.f) && (Hit.Normal.Z > UE_KINDA_SMALL_NUMBER) && IsWalkable(Hit))
{
    const float InitialPercentRemaining = 1.f - PercentTimeApplied;
    RampVector = ComputeGroundMovementDelta(Delta * InitialPercentRemaining, Hit, false);
    LastMoveTimeSlice = InitialPercentRemaining * LastMoveTimeSlice;
    SafeMoveUpdatedComponent(RampVector, UpdatedComponent->GetComponentQuat(), true, Hit);
    
    const float SecondHitPercent = Hit.Time * InitialPercentRemaining;
    PercentTimeApplied = FMath::Clamp(PercentTimeApplied + SecondHitPercent, 0.f, 1.f);
}

处理逻辑:

  1. 计算剩余移动距离(1.f - PercentTimeApplied)
  2. 基于新的斜坡重新计算移动向量
  3. 执行剩余的移动
  4. 累计已应用的移动百分比

应用场景:

  • 从一个斜坡移动到另一个斜坡
  • 斜坡之间的平滑过渡

子情况 B2:碰到障碍物,尝试踩上去(Step Up)
if (CanStepUp(Hit) || (CharacterOwner->GetMovementBase() != nullptr && 
    Hit.HitObjectHandle == CharacterOwner->GetMovementBase()->GetOwner()))
{
    const FVector PreStepUpLocation = UpdatedComponent->GetComponentLocation();
    const FVector GravDir = GetGravityDirection();
    
    if (!StepUp(GravDir, Delta * (1.f - PercentTimeApplied), Hit, OutStepDownResult))
    {
        // StepUp 失败
        HandleImpact(Hit, LastMoveTimeSlice, RampVector);
        SlideAlongSurface(Delta, 1.f - PercentTimeApplied, Hit.Normal, Hit, true);
    }
    else
    {
        // StepUp 成功
        if (!bMaintainHorizontalGroundVelocity)
        {
            bJustTeleported = true;
            const float StepUpTimeSlice = (1.f - PercentTimeApplied) * DeltaSeconds;
            
            if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity() && 
                StepUpTimeSlice >= UE_KINDA_SMALL_NUMBER)
            {
                Velocity = (UpdatedComponent->GetComponentLocation() - PreStepUpLocation) / StepUpTimeSlice;
                Velocity = FVector::VectorPlaneProject(Velocity, -GravDir);
            }
        }
    }
}

StepUp 失败时:

  • 处理碰撞影响
  • 沿障碍物表面滑动

StepUp 成功时:

  • 如果不维持水平速度(!bMaintainHorizontalGroundVelocity):
    • 标记为”刚传送”(避免速度突变)
    • 根据实际移动距离重新计算速度
    • 将速度投影到水平面(移除垂直分量)

应用场景:

  • 走上台阶
  • 跨越小障碍物

子情况 B3:障碍物不允许踩上去
else if (Hit.Component.IsValid() && !Hit.Component.Get()->CanCharacterStepUp(CharacterOwner))
{
    HandleImpact(Hit, LastMoveTimeSlice, RampVector);
    SlideAlongSurface(Delta, 1.f - PercentTimeApplied, Hit.Normal, Hit, true);
}

处理逻辑:

  • 处理碰撞
  • 沿表面滑动

应用场景:

  • 碰到墙壁
  • 碰到标记为”不可踩踏”的物体

🎮 实际应用场景

场景 1:在平地上移动

玩家输入 → InVelocity → 计算Delta → 直接移动 → 无碰撞 → 完成

场景 2:上斜坡

玩家输入 → 计算Delta → ComputeGroundMovementDelta投影到斜坡 → 沿斜坡移动 → 完成

场景 3:从一个斜坡到另一个斜坡

第一次移动 → 碰到新斜坡 → 重新计算剩余移动 → 沿新斜坡移动 → 完成

场景 4:碰到台阶

移动 → 碰到障碍 → CanStepUp检查 → StepUp成功 → 调整速度 → 完成

场景 5:碰到墙壁

移动 → 碰到墙 → 不能StepUp → SlideAlongSurface → 沿墙滑动 → 完成

🔑 关键设计要点

1. 多层碰撞处理

  • 第一次碰撞:尝试继续移动
  • 第二次碰撞:尝试 StepUp
  • 失败后:滑动处理

2. 速度维护策略

if (!bMaintainHorizontalGroundVelocity)
{
    // 根据实际移动重新计算速度
    Velocity = (NewLocation - OldLocation) / TimeSlice;
}
  • bMaintainHorizontalGroundVelocity = true:保持输入速度
  • bMaintainHorizontalGroundVelocity = false:根据实际移动调整速度

3. 百分比追踪

float PercentTimeApplied = Hit.Time;
  • 追踪已完成的移动百分比
  • 用于计算剩余移动距离
  • 确保整个移动过程的连续性

🎯 总结

MoveAlongFloor 函数的核心职责:

  1. ✅ 地面约束移动:确保角色沿地面移动,不穿透地面
  2. ✅ 斜坡处理:正确处理各种角度的斜坡
  3. ✅ 碰撞响应:智能处理碰撞(滑动、踩上、阻挡)
  4. ✅ 速度管理:维护合理的移动速度
  5. ✅ 边缘情况:处理穿透、卡住等异常情况

这个函数是 UE 角色移动系统的核心组件,确保了角色在复杂地形上的平滑、自然移动体验!🎮