一、根运动
Animancer的根运动系统与原生的工作原理完全相同,但我们可以通过继承Transition
类型或实现ITransition
接口,来将额外的数据与动画绑定,从而更方便地控制根运动。
在下面这个示例中,我们通过自定义的Transition
类实现动画根运动的灵活控制。
首先创建一个脚本RootMotion
,并编写如下代码
// 自定义Transition类,将是否启用根运动封装
[Serializable]
public class MotionTransition:ClipTransition
{
[SerializeField,Tooltip("是否启用根运动")]
private bool applyRootMotion;
// 在Play()时调用
public override void Apply(AnimancerState state)
{
base.Apply(state);
state.Root.Component.Animator.applyRootMotion = applyRootMotion;
}
}
public class RootMotion : MonoBehaviour
{
[SerializeField]
private MotionTransition[] animations;
[SerializeField]
private AnimancerComponent animancer;
private void OnEnable()
{
// 启用时播放第一段动画
Play(0);
}
/// <summary>
/// 播放指定动画
/// </summary>
/// <param name="index"></param>
public void Play(int index)
{
animancer.Play(animations[index]);
}
}
接下来给角色挂载这个脚本并指定两个动画,一个动画启用根运动,另一个不启用
再通过UI动态控制角色播放的动画。看下效果
当然,在Animancer中我们也可以通过OnAnimatorMove
方法来控制根运动
[SerializeField] private Rigidbody rigid;
private void OnAnimatorMove()
{
rigid.MovePosition(rigid.position+animancer.Animator.deltaPosition);
rigid.MoveRotation(rigid.rotation*animancer.Animator.deltaRotation);
}
二、线性混合
通过Animancer,我们可以很方便地控制动画状态机进行播放。通过ControllerTransition
可以接收一个动画状态机,并通过Animancer进行播放
public class LinearBlendTreeLocomotion : MonoBehaviour
{
[SerializeField] private AnimancerComponent animancer;
[SerializeField] private ControllerTransition controller;
private void OnEnable()
{
animancer.Play(controller);
}
}
但动画状态机中可能存在许多变量,并通过这些变量控制状态的切换或混合。比如下面的混合树
对于这种情况,Animancer提供了封装好参数的ControllerTransition
类型。上面的混合树只需要一个float类型的参数Speed
,所以我们可以直接通过如下方式对参数进行控制
public class LinearBlendTreeLocomotion : MonoBehaviour
{
[SerializeField] private AnimancerComponent animancer;
[SerializeField] private Float1ControllerTransition controller;
private void OnEnable()
{
animancer.Play(controller);
}
// 设置Speed参数
public float Speed
{
set => controller.State.Parameter = value;
get => controller.State.Parameter;
}
}
在Inspector面板中可以对参数进行绑定
接下来,我们可以通过一个Slider对Speed
属性进行控制,效果如下
如果有许多角色应用了相同的动画状态机和参数,我们也不必为每个角色单独设置。只需要将前面的Float1ControllerTransition
更改为Float1ControllerTransitionAsset.UnShared
类型,就可以直接指定一个配置文件。
右键「Create -> Animancer -> Controller Transition -> Float 1」创建一个配置文件,然后对其中的属性赋值。
接下来就可以复用这个配置文件了,将其拖拽到相应的位置即可。
也可以抛弃动画状态机,完全使用Animancer封装好的混合树(在Animancer中叫混合器)。混合器本质上就是在运行时构建的混合树。它的使用方法如下
public class LinearBlendTreeLocomotion : MonoBehaviour
{
[SerializeField] private AnimancerComponent animancer;
[SerializeField] private LinearMixerTransition controller;
private void OnEnable()
{
animancer.Play(controller);
}
// 设置权重
public float Speed
{
set => controller.State.Parameter = value;
get => controller.State.Parameter;
}
}
只是将前面代码中的ControllerTransition
类型换成了LinearMixerTransition
。通过改变其中的参数可以控制动画的权重,从而实现混合效果。接下来只需要在Inspector面板中指定动画即可。注意:对于Idle这种动画应该关闭Sync选项。Sync是为了同步每个状态的时间,以解决两个状态混合时动作错位的问题(比如walk动画左脚迈出,run动画右脚迈出,此时两个动画混合会产生问题)。但一般情况下Idle动画会比walk或run动画时间长的多,此时如果开启时间同步,就会导致walk动画播放速度慢很多。
演示效果与前面相同,这里不再赘述。
三、定向混合
除了前面的1D混合树效果,Animancer也可以通过MixerTransition2D
实现2D混合树的效果。
下面我们来通过MixerTransition2D
实现一个机器人跟随鼠标移动的效果。首先将之前写过的SpiderBot
脚本拿过来,将move
的类型替换为MixerTransition2D
public class SpiderBot2 : MonoBehaviour
{
public AnimancerComponent animancer;
public ClipTransition wakeUp;
// 将原本的ClipTransition替换为MixerTransition2D
public MixerTransition2D move;
private bool _isMoving;
public bool IsMoving
{
get => _isMoving;
set
{
if (value)
WakeUp();
else
GoToSleep();
}
}
/// <summary>
/// 睡眠
/// </summary>
private void GoToSleep()
{
if (!_isMoving) return;
_isMoving = false;
// 反向播放Wake Up动画
var state = animancer.Play(wakeUp);
state.Speed = -1;
if (state.Weight == 0 || state.NormalizedTime > 1)
{
state.NormalizedTime = 1;
}
}
/// <summary>
/// 唤醒
/// </summary>
private void WakeUp()
{
if (_isMoving) return;
_isMoving = true;
// 正向播放Wake Up动画
var state = animancer.Play(wakeUp);
state.Speed = 1;
animancer.Playable.UnpauseGraph();
}
private void Awake()
{
animancer.Play(wakeUp);
// 暂停整个图
animancer.Playable.PauseGraph();
// 计算第一帧
animancer.Evaluate();
wakeUp.Events.OnEnd = OnWakeUpEnd;
}
private void OnWakeUpEnd()
{
// 速度大于0时唤醒
if (wakeUp.State.Speed > 0)
{
animancer.Play(move);
}
// 否则是睡眠
else
{
animancer.Playable.PauseGraph();
}
}
}
将这个脚本挂载到机器人身上,即可在Inspector面板中看到2D混合树界面
接下来编写机器人的移动脚本SpiderBotController
public class SpiderBotController : MonoBehaviour
{
[SerializeField] private SpiderBot2 spider;
[SerializeField] private Rigidbody body;
// 移动速度
[SerializeField] private float moveSpeed;
// 旋转速度
[SerializeField] private float rotateSpeed;
// 加速倍率
[SerializeField] private float sprintMultiplier;
// 2D混合器
private MixerState<Vector2> _moveState;
// 移动方向
private Vector3 _moveDir;
private void Awake()
{
// 获取混合器
var state = spider.animancer.States.GetOrCreate(spider.move);
_moveState = (MixerState<Vector2>) state;
}
/// <summary>
/// 获取移动方向
/// </summary>
/// <returns></returns>
private Vector3 GetMoveDir()
{
// 射线检测
var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (!Physics.Raycast(ray, out RaycastHit hit))
return default;
// 移动方向
var dir = hit.point - transform.position;
dir.y = 0;
// 一帧移动的距离
var movementThisFrame = moveSpeed * sprintMultiplier * Time.fixedDeltaTime;
var distance = dir.magnitude;
// 目标距离小于一帧移动的距离,则返回zero
if (distance <= movementThisFrame)
return default;
return dir.normalized;
}
private void Update()
{
_moveDir = GetMoveDir();
spider.IsMoving = _moveDir != default;
if (_moveState.IsActive)
{
// 旋转
var eulerAngles = transform.eulerAngles;
var targetEulerY = Camera.main.transform.eulerAngles.y;
eulerAngles.y = Mathf.MoveTowardsAngle(eulerAngles.y, targetEulerY, rotateSpeed * Time.deltaTime);
transform.eulerAngles = eulerAngles;
// 设置混合器参数
_moveState.Parameter = new Vector2(
Vector3.Dot(transform.right, _moveDir),
Vector3.Dot(transform.forward, _moveDir));
// 鼠标左键按下奔跑
var isSprinting = Input.GetMouseButton(0);
_moveState.Speed = isSprinting ? sprintMultiplier : 1;
}
else
{
// 混合器未激活,将速度和参数归零
_moveState.Parameter = default;
_moveState.Speed = 0;
}
}
private void FixedUpdate()
{
// 利用刚体移动
body.velocity = _moveState.Speed * moveSpeed * _moveDir;
}
}
看下效果
四、参考资料
[1]. https://kybernetik.com.au/animancer/docs/examples/locomotion/