UE4移动检测(下)

425 阅读2分钟

StepUp

CanStepUp

UCharacterMovementComponent::CanStepUp流程图:

StepUp详解

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

  1. StepUp阶段, 使用胶囊体向上做碰撞检测.
  2. StepForward阶段, 使用上一步胶囊体的位置向前做碰撞检测. 如果发生碰撞, 调用SlideAlongSurface沿着碰撞表面移动
  3. StepDown阶段, 通过FindFloor找到新的移动落脚点.

关键点:

  1. 根据MaxStepHeight计算向上距离, 根据移动Delta计算向前距离, 根据地板高度计算向下距离.
  2. 在碰撞过程中任何Penetrating都将回滚本次StepUp
  3. 向前移动过程中如果有碰撞, 则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;

斜面ABAB, 位移Delta为EF\vec{EF}, 其中FC\vec{FC}为斜面ABAB法线, EM\vec{EM}为最终位移结果. 其中n\vec{n}为斜面ABAB的法线, 并且将n\vec{n}分解成水平分量v水平\vec{v_{水平}}和竖直向量v竖直\vec{v_{竖直}}. 易知n\vec{n}的模为1, 并且其竖直分量大小为FloorNormal.Z.

证明过程:

ECFEFM\because \triangle{ECF} \sim \triangle{EFM}

CFMF=EFEM=ECEF\therefore \frac{CF}{MF} = \frac{EF}{EM} = \frac{EC}{EF}

MF=CFEMEF=CF1cosCEF\therefore MF = CF\frac{EM}{EF} = CF \cdot \frac{1}{\cos\angle{CEF}}

1cosCEF\frac{1}{\cos\angle{CEF}}可以用1FloorNormal.Z\frac{1}{FloorNormal.Z}表示. CFCF可以用FloorNormalDelta-FloorNormal | Delta表示. 所以可以使用以下计算方式计算斜面位移.

注意:

  1. 位移EF\vec{EF}必须保证水平, 否则EM\vec{EM}不会平行于斜面ABAB, 即图中ECECEMEM不在同一条直线上了.
  2. ComputeGroundMovementDelta的计算结果保证水平方向位移不变, 竖直方向位移根据斜面而定. 
  3. 由于只有Z值不同, 则可以保证原位移Delta和新位移NewDelta在同一竖直平面内, 最终移动距离大于之前移动距离.
  4. EM\vec{EM}为最终移动的结果.

下图为位移EF\vec{EF}不水平的情况:

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移动过程中, 需要保证离地面[MIN_FLOOR_DIST,MAX_FLOOR_DIST][MIN\_FLOOR\_DIST, MAX\_FLOOR\_DIST], 即[1.9,2.4][1.9, 2.4]​, 相关配置: UCharacterMovementComponent::MIN_FLOOR_DIST, UCharacterMovementComponent::MAX_FLOOR_DIST

在Walking状态下, 会调用AdjustFloorHeight进行处理, 来保证距离Floor一定距离.

重点

TwoWallAdjust

两堵墙夹角大于9090^\circ, 沿着新墙壁继续行走:

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

SlideAlongSurface

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

StepUp三部曲

SafeMove触发关键节点

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

碰撞退步

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

移动碰撞抛出HitDebug信息

SafeMoveUpdatedComponent过程中, 开启UCheatManager.IsDebugCapsuleSweepPawnEnabled, 关键函数:UCheatManager.AddCapsuleSweepDebugInfo

引用

  1. UMovementComponent
  2. 移动同步算法