StepUp
CanStepUp
UCharacterMovementComponent::CanStepUp流程图:

StepUp详解
StepUp分成三个阶段: StepUp阶段, StepForward阶段, StepDown阶段.

- StepUp阶段, 使用胶囊体向上做碰撞检测.
- StepForward阶段, 使用上一步胶囊体的位置向前做碰撞检测. 如果发生碰撞, 调用
SlideAlongSurface沿着碰撞表面移动 - StepDown阶段, 通过
FindFloor找到新的移动落脚点.
关键点:
- 根据MaxStepHeight计算向上距离, 根据移动Delta计算向前距离, 根据地板高度计算向下距离.
- 在碰撞过程中任何
Penetrating都将回滚本次StepUp - 向前移动过程中如果有碰撞, 则MoveAlongSurface.
详细流程:

MoveAlongFloor
MoveAlongFloor:沿着地板移动. 可以看到, 移动过程中一定会保证速度水平:

ComputeGroundMovementDelta
// Compute a vector of movement, given a delta and a hit result of the surface we are on.
//
// @param Delta: Attempted movement direction
// @param RampHit: Hit result of sweep that found the ramp below the capsule
// @param bHitFromLineTrace: Whether the floor trace came from a line trace
//
// @return If on a walkable surface, this returns a vector that moves parallel to the surface. The magnitude may be scaled if bMaintainHorizontalGroundVelocity is true.
// If a ramp vector can't be computed, this will just return Delta.
// 根据给定的期望移动的DeltaMove和碰撞表面的法线, 计算沿着表面Surface移动的距离
virtual FVector ComputeGroundMovementDelta(const FVector& Delta, const FHitResult& RampHit, const bool bHitFromLineTrace) const;

斜面, 位移Delta为, 其中为斜面法线, 为最终位移结果. 其中为斜面的法线, 并且将分解成水平分量和竖直向量. 易知的模为1, 并且其竖直分量大小为FloorNormal.Z.
证明过程:
而可以用表示. 可以用表示. 所以可以使用以下计算方式计算斜面位移.

注意:
- 位移必须保证水平, 否则不会平行于斜面, 即图中和不在同一条直线上了.
ComputeGroundMovementDelta的计算结果保证水平方向位移不变, 竖直方向位移根据斜面而定.- 由于只有Z值不同, 则可以保证原位移Delta和新位移NewDelta在同一竖直平面内, 最终移动距离大于之前移动距离.
- 为最终移动的结果.
下图为位移不水平的情况:

UCharacterMovementComponent::MoveAlongFloor
它的本质是如果发生碰撞, 重新计算斜坡, 然后沿着斜坡再次移动.
沿着当前斜坡移动:

针对卡住情况, 尝试沿着表面移动.

如果发生碰撞(第一次碰撞), 则计算斜坡, 尝试沿着斜坡再次移动.

如果再次发生碰撞(第二次碰撞), 则尝试StepUp. StepUp成功, 则沿着斜坡再次移动. StepUp失败, 则沿着表面再次移动

流程图:

WalkOffLedges
在移动过程中, 存在WalkOffLedges, 即是否能在建筑物边缘通过Walking方式掉下去. 如果UCharacterMovementComponent.bCanWalkOffLedges为true, 表明可以从建筑物边缘掉下去, 否则不能掉下去(移动到边缘后不能继续向边缘方向移动).
CanWalkOffLedges
UCharacterMovementComponent.bCanWalkOffLedges默认为true, 表明可以从边缘通过Walking掉下去.

CheckLedgeDirection
// 检测移动方向是否存在合适的Floor, 如果存在返回true, 否则返回false
bool UCharacterMovementComponent::CheckLedgeDirection(const FVector& OldLocation, const FVector& SideStep, const FVector& GravDir) const
{
const FVector SideDest = OldLocation + SideStep;
FCollisionQueryParams CapsuleParams(SCENE_QUERY_STAT(CheckLedgeDirection), false, CharacterOwner);
FCollisionResponseParams ResponseParam;
InitCollisionParams(CapsuleParams, ResponseParam);
const FCollisionShape CapsuleShape = GetPawnCapsuleCollisionShape(SHRINK_None);
const ECollisionChannel CollisionChannel = UpdatedComponent->GetCollisionObjectType();
FHitResult Result(1.f);
// 使用胶囊体进行水平Sweep(由于传入的SideStep z方向为0)
GetWorld()->SweepSingleByChannel(Result, OldLocation, SideDest, FQuat::Identity, CollisionChannel, CapsuleShape, CapsuleParams, ResponseParam);
if ( !Result.bBlockingHit || IsWalkable(Result) )
{ // 针对没有碰撞的情况或者有碰撞并且walkable的情况
if ( !Result.bBlockingHit )
{ // 针对水平方向没有碰撞的情况, 即检测方向没有碰撞物, 可行走, 需要检测脚下是否有碰撞
GetWorld()->SweepSingleByChannel(Result, SideDest, SideDest + GravDir * (MaxStepHeight + LedgeCheckThreshold), FQuat::Identity, CollisionChannel, CapsuleShape, CapsuleParams, ResponseParam);
}
if ( (Result.Time < 1.f) && IsWalkable(Result) )
{ // 有碰撞并且walkable, 则存在合适的Floor
return true;
}
}
// 没有合适的floor
return false;
}
GetLedgeMove
将移动向量做Y轴镜像, 检测是否存在合适的Floor. 如果没有, 则再将之反向, 检测是否有合适的floor.

LedgeMove
关键函数:UCharacterMovementComponent.PhysWalking
const bool bCheckLedges = !CanWalkOffLedges();
if ( bCheckLedges && !CurrentFloor.IsWalkableFloor() )
{
// calculate possible alternate movement
const FVector GravDir = FVector(0.f,0.f,-1.f);
// 获取LedgeMove移动距离
const FVector NewDelta = bTriedLedgeMove ? FVector::ZeroVector : GetLedgeMove(OldLocation, Delta, GravDir);
if ( !NewDelta.IsZero() )
{
// first revert this move
RevertMove(OldLocation, OldBase, PreviousBaseLocation, OldFloor, false);
// avoid repeated ledge moves if the first one fails
bTriedLedgeMove = true;
// Try new movement direction
// 找到了新的移动方向, 尝试移动
Velocity = NewDelta/timeTick;
remainingTime += timeTick;
continue;
}
else
{
// see if it is OK to jump
// @todo collision : only thing that can be problem is that oldbase has world collision on
bool bMustJump = bZeroDelta || (OldBase == NULL || (!OldBase->IsQueryCollisionEnabled() && MovementBaseUtility::IsDynamicBase(OldBase)));
if ( (bMustJump || !bCheckedFall) && CheckFall(OldFloor, CurrentFloor.HitResult, Delta, OldLocation, remainingTime, timeTick, Iterations, bMustJump) )
{
return;
}
bCheckedFall = true;
// revert this move
RevertMove(OldLocation, OldBase, PreviousBaseLocation, OldFloor, true);
// 没有找到新的移动方向, 停止移动
remainingTime = 0.f;
break;
}
}
问
SetUp碰头怎么办?
如果有一个门类似的物体, 有门槛, 门框, SetUp时候怎么处理?
答: 使用SlideAlongSurface计算滑行方向, 尝试划过去.

当Flying时候, StepUp还有必要StepDown吗?
答案: 有必要 因为不能每次Flying StepUp的时候飞那么高, 但需要根据需求调整. 比如当StepDown阶段没有检测到碰撞物体时候, 可以适当调整高度, 不要直接下移StepTravelDownHeight.
StepUp分成三个阶段: StepUp+StepForward+StepDown. 当Flying的时候, 第三个阶段很可能遇到没有地板的情况: 即如下图:

如果是Walking, 可以不用管理, 因为没有地板, 会切换到Falling状态. 但是针对Flying状态, StepDown后很可能造成人物莫名奇妙下移. 下面具体解析实例, 其中, 黄色胶囊体为原本位置, 白色半透明胶囊体为移动后位置.
StepUp阶段: 向上移动, 在沿着斜面向上飞行时, 会碰到斜面.

StepFoward阶段: 向前移动, 因为紧贴墙壁, 实际移动为0. 然后紧接着执行SlideAlongSurface, 沿着墙面向上滑动.

StepForward阶段, 沿着斜面滑动相关代码:

StepDown阶段: 向下移动, 当沿着墙壁Flying的时候, 如果底下没有碰撞物体, 就不会发生碰撞, 会移动StepTravelDownHeight. 造成下落.

相关代码:

移动过程中, 如何保证离floor一定距离
在UE移动过程中, 需要保证离地面, 即, 相关配置: UCharacterMovementComponent::MIN_FLOOR_DIST, UCharacterMovementComponent::MAX_FLOOR_DIST
在Walking状态下, 会调用AdjustFloorHeight进行处理, 来保证距离Floor一定距离.
重点
TwoWallAdjust
两堵墙夹角大于, 沿着新墙壁继续行走:

两堵墙夹角小于, 如果可以走上新墙, 则沿着新墙壁继续行走:

SlideAlongSurface
沿着表面继续滑动. 最多触发两次物理检测.

StepUp三部曲

SafeMove触发关键节点
该函数触发关键节点. 包括碰撞检测+碰撞回退+抛出Debug信息, +设置位置+触发Overlap+触发Hit.

碰撞退步
如果放生碰撞,不能直接移动到碰撞位置, 需要进行退步处理. 该数值为经验数值.

移动碰撞抛出HitDebug信息
在SafeMoveUpdatedComponent过程中, 开启UCheatManager.IsDebugCapsuleSweepPawnEnabled, 关键函数:UCheatManager.AddCapsuleSweepDebugInfo
