Unity3D学习之路——AI小坦克

Unity3D学习之路——AI小坦克

作业要求:

  • 坦克对战游戏 AI 设计
    从商店下载游戏:“Kawaii” Tank 或 其他坦克模型,构建 AI 对战坦克。具体要求
    • 使用“感知-思考-行为”模型,建模 AI 坦克
    • 场景中要放置一些障碍阻挡对手视线
    • 坦克需要放置一个矩阵包围盒触发器,以保证 AI 坦克能使用射线探测对手方位
    • AI 坦克必须在有目标条件下使用导航,并能绕过障碍。(失去目标时策略自己思考)
      实现人机对战

成品图

主要是参考师兄的博客做的,明明很好玩的东西但自己做出来的效果不好。

实现的要求:

  • AI小坦克自动追踪玩家坦克,利用课程上的AI寻路,当与玩家坦克距离小于一定程度时发射子弹射击。
  • 玩家可操控角色躲避攻击并发射子弹攻击AI坦克。
  • 为了增强游戏的可玩性,玩家坦克的子弹伤害比AI坦克子弹伤害要高。

实现过程:

  • 有了之前几次作业的基础,现在懂得尽量减少类与类之间的耦合,这在大项目里是很重要的要求。
  • Step1:坦克基类Tank.cs:血量,以及被攻击后血量改变的相关函数,还有开炮对应的操作:
public class Tank : MonoBehaviour {
     private float hp =500.0f;
    // 初始化
     public Tank()
     {
         hp = 500.0f;
     }

     public float getHP()
     {
         return hp;
     }

     public void setHP(float hp)
     {
         this.hp = hp;
     }
     //标明子弹所属的坦克类型,TankType是枚举变量,在工厂类里MyFactory定义。
     public void shoot(TankType type)
     {
         GameObject bullet = Singleton.Instance.getBullets(type);
         bullet.transform.position = new Vector3(transform.position.x, 1.5f, transform.position.z) + transform.forward * 1.5f;
         bullet.transform.forward = transform.forward; //方向
         bullet.GetComponent().AddForce(bullet.transform.forward * 20, ForceMode.Impulse);
     }
}
  • Step2:玩家类的实现:继承Tank类
public class Player : Tank{
   // player被摧毁时发布信息;
   public delegate void DestroyPlayer();
   public static event DestroyPlayer destroyEvent;
   void Start () {
       setHP(500);
}

// Update is called once per frame
void Update () {
    if(getHP() <= 0)    // Tank is destoryed
       {
           this.gameObject.SetActive(false);
           destroyEvent();
       }
}

   //向前移动
   public void moveForward()
   {
       gameObject.GetComponent().velocity = gameObject.transform.forward * 30;
   }
   //向后移动
   public void moveBackWard()
   {
       gameObject.GetComponent().velocity = gameObject.transform.forward * -30;
   }

   //通过水平轴上的增量,改变玩家坦克的欧拉角,从而实现坦克转向
   public void turn(float offsetX)
   {
       float x = gameObject.transform.localEulerAngles.x;
       float y = gameObject.transform.localEulerAngles.y + offsetX*2;
       gameObject.transform.localEulerAngles = new Vector3(x, y, 0);
   }
}
  • Step3:AI坦克类的实现,也是要继承Tank类:
public class Enemy : Tank {
   public delegate void RecycleEnemy(GameObject enemy);
   //当enemy被摧毁时,通知工厂回收;
   public static event RecycleEnemy recycleEnemy;
   // player 的位置
   private Vector3 playerLocation;
   //游戏是否结束
   private bool gameover;
   private void Start()
   {
       playerLocation = GameDirector.getInstance().currentSceneController.getPlayer().transform.position;
       StartCoroutine(shoot());
   }

   void Update() {
       playerLocation = GameDirector.getInstance().currentSceneController.getPlayer().transform.position;
       gameover = GameDirector.getInstance().currentSceneController.getGameOver();
       if (!gameover)
       {
           if (getHP() <= 0 && recycleEnemy != null)
           {
               recycleEnemy(this.gameObject);
           }
           else
           {
               // 自动向player移动
               NavMeshAgent agent = gameObject.GetComponent();
               agent.SetDestination(playerLocation);
              // gameObject.transform.position = new Vector3(transform.position.x+0.01f,transform.position.y,transform.position.z);
           }
       }
       else
       {
           //游戏结束,停止寻路
           NavMeshAgent agent = gameObject.GetComponent();
           agent.velocity = Vector3.zero;
           agent.ResetPath();
       }
   }
   // 协程实现每隔1s进行射击,开始喜欢协程了
   IEnumerator shoot()
   {
       while (!gameover)
       {
           for(float i =1;i> 0; i -= Time.deltaTime)
           {
               yield return 0;
           }
           if(Vector3.Distance(playerLocation,gameObject.transform.position) < 14)
           {
               shoot(TankType.ENEMY);
           }
       }
   }
}

以上就实现了这个游戏中主要角色的动作了,将player和enemy坦克都会有的属性与动作,比如血量和射击shoot放到基类Tank,减少代码的冗余并增强代码的可读性;另外,也尽量着减少与其它类的关联。

  • Step4: 子弹bullet类,有些要求是要注意的:设置发射子弹的坦克类型, 因为如果射到队友是不能算伤害的;如果是玩家发出的子弹伤害高一点,AI坦克的子弹伤害低一点:
public class Bullet : MonoBehaviour {
    public float explosionRadius = 3.0f;
    private TankType tankType;

    //设置发射子弹的坦克类型, 因为如果射到队友是不能算伤害的.
    public void setTankType(TankType type)
    {
        tankType = type;
    }
    private void OnCollisionEnter(Collision collision)
    {
        if(collision.transform.gameObject.tag == "tankEnemy" && this.tankType == TankType.ENEMY ||
            collision.transform.gameObject.tag == "tankPlayer" && this.tankType == TankType.PLAYER)
        {
            return;
        }
        MyFactory factory = Singleton.Instance;
        ParticleSystem explosion = factory.getParticleSystem();
        explosion.transform.position = gameObject.transform.position;
        //获取爆炸范围内的所有碰撞体
        Collider[] colliders = Physics.OverlapSphere(gameObject.transform.position, explosionRadius);

        foreach(var collider in colliders)
        {
            //被击中坦克与爆炸中心的距离
            float distance = Vector3.Distance(collider.transform.position, gameObject.transform.position);
            float hurt;
            // 如果是玩家发出的子弹伤害高一点
            if (collider.tag == "tankEnemy" && this.tankType == TankType.PLAYER)
            {
                hurt = 300.0f / distance;
                collider.GetComponent().setHP(collider.GetComponent().getHP() - hurt);
            }
            else if(collider.tag == "tankPlayer" && this.tankType == TankType.ENEMY)
            {
                hurt = 100.0f / distance;
                collider.GetComponent().setHP(collider.GetComponent().getHP() - hurt);
            }
            explosion.Play();
        }

        if (gameObject.activeSelf)
        {
            factory.recycleBullet(gameObject);
        }
    }

}
  • Step5:接着就是工厂类了,工厂类无非就是场景要“演员”的时候就生产出来,不需要的时候就回收,代码与之前作业很类似,在这就不详细介绍了:
//其中public的四个变量是用来在unity中将player,enemy,bullet,explosion预制拖进代码里用于实例化对象的。
//其它变量和方法应该看名称都可以看懂。
public enum TankType { PLAYER , ENEMY};
public class MyFactory : MonoBehaviour {

   public GameObject player;
   public GameObject enemy;
   public GameObject bullet;
   public ParticleSystem explosion;

   private List usingTanks;
   private List freeTanks;
   private List usingBullets;
   private List freeBullets;
   private GameObject role;
   private List particles;

   private void Awake()
   {
       usingTanks = new List();
       freeTanks = new List();
       usingBullets = new List();
       freeBullets = new List();
       particles = new List();

       role = GameObject.Instantiate(player) as GameObject;
       role.SetActive(true);
       role.transform.position = Vector3.zero;
   }
   // Use this for initialization
   void Start () {
       Enemy.recycleEnemy += recycleEnemy;
   }

// Update is called once per frame
public GameObject getPlayer()
   {      
       return role;
   }

   public GameObject getEnemys()
   {
       GameObject newTank = null;
       if (freeTanks.Count <= 0)
       {
           newTank = GameObject.Instantiate(enemy) as GameObject;
           usingTanks.Add(newTank);
           newTank.transform.position = new Vector3(Random.Range(-100, 100), 0, Random.Range(-100, 100));
       }
       else
       {
           newTank = freeTanks[0];
           freeTanks.RemoveAt(0);
           usingTanks.Add(newTank);
       }
       newTank.SetActive(true);
       return newTank;
   }

   public GameObject getBullets(TankType type)
   {
       GameObject newBullet;
       if(freeBullets.Count <= 0)
       {
           newBullet = GameObject.Instantiate(bullet) as GameObject;
           usingBullets.Add(newBullet);
           newBullet.transform.position = new Vector3(Random.Range(-100, 100), 0, Random.Range(-100, 100));
       }
       else
       {
           newBullet = freeBullets[0];
           freeBullets.RemoveAt(0);
           usingBullets.Add(newBullet);
       }
       newBullet.GetComponent().setTankType(type);
       newBullet.SetActive(true);
       return newBullet;
   }

   public ParticleSystem getParticleSystem()
   {
       foreach(var particle in particles)
       {
           if (!particle.isPlaying)
           {
               return particle;
           }
       }
       ParticleSystem newPS = GameObject.Instantiate(explosion);
       particles.Add(newPS);
       return newPS;
   }

   public void recycleEnemy(GameObject enemyTank)
   {
       usingTanks.Remove(enemyTank);
       freeTanks.Add(enemyTank);
       enemyTank.GetComponent().velocity = Vector3.zero;
       enemyTank.SetActive(false);
   }

   public void recycleBullet(GameObject Bullet)
   {
       usingBullets.Remove(Bullet);
       freeBullets.Add(Bullet);
       Bullet.GetComponent().velocity = Vector3.zero;
       Bullet.SetActive(false);
   }
}

Unity3D学习之路——AI小坦克_第1张图片

  • Step6:场景类:获得工厂实例,做好初始化工作,接受从UI类里传过来的要求实现对应的操作,由于Player和Enemy已经实现了自身对应的动作,所以场景中如果接受UI传来的要求即让对应的Player和Enemy实例执行对应的操作就行:
// SceneController.cs
public class SceneController : MonoBehaviour,IUserAction{
    public GameObject player;
    private int enemyCount = 6;
    private bool gameOver = false;
    private GameObject[] enemys;
    private MyFactory myFactory;
    public GameDirector director;
    private void Awake()
    {
        director = GameDirector.getInstance();
        director.currentSceneController = this;
        enemys = new GameObject[enemyCount];
        gameOver = false;
        myFactory = Singleton.Instance;

    }

    void Start () {
        player = myFactory.getPlayer();
        for (int i = 0; i < enemyCount; i++)
        {
            enemys[i]=myFactory.getEnemys();
        }
        Player.destroyEvent += setGameOver;
    }

    // Update is called once per frame
    void Update () {
        Camera.main.transform.position = new Vector3(player.transform.position.x, 18, player.transform.position.z);
    }

    //返回玩家坦克的位置
    public GameObject getPlayer()
    {
        return player;
    }

    //返回游戏状态
    public bool getGameOver()
    {
        return gameOver;
    }

    //设置游戏结束
    public void setGameOver()
    {
        gameOver = true;
    }

    public void moveForward()
    {
        player.GetComponent().moveForward();
    }
    public void moveBackWard()
    {
        player.GetComponent().moveBackWard();
    }

    //通过水平轴上的增量,改变玩家坦克的欧拉角,从而实现坦克转向
    public void turn(float offsetX)
    {
        player.GetComponent().turn(offsetX);
    }

    public void shoot()
    {
        player.GetComponent().shoot(TankType.PLAYER);
    }
}

//IUserAction.cs
public interface IUserAction
{
    void moveForward();
    void moveBackWard();
    void turn(float offsetX);
    void shoot();
    bool getGameOver();
}
  • Step7:最后就是UI类和导演Director类了,UI类就是检测玩家的输入,再对player坦克进行对应的操作,IUserGUI 类如下:
//IUserGUI.cs
public class IUserGUI : MonoBehaviour {
  IUserAction action;

    // Use this for initialization
    void Start () {
       action = GameDirector.getInstance().currentSceneController as IUserAction;
    }

// Update is called once per frame
    void Update () {
     if (!action.getGameOver())
     {
         if (Input.GetKey(KeyCode.W))
         {
             action.moveForward();
         }

         if (Input.GetKey(KeyCode.S))
         {
             action.moveBackWard();
         }


         if (Input.GetKeyDown(KeyCode.Space))
         {
             action.shoot();
         }
         //获取水平轴上的增量,目的在于控制玩家坦克的转向
         float offsetX = Input.GetAxis("Horizontal");
         action.turn(offsetX);
     }
}

 void OnGUI()
 {
     //gameover时生成提示
     if (action.getGameOver())
     {
         GUIStyle fontStyle = new GUIStyle();
         fontStyle.fontSize = 30;
         fontStyle.normal.textColor = new Color(0, 0, 0);
         GUI.Button(new Rect(Screen.width/2-50, Screen.height/2-50, 200, 50), "GameOver!");
     }
   }
}

导演类还是和以前一样:

//GameDirector.cs
public class GameDirector : System.Object {
   private static GameDirector _instance;
   public SceneController currentSceneController { get; set; }

   private GameDirector() { }
   public static GameDirector getInstance()
   {
       if(_instance == null)
       {
           _instance = new GameDirector();
       }
       return _instance;
   }
}

哦还有个用于产生单例的类,这个类用了泛型编程:

//Singleton.cs
public class Singleton : MonoBehaviour where T :MonoBehaviour {
   protected static T instance;
   public static T Instance
   {
       get
       {
           if(instance == null)
           {
               instance = (T)FindObjectOfType(typeof(T));
               if(instance == null)
               {
                   Debug.LogError("An instance of " + typeof(T) + " is needed in the scene , but there is none.");
               }
           }
           return instance;
       }
   }
}

总结

  • 这次作业学习了unity的AI技术还是很好玩的,虽然项目做得不好,但对自身代码的能力还是有所提高的,这是我整个项目的传送门。
  • 如果blog编辑有错欢迎指出错误。

你可能感兴趣的:(Unity3d学习)