赞
踩
一个完整的动画系统工作流包含如下几个部分:
如果学习过视频剪辑或动画制作相关的知识,应该对它再熟悉不过了。简单来讲,就是在时间轴上打上一个个关键帧,并改变每个关键帧时物体的属性。然后Unity就会自动生成关键帧之间的形状、动作补间,使物体具有连贯、流畅的动画。
选中一个物体,按「Ctrl+6」打开「Animation」面板,为其创建一个动画剪辑。
创建完成后,点击「Add Property」按钮,就可以添加你想要改变的属性
添加属性后,在时间轴上打上对应的关键帧,并改变属性的数值,就可以形成一段连续的动画
点击左上角的录制模式,我们就可以直接在场景中调整物体的各项属性,Animation会自动添加关键帧并记录下改变后的数值。
点击左下角的「Curves」可以进入动画曲线界面
在给物体创建完动画剪辑后,在目录中会自动生成一个「Animator Controller」,这个文件就是动画状态机。通过动画状态机,我们可以控制模型在各个动画状态之间进行切换。
现在来尝试实现实现一个让角色从待机状态切换到死亡状态的动画。首先将两个动画剪辑导入到状态机,然后将待机状态设置为当前层级的默认状态
再创建一条从待机状态到死亡状态的切换
添加一个参数「isDead」,并设置为转换条件
将状态机挂载到主角身上的「Animator」组件中,运行游戏。通过手动控制「isDead」参数观察效果。
要通过代码控制参数的改变也很简单
_animator.SetBool("isDead",true);
或
private static readonly int IsDead = Animator.StringToHash("isDead");
// ...
_animator.SetBool(IsDead,true);
在某些情况下,我们需要动画之间进行平滑的过渡,而不是从一个状态直接切换到另一个状态。比如处在走路状态时,要切换到跑步状态,就需要一个加速的过程。这种效果可以通过混合树实现。
对于从走路到跑步的动画切换,可以通过一个简单的1D混合树来实现。
首先在状态机中创建一个混合树,然后双击打开
可以发现,混合树自动创建了一个参数,且混合类型默认是1D混合。
我们需要根据速度来进行走路到奔跑的切换,所以将参数名改为Speed。然后将行走和奔跑的动画剪辑添加进来
然后在Idle状态和混合树之间进行连线,如果Speed大于0,则进入混合树,否则维持在Idle状态。这里为了防止精度问题,将阈值设置为0.1。运行游戏看下效果
PS:如果从外部导入的动画有如下这种报错的话,可以将动画剪辑中的Events事件删除。
当我们的动画混合比较复杂,一个参数已经无法满足需求时,就可以采用2D混合。2D混合具有如下几种类型
下面我们来使用「2D Freedom Directional」模式来制作角色完整的移动效果。首先增加两个控制参数「Horizontal」和「Vertical」用来控制水平方向的动画和垂直方向的动画。
然后将各个方向的动画添加到混合树中,根据方向设置「Horizontal」和「Vertical」的值
挪动中间的红点,可以预览混合的效果
然后在代码中根据输入,设置「Horizontal」和「Vertical」参数
_horizontal = Input.GetAxis("Horizontal");
_vertical = Input.GetAxis("Vertical");
_animator.SetFloat("Horizontal",_horizontal);
_animator.SetFloat("Vertical",_vertical);
看下效果
游戏中一个角色的动画状态机内可能会包含数十个动画剪辑,如果这些动画剪辑都堆在同一个界面中,势必会造成状态机的混乱和难以维护。为了解决这一问题,我们可以使用子状态机。它可以将一组相关的动画提取出来,放入同一个状态机内。
接下来我们尝试将跳跃动画放入一个子状态机中。首先创建一个子状态机,命名为Jump,然后双击进入。
可以看到,子状态机与普通的状态机几乎相同,唯一的区别在于多了一个回到上层的入口((Up)Base Layer)。
接下来导入跳跃的动画。一般跳跃动画有三个,分别是起跳、降落、落地。因为在跳跃过程中,落地的时机是不确定的,因此我们将Land设置为当前状态机的默认状态,且从Fall到Land的切换不需要等待动画片段播放完成。添加一个isLand
参数,用来判断当前是否落地。如果落地,则执行从Fall到Land状态的切换。
然后在代码中通过Animator.CrossFade()
方法触发该状态机。该方法需要传入子状态机名称和过渡时间。
_animator.CrossFade("Jump",0.1f);
看下效果
可以发现落地后会暂停一会儿才会切换到奔跑动作,这是因为落地动画播放完成后,状态先转换到Idle,然后再转换到Move导致的。我们可以将Land直接连接到上层的Idle和Move,使其能够快速切换到相应的状态。
看下效果
我们在前面完成了一套基础的动画状态控制器,但假如我们的角色要换一个职业,该职业有着相同的动画状态,但却有不同的动画剪辑,难道我们需要重新复制一份动画控制器吗?显然不是,Unity为我们提供了重写动画控制器的选项。
在工程目录中点击右键「Create -> Animator Override Controller」就可以创建一个重写动画控制器。
然后将原本的动画控制器拖入,即可识别出所有的动画状态,我们只需要把对应的动画剪辑拖入即可。如果没有指定新的动画剪辑,则会播放原本的动画控制器对应的动画。
指定完动画剪辑后,将重写的控制器挂载到角色身上,看下效果
角色控制器
public class PlayerController : MonoBehaviour { public float MoveSpeed = 5f; public float RotateSpeed = 40f; public float JumpScale = 10f; private Rigidbody _rigidbody; private TriggerCheck _groundCheck; private Animator _animator; private float _horizontal; private float _vertical; private static readonly int Speed = Animator.StringToHash("Speed"); private static readonly int IsLand = Animator.StringToHash("isLand"); private void Awake() { _rigidbody = GetComponent<Rigidbody>(); _groundCheck = transform.Find("GroundCheck").GetComponent<TriggerCheck>(); _animator = GetComponent<Animator>(); } private void Update() { _horizontal = Input.GetAxis("Horizontal"); _vertical = Input.GetAxis("Vertical"); _animator.SetFloat("Horizontal",_horizontal); _animator.SetFloat("Vertical",_vertical); if (Input.GetKeyDown(KeyCode.Space) && _groundCheck.IsTrigger) { _rigidbody.AddForce(Vector3.up*JumpScale); _animator.CrossFade("Jump",0.1f); } _animator.SetBool(IsLand,_groundCheck.IsTrigger); } private void FixedUpdate() { if (_vertical != 0) { _rigidbody.MovePosition(transform.position+transform.forward * (MoveSpeed * Time.fixedDeltaTime * _vertical)); } else if(_horizontal != 0) { _rigidbody.MovePosition(transform.position+transform.right * (MoveSpeed * Time.fixedDeltaTime * _horizontal)); } if (_horizontal != 0 && _vertical != 0) { transform.eulerAngles += Vector3.up * (RotateSpeed * _horizontal * Time.fixedDeltaTime); } _animator.SetFloat(Speed,_vertical); } }
落地触发检测
public class TriggerCheck : MonoBehaviour { private int _count; public bool IsTrigger => _count > 0; public LayerMask TargetLayers; public Action OnTriggered; private void OnTriggerEnter(Collider other) { if (IsTargetLayer(other.gameObject, TargetLayers)) _count++; } private void OnTriggerExit(Collider other) { if (IsTargetLayer(other.gameObject, TargetLayers)) _count--; } private bool IsTargetLayer(GameObject obj, LayerMask targetLayers) { // 根据Layer数值进行位移获得用于运算的Mask值 int objLayerMask = 1 << obj.layer; return (targetLayers.value & objLayerMask) > 0; } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。