Unity动画系统学习笔记(一)动画剪辑与状态机

一、动画系统工作流

一个完整的动画系统工作流包含如下几个部分:

  • 动画剪辑(Animation Clips):包含某些对象如何随时间更改其位置、旋转或其他属性的信息。
  • 状态机(Animator Controller):跟踪当前正在播放的动画剪辑,以及当动画剪辑应该改变或混合在一起时的状态信息。
  • 骨骼(Avatar):用来映射人形角色的一种通用内部格式。通过骨骼可以将外部的人形动画重定向到我们自己的角色模型中。
  • 动画组件(Animator):动画剪辑、状态机、骨骼一同通过动画组件附加到某个游戏物体上。

二、动画剪辑

如果学习过视频剪辑或动画制作相关的知识,应该对它再熟悉不过了。简单来讲,就是在时间轴上打上一个个关键帧,并改变每个关键帧时物体的属性。然后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);

3.1 混合树

在某些情况下,我们需要动画之间进行平滑的过渡,而不是从一个状态直接切换到另一个状态。比如处在走路状态时,要切换到跑步状态,就需要一个加速的过程。这种效果可以通过混合树实现。

3.1.1 1D混合

对于从走路到跑步的动画切换,可以通过一个简单的1D混合树来实现。
首先在状态机中创建一个混合树,然后双击打开

可以发现,混合树自动创建了一个参数,且混合类型默认是1D混合。

我们需要根据速度来进行走路到奔跑的切换,所以将参数名改为Speed。然后将行走和奔跑的动画剪辑添加进来

然后在Idle状态和混合树之间进行连线,如果Speed大于0,则进入混合树,否则维持在Idle状态。这里为了防止精度问题,将阈值设置为0.1。运行游戏看下效果

PS:如果从外部导入的动画有如下这种报错的话,可以将动画剪辑中的Events事件删除。

3.1.2 2D混合

当我们的动画混合比较复杂,一个参数已经无法满足需求时,就可以采用2D混合。2D混合具有如下几种类型

  • 2D Simple Directional:这种混合模式适用于不同方向动画的混合,比如前进、后退、向左、向右。但在同一方向上有多个动画,如行走和奔跑,则不建议用这种模式
  • 2D Freedom Directional:这种混合模式适用于存在多个相同方向动画的情况,需要有一个原点(比如Idle)。
  • 2D Freedom Cartesian:这种混合模式适用于两个参数类型不同的动画,比如角速度和线速度。

下面我们来使用「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);

看下效果

3.2 子状态机

游戏中一个角色的动画状态机内可能会包含数十个动画剪辑,如果这些动画剪辑都堆在同一个界面中,势必会造成状态机的混乱和难以维护。为了解决这一问题,我们可以使用子状态机。它可以将一组相关的动画提取出来,放入同一个状态机内。

接下来我们尝试将跳跃动画放入一个子状态机中。首先创建一个子状态机,命名为Jump,然后双击进入。

可以看到,子状态机与普通的状态机几乎相同,唯一的区别在于多了一个回到上层的入口((Up)Base Layer)。

接下来导入跳跃的动画。一般跳跃动画有三个,分别是起跳、降落、落地。因为在跳跃过程中,落地的时机是不确定的,因此我们将Land设置为当前状态机的默认状态,且从Fall到Land的切换不需要等待动画片段播放完成。添加一个isLand参数,用来判断当前是否落地。如果落地,则执行从Fall到Land状态的切换。

然后在代码中通过Animator.CrossFade()方法触发该状态机。该方法需要传入子状态机名称和过渡时间。

_animator.CrossFade("Jump",0.1f);

看下效果

可以发现落地后会暂停一会儿才会切换到奔跑动作,这是因为落地动画播放完成后,状态先转换到Idle,然后再转换到Move导致的。我们可以将Land直接连接到上层的Idle和Move,使其能够快速切换到相应的状态。

看下效果

3.3 重写动画控制器

我们在前面完成了一套基础的动画状态控制器,但假如我们的角色要换一个职业,该职业有着相同的动画状态,但却有不同的动画剪辑,难道我们需要重新复制一份动画控制器吗?显然不是,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;
	}
}

猜你喜欢

转载自blog.csdn.net/LWR_Shadow/article/details/127180451
今日推荐