unity3D学习---使用Navmesh实现AI坦克大战

一点写在前面的内容:一直沉迷于斗地主,搞了很久想要自己实现一个斗地主的人机对打,传说中的托管,实际证明还是实力薄弱,快到截止时间还没做好,只能做做已经有模板的坦克大战,之后再补上我喜欢的斗地主。


本周内容:

4、作业 以下作业三选一
1、 有趣 AR 小游戏制作
2、 坦克对战游戏 AI 设计
从商店下载游戏:“Kawaii” Tank 或 其他坦克模型,构建 AI 对战坦克。具体要求

  • 使用“感知-思考-行为”模型,建模 AI 坦克
  • 场景中要放置一些障碍阻挡对手视线
  • 坦克需要放置一个矩阵包围盒触发器,以保证 AI 坦克能使用射线探测对手方位
  • AI 坦克必须在有目标条件下使用导航,并能绕过障碍。(失去目标时策略自己思考)
  • 实现人机对战
3、P&D 过河游戏智能帮助实现,程序具体要求:
  • 实现状态图的自动生成
  • 讲解图数据在程序中的表示方法
  • 利用算法实现下一步的计算
  • 参考:P&D 过河游戏智能帮助实现
(P&D状态机真的静下心去学可以学到很多东西,可惜一整个周末都交给了云计算,还没有好好搞懂) 本片博客代码框架和格式参考师兄博客:http://www.chenxd59.cn/?p=213
师兄博客代码简单易懂,思路清晰整洁,UML图画的很好。
首先挂个图展示最终成果:
图片有点大(上传不了): https://pan.baidu.com/s/1W0cVufmrCeu_KhoDVAZbJA
接下去讲实现步骤和操作代码:
1.完成地图预制和渲染,完成nevmesh
导入资源:
1.到官方商店下载项目Tanks! Tutorial。
2.打开原配置地图进行部分修改和渲染,完成nevmesh
3.借用资源包里面的坦克预制体实现player坦克和Enemy坦克预制


unity3D学习---使用Navmesh实现AI坦克大战
文章图片


坦克预制体如下:
unity3D学习---使用Navmesh实现AI坦克大战
文章图片
(player预制体,下面一圈红色是血条,用来显示坦克生命值)
unity3D学习---使用Navmesh实现AI坦克大战
文章图片
(普通坦克预制体,同样是附带一个血条表示生命值)

这里两个坦克分别预制主要是为了好区分,当然你也可以实现代码彩色渲染区别,那么也可以不要两个预制。
渲染生成Nevmesh的时候记住设置一些障碍物是不可以行走的,不然坦克就穿越而过


2.AI算法实现
unity3D学习---使用Navmesh实现AI坦克大战
文章图片




大致是跟着师兄的UML实现的,删减了修改了少部分的方法,增加了血条记录等功能。
导演场记和工厂模式因为比较熟悉所以这里不讲,代码详见github。
【unity3D学习---使用Navmesh实现AI坦克大战】首先定义6个接口函数
using System.Collections; using System.Collections.Generic; using UnityEngine; public interface IUserAction{ void moveForward(); //向前进 void moveBackWard(); //向后退 void turn(float offsetX); //偏转一定角度 void shoot(); //坦克射击 bool isGameOver(); //判断游戏状态 void show(); //记录游戏规则 }

场控书写和实现:
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using UnityEngine.UI; public class SceneController : MonoBehaviour, IUserAction{ public GameObject player; //玩家坦克 private bool gameOver = false; //游戏是否结束 private myFactory mF; //工厂 void Awake() {//一些初始的设置 GameDirector director = GameDirector.getInstance(); director.sencontor = this; mF = Singleton.Instance; player = mF.getPlayer(); } void Start () { } void Update () { // 相机跟随玩家坦克 Camera.main.transform.position = new Vector3(player.transform.position.x, 15, player.transform.position.z); } public Vector3 get_pos_of_player() {//返回玩家坦克的位置 return player.transform.position; } public bool isGameOver() {//返回游戏是否结束 return gameOver; } public void moveForward() { player.GetComponent().velocity = player.transform.forward * 20; } public void moveBackWard() { player.GetComponent().velocity = player.transform.forward * -20; } public void moveLeftWard() {//可以实现左转和前进同步 } public void moveRightWard() { } public void show() { GUI.Label(new Rect(100, 100, 200, 20), "上下键或者ws键控制坦克前进后退,ad键或者左右键控制坦克移动,空格键或者enter键射击,摧毁所有坦克,您将取得胜利"); } public void turn(float offsetX) {//通过水平轴上的增量,改变玩家坦克的欧拉角,从而实现坦克转向 float x = player.transform.localEulerAngles.y + offsetX * 5; float y = player.transform.localEulerAngles.x; player.transform.localEulerAngles = new Vector3 (y, x, 0); } public void shoot(){ //增加游戏难度 for (float i = 0.1f; i > 0; i -= Time.deltaTime) {} GameObject bullet = mF.getBullet(tankType.Player); //获取子弹,传入的参数表示发出子弹的坦克类型 bullet.transform.position = new Vector3(player.transform.position.x, 1.5f, player.transform.position.z) + player.transform.forward * 1.5f; //设置子弹位置 bullet.transform.forward = player.transform.forward; //设置子弹方向 Rigidbody rb = bullet.GetComponent(); rb.AddForce(bullet.transform.forward * 20, ForceMode.Impulse); //发射子弹 }}

因为子弹偏多和普通坦克偏多,所以选择共产模式,同时增加了订阅与发布的模式,回收子弹和废弃坦克
using System.Collections; using System.Collections.Generic; using UnityEngine; public enum tankType:int{Player, Enemy} public class myFactory : MonoBehaviour {//工厂模式 public GameObject bullet; //子弹 public ParticleSystem poxpS; //爆炸粒子系统 public GameObject player; //player public GameObject tank; //npc//键值对,记录保存子弹信息和坦克信息 private Dictionary usedtank; private Dictionary freetank; private Dictionary usedbullet; private Dictionary freebullet; private List contanier; void Awake() { usedtank = new Dictionary(); freetank = new Dictionary(); usedbullet = new Dictionary(); freebullet = new Dictionary(); contanier = new List(); }//npc坦克被摧毁时,会执行这个委托函数 void Start() { Enemy.recycleEvent += recycleTank; } //获取玩家坦克 public GameObject getPlayer() { return player; } public GameObject getBullet(tankType type) { if (freebullet.Count == 0) { GameObject newBullet = Instantiate(bullet); newBullet.GetComponent().setTankType(type); usedbullet.Add(newBullet.GetInstanceID(), newBullet); return newBullet; } foreach (KeyValuePair pair in freebullet) { pair.Value.SetActive(true); pair.Value.GetComponent().setTankType(type); freebullet.Remove(pair.Key); usedbullet.Add(pair.Key, pair.Value); return pair.Value; } return null; } public ParticleSystem getPs() { for (int i = 0; i < contanier.Count; i++) { if (!contanier[i].isPlaying) { return contanier[i]; } } ParticleSystem newPs = Instantiate(pS); contanier.Add(newPs); return newPs; } public void recycleTank(GameObject tank) { usedtank.Remove(tank.GetInstanceID()); freetank.Add(tank.GetInstanceID(), tank); tank.GetComponent().velocity = new Vector3(0, 0, 0); tank.SetActive(false); } public void recycleBullet(GameObject bullet) { usedbullet.Remove(bullet.GetInstanceID()); freebullet.Add(bullet.GetInstanceID(), bullet); bullet.GetComponent().velocity = new Vector3(0, 0, 0); bullet.SetActive(false); } }

两种坦克自带属性方法的完成:
普通坦克距离目标坦克(player)一定范围内能发动射击,造成player的伤害,同时player血条值下降。如果距离目标坦克较远,那么普通坦克将朝着player移动,移动与射击两个活动是同时可以触发的,协程处理
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using UnityEngine.UI; public class Enemy : Tank {//npc坦克 public delegate void recycle(GameObject Tank); public static event recycle recycleEvent; //npc坦克被销毁之后,可通知工厂回收 private Vector3 target; //目标,即玩家坦克的位置 private bool gameover; //游戏是否结束,决定是否继续运动或射击 public Slider m_Slider; void Start() { setHp(500f); //设置初始生命值为200 StartCoroutine(shoot()); //开始射击的协程 this.m_Slider.value = https://www.it610.com/article/getHp(); } void Update () { gameover = GameDirector.getInstance().sencontor.isGameOver(); if (!gameover) { target = GameDirector.getInstance().sencontor.get_pos_of_player(); //触发回收事件 if (getHp() <= 0 && recycleEvent != null) { recycleEvent(this.gameObject); } else if (getHp()>= 0) {//向玩家坦克移动 NavMeshAgent agent = GetComponent(); agent.SetDestination(target); this.m_Slider.value = https://www.it610.com/article/getHp(); } else {} } } //协程实现npc坦克每隔1.5s进行射击 IEnumerator shoot() { while(!gameover) { for (float i = 1.5f; i> 0; i -= Time.deltaTime) { yield return 0; } //和玩家坦克距离小于15,则射击 if (Vector3.Distance(transform.position, target) < 15) { myFactory mF = Singleton.Instance; GameObject bullet = mF.getBullet(tankType.Enemy); //获取子弹,传入的参数表示发射子弹的坦克类型 bullet.transform.position = new Vector3(transform.position.x, 1.5f, transform.position.z) + transform.forward * 1.5f; //设置子弹 bullet.transform.forward = transform.forward; //设置子弹方向 Rigidbody rb = bullet.GetComponent(); rb.AddForce(bullet.transform.forward * 20, ForceMode.Impulse); //发射子弹 } } } }

player坦克能够随时射击子弹,但是为了控制卡顿和没必要的资源浪费,规定了player一秒能发射的子弹数目是固定的(有上限),每个子弹发射存在时间间隔,虽然人为控制的速度也会造成子弹其实不会那么密集。
player子弹射击到npc,将会咋成npc血条下降,血条为0时发布坦克回收时间,回收坦克和子弹。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using UnityEngine.UI; public class Player : Tank {//玩家坦克 public delegate void destroy(); public static event destroy destroyEvent; public Slider m_Slider; void Start() { setHp(500f); //设置初始生命值为500 m_Slider.value = https://www.it610.com/article/getHp(); } void Update () { if (getHp() <= 0 ) {//生命值<=0,表示玩家坦克被摧毁 this.gameObject.SetActive(false); if (destroyEvent != null) {//执行委托事件 destroyEvent(); } } else { //控制血条 m_Slider.value = getHp(); } } }

大体代码就是这样,这部分着重于对AI的理解以及Nevmesh的渲染处理,难度比不上状态机,但是也是一个值得学习的方面,我的斗地主得接下去找时间实现了,没有在这周之前实现实在是有点忏愧。


视屏链接:https://pan.baidu.com/s/1ZD_2BrlossS4pLH9dYieEQ









    推荐阅读