
仿刺客信条跑酷的失败画面几乎总是同一种:角色“飘”上障碍、手掌穿墙、落地滑步。根源不在动画资源,而在检测阶段没有算准落点与法线,对齐阶段没有把根骨骼位移对到那个落点上。蓝图能完成这件事,但需要把检测封装成唯一事实来源,把动作选择变成阈值表,把位置修正交给Motion Warping,而不是用SetActorLocation硬拉。
检测层:Sphere Trace产出可复用的结构体
单根Line Trace只能回答“前方有东西”,回答不了“顶部在哪、顶面多宽、墙面朝哪边”。检测必须用球体扫掠加二次射线,输出一个供后续所有环节消费的结构体。以下函数可作为UBlueprintFunctionLibrary暴露给蓝图,蓝图侧只需传入前向向量与半径:
USTRUCT(BlueprintType)
struct FTraversalResult {
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly) bool bValid = false;
UPROPERTY(BlueprintReadOnly) float WallHeight = 0.f;
UPROPERTY(BlueprintReadOnly) float StandDepth = 0.f;
UPROPERTY(BlueprintReadOnly) FVector TargetLocation = FVector::ZeroVector;
UPROPERTY(BlueprintReadOnly) FRotator TargetRotation = FRotator::ZeroRotator;
};
UFUNCTION(BlueprintCallable, Category="Parkour")
static FTraversalResult ComputeTraversalHit(AActor* Avatar, FVector Forward, float Radius) {
FTraversalResult Out;
if (!Avatar) return Out;
UWorld* World = Avatar->GetWorld();
FVector Origin = Avatar->GetActorLocation();
FCollisionQueryParams P; P.AddIgnoredActor(Avatar);
TArray<FHitResult> Hits;
FVector Start = Origin + Forward * 50.f;
World->SweepMultiByChannel(Hits, Start, Start + Forward * 150.f, FQuat::Identity,
ECC_GameTraceChannel1, FCollisionShape::MakeSphere(Radius), P); // Parkour 通道
if (Hits.IsEmpty()) return Out;
const FHitResult* Highest = &Hits[0];
for (const FHitResult& H : Hits)
if (H.ImpactPoint.Z > Highest->ImpactPoint.Z) Highest = &H;
FVector WallNormal = Highest->ImpactNormal;
FHitResult TopHit;
FVector TopStart = Highest->ImpactPoint + WallNormal * 2.f + FVector(0, 0, 200.f);
World->LineTraceSingleByChannel(TopHit, TopStart, TopStart - FVector(0, 0, 400.f),
ECC_GameTraceChannel1, P);
Out.WallHeight = Origin.Z - TopHit.ImpactPoint.Z;
FHitResult DepthHit;
FVector DepthEnd = TopHit.ImpactPoint - WallNormal * 120.f;
World->SweepSingleByChannel(DepthHit, TopHit.ImpactPoint, DepthEnd, FQuat::Identity,
ECC_GameTraceChannel1, FCollisionShape::MakeSphere(15.f), P);
Out.StandDepth = FVector::Dist(TopHit.ImpactPoint, DepthHit.ImpactPoint);
Out.TargetLocation = DepthHit.ImpactPoint;
Out.TargetRotation = (-WallNormal).Rotation();
Out.bValid = true;
return Out;
}StandDepth小于胶囊半径时判定为栏杆,只走ThinVault;深度足够且WallHeight超过角色半高时才进入Mantle。碰撞通道必须独立于WorldStatic,否则路灯柱也会触发翻越检测。
决策层:阈值表在DataAsset里,不在动画蓝图中
检测输出后,动作类型由WallHeight与StandDepth查表决定。蓝图侧只需一个Switch on EParkourType,阈值和蒙太奇映射放在DataAsset中,设计师调参数不必改连线:
// 蓝图事件图表(文本化)
OnParkourInputPressed:
Result = ComputeTraversalHit(Self, GetActorForwardVector(), 40)
Branch (Result.bValid)
True:
Type = ParkourData->ResolveType(Result.WallHeight, Result.StandDepth)
PlayParkourAction(Type, Result)
False: Do Nothing关键纪律是:决策层只选蒙太奇、写Warp Target,绝不直接SetActorLocation。位置修改由根运动和Motion Warping完成,否则检测落点与角色实际位置会分叉。
对齐层:Motion Warping写入前后缘目标
Motion Warping的机制是在蒙太奇中预设命名目标,运行时把检测到的落点和朝向写入,引擎在播放期间自动扭动根骨骼,让手和脚精确到达边缘。写入必须用AddOrUpdateWarpTargetFromLocationAndRotation,一帧内同时更新位置与旋转,避免分别设置时的短暂不同步:
void AMyCharacter::PlayParkourAction(EParkourType Type, const FTraversalResult& Hit) {
UAnimMontage* Montage = ParkourData->GetMontage(Type);
if (!Montage || !MotionWarping) return;
MotionWarping->AddOrUpdateWarpTargetFromLocationAndRotation(
TEXT("FrontLedge"), Hit.TargetLocation, Hit.TargetRotation);
// 栏杆/箱体需要后缘目标,让蒙太奇后段身体对齐
if (Type == EParkourType::ThinVault || Type == EParkourType::MidVault) {
FVector BackLedge = Hit.TargetLocation + Hit.TargetRotation.Vector() * 80.f;
MotionWarping->AddOrUpdateWarpTargetFromLocationAndRotation(
TEXT("BackLedge"), BackLedge, Hit.TargetRotation);
}
PlayAnimMontage(Montage);
}Motion Warping负责播放期间的落点校正,Two Bone IK负责播放之后的持续贴合。角色攀住边缘横移时,手掌必须跟随连续边缘线,纯蒙太奇做不到,需要在动画蓝图中用Two Bone IK把手的IK目标钉在墙面法线偏移的位置上。两者分工不能混:只挂Warping不做IK,横移会滑手;只做IK不挂Warping,初始落点已经歪了。
性能:检测不进Tick
ComputeTraversalHit不应挂在Event Tick上。用一个0.15秒的定时器做粗扫,只有短距离球体追踪命中Parkour通道时才触发完整检测,离开障碍后自动停止查询。所有输出汇入同一个FTraversalResult,动作决策、Warp Target写入和调试绘制消费同一份数据——检测只有一个事实来源,动画和IK才不会各自猜一个位置。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。