一、通过名称播放动画
前面我们讲的都是直接通过动画片段的引用播放动画,Animancer也提供了直接通过动画名称来播放动画的方法。但这并不是推荐的使用方式,因为通过字符串播放比通过引用播放效率略低,且更难维护。
首先我们需要在角色身上挂载NamedAnimancerComponent
组件。NamedAnimancerComponent
继承于AnimancerComponent
,它的内部多了一个字典,可以用来映射动画名称与动画引用的对应关系。我们可以在面板上指定一个默认动画状态,这里指定为Idle,但取消自动播放选项。
接下来创建一个脚本NamedAnimations
,并编写如下代码。
public class NamedAnimations : MonoBehaviour
{
public NamedAnimancerComponent animancer;
public AnimationClip walk;
public AnimationClip run;
public void PlayIdle()
{
animancer.TryPlay("Relax-Idle");
}
public void PlayWalk()
{
var state = animancer.TryPlay("Relax-Walk-Forward");
if (state == null)
Debug.LogWarning("'Relax-Walk-Forward' 没有被注册");
}
public void InitializeWalkState()
{
animancer.States.Create(walk);
Debug.Log("创建状态:" + walk, this);
}
public void PlayRun()
{
animancer.Play(run);
}
}
将脚本挂载到角色身上,并对变量进行赋值。然后在场景中添加四个按钮,分别绑定脚本中的四个方法
接下来运行游戏。先来看PlayIdle
的效果
正常播放。这是因为在NamedAnimancerComponent
的字典中,我们事先添加了Idle动画,从而可以映射成功。
接下来再试试PlayWalk
显然,Walk动画并不在字典中,所以播放失败了。要在运行时动态注册动画,只需要像脚本中的InitializeWalkState()
方法一样,创建一个包含对应动画剪辑的状态即可。
最后PlayRun
就是直接通过动画剪辑的引用播放动画,并不需要字典中持有该动画的映射。
二、控制动画的速度和时间
接下来我们来实现一个蜘蛛机器人的动画。它只有Move
和WakeUp
两个动画,我们要利用这两个动画实现两种状态间的无缝切换。这就需要对动画的速度和时间进行控制。
首先创建一个Spider Bot
脚本。对于机器人当前的状态,我们可以通过对外界暴露一个IsMoving
属性来进行控制。
public class SpiderBot : MonoBehaviour
{
private bool _isMoving;
public bool IsMoving
{
get => _isMoving;
set => _isMoving = value;
}
}
然后在Toggle
的值改变事件中绑定这个属性,就可以通过UI来控制机器人的状态了。
接下来,我们要让机器人一开始就处于Sleep状态。这可以通过反向播放WakeUp
动画实现。因为WakeUp
动画并不是循环的,所以如果动画的Time
达到0时,将会一直保持第一帧的姿势。一种简单的实现方式是直接把动画的Speed
设置为-1。
public AnimancerComponent animancer;
public ClipTransition wakeUp;
public ClipTransition move;
private void Awake()
{
var state = animancer.Play(wakeUp);
state.Speed = -1;
}
但这种方式存在一些问题:首先,动画的Time
会变成负数;其次,虽然实际上没有播放动画,但动画仍然会在每一帧进行计算,这会造成性能上的浪费。除此之外,将Speed
设置为0或将IsPlaying
设置为false,都无法避免类似的性能消耗。
一种更好的方式是直接暂停整个Playable Graph
,这样就可以避免无意义的计算了。
private void Awake()
{
animancer.Play(wakeUp);
// 暂停整个图
animancer.Playable.PauseGraph();
// 计算第一帧
animancer.Evaluate();
}
接下来就是在IsMoving
的值改变时,进行动画的切换了。定义两个方法WakeUp()
和GoToSleep()
,在IsMoving
发生变化时调用
public bool IsMoving
{
get => _isMoving;
set
{
if (value)
WakeUp();
else
GoToSleep();
}
}
在WakeUp()
方法中,我们需要播放WakeUp
动画,将其速度设置为1,并解除暂停Playable Graph
private void WakeUp()
{
if(_isMoving) return;
_isMoving = true;
var state = animancer.Play(wakeUp);
state.Speed = 1;
animancer.Playable.UnpauseGraph();
}
在WakeUp
播放结束后,我们需要播放Move
动画,可以通过添加结束事件实现(注意需要区分是唤醒还是睡眠)
private void Awake()
{
// ...
wakeUp.Events.OnEnd = OnWakeUpEnd;
}
private void OnWakeUpEnd()
{
// 速度大于0是唤醒
if (wakeUp.State.Speed > 0)
{
animancer.Play(move);
}
// 否则是睡眠
else
{
animancer.Playable.PauseGraph();
}
}
在GoToSleep()
方法中,我们需要反向播放WakeUp
动画。这里需要注意,WakeUp
动画在播放结束时NormalizedTime
等于1,但向Move
过渡时,NormalizedTime
仍会继续增长并超过1。这意味着如果我们反转动画,NormalizedTime
也需要花时间回到1,并在达到1时触发OnEnd
事件,导致Playable Graph
被暂停。此时角色会卡在一个奇怪的姿势。所以我们需要将NormalizedTime
手动赋值为1。
private void GoToSleep()
{
if(!_isMoving) return;
_isMoving = false;
var state = animancer.Play(wakeUp);
state.Speed = -1;
if (state.Weight == 0 || state.NormalizedTime > 1)
{
state.NormalizedTime = 1;
}
}
最后来看下效果
三、更新频率
为了节省性能,在某些情况下,当角色离相机较远时,我们希望降低动画的更新频率。Animancer也可以很轻松地实现这点,只需要在Evaluate()
方法的参数中传入一个时间差x,就可以计算出距离上次更新x秒后动画的动作,并更新到角色身上。
首先创建一个脚本LowUpdateRate
,并编写如下代码
public class LowUpdateRate : MonoBehaviour
{
public AnimancerComponent animancer;
// 更新速率
public float updatesPerSecond = 10;
// 上次更新时间
private float _lastUpdateTime;
private void OnEnable()
{
animancer.Playable.PauseGraph();
_lastUpdateTime = Time.time;
}
private void OnDisable()
{
if (animancer != null && animancer.IsPlayableInitialized)
animancer.Playable.UnpauseGraph();
}
private void Update()
{
var time = Time.time;
// 计算距离上次更新的时间差
var timeSinceLastUpdate = time - _lastUpdateTime;
// 如果时间差超过了更新速率则更新动画
if (timeSinceLastUpdate > 1 / updatesPerSecond)
{
animancer.Evaluate(timeSinceLastUpdate);
_lastUpdateTime = time;
}
}
}
可以给角色挂载NamedAnimancerComponent
,并指定一个默认播放动画。运行一下看看效果
左侧是正常播放的机器人,右侧是低速率动画的机器人。可以看出左侧机器人的动画要更丝滑一些(受录制帧率的限制可能不明显)。
下面来实现根据摄像机距离动态启用/禁用LowUpdateRate
的脚本。创建一个脚本DynamicUpdateRate
,并编写如下代码
public class DynamicUpdateRate : MonoBehaviour
{
public LowUpdateRate lowUpdateRate;
public float slowUpdateDistance = 5;
private Transform _camera;
private void Awake()
{
_camera = Camera.main.transform;
}
private void Update()
{
// 计算相机与角色的距离
var offset = _camera.position - transform.position;
var squaredDistance = offset.sqrMagnitude;
// 根据距离启用/禁用LowUpdateRate脚本
lowUpdateRate.enabled = squaredDistance > slowUpdateDistance * slowUpdateDistance;
}
}
将挂载LowUpdateRate
的机器人复制一份,并挂载上面这个脚本。运行游戏看下效果
最左侧是正播放的机器人,中间是始终低速率播放动画的机器人,最右侧是应用了动态控制动画速率的机器人。
四、独播动画
有些物体只有一个动画,且不需要任何控制播放的逻辑,那就不需要前面那样复杂的操作,我们只需要挂载一个SoloAnimation
组件即可实现。
如图所示,我们只需要指定一个动画片段,它就能在游戏运行时自动进行播放。
五、参考资料
[1]. https://kybernetik.com.au/animancer/docs/