Unity3D RPG Core | 13 设置敌人的动画控制器
这节视频完善史莱姆敌人的追击逻辑,并播放相应的动画。
在此之前,我们为史莱姆对象创建预制体,以及更新角色预制体。之前为角色再单独创建一个预制体时,就有疑惑为什么要再创建一个。现在找到原因了,因为在原先预制体上,我们增加了许多组件,比如 Amimator 和 Nav Mesh Agent。创建预制体后,这些添加的组件和属性就也能复用了。
创建预制体的一个原因。
为史莱姆对象创建预制体和之前的操作一样,只需要把现有 Hierarchy 窗体中的史莱姆对象拖拽到 Assets 窗体中就可以了。
更新已有预制体,我们可以在预制体对应的 Inspector 窗体中的 Prefab - Overrides 下,点击 Apply All,更新所有修改。
1. 追击逻辑
如代码清单 1 所示,上一节中我们在发现角色后会将敌人的状态设置为追击状态(第 27 行),现在我们开始完成追击逻辑。
我们改造之前的寻找角色函数,将返回值修改为角色的 GameObject(第 49 至 59 行)。追击的逻辑先简单实现,追击时如果找到角色,则将 NavMeshAgent.destination 设置为角色的位置(第 37 行);如果没找到角色,即脱战了,敌人需要回到原来的位置,这块逻辑留作之后实现。
视频里还实现了一个小细节,敌人追击时候的速度为设置的默认值(第 34 行),当巡逻时速度减慢(第 31 行)。
- [RequireComponent(typeof(NavMeshAgent))]
- public class EnemyController : MonoBehaviour
- {
- public EnemyStates m_enemyStates;
- NavMeshAgent m_navMeshAgent;
- public float m_sightRadius = 5;
- float m_defaultSpeed;
- void Awake()
- {
- m_navMeshAgent = GetComponent<NavMeshAgent>();
- m_defaultSpeed = m_navMeshAgent.speed;
- }
- void SwitchState()
- {
- GameObject attackObject = null;
- switch (m_enemyStates)
- {
- case EnemyStates.GUARD:
- if (FindPlayer() != null)
- {
- Debug.Log("HasFoundPlayer");
- m_enemyStates = EnemyStates.PURSUIT;
- }
- break;
- case EnemyStates.PATROL:
- m_navMeshAgent.speed = m_defaultSpeed / 1.5f;
- break;
- case EnemyStates.PURSUIT:
- m_navMeshAgent.speed = m_defaultSpeed;
- if ((attackObject = FindPlayer()) != null)
- {
- m_navMeshAgent.destination = attackObject.transform.position;
- }
- else
- {
- }
- break;
- case EnemyStates.DEATH:
- break;
- }
- }
- GameObject FindPlayer()
- {
- Collider[] colliders = Physics.OverlapSphere(transform.position, m_sightRadius);
- foreach (Collider collider in colliders)
- {
- if (collider.CompareTag("Player"))
- return collider.gameObject;
- }
- return null;
- }
- }
写完代码后,回到 Unity 窗体中运行验证。可以看到敌人发现角色后,如预期跟着角色,拉开距离后便呆在原地。
2. 设置动画控制器
目前还缺少史莱姆相关的动画,我们为其新建并指定一个动画控制器。首先如图 1 所示,在默认的 Base Layer 下,代表史莱姆守卫和巡逻状态下的动画。我们添加素材中的 IdleNormal 和 WalkFWD 动画,分别代表待机和行走状态。两者之间的状态转移通过 bool 变量控制,这边我们新建 bool 变量 Walking。同时去掉 Has Exit Time、Fixed Duration 的勾选,Transition Duration 设置为 0。

接着如图 2 所示,点击加号按钮,再创建一个新层,命名为 Attack Layer,它代表追击和攻击状态下的动画。层设置界面上,我们将权重设置为 1,融合方式设置为 Override,即新的层和原有的层是覆盖关系。这边我们有三个状态,一个空状态 BaseState、一个战斗待机状态 IdleBattle 以及奔跑 RunFWD。空状态代表其他层的任意状态。BaseState 和 IdleBattle 状态之间的转移通过 bool 变量 Pursuing;IdleBattle 和 RunFWD 状态之间的转移通过 bool 变量 Following。

这边尝试理解层之间的切换逻辑:此例中 Pursuing 和 Walking 是触发层切换的;空的状态代表其他层任意状态。
有一个问题,Base Layer 切到 Attack Layer,Attack Layer 中会有一个空状态。为什么 Attack Layer 切到 Base Layer,Base Layer 中没有空状态。
设置完动画控制器和相关转移变量后,如代码清单 2 所示,我们实现动画的切换逻辑。首先获取史莱姆对象上的 Animator 组件(第 24 行);接着定义对应的动画转移变量,此处定义了 AnimatorState 结构体,转移变量分别对应 isWalking、isPursuing 和 isFollowing(第 11 至 19 行)。
动画切换的逻辑后续可以慢慢实现,因为我们逐帧更新动画对应的变量:在 Update() 中调用 SwitchAnimation() 函数。
我们先实现追击状态下的动画切换逻辑,此时需要切换到 Attack Layr,所以设置 Walking 为 false,Pursuing 为 true。能发现角色时,设置奔跑状态,Following 为 true;否则为待机状态,Following 为 fals。
- [RequireComponent(typeof(NavMeshAgent))]
- public class EnemyController : MonoBehaviour
- {
- public EnemyStates m_enemyStates;
- NavMeshAgent m_navMeshAgent;
- Animator m_animator;
- public float m_sightRadius = 5;
- float m_defaultSpeed;
- struct AnimatorState
- {
- // Base Layer
- public bool isWalking;
- // Attack Layer
- public bool isPursuing;
- public bool isFollowing;
- }
- AnimatorState m_animatorState;
- void Awake()
- {
- m_navMeshAgent = GetComponent<NavMeshAgent>();
- m_animator = GetComponent<Animator>();
- m_defaultSpeed = m_navMeshAgent.speed;
- }
- // Update is called once per frame
- void Update()
- {
- SwitchState();
- SwitchAnimation();
- }
- void SwitchAnimation()
- {
- m_animator.SetBool("Walking", m_animatorState.isWalking);
- m_animator.SetBool("Pursuing", m_animatorState.isPursuing);
- m_animator.SetBool("Following", m_animatorState.isFollowing);
- }
- void SwitchState()
- {
- GameObject attackObject = null;
- switch (m_enemyStates)
- {
- case EnemyStates.GUARD:
- if (FindPlayer() != null)
- {
- Debug.Log("HasFoundPlayer");
- m_enemyStates = EnemyStates.PURSUIT;
- }
- break;
- case EnemyStates.PATROL:
- m_navMeshAgent.speed = m_defaultSpeed / 1.5f;
- break;
- case EnemyStates.PURSUIT:
- m_navMeshAgent.speed = m_defaultSpeed;
- m_animatorState.isWalking = false;
- m_animatorState.isPursuing = true;
- if ((attackObject = FindPlayer()) != null)
- {
- m_animatorState.isFollowing = true;
- m_navMeshAgent.destination = attackObject.transform.position;
- }
- else
- {
- m_animatorState.isFollowing = false;
- }
- break;
- case EnemyStates.DEATH:
- break;
- }
- }
- }
返回 Unity 界面运行查看效果,当史莱姆发现角色后会如预期播放奔跑动画。但是还有一个小问题,脱战后史莱姆还会保持待机状态移动一段距离,效果有延迟。
为了解决上述问题,如代码清单 3 所示,当拉脱战时,将敌人的 NavMeshAgent.destination 设置为当前的坐标点(第 31 行),实现快速停止。
- public class EnemyController : MonoBehaviour
- {
- void SwitchState()
- {
- GameObject attackObject = null;
- switch (m_enemyStates)
- {
- case EnemyStates.GUARD:
- if (FindPlayer() != null)
- {
- Debug.Log("HasFoundPlayer");
- m_enemyStates = EnemyStates.PURSUIT;
- }
- break;
- case EnemyStates.PATROL:
- m_navMeshAgent.speed = m_defaultSpeed / 1.5f;
- break;
- case EnemyStates.PURSUIT:
- m_navMeshAgent.speed = m_defaultSpeed;
- m_animatorState.isWalking = false;
- m_animatorState.isPursuing = true;
- if ((attackObject = FindPlayer()) != null)
- {
- m_animatorState.isFollowing = true;
- m_navMeshAgent.destination = attackObject.transform.position;
- }
- else
- {
- m_animatorState.isFollowing = false;
- m_navMeshAgent.destination = transform.position;
- }
- break;
- case EnemyStates.DEATH:
- break;
- }
- }
- }
最终的效果如图 3 所示,可以看到史莱姆发现角色后播放奔跑动画,拉脱战之后播放待机动画。
