Table of Contents

自定义混合树

高级 程序员

AnimationComponent 具有 AnimationComponent.BlendTreeBuilder 属性。如果你想完全控制播放哪些动画、如何混合它们以及它们的权重是多少,可以创建一个实现 IBlendTreeBuilder 的脚本,并将其赋给动画组件下的 BlendTreeBuilder。

当动画组件更新时,它会调用你的脚本上的 void BuildBlendTree(FastList<AnimationOperation> animationList),而不是自己更新动画。这让你可以自由选择动画剪辑、速度和混合的任意组合,但也更加困难,因为所有繁重的工作都转移到了脚本一侧。

Stride 附带的第一人称射击、第三人称平台游戏和俯视角 RPG 模板就是使用自定义混合树的示例。

代码示例

public class AnimationBlendTree : SyncScript, IBlendTreeBuilder
{
    /// <summary>
    /// 需要动画组件
    /// </summary>
    [Display("Animation Component")]
    public AnimationComponent AnimationComponent { get; set; }

    [Display("Walk")]
    public AnimationClip AnimationWalk { get; set; }

    [Display("Run")]
    public AnimationClip AnimationRun { get; set; }

    [Display("Lerp Factor")]
    public float LerpFactor = 0.5f;

    private AnimationClipEvaluator animEvaluatorWalk;
    private AnimationClipEvaluator animEvaluatorRun;
    private double currentTime = 0;

    public override void Start()
    {
        base.Start();

        // 重要步骤
        // 通过设置自定义的混合树构建器,我们可以覆盖动画系统的默认行为。
        // 取而代之的是,每帧都会调用 BuildBlendTree(FastList<AnimationOperation> blendStack)。
        // 我们需要在 Update() 中更新动画状态,然后
        // 将新的动画状态(stack = 混合树)传递给动画系统。
        AnimationComponent.BlendTreeBuilder = this;

        // 由于我们覆盖了动画系统,需要为要使用的每个剪辑创建一个 AnimationClipEvaluator
        animEvaluatorWalk = AnimationComponent.Blender.CreateEvaluator(AnimationWalk);
        animEvaluatorRun = AnimationComponent.Blender.CreateEvaluator(AnimationRun);
    }

    public override void Cancel()
    {
        // 脚本被取消时,别忘了释放 Start() 中创建的所有动画资源 - AnimationClipEvaluators
        AnimationComponent.Blender.ReleaseEvaluator(animEvaluatorWalk);
        AnimationComponent.Blender.ReleaseEvaluator(animEvaluatorRun);
    }
        
    public override void Update()
    {
        // 使用 DrawTime 而不是 UpdateTime,因为动画只在被绘制时才更新
        var time = Game.DrawTime;

        // 此更新函数考虑了时长不同的动画,
        // 维护一个相对于混合后最大时长的当前时间
        long blendedMaxDuration = (long)MathUtil.Lerp(AnimationWalk.Duration.Ticks, AnimationRun.Duration.Ticks, LerpFactor);

        var currentTicks = TimeSpan.FromTicks((long)(currentTime * blendedMaxDuration));

        currentTicks = blendedMaxDuration == 0
            ? TimeSpan.Zero
            : TimeSpan.FromTicks((currentTicks.Ticks + (long)(time.Elapsed.Ticks)) % blendedMaxDuration);

        currentTime = ((double)currentTicks.Ticks / (double)blendedMaxDuration);
    }

    /// 当需要求值 AnimationComponent 时,动画系统每帧都会调用 BuildBlendTree。
    /// 它通过设置自定义混合树来覆盖 AnimationComponent 的默认行为。
    public void BuildBlendTree(FastList<AnimationOperation> blendStack)
    {
        var timeWalk = TimeSpan.FromTicks((long) (currentTime * AnimationWalk.Duration.Ticks));
        var timeRun  = TimeSpan.FromTicks((long) (currentTime * AnimationRun.Duration.Ticks));

        // 构建动画混合树(栈)
        blendStack.Add(AnimationOperation.NewPush(animEvaluatorWalk, timeWalk));    // 将动画状态 PUSH 入栈,在指定时间求值
        blendStack.Add(AnimationOperation.NewPush(animEvaluatorRun, timeRun));      // 将另一个动画状态 PUSH 入栈,在指定时间求值
        blendStack.Add(AnimationOperation.NewBlend(CoreAnimationOperation.Blend, LerpFactor));   // POP 出最后两个状态,按因子混合后将结果 PUSH 回栈

        // 注意
        // 由于混合操作是以栈的形式排列的,你必须以这种方式打包操作。
        // 一般来说,对二叉树进行深度优先遍历,并在*离开*已处理节点时添加操作就足够了。
        // 对于非二叉树,你还必须正确地设置混合因子的权重

        // 完成
        // 现在栈顶就是用于动画模型的最终状态
    }
}

另请参阅