Unity3D|Unity3d之坦克对战游戏 AI设计

作业要求

  • 使用“感知-思考-行为”模型,建模 AI 坦克
  • 场景中要放置一些障碍阻挡对手视线
  • 坦克需要放置一个矩阵包围盒触发器,以保证 AI 坦克能使用射线探测对手方位
  • AI 坦克必须在有目标条件下使用导航,并能绕过障碍。(失去目标时策略自己思考)
  • 实现人机对战
这次作业的具体要求如上面所述,简单来说就是实现一个AI坦克,能够搜索玩家并且发现玩家之后会追踪玩家并且发射子弹,同时满足能够避开障碍物。
演示视频 请移步: https://www.bilibili.com/video/av25190155/
游戏实现 游戏场景设计
这次作业参考了去年师兄的作品,利用官方教程Tanks! Tutorial里面的模型构建了自己的游戏场景。效果如下:
Unity3D|Unity3d之坦克对战游戏 AI设计
文章图片

接着打开Window -> Navigation,设置游戏对象的Navigation,如果是障碍物则设置Navigation Area为not walkable。如下:
Unity3D|Unity3d之坦克对战游戏 AI设计
文章图片

接着进行Bake,以便AI寻路。
Unity3D|Unity3d之坦克对战游戏 AI设计
文章图片

游戏关键代码解析
AI坦克的状态图如下:
Unity3D|Unity3d之坦克对战游戏 AI设计
文章图片

为了实现坦克自动寻路,需要为坦克对象添加NavMeshAgent组件。这么做是为了当AI坦克发现玩家之后,只需要为NavMeshAgent设置destination,AI坦克便能够自动寻路,前往玩家所在位置。
Unity3D|Unity3d之坦克对战游戏 AI设计
文章图片

【Unity3D|Unity3d之坦克对战游戏 AI设计】从状态图中可以看出,一开始AI坦克如果在自己附近没有发现玩家,则会进入巡逻状态,这里我预先设置了几个点,AI坦克会随机选取一个作为目的点,并自动寻路移动到目的点,并继续下次巡逻;在这个过程中如果AI坦克发现了附近的玩家,则会进行追捕,我把玩家的位置设置为AI坦克的目的点,从而使AI坦克自动向玩家方向移动;当距离进入了AI坦克的射程范围,则AI坦克会通过协程每隔一秒发射一颗子弹。
相关代码:
public class AITank : Tank {public delegate void recycle(GameObject tank); public static event recycle recycleEvent; private Vector3 target; private bool gameover; // 巡逻点 private static Vector3[] points = { new Vector3(37.6f,0,0), new Vector3(40.9f,0,39), new Vector3(13.4f, 0, 39), new Vector3(13.4f, 0, 21), new Vector3(0,0,0), new Vector3(-20,0,0.3f), new Vector3(-20, 0, 32.9f), new Vector3(-37.5f, 0, 40.3f), new Vector3(-37.5f,0,10.4f), new Vector3(-40.9f, 0, -25.7f), new Vector3(-15.2f, 0, -37.6f), new Vector3(18.8f, 0, -37.6f), new Vector3(39.1f, 0, -18.1f) }; private int destPoint = 0; private NavMeshAgent agent; private bool isPatrol = false; private void Awake() { destPoint = UnityEngine.Random.Range(0, 13); }// Use this for initialization void Start () { setHp(100f); StartCoroutine(shoot()); agent = GetComponent(); }private IEnumerator shoot() { while (!gameover) { for(float i = 1; i > 0; i -= Time.deltaTime) { yield return 0; } // 当敌军坦克距离玩家坦克不到20时开始射击 if(Vector3.Distance(transform.position, target) < 20) { GameObjectFactory 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); } } }// Update is called once per frame void Update () { gameover = GameDirector.getInstance().currentSceneController.isGameOver(); if (!gameover) { target = GameDirector.getInstance().currentSceneController.getPlayerPos(); if (getHp() <= 0 && recycleEvent != null) {//如果npc坦克被摧毁,则回收它 recycleEvent(this.gameObject); } else { if(Vector3.Distance(transform.position, target) <= 30) { isPatrol = false; //否则向玩家坦克移动 agent.autoBraking = true; agent.SetDestination(target); } else { patrol(); } } } else { NavMeshAgent agent = GetComponent(); agent.velocity = Vector3.zero; agent.ResetPath(); } }private void patrol() { if(isPatrol) { if(!agent.pathPending && agent.remainingDistance < 0.5f) GotoNextPoint(); } else { agent.autoBraking = false; GotoNextPoint(); } isPatrol = true; }private void GotoNextPoint() { agent.SetDestination(points[destPoint]); destPoint = (destPoint + 1) % points.Length; } }

子弹类的要点在于通过OnCollisionEnter事件判断在子弹碰撞到其他物体时,爆炸范围内的所有碰撞体对象,如果子弹是AI坦克发射的并且碰撞体为玩家,则玩家坦克会扣血,子弹失活回收;如果子弹是玩家发射并且碰撞体是AI坦克,则AI坦克扣血。还要注意当子弹落地时(我通过transform.position.y < 0 判断)应该把子弹回收。
public class Bullet : MonoBehaviour { // 子弹伤害半径 public float explosionRadius = 3f; private tankType type; public void setTankType(tankType type) { this.type = type; }private void Update() { if(this.transform.position.y < 0 && this.gameObject.activeSelf) { GameObjectFactory mf = Singleton.Instance; // 落地爆炸 ParticleSystem explosion = mf.getPs(); explosion.transform.position = transform.position; explosion.Play(); mf.recycleBullet(this.gameObject); } }void OnCollisionEnter(Collision other) { // 获得单实例工厂 GameObjectFactory mf = Singleton.Instance; ParticleSystem explosion = mf.getPs(); explosion.transform.position = transform.position; // 获取爆炸范围内的所有碰撞体 Collider[] colliders = Physics.OverlapSphere(transform.position, explosionRadius); for(int i = 0; i < colliders.Length; i++) { if(colliders[i].tag == "tankPlayer" && this.type == tankType.Enemy || colliders[i].tag == "tankEnemy" && this.type == tankType.Player) { // 根据击中坦克与爆炸中心的距离计算伤害值 float distance = Vector3.Distance(colliders[i].transform.position, transform.position); //被击中坦克与爆炸中心的距离 float hurt = 100f / distance; float current = colliders[i].GetComponent().getHp(); c`这里写代码片`olliders[i].GetComponent().setHp(current - hurt); } }explosion.Play(); if (this.gameObject.activeSelf) { mf.recycleBullet(this.gameObject); } } }

这个项目中我通过单实例工厂GameObjectFactory来统一管理玩家player、 AI坦克、子弹、爆炸粒子系统等游戏对象,实现方法与老师前面课上讲的内容一致。通过Dictionary来维护。
public class GameObjectFactory : MonoBehaviour { // 玩家 public GameObject player; // npc public GameObject tank; // 子弹 public GameObject bullet; // 爆炸粒子系统 public ParticleSystem ps; private Dictionary usingTanks; private Dictionary freeTanks; private Dictionary usingBullets; private Dictionary freeBullets; private List psContainer; private void Awake() { usingTanks = new Dictionary(); freeTanks = new Dictionary(); usingBullets = new Dictionary(); freeBullets = new Dictionary(); psContainer = new List(); }// Use this for initialization void Start () { //回收坦克的委托事件 AITank.recycleEvent += recycleTank; }public GameObject getPlayer() { return player; }public GameObject getTank() { if(freeTanks.Count == 0) { GameObject newTank = Instantiate(tank); usingTanks.Add(newTank.GetInstanceID(), newTank); //在一个随机范围内设置坦克位置 newTank.transform.position = new Vector3(Random.Range(-100, 100), 0, Random.Range(-100, 100)); return newTank; } foreach (KeyValuePair pair in freeTanks) { pair.Value.SetActive(true); freeTanks.Remove(pair.Key); usingTanks.Add(pair.Key, pair.Value); pair.Value.transform.position = new Vector3(Random.Range(-100, 100), 0, Random.Range(-100, 100)); return pair.Value; } return null; }public GameObject getBullet(tankType type) { if (freeBullets.Count == 0) { GameObject newBullet = Instantiate(bullet); newBullet.GetComponent().setTankType(type); usingBullets.Add(newBullet.GetInstanceID(), newBullet); return newBullet; } foreach (KeyValuePair pair in freeBullets) { pair.Value.SetActive(true); pair.Value.GetComponent().setTankType(type); freeBullets.Remove(pair.Key); usingBullets.Add(pair.Key, pair.Value); return pair.Value; } return null; }public ParticleSystem getPs() { for(int i = 0; i < psContainer.Count; i++) { if (!psContainer[i].isPlaying) return psContainer[i]; } ParticleSystem newPs = Instantiate(ps); psContainer.Add(newPs); return newPs; }public void recycleTank(GameObject tank) { usingTanks.Remove(tank.GetInstanceID()); freeTanks.Add(tank.GetInstanceID(), tank); tank.GetComponent().velocity = new Vector3(0, 0, 0); tank.SetActive(false); }public void recycleBullet(GameObject bullet) { usingBullets.Remove(bullet.GetInstanceID()); freeBullets.Add(bullet.GetInstanceID(), bullet); bullet.GetComponent().velocity = new Vector3(0, 0, 0); bullet.SetActive(false); } }

然后为了实现一个比较好的游戏体验,我实现了一个MainCameraControl来控制主摄像机的移动跟随效果,并且能够通过游戏场景中所有坦克的距离大小来设置摄像机的Size,从而视觉体验更佳~具体实现见下面代码,注释比较清楚~
public class MainCameraControl : MonoBehaviour {public float m_DampTime = 0.2f; // 相机refocus的时间 public float m_ScreenEdgeBuffer = 4f; // 最靠近边界的坦克与边界之间的缓冲大小 public float m_MinSize = 6.5f; // 相机Size最小值 [HideInInspector] public List m_Targets; // 保存所有坦克的transformprivate Camera m_Camera; private float m_ZoomSpeed; private Vector3 m_MoveVelocity; private Vector3 m_DesiredPosition; private void Awake() { m_Camera = Camera.main; }public void setTarget(Transform transform) { m_Targets.Add(transform); }private void FixedUpdate() { // 把相机移动到希望的位置 Move(); // 改变相机size Zoom(); }private void Move() { FindAveragePosition(); transform.position = Vector3.SmoothDamp(transform.position, m_DesiredPosition, ref m_MoveVelocity, m_DampTime); }// 计算平均位置 private void FindAveragePosition() { Vector3 averagePos = new Vector3(); int numTargets = 0; for (int i = 0; i < m_Targets.Count; i++) { if (!m_Targets[i].gameObject.activeSelf) continue; averagePos += m_Targets[i].position; numTargets++; }if (numTargets > 0) averagePos /= numTargets; averagePos.y = transform.position.y; m_DesiredPosition = averagePos; }private void Zoom() { float requiredSize = FindRequiredSize(); m_Camera.orthographicSize = Mathf.SmoothDamp(m_Camera.orthographicSize, requiredSize, ref m_ZoomSpeed, m_DampTime); }// 计算合适的Size private float FindRequiredSize() { Vector3 desiredLocalPos = transform.InverseTransformPoint(m_DesiredPosition); float size = 0f; for (int i = 0; i < m_Targets.Count; i++) { if (!m_Targets[i].gameObject.activeSelf) continue; Vector3 targetLocalPos = transform.InverseTransformPoint(m_Targets[i].position); Vector3 desiredPosToTarget = targetLocalPos - desiredLocalPos; size = Mathf.Max(size, Mathf.Abs(desiredPosToTarget.y)); size = Mathf.Max(size, Mathf.Abs(desiredPosToTarget.x) / m_Camera.aspect); }size += m_ScreenEdgeBuffer; size = Mathf.Max(size, m_MinSize); return size; }// 初始化相机 public void SetStartPositionAndSize() { FindAveragePosition(); transform.position = m_DesiredPosition; m_Camera.orthographicSize = FindRequiredSize(); } }

场记SceneController内容也比较简单,主要负责通知工厂初始化各种游戏对象,如player、AI坦克等,并初始化主摄像机,然后实现IUserAction接口中声明的函数即可。
public class SceneController : MonoBehaviour, IUserAction {public GameObject player; private bool gameOver = false; private int enemyCount = 6; private GameObjectFactory mf; private MainCameraControl cameraControl; private void Awake() { GameDirector director = GameDirector.getInstance(); director.currentSceneController = this; mf = Singleton.Instance; player = mf.getPlayer(); cameraControl = GetComponent(); cameraControl.setTarget(player.transform); }// Use this for initialization void Start () { for(int i = 0; i < enemyCount; i++) { GameObject gb = mf.getTank(); cameraControl.setTarget(gb.transform); } Player.destroyEvent += setGameOver; // 初始化相机位置 cameraControl.SetStartPositionAndSize(); }// 更新相机位置 void Update () { Camera.main.transform.position = new Vector3(player.transform.position.x, 15, player.transform.position.z); }public Vector3 getPlayerPos() { return player.transform.position; }public bool isGameOver() { return gameOver; } public void setGameOver() { gameOver = true; }public void moveForward() { player.GetComponent().velocity = player.transform.forward * 20; } public void moveBackWard() { player.GetComponent().velocity = player.transform.forward * -20; }public void turn(float offsetX) { float y = player.transform.localEulerAngles.y + offsetX * 5; float x = player.transform.localEulerAngles.x; player.transform.localEulerAngles = new Vector3(x, y, 0); }public void shoot() { 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); } }

完整代码请到我的github上查看:https://github.com/CarolSum/Unity3d-Learning/tree/master/hw9

    推荐阅读