前言

  为了提高玩家的游戏体验,我们常常需要在角色运动的时候,根据它的运动状态播放对应的动画。为了能够控制当前播放什么动画,我们首先需要制作动画状态机,再用代码去控制动画状态机切换当前的状态,从而切换当前播放的动画。


制作角色的动画状态机

  我们在给角色制作动画状态机来控制我们之前制作好的角色动画之前,我们需要确定我们有哪些状态,以及控制这些状态转移的条件是什么。

各个角色动画之间的状态转换规则如下:

  1. 当角色不着地时,不播放任何状态
  2. 角色着地时播放Idle播放动画
  3. 当角色着地水平移动时,播放Walk动画
  4. 当角色触发跳跃事件时,播放Jump动画
  5. Jump动画播放完之后,着地播放Idle动画
  6. 当角色触发射击事件时,播放Shoot动画
  7. Shoot动画播放完之后,着地播放Idle动画
  8. 当角色触发死亡事件时,播放Death动画
  9. Death动画播放完之后,自动播放Falling动画

  根据状态转换规则,我们可以得出控制状态转换的所有参数

状态转换参数:

  • Grounded:Bool
  • Speed:Float
  • Jump:Trigger
  • Death:Trigger
  • Shoot:Trigger

  接着,我们在Unity顶部菜单栏选中Window->Animator打开动画状态机编辑窗口,然后在Hierarchy窗口中选中Player编辑角色的动画状态机。在Unity中,一个状态控制一个动画。根据上面状态转换规则,我们还需要创建一个没有任何动画的空状态,并将其作为整个状态机的默认状态。我们在Animator窗口中点击鼠标右键选择Empty创建一个空状态,将其命名为Empty之后,点击鼠标右键编辑这个状态,然后选择Set as Layer Default State将其设置为默认状态。最后,我们根据上面得到的控制状态转换的所有参数来创建状态转换参数,并构建角色的动画状态机如下:

创建状态机
创建状态机

  创建完动画状态机之后,我们为状态之间的转移边设置条件。点击状态之间的转移边,然后在右侧的Inspector面板进行设置。

设置转移边
设置转移边

每条转移边的具体设置如下:

  • Empty -> Idle:
    • Has Exit Time: false
    • Interruption Source”: Next State
    • Conditions: Grounded(true)
  • Idle -> Empty:
    • Has Exit Time: false
    • Conditions: Grounded(false)
  • Idle -> Walk:
    • Has Exit Time: false
    • Conditions: Speed(Greater 0.1)
  • Walk -> Idle:
    • Has Exit Time: false
    • Interruption Source”: Next State
    • Conditions: Speed(Less 0.1)
  • Walk -> Empty:
    • Has Exit Time: false
    • Conditions: Grounded(false)
  • AnyState -> Jump:
    • Has Exit Time: false
    • Can Transition To Self: false
    • Conditions: Jump(Trigger)
  • Jump -> Empty:
    • Has Exit Time: true
    • Interruption Source”: Next State
    • Conditions: List is Empty
  • AnyState -> Shoot:
    • Has Exit Time: false
    • Can Transition To Self: True
    • Conditions: Shoot(Trigger)
  • Shoot -> Empty:
    • Has Exit Time: true
    • Interruption Source”: Current State then Next State
    • Conditions: List is Empty
  • AnyState -> Death:
    • Has Exit Time: false
    • Can Transition To Self: false
    • Conditions: Death(Trigger)
  • Death -> Falling:
    • Has Exit Time: true
    • Conditions: List is Empty

加入控制代码

  角色的动画状态机编辑完成之后,我们还需要使用代码来控制动画状态机的状态转移。打开PlayerController.cs,插入动画控制代码:

PlayerController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(AudioSource))]
[RequireComponent(typeof(Animator))]
public class PlayerController : MonoBehaviour {
[Tooltip("角色初始朝向是否朝向右边")]
public bool FacingRight = true;
[Tooltip("移动时角色加速的力大小")]
public float MoveForce = 365f;
[Tooltip("角色移动的最大速度")]
public float MaxSpeed = 5f;
[Tooltip("跳跃时向上加速的力大小")]
public float JumpForce = 1000f;
[Tooltip("检测角色是否落地")]
public Transform GroundCheck;

[Tooltip("跳跃音效")]
public AudioClip[] JumpClips;

// 记录角色当前是否处于准备跳跃状态
private bool m_IsReadyToJump;
// 记录角色当前是否正处于跳跃状态
private bool m_IsJumping;
// 记录角色当前是否处于着地状态
private bool m_GroundedStatus;

// 组件引用变量
private Rigidbody2D m_Rigidbody2D;
private AudioSource m_AudioSource;
private Animator m_Animator;

private void Awake() {
// 获取组件引用
m_Rigidbody2D = GetComponent<Rigidbody2D>();
m_AudioSource = GetComponent<AudioSource>();
m_Animator = GetComponent<Animator>();
}

private void Start() {
// 监测变量是否正确赋值
if(GroundCheck == null) {
Debug.LogError("请先设置GroundCheck");
}

// 初始化变量
m_IsReadyToJump = false;
m_IsJumping = false;
m_GroundedStatus = false;
}

private void Update() {
// 通过检测角色和groundCheck之间是否存在Ground层的物体来判断当前是否落地
m_GroundedStatus = Physics2D.Linecast(
transform.position,
GroundCheck.position,
LayerMask.GetMask("Obstacle")
);

// 设置动画状态机控制参数
m_Animator.SetBool("Grounded", m_GroundedStatus);

// 着地时,如果当前不处于跳跃状态且按下了跳跃键,进入准备跳跃状态
if(m_GroundedStatus && !m_IsJumping && Input.GetButtonDown("Jump")) {
m_IsReadyToJump = true;
}

// 刚刚落地,退出跳跃状态
if(m_GroundedStatus && m_IsJumping) {
m_IsJumping = false;
}
}

private void FixedUpdate() {
//获取水平输入
float h = Input.GetAxis("Horizontal");

// 设置动画状态机控制参数
m_Animator.SetFloat("Speed", Mathf.Abs(h));

// 若h * m_Rigidbody2D.velocity.x为正数且小于MaxSpeed,表示需要继续加速
// 若h * m_Rigidbody2D.velocity.x为负数,则表示需要反向加速
if(h * m_Rigidbody2D.velocity.x < MaxSpeed) {
m_Rigidbody2D.AddForce(Vector2.right * h * MoveForce);
}

//设置物体速度的阈值
if(Mathf.Abs(m_Rigidbody2D.velocity.x) > MaxSpeed) {
m_Rigidbody2D.velocity = new Vector2(
Mathf.Sign(m_Rigidbody2D.velocity.x) * MaxSpeed,
m_Rigidbody2D.velocity.y
);
}

//判断当前是否需要转向
if(h > 0 && !FacingRight) {
Flip();
}else if(h < 0 && FacingRight) {
Flip();
}

// 跳跃
if(m_IsReadyToJump) {
Jump();
}
}

private void Jump() {
// 进入跳跃状态
m_IsJumping = true;

// 设置一个竖直向上的力
m_Rigidbody2D.AddForce(new Vector2(0f, JumpForce));

// 设置动画状态机控制参数
m_Animator.SetTrigger("Jump");

// 退出准备跳跃状态,避免重复跳跃
m_IsReadyToJump = false;

//随机在角色当前所处的位置播放一个跳跃的音频
if(JumpClips.Length > 0) {
int i = Random.Range(0, JumpClips.Length);
AudioSource.PlayClipAtPoint(JumpClips[i], transform.position);
}
}

private void Flip() {
// 修改当前朝向
FacingRight = !FacingRight;

// 修改scale的x分量实现转向
this.transform.localScale = Vector3.Scale(
new Vector3(-1, 1, 1),
this.transform.localScale
);
}
}


修改动画和状态设置

  点击运行游戏,可以看到角色已经有了动画。但是,我们发现角色跳跃的时候,一直在重复播放Jump动画。这是因为我们在制作动画的时候,为了便于预览,Unity默认将我们的动画设置为循环动画。我们需要修改一下JumpShootDeath这三个动画的动画设置。打开Assets\Animation\Player,分别选中这三个动画,然后取消Loop Time的勾选。

修改动画设置
修改动画设置

  此外,为了让优化动画效果,我们还可以在Animator设置各个状态的动画播放速度。这里,我们将JumpShoot这两个状态的Speed分别设置为2.54

修改状态设置
修改状态设置

  修改完成之后,再次运行游戏,可以看到Jump动画的播放速度变快了。


后言

  至此,我们已经制作好了角色的动画状态机,并使用代码控制IdleWalkJump这三个动画的切换,剩下的动画控制将在后面提到。最后,本篇文章所做的修改,可以在PotatoGloryTutorial这个仓库的essay6分支下看到,读者可以clone这个仓库到本地进行查看。


参考链接

  1. Animator Controller
  2. Animation States
  3. Animation transitions