在很多实际场景中,我们经常需要根据特定的事件(比如玩家输入,敌人受到攻击等)来播放不同的动画。这需要我们了解一下Animator,Animator Controller和基础的动画状态机。

创建一个开门的动画

首先我们来创建一个简单的开门动画,示例中的门的模型来自官方教程。其实我们也可以用一个简单的Cube调整一下做成门的形状来做。无论是哪种方式,我们首先需要检查一下这个游戏物体的pivot point。一个pivot point是物体进行移动的基准点。对于门来说,这个点非常重要。如果pivot点设置不对,则开门动画会看起来非常奇怪(比如门沿着自身中轴线旋转,当然,如果你是要制作旋转门动画就另说,示例中是一个普通的门)。

1. 首先,选中门对应的游戏物体。此时Unity编辑器中会出现移动widget,如果这个widget的位置处于游戏物体正中心,如下图:

2. 我们点击Toggle Tool Handle Position按钮(上图左上角红框处),将它从Center改为Pivot

3. 在Animation编辑器中,点击Create按钮,创建一个新的动画片段,我们将其命名为Door_Open并保存

4. 在Animation编辑器中,点击Record,然后点击Add Property,添加Transform -> Rotation属性

5. 将Playhead移动到1:00处(第二个关键帧),然后调整一下门的旋转角度,将门的Inspector里的Transform -> Rotation里的Y改成90

6. 预览一下开门动画,没问题后再次点击Record退出Recording模式。

7. 在工程文件窗口中,找到刚刚创建的Door_Open动画文件,点击后,在Inspector窗口中,禁用掉Loop Time。禁用这个选项后动画只会播放一次,而不是无限循环。

Animator Controller

再次选择Door游戏物体,在Inspector中我们可以看到出现了一个新的Animator组件。对于一个选中的游戏物体,当我们第一次为其创建动画片段时,Unity会自动为它创建一个Animator组件。

Animator负责为游戏物体分配动画。但它并不控制实际的动画片段,这个任务交给了Animator Controller,这个也是在第一个动画片段创建时自动创建的。在Animator组件中的Controller这一栏中,可以看到有一个名为Door的Animator Controller(在保存动画的目录中,一般为Animations目录,下面也有一个同样名字的Door文件)。

1. 在工程文件窗口中,选中Door这个文件,将其重命名为Door_Controller

2. 双击这个文件,打开Animator窗口

3. 为了方便演示,将Animator窗口拖动到工程窗口处

4. 在场景中选中Door游戏物体,然后点击Play进入游戏模式

当场景进入Play模式后,可以看到门被打开了,同时,我们会看到Animator窗口中的Door_Open动画下方有一个蓝色的进度条开始慢慢填满,这表示Door_Open动画片段的播放过程。Animator窗口中会实时地显示当前正在播放的动画片段以及动画片段播放进度。

动画状态机

Animation Controller运行在一种叫做状态机(State Machine)的特殊系统上。状态机会跟踪一个物体所能执行的所有可能的动作,并且根据当前的情况(状态,state)选择合适的动作。对于当前状态的控制逻辑,一般是通过脚本来控制。状态机处理的从一个动作转到另一个动作的过程,叫做转换(Transition)。

为了更好地理解状态机是如何工作的,我们以案例里的门为例子,它可以执行的动作有开门和关门两种。

因此门可能处于的状态有:

  • 正在开门过程中(Currently Opening)
  • 正在关门过程中(Currently Closing)
  • 打开状态(Currently Open)
  • 关闭状态(Currently Close)

接下来我们画出这些状态之间的关系:

门的默认状态处于关闭状态。当玩家足够靠近门时,状态机会切换到正在开门的状态,然后会切换到打开状态。在玩家离开之前,门会一直处于打开状态。玩家离开后,门会进入正在关闭状态,然后切换到关闭状态。此时状态机又回到了初始状态。

基于此,我们可以看到有两种转换(transitions)需要一些逻辑输入(检测玩家靠近和远离门),有两种状态会自动转换到下一个状态(正在开门转换到开门,正在关门转换到关门)。

创建默认动画状态

前面小节中,我们创建的Door_Open动画,只要场景开始运行就会自动播放。这不是我们想要的效果,造成这个问题的原因是,当前只有这个动画和门关联,在添加这个动画时,Unity的Animation Controller自动将其设置为了默认状态。接下来我们来解决一下这个问题。

1. 在Animator的工作区中(背景是网格,包含了各种状态的区域),点击右键并选择Create State -> Empty,创建一个空状态

2. 选择新创建的节点,在Inspector面板中,将它的名字从New State改为Closed

3. 右键点击Closed状态,然后选择“Set as Layer Default State”

我们可以看到,现在Closed状态变成了橙色,并且Entry节点指向了它。Entry节点和Closed节点之间的橙色线表示了一个转换(transition)。Entry节点是状态机开始运行时所处的第一个节点,在场景开始运行的时候会自动运行Entry节点。

4. 再次点击运行按钮,看看门的行为有什么差异。

实现开门状态转换

下一步,我们来实现检测玩家并切换到开门动画的功能。要检测玩家是否靠近到门,通常的逻辑是通过一个Trigger碰撞器加上脚本来实现。我们先完成状态机部分的设置,增加脚本要使用的引用变量即可。

1. 右键点击Closed节点,选择Make Transition。此时有一个白色的箭头会出现在光标处。

2. 选择Door_Open节点,让这个箭头和它连接起来。

3. 点击运行按钮测试一下效果。

运行过程中,我们可以看到,在Animation Controller中,首先会经过Closed节点。当Closed节点完成后,会立即调到Door_Oepn动画,随后停在了开门动画上。为了避免Closed节点直接调到开门动画节点,我们要在转换条件上附加一些逻辑来做控制。要实现这个控制,我们需要使用状态机参数(Parameters)参数是Animator内部创建的变量,可以在Animator编辑器的左边找到Parameters标签页。

4. 选择Parameters标签页,在Parameteres列表的顶部,点击“+”并且选择Trigger(触发器)

Trigger参数会在它被脚本所修改时立即变化。

5. 将Trigger变量的名字设置为PlayerProximity,注意后面在脚本里引用这个变量时,名字一定不要搞错了。

6. 选中Closed和Door_Open节点中间的带白色箭头线。然后在其Inspector中找到Conditions。

7. 在Condiditons列表底部,选择“+”。由于目前只有一个参数,PlayerProximity会自动加到列表中。

8. 点击运行按钮,运行场景。

9. 在参数列表中,点击PlayerProximity参数旁边的单选按钮,手动触发这个变量。

我们可以看到,当PlayerProximity被修改后,状态切换到了Door_Open并开始播放开门动画。

实现关门状态转换

接下来我们用类似的方法实现关门状态。

1. 在Animator里选中Door_Open节点,然后按Ctrl/Command + d复制一份这个节点。

2. 在Inspector中,将复制的节点的名字修改为Door_Close,并且将它的Speed参数设置为-1,这样会让动画的帧反向播放。

3. 右键点击Door_Open节点,选择Make Transition,然后将Door_Open和Door_Close节点连接起来。

4. 选择这个新建的转换(transition),在Condiditons列表中点击“+”。同样,PlayerProximity会自动出现。

5. 点击运行按钮,运行场景,同样我们使用PlayerProximity旁边的单选按钮,来测试先开门后关门的动作是否正常。

Animator中的Trigger参数在当前转换(从一个状态切换到下一个状态)完成后,会立即失效。因此我们在开门和关门的时候,可以安全地复用这个参数。后面我们会用脚本来操作这个参数。

复位状态机

到目前为止,我们以及实现了通过Animator参数来实现开门和关门的动画播放和相关状态。但状态机最后会停留在关门状态,这个过程只会发生一次。为了让动画每次在PlayerProximity发生变化时还能正常表现,当关门动画播放完毕时,我们要将动画状态机复位到初始状态。我们可以通过使用Exit节点来实现这个功能。当状态转换到Exit节点时,状态机会自动地切换到Entry节点。

1. 右键点击Door_Close节点,选择Make Transition。

2. 点击Exit节点,连接Door_Close节点和Exit节点。

3. 点击运行按钮,运行场景

现在我们可以看到,当Door_Close播放完成后会自动转换到Exit,然后再回到Closed状态。这里我们还会注意到一点,在我们修改了PlayerProximity的值之后到门被打开之间会有点延迟。这是因为默认情况下,在一个状态节点执行转换之前,要先走完整个动画播放时间(Animator里状态节点的进度条可以直观感受到这个时间)。我们可以选择将禁用这个行为,让切换到Door_Open的转换立即发生。

4. 选择Closed和Door_Open节点之间的转换(transition),在Inspector中,禁用掉Has Exit Time。

6. 重新运行场景,看看现在的效果。

添加Trigger检测碰撞器,用脚本控制PlayerProximity参数

前面我们通过手动点击PlayerProximity的方式来做状态机的测试。接下来我们用一个Trigger碰撞器和一个脚本来实现对这个参数的动态控制。关于普通的collider和tirgger collider的区别,本笔记不会讨论,请自行参考网上资料。

1. 在Hierarchy中,点击右键选择Create Empty创建一个空游戏物体。

2. 将这个游戏物体命名为Door_Trigger。

3. 选中Door_Trigger,在Inspector中选择Add Component,添加一个Box Collider。

4. 在Box Collider组件中,启用“Is Trigger”。

5. 调整Box Collider的大小,让其能够覆盖到门前后一片空间。

6. 编写DoorTrigger脚本,并且将脚本添加到Door_Trigger游戏物体上。

DoorTrigger脚本代码如下:

代码意思很好懂,OnTriggerEnter/Exit函数中,检测到Player进入/离开Trigger Collider之后,会去修改PlayerProximity参数。

7. 在Door_Trigger的Inspector中,将Door游戏物体拖动到Door Trigger脚本里的Anim属性方框中,这能够将Door的animator和脚本里的Anim参数关联到一起(当然,也可以直接在脚本里通过GetComponent方法获取这个Animator)。

8. 运行场景,控制Player进入和离开Trigger Collider的区域,观察结果(案例中有一个可用键盘控制的人物模型,我们也可以直接在场景中添加一个小球或立方体来代替,通过在Editor中拖动这个物体来实现Trigger Collider的检测)。