前言
Distance Matching
用于解决角色启动和停止的时候的滑步问题, 理论上我们可以用RootMotion
的方式保证启动和停止的滑步问题, 但是动画序列的同步是个问题, 所以生产环境下一般会用程序化的位移来解决滑步问题
Paragon Feature Examples: Animation Techniques 里有讲到相关技术, 本文就简单的对此技术进行研究
提取位移信息
有朋友会问,起步和停步动作需要制作成RootMotion
的模式吗? 我回答是需要, 这样我们方便提取里面的位移信息, 虽然我们还是会关闭RootMotion
效果
对于帕拉共的资源, 这个位移信息都已经放在每个起步停步动画里了, 比如
当然如果不适用帕拉共, 那么我们自己写一个AnimationModify
修改器也可以简单实现
预估位移
UCharacterMovementComponent::ApplyVelocityBrakin方法里有相关的预估位移的方法, 稍微整理一下封装到函数库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| bool UAnimExtensionBlueprintLibrary::PredictStopLocation(FVector& OutStopLocation, const FVector& CurrentLocation, const FVector& Velocity, const FVector& Acceleration, float Friction, float BrakingDeceleration, const float TimeStep, const int MaxSimulationIterations ) { const float MIN_TICK_TIME = 1e-6; if (TimeStep < MIN_TICK_TIME) { return false; }
const bool bZeroAcceleration = Acceleration.IsZero();
if ((Acceleration | Velocity) > 0.0f) { return false; }
BrakingDeceleration = FMath::Max(BrakingDeceleration, 0.f); Friction = FMath::Max(Friction, 0.f); const bool bZeroFriction = (Friction == 0.f); const bool bZeroBraking = (BrakingDeceleration == 0.f);
if (bZeroAcceleration && bZeroFriction) { return false; }
FVector LastVelocity = bZeroAcceleration ? Velocity : Velocity.ProjectOnToNormal(Acceleration.GetSafeNormal()); LastVelocity.Z = 0;
FVector LastLocation = CurrentLocation;
int Iterations = 0; while (Iterations < MaxSimulationIterations) { Iterations++;
const FVector OldVel = LastVelocity;
if (bZeroAcceleration) {
float RemainingTime = TimeStep; const float MaxTimeStep = (1.0f / 33.0f);
const FVector RevAccel = (bZeroBraking ? FVector::ZeroVector : (-BrakingDeceleration * LastVelocity.GetSafeNormal())); while (RemainingTime >= MIN_TICK_TIME) { const float dt = ((RemainingTime > MaxTimeStep && !bZeroFriction) ? FMath::Min(MaxTimeStep, RemainingTime * 0.5f) : RemainingTime); RemainingTime -= dt;
LastVelocity = LastVelocity + ((-Friction) * LastVelocity + RevAccel) * dt;
if ((LastVelocity | OldVel) <= 0.f) { LastVelocity = FVector::ZeroVector; break; } }
const float VSizeSq = LastVelocity.SizeSquared(); if (VSizeSq <= 1.f || (!bZeroBraking && VSizeSq <= FMath::Square(10))) { LastVelocity = FVector::ZeroVector; } } else { FVector TotalAcceleration = Acceleration; TotalAcceleration.Z = 0;
const FVector AccelDir = TotalAcceleration.GetSafeNormal(); const float VelSize = LastVelocity.Size(); TotalAcceleration += -(LastVelocity - AccelDir * VelSize) * Friction;
LastVelocity += TotalAcceleration * TimeStep; }
LastLocation += LastVelocity * TimeStep;
const float VSizeSq = LastVelocity.SizeSquared(); if (VSizeSq <= 1.f || (LastVelocity | OldVel) <= 0.f) { OutStopLocation = LastLocation; return true; } }
return false;
}
|
然后还需要一个函数来通过得到的预估距离来反向查找对应的时间点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| float UAnimExtensionBlueprintLibrary::GetDistanceCurveTime(UAnimSequence* Sequence, float Distance, FName DistanceCurveName) { FRawCurveTracks CurvesOfAnim = Sequence->GetCurveData(); TArray<FFloatCurve> Curves = CurvesOfAnim.FloatCurves;
for (int i = 0; i < Curves.Num(); i++) { if (Curves[i].Name.DisplayName == DistanceCurveName) { auto& Keys = Curves[i].FloatCurve.Keys; for (int j = 0; j < Keys.Num(); j++) { if (Keys[j].Value >= Distance) { float NextTime = Keys[j].Time; float NextValue = Keys[j].Value; float PrevValue = 0; float PrevTime = 0; if (j > 0) { PrevValue = Keys[j - 1].Value; PrevTime = Keys[j - 1].Time; } float Lerp = (Distance - PrevValue) / (NextValue - PrevValue); return PrevTime + (NextTime - PrevTime) * Lerp; } } } }
return 0; };
|
动画实例
我在封装蓝图组件和使用动画实例之间犹豫了一段时间, 但是考虑到起步和停步动画的不确定性(不同角色, 混合等), 还是决定把逻辑计算部分放到动画实例比较好
在动画实例的Tick
中, 我们先在有速度状态下刷新预估位置DistanceMatchingLocation
, 然后通过这个位置来获取并动态的设置起步和停步动画的播放时间
1 2 3 4 5 6 7 8 9 10
| if (IsAccelerating) { Time = UAnimExtensionBlueprintLibrary::GetDistanceCurveTime(JogStartAnimSequence, Distance); Target = &JogDistanceCurveStartTime; } else { Time = UAnimExtensionBlueprintLibrary::GetDistanceCurveTime(JogStopAnimSequence, -Distance); Target = &JogDistanceCurveStopTime; }
|
然后为了把动画播放完, 需要对时间进行累积
1 2 3 4 5 6 7 8
| if (Time > *Target) { *Target = Time; } else { *Target += DeltaTimeX; }
|
后记
DistanceMatching
效果需要修改CharacterMovementComponent
组件的一些参数, 比如BrakingDecelerationWalking
(参考500)需要适当改小一点, 不然减速太快根本也不需要播放停步动作, 强行播放就造成滑步 ; 需要减小BrakingFrictionFactor
(参考0.05)或者打开UseSeparateBrakingFriction
并设置BrakingFriction
参数(参考0.1)
- 起步动作还有优化空间, 现在遇到的问题是, 如果速度改变过大, 因为需要保证不滑步, 起步动作看上去就是修改了动画速率的感觉
插件地址