在这个实例中,我们要做一些敌人AI的简单实现,其中自动跟随和动画是重点,我们要达到的目标如下:
1.敌人能够自动跟随主角
2.敌人模型一共有四个动作:Idle(空闲) Run(奔跑) Attack(攻击) Death(死亡).
3.要求敌人在合适的时机能够做出合适动作
(一)自动跟随的实现
1)首先,新建一个场景 如图,场景里至少有两个角色: 有一个敌人(刀骷髅兵) 还有一个主角(没错,就是那个胶囊体)
2)先选择场景模型,然后在 Inspector 窗口选项 Static旁边的小三角显示出下拉菜单,确定其中 Navigation Static 被选中. 对于与场景地形无关的模型选项,则要确定没有被选中,如图所示。
Navigation 窗口的选项主要是定义地形对寻路的影响。Radius 和 Height 可以理解为寻路者的半径和高度。Max Slope 是最大坡度,超过这个坡度寻路者则无法通过。Step Height 是楼梯的最大高度 ,超过这个高度寻路者则无法通过。Drop Height表示寻路者可以跳落的高度极限。Jump Distance 表示寻路者的跳跃距离极限。
3)选择菜单栏里的Window 选项里的Navigation选项 点击下面的bake 渲染完成后是这个样子 这些蓝色的区域就是能自动寻路的区域
4) 然后给主角写 移动控制脚本 和 镜头控制脚本 并赋给主角(胶囊体):
1 public class PlayerControl : MonoBehaviour 2 { 3 4 //定义玩家的Transform 5 public Transform m_transform; 6 //定义玩家的角色控制器 7 CharacterController m_ch; 8 //定义玩家的移动速度 9 float m_movespeed = 10.0f; 10 //定义玩家的重力 11 float m_gravity = 2.0f; 12 //定义玩家的生命 13 public int m_life = 5; 14 15 //定义摄像机的Transform 16 Transform m_cameraTransform; 17 //定义摄像机的旋转角度 18 Vector3 m_cameraRotation; 19 //定义摄像机的高度 20 float m_cameraHeight = 1.4f; 21 //定义小地图摄像机 22 public Transform m_miniMap; 23 24 //定义枪口的Transform m_muzzlepPoint; 25 Transform m_muzzlePoint; 26 //定义射击时,射线射到的碰撞层 27 public LayerMask m_layer; 28 //定义射中目标后粒子效果的Transform 29 public Transform m_fx; 30 //定义射击音效 31 public AudioClip m_shootAudio; 32 //定义射击间隔时间计时器 33 float m_shootTimer = 0; 34 35 36 37 // Use this for initialization 38 void Start() 39 { 40 //获取玩家本身的Transform 赋给 m_transform 41 m_transform = this.transform; 42 //获取玩家本身的CharacterController组件 赋给 m_ch 43 m_ch = this.GetComponent<CharacterController>(); 44 45 //摄像机的控制的初始化 46 //获取摄像机的Transform 47 m_cameraTransform = Camera.main.transform; 48 //定义一个三维向量用来表示摄像机位置 并把玩家的位置赋给它 设置摄像机初始位置 49 Vector3 pos = m_transform.position; 50 //摄像机的Y轴坐标 为 本来的坐标加上上面定义的摄像机高度 51 pos.y += m_cameraHeight; 52 //把修改后的摄像机坐标重新赋给m_cameraTransform 53 m_cameraTransform.position = pos; 54 //把主角的旋转角度 赋给 摄像机的旋转角度 55 m_cameraTransform.rotation = m_transform.rotation; 56 //获取摄像机的角度 57 m_cameraRotation = m_transform.eulerAngles; 58 59 //隐藏鼠标 60 Cursor.visible = false; 61 } 62 63 // Update is called once per frame 64 void Update() 65 { 66 //如果玩家的生命小于等于0 什么也不做 67 if (m_life <= 0) 68 { 69 return; 70 } 71 72 //如果玩家的生命大于0 那么调用玩家控制函数 73 //移动函数 74 MoveControl(); 75 //摄像机控制函数 76 CameraControl(); 77 //跳跃函数 78 Jump(); 79 } 80 81 82 //定义玩家的控制函数 83 void MoveControl() 84 { 85 86 //定义玩家在XYZ轴上的移动量 87 float xm = 0, ym = 0, zm = 0; 88 89 //玩家的重力运动 为 减等于玩家的重力乘以每帧时间 90 ym -= m_gravity * Time.deltaTime; 91 92 //实现玩家上下左右的运动 93 //如果按下 W键 玩家在Z轴上的量增加 94 if (Input.GetKey(KeyCode.W)) 95 { 96 zm += m_movespeed * Time.deltaTime; 97 } 98 //如果按下 S键 玩家在Z轴上的量减少 这里用else if是因为每帧只能按下相反方向的一个键 99 else if (Input.GetKey(KeyCode.S)) 100 { 101 zm -= m_movespeed * Time.deltaTime; 102 } 103 //如果按下 A键 玩家在X轴上的量减少 104 if (Input.GetKey(KeyCode.A)) 105 { 106 xm -= m_movespeed * Time.deltaTime; 107 } 108 //如果按下 D键 玩家在X轴上的量增加 109 else if (Input.GetKey(KeyCode.D)) 110 { 111 xm += m_movespeed * Time.deltaTime; 112 } 113 114 ////当玩家在地面上的时候 才能前后左右移动 在空中不能移动 115 if (!m_ch.isGrounded) 116 { 117 xm = 0; 118 zm = 0; 119 } 120 121 //通过角色控制器的Move()函数,实现移动 122 m_ch.Move(m_transform.TransformDirection(new Vector3(xm, ym, zm))); 123 124 125 } 126 127 128 //定义玩家的摄像机控制函数 129 void CameraControl() 130 { 131 132 //实现对摄像机的控制 133 //定义主角在horizon方向X轴移动的量 也就是获取主角鼠标移动的量 134 float rh = Input.GetAxis("Mouse X"); 135 //定义主角在Vertical 方向Y轴移动的量 136 float rv = Input.GetAxis("Mouse Y"); 137 138 139 //旋转摄像机 140 //把鼠标在屏幕上移动的量转化为摄像机的角度 rv(上下移动的量) 等于 角色X轴的角度 rh(水平移动的量) 等于 角色Y轴上的角度 141 m_cameraRotation.x -= rv; 142 //Debug.Log(rv); 向下时 rv 为正值(顺时针) 向上时 rv 为负值(逆时针) 143 m_cameraRotation.y += rh; 144 //Debug.Log(rh); 向右时 rh 为正值(顺时针) 向左时 rh 为负值(逆时针) 145 146 147 //限制X轴的移动在-60度到60度之间 148 if (m_cameraRotation.x >= 60) 149 { 150 m_cameraRotation.x = 60; 151 } 152 if (m_cameraRotation.x <= -60) 153 { 154 m_cameraRotation.x = -60; 155 } 156 m_cameraTransform.eulerAngles = m_cameraRotation; 157 158 //使主角的面向方向与摄像机一致 用Vector3定义一个中间变量是因为 eularAngles 无法直接作为变量 159 Vector3 camrot = m_cameraTransform.eulerAngles; 160 //初始化摄像机的欧拉角为0 161 camrot.x = 0; 162 camrot.z = 0; 163 //把摄像机的欧拉角 赋给 主角 164 m_transform.eulerAngles = camrot; 165 166 //使摄像机的位置与主角一致 用Vector3定义一个中间变量是因为 position 无法直接作为变量 167 Vector3 pos = m_transform.position; 168 //摄像机的Y轴位置 为 主角的Y轴位置加上摄像机的高度 169 pos.y += m_cameraHeight; 170 //把主角的位置 赋给 摄像机的位置 171 m_cameraTransform.position = pos; 172 173 } 174 175 176 //定义玩家的Jump函数 177 void Jump() 178 { 179 //当玩家在地面上的时候 玩家的i跳才有效果 180 if (m_ch.isGrounded) 181 { 182 //此时玩家的重力为10 183 m_gravity = 10; 184 //如果按下 space键 玩家的重力变为负数 实现向上运动 185 if (Input.GetKey(KeyCode.Space)) 186 { 187 m_gravity = -8; 188 } 189 } 190 //此时玩家跳了起来 191 else 192 { 193 //玩家的重力 为 玩家的重力10 乘以 每帧的时间 194 m_gravity +=10f*Time.deltaTime; 195 //如果玩家的重力大于10的话 让他等于10 196 if (m_gravity>=10) 197 { 198 m_gravity = 10f; 199 } 200 } 201 202 }
5)给敌人写自动追踪脚本并赋给敌人 :
1 public class Enemy : MonoBehaviour 2 { 3 4 //定义敌人的Transform 5 Transform m_transform; 6 //CharacterController m_ch; 7 8 //定义动画组件 9 Animator m_animator; 10 11 //定义寻路组件 12 NavMeshAgent m_agent; 13 14 //定义一个主角类的对象 15 PlayerControl m_player; 16 //角色移动速度 17 float m_moveSpeed = 0.5f; 18 //角色旋转速度 19 float m_rotSpeed = 120; 20 //定义生命值 21 int m_life = 15; 22 23 //定义计时器 24 float m_timer = 2; 25 //定义生成点 26 //protected EnemySpawn m_spawn; 27 28 29 // Use this for initialization 30 void Start() 31 { 32 //初始化m_transform 为物体本身的tranform 33 m_transform = this.transform; 34 35 //初始化动画m_ani 为物体的动画组件 36 m_animator = this.GetComponent<Animator>(); 37 38 //初始化寻路组件m_agent 为物体的寻路组件 39 m_agent = GetComponent<NavMeshAgent>(); 40 41 //初始化主角 42 m_player = GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerControl>(); 43 44 45 } 46 47 // Update is called once per frame 48 void Update() 49 { 50 //设置敌人的寻路目标 51 m_agent.SetDestination(m_player.m_transform.position); 52 53 //调用寻路函数实现寻路移动 54 MoveTo(); 55 56 57 } 58 59 60 //敌人的自动寻路函数 61 void MoveTo() 62 { 63 //定义敌人的移动量 64 float speed = m_moveSpeed * Time.deltaTime; 65 66 //通过寻路组件的Move()方法实现寻路移动 67 m_agent.Move(m_transform.TransformDirection(new Vector3(0, 0, speed))); 68 } 69 70 71 72 }
这时,运行游戏,敌人就能自动跟随了.
(二)敌人动画的逻辑实现
1)首先,在Project面板里面create一个Animator Controller 双击它 就会发现多了一个BaseLayer面板 如下面第一个图 这个是用来控制动画的逻辑关系的 在敌人模型的动画分类里 如下图中间 选择自己需要的动画 然后拖到BaseLayer面板里面 右键标签可以创建箭头,这里为了便于讲解,选了四个动画(idle空闲 run奔跑 attack攻击 death死亡) 按照下面右图把动画标签的关系调节好.
2)给敌人添加动画播放脚本 这个脚本与上面的敌人脚本不同 注释的很清楚 很容易理解
1 public class Enemy : MonoBehaviour 2 { 3 4 //定义敌人的Transform 5 Transform m_transform; 6 //CharacterController m_ch; 7 8 //定义动画组件 9 Animator m_animator; 10 11 //定义寻路组件 12 NavMeshAgent m_agent; 13 14 //定义一个主角类的对象 15 PlayerControl m_player; 16 //角色移动速度 17 float m_moveSpeed = 0.5f; 18 //角色旋转速度 19 float m_rotSpeed = 120; 20 //定义生命值 21 int m_life = 15; 22 23 //定义计时器 24 float m_timer = 2; 25 //定义生成点 26 //protected EnemySpawn m_spawn; 27 28 29 // Use this for initialization 30 void Start() 31 { 32 //初始化m_transform 为物体本身的tranform 33 m_transform = this.transform; 34 35 //初始化动画m_ani 为物体的动画组件 36 m_animator = this.GetComponent<Animator>(); 37 38 //初始化寻路组件m_agent 为物体的寻路组件 39 m_agent = GetComponent<NavMeshAgent>(); 40 41 //初始化主角 42 m_player = GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerControl>(); 43 44 45 } 46 47 // Update is called once per frame 48 void Update() 49 { 50 ////设置敌人的寻路目标 51 //m_agent.SetDestination(m_player.m_transform.position); 52 53 ////调用寻路函数实现寻路移动 54 //MoveTo(); 55 56 //敌人动画的播放与转换 57 //如果玩家的生命值小于等于0时,什么都不做 (主角死后 敌人无需再有动作) 58 if (m_player.m_life <= 0) 59 { 60 return; 61 } 62 63 //获取当前动画状态(Idle Run Attack Death 中的一种) 64 AnimatorStateInfo stateInfo = m_animator.GetCurrentAnimatorStateInfo(0); 65 66 //Idle 如果角色在等待状态条 并且 没有处于转换状态 (0代表的是Base Layer) 67 if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.Idle") && !m_animator.IsInTransition(0)) 68 { 69 //此时把Idle状态设为false (此时把状态设置为false 一方面Unity 动画设置里面has exit time已经取消 另一方面为了避免和后面的动画冲突 ) 70 m_animator.SetBool("Idle", false); 71 72 //待机一定时间后(Timer) 之所以有这个Timer 是因为在动画播放期间 无需对下面的语句进行判断(判断也没有用) 从而起到优化的作用 73 m_timer -= Time.deltaTime; 74 75 //如果计时器Timer大于0 返回 (什么也不干,作用是优化 优化 优化) 76 if (m_timer > 0) 77 { 78 return; 79 } 80 81 //如果距离主角小于3米 把攻击动画的Bool值设为true (激活指向Attack的通道) 82 if (Vector3.Distance(m_transform.position, m_player.m_transform.position) < 3f) 83 { 84 m_animator.SetBool("Attack", true); 85 } 86 //如果距离主角不小于3米 87 else 88 { 89 //那么把计时器重置为1 90 m_timer = 1; 91 //重新获取自动寻路的位置 92 m_agent.SetDestination(m_player.m_transform.position); 93 //激活指向Run的通道 94 m_animator.SetBool("Run", true); 95 } 96 } 97 98 99 //Run 如果角色指向奔跑状态条 并且 没有处于转换状态 (0代表的是Base Layer) 100 if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.Run") && !m_animator.IsInTransition(0)) 101 { 102 //关闭指向Run的通道 103 m_animator.SetBool("Run", false); 104 //计时器时间随帧减少 105 m_timer -= Time.deltaTime; 106 //计时器时间小于0时 重新获取自动寻路的位置 重置计时器时间为1 107 if (m_timer < 0) 108 { 109 m_agent.SetDestination(m_player.m_transform.position); 110 m_timer = 1; 111 } 112 113 //调用跟随函数 114 MoveTo(); 115 116 //当角色与主角的距离小于等于3米时 117 if (Vector3.Distance(m_transform.position, m_player.m_transform.position) <= 3f) 118 { 119 //清楚当前路径 当路径被清除 代理不会开始寻找新路径直到SetDestination 被调用 120 m_agent.ResetPath(); 121 //激活指向Attack的通道 122 m_animator.SetBool("Attack", true); 123 124 } 125 } 126 127 128 //Attack 如果角色指向攻击状态条 并且 没有处于转换状态 (0代表的是Base Layer) 129 if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.Attack") && !m_animator.IsInTransition(0)) 130 { 131 //调用转向函数 132 RotationTo(); 133 134 //关闭指向Attack的通道 135 m_animator.SetBool("Attack", false); 136 137 //当播放过一次动画后 normalizedTime 实现状态的归1化(1就是整体和全部) 整数部分是时间状态的已循环数 小数部分是当前循环的百分比进程(0-1) 138 if (stateInfo.normalizedTime >= 1.0f) 139 { 140 //激活指向Idle的通道 141 m_animator.SetBool("Idle", true); 142 143 //计时器时间重置为2 144 m_timer = 2; 145 146 147 //m_player.OnDamage(1); 148 149 } 150 } 151 152 153 //Death 如果角色指向死亡状态条 并且 没有处于转换状态 (0代表的是Base Layer) 154 if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.Death") && !m_animator.IsInTransition(0)) 155 { 156 //摧毁这个物体的碰撞体 157 Destroy(this.GetComponent<Collider>()); 158 159 //自动寻路时间被归零 角色不再自动移动 160 m_agent.speed = 0; 161 162 //死亡动画播放一遍后 角色死亡 163 if (stateInfo.normalizedTime >= 1.0f) 164 { 165 //OnDeath() 166 } 167 168 169 } 170 } 171 172 173 //敌人的自动寻路函数 174 void MoveTo() 175 { 176 //定义敌人的移动量 177 float speed = m_moveSpeed * Time.deltaTime; 178 179 //通过寻路组件的Move()方法实现寻路移动 180 m_agent.Move(m_transform.TransformDirection(new Vector3(0, 0, speed))); 181 } 182 183 184 //敌人转向目标点函数 185 void RotationTo() 186 { 187 //定义当前角度 188 Vector3 oldAngle = m_transform.eulerAngles; 189 //获得面向主角的角度 190 m_transform.LookAt(m_player.m_transform); 191 192 //定义目标的方向 Y轴方向 也就是敌人左右转动面向玩家 193 float target = m_transform.eulerAngles.y; 194 //转向目标的速度 等于时间乘以旋转角度 195 float speed = m_rotSpeed * Time.deltaTime; 196 //通过MoveTowardsAngle() 函数获得转的角度 197 float angle = Mathf.MoveTowardsAngle(oldAngle.y, target, speed); 198 199 //实现转向 200 m_transform.eulerAngles = new Vector3(0, angle, 0); 201 } 202 203 }
自此,一个会自动寻找主角 并 攻击 而且 有动画 的敌人就做好了
---未完待续---