自定义混合树
高级 程序员
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 回栈
// 注意
// 由于混合操作是以栈的形式排列的,你必须以这种方式打包操作。
// 一般来说,对二叉树进行深度优先遍历,并在*离开*已处理节点时添加操作就足够了。
// 对于非二叉树,你还必须正确地设置混合因子的权重
// 完成
// 现在栈顶就是用于动画模型的最终状态
}
}