Unity Animancer插件(二)精确控制

一、通过名称播放动画

前面我们讲的都是直接通过动画片段的引用播放动画,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就是直接通过动画剪辑的引用播放动画,并不需要字典中持有该动画的映射。

二、控制动画的速度和时间

接下来我们来实现一个蜘蛛机器人的动画。它只有MoveWakeUp两个动画,我们要利用这两个动画实现两种状态间的无缝切换。这就需要对动画的速度和时间进行控制。

首先创建一个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/

猜你喜欢

转载自blog.csdn.net/LWR_Shadow/article/details/128247212