3.4.1 寻路 在很多游戏中,敌人经常要在复杂的地形中追着主角跑,因为场景中存在很多障碍物,敌人的AI要足够聪明,才能找出到达目标点的最近道路,且绕开障碍物。写一个完善的寻路算法是比较有挑战的,特别是在复杂的3D场景中,好在Unity Pro版提供了一个非常实用的寻路功能,只需要较少的代码即可实现复杂的寻路功能。 Unity的寻路系统分为两部分,一部分是对场景进行设置,使其满足寻路算法的需求,另一部分是设置寻路者。 选择场景模型,然后在Inspector窗口选项Static旁边的小三角显示出下拉菜单,确定其中Navigation Static被选中。对于与场景地形无关的模型选项,则要确定没有被选中,如图3-6所示。 图3-6 第一人称视角 在菜单栏选择【Window】→【Navigation】打开Navigation窗口,如图3-7所示。 图3-7 寻路窗口 Navigation窗口的选项主要是定义地形对寻路的影响。 Radius和Height可以理解为寻路者的半径和高度。 Max Slope是最大坡度,超过这个坡度寻路者则无法通过。 Step Height是楼梯的最大高度,超过这个高度寻路者则无法通过。 Drop Height表示寻路者可以跳落的高度极限。 Jump Distance表示寻路者的跳跃距离极限。 在Navigation窗口设置好选项后,选择Bake对地形进行计算。如果不小心将数值搞乱了,选择Reset可以恢复默认,选择Clear会精除计算结果。 接下来设置寻路者,也就是游戏中的敌人。 在当前工程Assets/Prefabs内找到Zombie.prefab,将其拖入场景,它是一个僵尸模型,将作为游戏中的敌人。 在菜单栏选择【Component】→【Nav Mesh Agent】将寻路组件指定给敌人。然后在Inspector窗口可以进行进一步的设置,Radius和Height表示寻路者的半径和高度,Speed是最大运动速度,Angular Speed是最大旋转速度,如图3-8所示。 图3-8 设置敌人的寻路 创建脚本Enemy.cs,添加代码如下: using UnityEngine; using System.Collections; public class Enemy : MonoBehaviour { // Transform组件 Transform m_transform; // 主角 Player m_player; // 寻路组件 NavMeshAgent m_agent; //移动速度 float m_movSpeed =0.5f; void Start () { // 获取组件 m_transform = this.transform; // 获得主角 m_player = GameObject.FindGameObjectWithTag("Player").GetComponent<Player>(); // 获得寻路组件 m_agent = GetComponent<NavMeshAgent>(); // 设置寻路目标 m_agent.SetDestination(m_player. m_transform .position); } void Update () { MoveTo(); } // 寻路移动 void MoveTo() { float speed= m_movSpeed * Time.deltaTime; m_agent.Move(m_transform.TransformDirection((new Vector3(0, 0, speed)))); } } 这是敌人的脚本。在Start函数中获得NavMeshAgent组件,然后调用SetDestination函数设置一个目标点,在MoveTo函数中,调用NavMeshAgent组件提供的Move功能即可自动寻找目标点。 将脚本Enemy.cs指定给敌人。运行游戏,敌人会找出最短路径朝主角的位置前进,并会躲开障碍物。 3.4.2 设置动画 在前面,我们创建了可以自动寻路的敌人角色,接下来将为其增加动画效果。敌人共有四种动画,对应其状态,包括待机,行走,攻击和死亡。在本示例中,敌人的动画已经预先导入Unity工程并进行了基本设置,导入和设置动画的具体步骤请参考本书第5章动画部分。 在场景中选择敌人,默认它有一个Animator组件,并在Controller中已经预设了一个Animator Controller。取消选择Apply Root Motion选项,强迫使用脚本控制游戏体的位置而不是通过动画,如图3-9所示。 图3-9 Animator组件 在菜单栏选择【Window】→【Animator】打开Animator窗口,Animator Controller的信息都显示在这里。在这个窗口中能看到敌人的全部四个动画,双击动画图标即可在Project窗口找到原始动画资源。选择Parameters旁边的 + ,然后在子菜单中选择【Bool】,创建4个bool类型数值,名称分别为idle、run、attack、death,我们将会使其与动画过渡关联,并在脚本中控制它们,如图3-10所示。 图3-10 Animator窗口 在当前工程中,动画之间已预设好动画过渡,不同动画之间过渡是用连接线表示的,默认情况下动画之间通过播放时间自动过渡,我们需要使其受脚本控制。选择连线,比如从待机动画到跑步动画,在Conditions中将动画过渡条件设为run,当bool值run为true时即从待机动画过渡到跑步动画,如图3-11所示。 图3-11 设置动画过渡 重复步骤3为每个动画过渡设置条件。 3.4.3 行为 敌人的行为与动画的状态紧密关联,我们将修改敌人的脚本,在不同的动画状态使敌人的行为也发生改变。 打开Enemy.cs脚本,添加动画组件等属性: // Transform组件 Transform m_transform; // 动画组件 Animator m_ani; // 寻路组件 NavMeshAgent m_agent; // 主角 Player m_player; // 角色移动速度 float m_movSpeed = 0.5f; // 角色旋转速度 float m_rotSpeed = 120; // 计时器 float m_timer=2; // 生命值 int m_life = 15; void Start () { // 获取组件 m_transform = this.transform; m_ani = this.GetComponent<Animator>(); m_agent = GetComponent<NavMeshAgent>(); // 获得主角 m_player = GameObject.FindGameObjectWithTag("Player").GetComponent<Player>(); } m_rotSpeed用于控制旋转速度,当敌人进攻主角时,它将始终旋转到面向主角的角度。m_timer用来计算时间间隔,比如待机一定时间,每隔一定时间更新寻路。m_life是敌人的生命值。 添加RotateTo函数,它的作用是使敌人始终旋转到面向主角的角度: // 转向目标点 void RotateTo() { // 当前角度 Vector3 oldangle = m_transform.eulerAngles; // 获得面向主角的角度 m_transform.LookAt(m_player. m_transform); float target = m_transform.eulerAngles.y; // 转向主角 float speed = m_rotSpeed * Time.deltaTime; float angle = Mathf.MoveTowardsAngle(oldangle.y, target, speed); m_transform.eulerAngles = new Vector3(0, angle, 0); } MoveTowardsAngle是一个实用的函数,它的作用是基于旋转速度,计算出当前角度转向目标角度的旋转角度。 修改Update函数: void Update () { // 如果主角生命为0,什么也不做 if (m_player .m_life <= 0) return; // 获取当前动画状态 AnimatorStateInfo stateInfo = m_ani.GetCurrentAnimatorStateInfo(0); // 如果处于待机状态 if (stateInfo.nameHash == Animator.StringToHash("Base Layer.idle") && !m_ani.IsInTransition(0)) { m_ani.SetBool("idle", false); // 待机一定时间 m_timer -= Time.deltaTime; if (m_timer > 0) return; // 如果距离主角小于1.5米,进入攻击动画状态 if (Vector3.Distance(m_transform.position, m_player. m_transform.position) < 1.5f) { m_ani.SetBool("attack", true); } else { // 重置定时器 m_timer=1; // 设置寻路目标点 m_agent.SetDestination(m_player. m_transform.position); // 进入跑步动画状态 m_ani.SetBool("run", true); } } // 如果处于跑步状态 if (stateInfo.nameHash == Animator.StringToHash("Base Layer.run") && !m_ani.IsInTransition(0)) { m_ani.SetBool("run", false); // 每隔1秒重新定位主角的位置 m_timer -= Time.deltaTime; if (m_timer < 0) { m_agent.SetDestination(m_player. m_transform.position); m_timer = 1; } // 追向主角 MoveTo(); // 如果距离主角小于1.5米,向主角攻击 if (Vector3.Distance(m_transform.position, m_player. m_transform.position) <= 1.5f) { //停止寻路 m_agent.ResetPath(); // 进入攻击状态 m_ani.SetBool("attack", true); } } // 如果处于攻击状态 if (stateInfo.nameHash == Animator.StringToHash("Base Layer.attack") && !m_ani.IsInTransition(0)) { // 面向主角 RotateTo(); m_ani.SetBool("attack", false); // 如果动画播完,重新进入待机状态 if (stateInfo.normalizedTime >= 1.0f) { m_ani.SetBool("idle", true); // 重置计时器 m_timer = 2; } } } 在Update函数中,首先获得了一个AnimatorStateInfo对象,它保存着动画的状态,敌人包括待机、跑步、攻击、死亡四种状态,我们根据不同的状态处理不同的逻辑。无论哪种状态,都使用了IsInTransition判断是否是过渡状态,如果是,什么也不做。 默认敌人处于待机状态,并播放待机动画,我们使用了一个计时器,当待机时间超过2秒,如果距离主角1.5米以内,则播放攻击动画,进入攻击状态,否则进入跑步状态。 在跑步状态中,使用计时器每间隔1秒更新一次主角的位置进行寻路,并始终追击主角,当距离主角1.5米以内,停止寻路,播放攻击动画进入攻击状态。 在攻击状态中,如果攻击动画播完则回到待机状态。 运行游戏,敌人在不同的状态下会播放相应的动作,当距离主角较近时,则会攻击主角。