在学习根运动前需要了解两个名词:
以上出自Unity官方文档,看完还是一脸懵逼。。。举个简单的例子来说:假如现在角色有向前走动的动画,如果是身体变换,就是角色的模型在走动,但角色在世界中的位置并没有变化;如果是根变换,那么角色在模型上的移动就会反映到根节点上,也就是说角色不仅模型在走动,在世界上的位置也在移动。
可以看到,上方的角色因为没有采用根变换,其位置始终没有改变,只是在重复播放模型的动画。
那么如何开启或关闭根运动呢?这里涉及到两个选项。
首先在动画剪辑面板上,如果动画能够影响角色的位置或旋转,一般会有如下选项
这里面有个属性叫做「Bake Into Pose」,意思是将方向保持在身体变换上。也就是说,如果勾选这个属性,就是将根变换存放在动画中,即使用身体变换。
除此之外,还有在角色身上挂载的「Animator」组件中,有个「Apply Root Motion」选项。只有在开启这个选项时,根变换才会应用到模型身上。
这两个选项不同的排列组合也会对动画产生不同的影响。
「Bake Into Pose」开启,「Apply Root Motion」关闭或开启
只要「Bake Into Pose」选项开启,动画就使用了身体变换。就会出现如下效果
「Bake Into Pose」关闭,「Apply Root Motion」关闭
此时因为使用了根变换,但不允许应用,所以角色会原地踏步
在某些情况下,我们希望一部分状态启用根运动,另一部分关闭根运动。此时就可以使用脚本控制根运动的发生。
在脚本中添加OnAnimatorMove()
生命周期函数,就会发现「Animator」组件的「Apply Root Motion」变为了「Handled By Script」。意味着根运动已被脚本接管
接下来我们只需要在OnAnimatorMove()
中实现我们的控制逻辑即可。
比如我们希望由根运动控制角色的位置。但在跳跃时,由于角色的根节点位置只存在Y轴方向的变换,就会造成只能原地起跳的问题。这种情况下就可以在这个函数中对当前状态进行判断。如果当前是跳跃状态,就直接通过代码控制角色的位置。
private void OnAnimatorMove()
{
// 如果当前状态的标签不是"NoRootMotion",则由根运动控制角色位置
if (!_animator.GetCurrentAnimatorStateInfo(0).IsTag("NoRootMotion"))
{
_noRootMotion = false;
_parent.position += _animator.deltaPosition;
_parent.rotation *= _animator.deltaRotation;
}
// 否则由其他代码控制
else
{
_noRootMotion = true;
}
}
看下效果
当我们的角色需要与其他角色或物体互动时,由于位置的原因,可能会出现严重的穿模现象。比如下方的踢腿动作
我们更希望在踢腿时,能够恰好踢中对方的某个位置,且不应该直接穿过对方的身体。这时就可以用到目标匹配。
简单来说,目标匹配实际上就是Animator
类中的MatchTarget()
方法。它需要传入如下几个参数:
Vector3 matchPosition
:目标位置Quaternion matchRotation
:目标旋转AvatarTarget targetBodyPart
:自身需要匹配的部位MatchTargetWeightMask weightMask
:位置和旋转的权重float startNormalizedTime
:动画开始百分比(0~1)float targetNormalizedTime
:动画结束百分比(0~1)bool completeMatch
:函数中断时是否强制移动到匹配位置我们可以将目标匹配的代码放在Update()
中,当动画状态机进入到踢腿的状态时执行
if (_animator.GetCurrentAnimatorStateInfo(0).IsName("Kick"))
{
_animator.MatchTarget(machTarget.position,transform.rotation,
AvatarTarget.LeftFoot,new MatchTargetWeightMask(Vector3.one,1 ),
0f,0.64f);
}
效果如下。可以看到这个方法会强制将角色的左脚匹配到目标点上,即便两者距离很远,也会直接位移到目标点前。
动画事件可以让我们在动画执行的过程中触发指定的脚本方法。可以在制作技能等场景时派上用场。
它使用起来也非常简单,打开角色的「Animation」面板。选择要添加事件的动画剪辑,然后点击右侧的「Add Event」按钮,就可以在时间轴上添加一个事件
如果直接选中动画剪辑文件,再打开「Animation」面板,选中之前添加的动画事件。就会发现检视面板多出来几个属性
也就是说我们可以为触发的方法添加参数,并在这里指定。
private void Shoot(int param)
{
Debug.Log("Shoot:"+param);
}
不过这种方式只能传递一个参数,我们并不能添加多个参数来接收面板上所有指定好的参数。不过Unity为我们提供了AnimationEvent
类来封装这些传入的参数。通过它就可以接收到所有传入的参数
private void Shoot(AnimationEvent param)
{
Debug.Log("Shoot:"+param.intParameter);
Debug.Log("Shoot:"+param.floatParameter);
Debug.Log("Shoot:"+param.stringParameter);
Debug.Log("Shoot:"+param.objectReferenceParameter);
Debug.Log("Shoot:"+param.functionName);
}
有了动画事件,我们就可以在动画播放过程中的适当的时机,触发一些指定的效果,比如在拉完弓箭时射出一枚箭矢、抬手后释放技能等。
Unity允许我们给动画状态机中的单个状态挂载独立的脚本,以在动画播放时处理额外的逻辑。具体的方法是:
首先选中动画状态机中的状态或子状态机,然后在检视面板中会出现「Add Behaviour」按钮。然后就可以手动创建脚本并进行挂载。
打开脚本可以发现,该类自动继承了StateMachineBehaviour
类,并用注释的形式给出了一系列生命周期函数。
接下来我们通过这种方式,实现「在角色攻击时不允许移动」的效果
首先在角色控制器中添加是否允许移动的判断条件,并在移动方法中进行判断
public bool CanMove = true;
public void Move()
{
if(!CanMove)
return;
// ...
}
然后在状态机行为脚本中,对条件进行控制
public class CharacterBehaviourController : StateMachineBehaviour
{
private SaCharacterController _controller;
public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
_controller = animator.gameObject.GetComponent<SaCharacterController>();
_controller.CanMove = false;
}
public override void OnStateMachineExit(Animator animator, int stateMachinePathHash)
{
_controller.CanMove = true;
}
}
看下使用状态机行为前的效果
再看下使用之后的效果