JustMakeGame 7. 速度混合&移动方向&八向混合

速度混合

计算速度混合量,速度在各个方向上的占比。
比如我们有前后左右四个方向的动作,角色向左前方向移动,那么四个动作融合后,左前两个动作的混合比重是要比右后大的。

void UCwlBaseAnimIns::CalculateVelocityBlend()
{
    FVector VelocityNormal = UKismetMathLibrary::Normal(Velocity);
    FVector LocalRelativeVelocityDir = UKismetMathLibrary::Quat_UnrotateVector(Character->GetActorRotation().Quaternion(), VelocityNormal);
    float Sum = FMath::Abs(LocalRelativeVelocityDir.X) + FMath::Abs(LocalRelativeVelocityDir.Y) + FMath::Abs(LocalRelativeVelocityDir.Z);
    LocalRelativeVelocityDir = LocalRelativeVelocityDir / Sum;

    FCwlVelocityBlend TargetVelocityBlend;
    TargetVelocityBlend.Forward = FMath::Clamp(LocalRelativeVelocityDir.X, 0.f, 1.f);
    TargetVelocityBlend.Backward= FMath::Abs(FMath::Clamp(LocalRelativeVelocityDir.X, -1.f, 0.f));
    TargetVelocityBlend.Left = FMath::Abs(FMath::Clamp(LocalRelativeVelocityDir.Y, -1.f, 0.f));
    TargetVelocityBlend.Right = FMath::Clamp(LocalRelativeVelocityDir.Y, 0.f, 1.f);

    float DeltaSeconds = GetWorld()->GetDeltaSeconds();
    VelocityBlend = InterVelocityBlend(VelocityBlend, TargetVelocityBlend, VelocityBlendInterpSpeed, DeltaSeconds);
}

速度去掉旋转在单位化。原作者的写法。感觉应该先去掉旋转再单位化?

    FVector VelocityNormal = UKismetMathLibrary::Normal(Velocity);
    FVector LocalRelativeVelocityDir = UKismetMathLibrary::Quat_UnrotateVector(Character->GetActorRotation().Quaternion(), VelocityNormal);

然后每一项除以总值就变成了百分比。然后再限制一下范围再abs一下。

    float Sum = FMath::Abs(LocalRelativeVelocityDir.X) + FMath::Abs(LocalRelativeVelocityDir.Y) + FMath::Abs(LocalRelativeVelocityDir.Z);
    LocalRelativeVelocityDir = LocalRelativeVelocityDir / Sum;

这样还没完。我们是用速度分量驱动动画。但是当前的值和目标值直接的过度也需要处理一下。

    VelocityBlend = InterVelocityBlend(VelocityBlend, TargetVelocityBlend, VelocityBlendInterpSpeed, DeltaSeconds);

其实就是每个分量根据配置的插值速度进行一个插值。

FCwlVelocityBlend UCwlBaseAnimIns::InterVelocityBlend(FCwlVelocityBlend Current, FCwlVelocityBlend Target, float InterSpeed, float DeltaX)
{
    FCwlVelocityBlend NewVelocityBlend;
    NewVelocityBlend.Forward = UKismetMathLibrary::FInterpTo(Current.Forward, Target.Forward, DeltaX, InterSpeed);
    NewVelocityBlend.Backward = UKismetMathLibrary::FInterpTo(Current.Backward, Target.Backward, DeltaX, InterSpeed);
    NewVelocityBlend.Left = UKismetMathLibrary::FInterpTo(Current.Left, Target.Left, DeltaX, InterSpeed);
    NewVelocityBlend.Right = UKismetMathLibrary::FInterpTo(Current.Right, Target.Right, DeltaX, InterSpeed);
    return NewVelocityBlend;
}

移动方向

通过角色方向与速度方向来计算移动方向是前后左右中的哪一个。

【这个笔记是以前写的,在看了Lyra后发现有一个 UKismetAnimationLibrary::CalculateDirection 接口可以用】

如果是冲刺,或者是旋转模式是查看方向,那么不论怎么样都是向前移动的。
否则,先根据速度方向和角色朝向的夹角计算出Angle。

在水平面上移动方向与angle的示意图如上。
我们根据如果angle在 AOB之间就认为是前方向。其他同理。同时这里允许右5度的误差。

void UCwlBaseAnimIns::CalculateMovementDirection()
{
    if (Gait == ECwlGait::Sprinting)
    {
        MovementDirection = ECwlMovementDirection::Forward;
        return;
    }

    if (RotationMode == ECwlRotationMode::LookingDirection || RotationMode == ECwlRotationMode::Aiming)
    {
        // UKismetMathLibrary::Conv_VectorToRotator(Velocity) 速度方向转到X轴正方向(对于摄像机这个是前)的旋转 减去 控制器的旋转
        // 获得的这个angle角色正前方是0,右边半圈正180,左边半圈负的180
        float Angle = UKismetMathLibrary::NormalizedDeltaRotator(UKismetMathLibrary::Conv_VectorToRotator(Velocity), AimingRotation).Yaw;
        CalculateQudrant(70, -70, 110, -110, 5, Angle);
    }
    else if (RotationMode == ECwlRotationMode::VelocityDirection)
    {
        MovementDirection = ECwlMovementDirection::Forward;
    }
}

void UCwlBaseAnimIns::CalculateQudrant(float Fr, float Fl, float Br, float Bl, float Buffer, float Angle)
{
    bool bIncreaseBuffer = MovementDirection != ECwlMovementDirection::Forward || MovementDirection != ECwlMovementDirection::Backward;
    if (AngleInRange(Angle, Fl, Fr, Buffer, bIncreaseBuffer))
    {
        MovementDirection = ECwlMovementDirection::Forward;
        return;
    }

    bIncreaseBuffer = MovementDirection != ECwlMovementDirection::Right || MovementDirection != ECwlMovementDirection::Left;
    if (AngleInRange(Angle, Fr, Br, Buffer, bIncreaseBuffer))
    {
        MovementDirection = ECwlMovementDirection::Right;
        return;
    }

    bIncreaseBuffer = MovementDirection != ECwlMovementDirection::Right || MovementDirection != ECwlMovementDirection::Left;
    if (AngleInRange(Angle, Bl, Fl, Buffer, bIncreaseBuffer))
    {
        MovementDirection = ECwlMovementDirection::Left;
        return;
    }

    MovementDirection = ECwlMovementDirection::Backward;
}

bool UCwlBaseAnimIns::AngleInRange(float Angle, float MinRange, float MaxRange, float Buffer, bool bIncreaseBuffer)
{
    if (bIncreaseBuffer)
    {
        return UKismetMathLibrary::InRange_FloatFloat(Angle, MinRange - Buffer, MaxRange + Buffer, true, true);
    }
    return UKismetMathLibrary::InRange_FloatFloat(Angle, MinRange + Buffer, MaxRange - Buffer, true, true);
}

八向移动状态机

输入姿势

我们在前面【移动步幅与走跑混合】得到一个混合空间

image.png

在【冲刺混合】章节获得了一个冲刺的Pose

image.png

我们通过步态来混合普通的前移动,和冲刺移动。保存为姿势 (N) F Movement

image.png

这只是Forward方向的输入。
但是由于作者walk,run动画都做了六个方向
image.png
image.png

所以我们有六个混合空间
image.png

那么我们就要处理,前,后,左前,左后,右前,右后六个移动状态。
除了前面的Forward。其他都只需要混合空间save pose
image.png

参数就是前文提到的,移动步幅,走跑混合,播放速率
同时注意这六个混合空间,六个冲刺动画要设置为同一同步组。
不然混合会出问题,我这里的表现为罗圈腿。查了很久。。。
image.png

把所有逻辑写在一个动画层中 (N)CycleBlending。这里包括了六个混合空间保持为变量。和一个状态机。

image.png

状态机里面是前,后,左前,左后,右前,右后。六个运动状态

image.png

输出状态

image.png

在每一个状态中都是类似的结构。根据前后左右四个方向速度的占比,决定四个方向动画的混合度。
目的是,你也许已经切换到了左前的状态机中,但是不能直接切到单个左前动画。有一定的过度过程。

如果这里只连一个左前动画,且动画从左后过度过来。那么效果就是动画 LB 过度到 动画LF。过度时间是这个条件的混合设置。

image.png

image.png

上面是LF,下面是LB
猜测这种切换会有问题,实际测试效果还是有明显差异的。

过度条件

我粘了一份状态机,去除了某些花里胡哨的切换条件,描述一下状态切换条件。

image.png

我们在【移动方向】章节算了一个移动方向 MovementDirection。

UENUM(BlueprintType)
enum class ECwlMovementDirection : uint8
{
    Forward,
    Right,
    Left,
    Backward
};

比如向【后】变化的状态,切换条件都是 MovementDirection == Backward。图中我描粗的绿色

image.png

切换条件是 MovementDirection == Right,图中的紫色。

===

外面一圈的混合设置

image.png

中间一圈的状态切换混合设置调整

image.png

image.png

中间一圈提升为共享Pivot。插值0.5,方式立方。
同时触发一个pivot事件,这个事件会设置一个bool值true。0.5秒后false。用于回转运动。回转运动指向左运动时候突然向右,有一个过度动作。

这里的change direction。打开骨骼,这里有一个混合配置。意图是转身的时候,上半身比下半身慢半拍


可以使用slomo 0.5 感受下效果

然后我们看一个问题,左移动,胸朝左,按右,胸还是朝左,需要前一下才能正确。
因为我们是对角切换,左前没法之间过来。


右前只能切到左后,而没有左前,需要添加左后到左前的过度条件。


共享混合设置

左边的条件如下,右边那个需要把 state weight 改成RB


也就是当完全到达左后状态,且满足曲线值为0,就可以自动切换到左前。曲线的含义是==0,那么步伐已经到了下半段
在walk LB, RB, LF, RF。run LB RB。动画中有这条Feet_Crossing曲线

分界点是脚步交叉,后面值为0

然后这还不完全


作者还加了一个臀部朝向的偏移值。这条曲线目前也是没有的,在覆盖层拉弓的侧身动作才会有这条曲线,所以现在这个一直是真。
过度提升为共享,内部都是Hips_Right,内部都是Hips_Left。混合设置都是switch_hips



然后条件如上
而且这四个条件的,优先顺序都是2

最后添加一些事件,后面备用

在动画蓝图中触发事件调到cpp层

image.png

void UCwlBaseAnimIns::OnAnimNotifyHipsChange(ECwlHipsDirection InHipsDirection)
{
    HipsDirection = InHipsDirection;
}

其他选择

以上是ALSv4的做法。还可以选择Lyra的做法。这个是Lyra的状态机。我们只看Cycle状态
截图来自我一个抄Lyra的项目[https://gitee.com/weilin3101/my-lyra-v1]

image.png

Cycle状态中是一个动画层
image.png

动画层中
image.png

UpdateCycleAnim的一部分
image.png

Sequence Player 通过 UpdateCycle函数,通过读取运动方向,从前后左右四个动画中选了一个,作为当前动画。然后调用朝向扭曲节点,实现了八项移动。需要包含 Animation Locomotion Library插件。

但是效果上我更喜欢ALSv4的版本

上一篇
下一篇