📋 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);
}
}处理逻辑:
- 处理碰撞影响
- 尝试沿表面滑动
- 如果仍然穿透,触发”卡住”事件
应用场景:
- 角色被挤压到墙角
- 物体突然出现在角色位置
情况 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.f - PercentTimeApplied) - 基于新的斜坡重新计算移动向量
- 执行剩余的移动
- 累计已应用的移动百分比
应用场景:
- 从一个斜坡移动到另一个斜坡
- 斜坡之间的平滑过渡
子情况 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 函数的核心职责:
- ✅ 地面约束移动:确保角色沿地面移动,不穿透地面
- ✅ 斜坡处理:正确处理各种角度的斜坡
- ✅ 碰撞响应:智能处理碰撞(滑动、踩上、阻挡)
- ✅ 速度管理:维护合理的移动速度
- ✅ 边缘情况:处理穿透、卡住等异常情况
这个函数是 UE 角色移动系统的核心组件,确保了角色在复杂地形上的平滑、自然移动体验!🎮