从商店下载游戏:“Kawaii” Tank 或 其他坦克模型,构建 AI 对战坦克。具体要求
- 使用“感知-思考-行为”模型,建模 AI 坦克
- 场景中要放置一些障碍阻挡对手视线
- 坦克需要放置一个矩阵包围盒触发器,以保证 AI 坦克能使用射线探测对手方位
- AI 坦克必须在有目标条件下使用导航,并能绕过障碍。(失去目标时策略自己思考)
- 实现人机对战
让坦克变得智能一点:
- 有目标时进行导航
- 没有目标时自动锁定下一个目标
- 离目标越近速度越慢,小于20时停止
- 当与目标之间没有障碍物时才会进行射击
让坦克变得笨一点:
- 躲避障碍物的动作很慢
- 射击有误差
- 只能以固定频率发射子弹(1s)
在游戏商店下载“Tanks! Tutorial”
自制地图后bake:
预制的地图,坦克和子弹:
在enemy,player上有碰撞器,地图上的几个比较重要的建筑也加上了碰撞器,而子弹有触发器:
enemy,player,bullet有不同的行为,他们都是刚体:
enemy和player都有作为坦克的数据类,它们都是NavMeshAgent:
- Director
导演类,单例模式
public class Director : System.Object { private static Director _instance; public ScenceController scence { get; set;} public static Director getInstance() { if (_instance == null) { _instance = new Director (); } return _instance; } }
- tankData
坦克的数据类
public class tankData : MonoBehaviour { public int hp; //血条 public GameObject bulletPrefab;//子弹预制 private Team team; //队伍 //设置队伍 public void setTeam(Team t){ team = t; } //返回队伍 public Team getTeam(){ return team; } }
- PublicQuote
公共引用的枚举,接口等
//队伍:红队,蓝队 public enum Team:int{Red, Blue} //游戏模式:自动,人机 public enum Mode:int{Auto, Play} //游戏状态:准备,游戏中,红队胜利,蓝队胜利 public enum State:int{Ready, Playing, Win, Lose}
- EventManager
事件发布器
public class EventManager: MonoBehaviour{ //坦克的破坏事件(HP降为0) public delegate void DestroyEvent (GameObject tank); public static event DestroyEvent destroy; //坦克的击中事件 public delegate void HitEvent (GameObject bullet); public static event DestroyEvent hit; public void destroyTank(GameObject tank){ if (destroy != null) { destroy (tank); } } public void hitTank(GameObject bullet){ if (hit != null) { hit (bullet); } } }
- ScenceController
场景管理器
(1) 成员变量
public int enemiesNum = 5; //敌人的数量 public GameObject enemyPrefab; //enemy预制 public GameObject playerPrefab; //player预制 public Mode mode = Mode.Play; //游戏模式 public Material bluePrefabs; //蓝色标志材料 public Material redPrefabs; //红色标志材料 public GameObject bulletExplosion; //子弹爆炸预制 public GameObject tankExplosion; //坦克爆炸预制 //敌人初始位置 private float enemyPos_x = -80; private float enemyPos_y = 40; //玩家初始位置 private Vector3 playerPos = new Vector3(80, 1, -40); private List<GameObject> teamR; //红队列表 private List<GameObject> teamB; //蓝队列表 private GameObject player; //玩家 private State state = State.Ready; //游戏状态 private float explosionTime = 2; //爆炸时长
(2)初始化
void Awake(){ Director.getInstance ().scence = this; teamR = new List<GameObject> (); teamB = new List<GameObject> (); } //添加事件响应 void OnEnable(){ EventManager.destroy += destroyTank; EventManager.hit += hitTank; } void OnDisable(){ EventManager.destroy -= destroyTank; EventManager.hit -= hitTank; }
(3)Update
主要功能是摄像机跟随,判断游戏是否结束
void Update () { if (state == State.Playing) { //游戏结束条件 if (teamR.Count == 0) { state = State.Lose; } else if (teamB.Count == 0) { state = State.Win; } //摄像机位置 if (mode == Mode.Play && state == State.Playing) { Camera.main.transform.position = new Vector3 (player.transform.position.x, Camera.main.transform.position.y, player.transform.position.z); } else if (mode == Mode.Auto && state == State.Playing) { Camera.main.transform.position = new Vector3 (0, 80, 0); } } }
(4)返回函数
用来返回各种信息,主要用来显示GUI
//返回当前游戏状态 public State getState(){ return state; } //返回红队HP public int[] RedHPs(){ int[] phs = new int[teamR.Count]; for (int i = 0; i < teamR.Count; i++) { phs [i] = Mathf.Max(teamR [i].GetComponent<tankData> ().hp, 0); } return phs; } //返回蓝队HP public int[] BlueHPs(){ int[] phs = new int[teamB.Count]; for (int i = 0; i < teamB.Count; i++) { phs [i] = Mathf.Max(teamB [i].GetComponent<tankData> ().hp, 0); } return phs; } //返回红队成员 public List<GameObject> getTeamR(){ return teamR; } //返回蓝队成员 public List<GameObject> getTeamB(){ return teamB; }
(5)游戏开始函数
两种模式的游戏开始,进行坦克的预制实例化
//自动模式 public void startWithAuto(){ mode = Mode.Auto; //实例化红队和蓝队的坦克 for (int i = 0; i < enemiesNum; i++) { //实例化坦克 GameObject enemy = Instantiate<GameObject> (enemyPrefab, new Vector3(-enemyPos_x, 1, -enemyPos_y + 8 * i), Quaternion.identity); //设置队伍为红队 enemy.GetComponent<tankData> ().setTeam (Team.Red); //将标志球变为红色 enemy.transform.GetChild (0).GetComponent<MeshRenderer> ().material = redPrefabs; //加入红队 teamR.Add (enemy); } for (int i = 0; i < enemiesNum; i++) { GameObject enemy = Instantiate<GameObject> (enemyPrefab, new Vector3(enemyPos_x, 1, enemyPos_y - 8 * i), Quaternion.identity); enemy.GetComponent<tankData> ().setTeam (Team.Blue); enemy.transform.GetChild (0).GetComponent<MeshRenderer> ().material = bluePrefabs; teamB.Add (enemy); } state = State.Playing; } //人机模式 public void startWithPlay(){ mode = Mode.Play; //实例化玩家,玩家默认为红队 player = Instantiate<GameObject> (playerPrefab, playerPos, Quaternion.identity); player.GetComponent<tankData> ().setTeam (Team.Red); player.transform.GetChild (0).GetComponent<MeshRenderer> ().material = redPrefabs; teamR.Add (player); //实例化蓝队 for (int i = 0; i < enemiesNum; i++) { GameObject enemy = Instantiate<GameObject> (enemyPrefab, new Vector3(enemyPos_x, 1, enemyPos_y - 8 * i), Quaternion.identity); enemy.GetComponent<tankData> ().setTeam (Team.Blue); enemy.transform.GetChild (0).GetComponent<MeshRenderer> ().material = bluePrefabs; teamB.Add (enemy); } state = State.Playing; }
(6)事件响应函数
之前订阅的事件的响应
//坦克的销毁 private void destroyTank(GameObject tank){ //坦克爆炸效果的实例化 GameObject ex = Instantiate<GameObject> (tankExplosion, tank.transform.position, Quaternion.identity); //播放 ex.GetComponent<ParticleSystem> ().Play (); //一定时间后销毁 Destroy (ex, explosionTime); //根据队伍的不同进行销毁 if (tank.GetComponent <tankData> ().getTeam () == Team.Blue) { teamB.Remove (tank); Destroy (tank); } else { teamR.Remove (tank); Destroy (tank); } } //子弹的爆炸 private void hitTank(GameObject bullet){ //子弹爆炸效果的实例化,同上 GameObject ex = Instantiate<GameObject> (bulletExplosion, bullet.transform.position, Quaternion.identity); ex.GetComponent<ParticleSystem> ().Play (); Destroy (ex, explosionTime); }
- PlayerAction
玩家坦克的动作类,主要是响应键盘的动作
public class PlayerAction : MonoBehaviour{ Director director; //导演对象 private int duration = 10; //射击时间的间隔 //前进 public void moveForward(){ this.GetComponent<Rigidbody> ().velocity = this.transform.forward * 20; } //后退 public void moveBack(){ this.GetComponent<Rigidbody> ().velocity = this.transform.forward * -20; } //转身 public void turn(float offset){ float y = this.transform.localEulerAngles.y + offset * 2; float x = this.transform.localEulerAngles.x; this.transform.localEulerAngles = new Vector3 (x, y, 0); } //射击 public void shoot(){ //子弹预制的实例化 GameObject bullet = Instantiate<GameObject> (this.GetComponent<tankData> ().bulletPrefab, this.transform.position + this.transform.forward * 3f, Quaternion.identity); //子弹所属队伍 bullet.GetComponent<bulletAction> ().team = Team.Red; //子弹方向 bullet.transform.forward = this.transform.forward; //给子弹一个前进的力(子弹不受重力) bullet.GetComponent<Rigidbody> ().AddForce (bullet.transform.forward * 35, ForceMode.Impulse); } // Use this for initialization void Start () { } // Update is called once per frame void Update () { director = Director.getInstance (); if (director != null && director.scence.getState() == State.Playing) { duration--; //通过键盘按钮进行动作 if (Input.GetKey (KeyCode.W)) { moveForward (); } else if (Input.GetKey (KeyCode.S)) { moveBack (); } else { this.GetComponent<Rigidbody> ().velocity = new Vector3 (0, 0, 0); } if (Input.GetKey (KeyCode.Space)) { if (duration < 0) { duration = 10; shoot (); } } float offset = Input.GetAxis ("Horizontal1"); turn (offset); } else { //游戏结束时保持不动 this.GetComponent<Rigidbody> ().velocity = Vector3.zero; this.GetComponent<Rigidbody> ().constraints = RigidbodyConstraints.FreezeAll; } } }
- EnemyAction
敌人的动作类,主要实现了AI的功能
public class EnemyAction : MonoBehaviour { Director director; //导演对象 public GameObject target; //目标坦克 State state; //游戏状态 private bool isIEnumerator = false; //是否进行了协程 private bool canShoot = false; //是否能进行射击 void Start () { director = Director.getInstance (); } void Update () { state = Director.getInstance ().scence.getState(); if (state == State.Playing) { //判断是否能进行射击 judgeShoot (); //判断是否进行协程 if (!isIEnumerator) { StartCoroutine(shoot()); isIEnumerator = true; } //当有目标坦克时进行导航 if (target != null) { this.transform.LookAt (target.transform.position); this.GetComponent<NavMeshAgent> ().SetDestination (target.transform.position); } //当丢失目标时锁定另一个目标 else if(director.scence.getTeamB().Count > 0 && director.scence.getTeamR().Count > 0) { getTarget (); } } //游戏结束时保持不动 else { this.GetComponent<NavMeshAgent> ().velocity = Vector3.zero; this.GetComponent<NavMeshAgent> ().ResetPath (); this.GetComponent<Rigidbody> ().constraints = RigidbodyConstraints.FreezeAll; } } //使坦克不被撞飞 void FixedUpdate(){ this.GetComponent<Rigidbody> ().velocity = new Vector3(0, 0, 0); } //协程,当有目标,距离够近且可以射击时,1s射击一次 IEnumerator shoot() { while(state == State.Playing) { for (float i = 1; i > 0; i -= Time.deltaTime) { yield return 0; } //当距离小于200且可以射击时 if (state == State.Playing && Vector3.Distance(this.transform.position, target.transform.position) < 100 && canShoot) { //当距离小于20,静止不动 if (Vector3.Distance (this.transform.position, target.transform.position) < 20) { this.GetComponent<NavMeshAgent> ().speed = 0; } //否则以1的速度移动(默认是8) else { this.GetComponent<NavMeshAgent> ().speed = 1.0f; } //射击,和玩家射击同理,但注意子弹的瞄准是有误差的 GameObject bullet = Instantiate<GameObject> (this.GetComponent<tankData> ().bulletPrefab, this.transform.position + this.transform.forward * 3f + new Vector3(0, 1, 0), Quaternion.identity); bullet.GetComponent<bulletAction> ().team = this.GetComponent<tankData>().getTeam(); bullet.transform.forward = reloadPos( this.transform.forward); bullet.GetComponent<Rigidbody> ().AddForce (bullet.transform.forward * 20, ForceMode.Impulse); } } } //锁定目标 void getTarget(){ List<GameObject> targets; //得到另一支队伍的成员列表 if (this.GetComponent<tankData> ().getTeam () == Team.Blue) { targets = director.scence.getTeamR (); } else { targets = director.scence.getTeamB (); } //随机抽取 if (targets.Count == 0) return; int max = targets.Count; int random = 0; do { random = (int)Mathf.Floor(Random.Range(0, max)); } while(targets [random] == null); target = targets [random]; } //射击位置的误差 Vector3 reloadPos(Vector3 pos){ //随机-1 到 1之间的数字 float randomX = Random.Range (-1, 1); float randomZ = Random.Range (-1, 1); //加上误差 return new Vector3 (pos.x + randomX / 6, pos.y, pos.z + randomZ / 6); } //判断是否可以射击,向目标发出一条射线如果射线直接到达目标而没有任何阻拦,则说明可以射击,否则不可以 void judgeShoot(){ if (target != null) { Ray ray = new Ray (this.transform.position, target.transform.position - this.transform.position); RaycastHit hit; bool result = Physics.Raycast (ray, out hit, 1000); if (result) { if (hit.collider.gameObject.tag == "enemy" || hit.collider.gameObject.tag == "player") { canShoot = true; return; } } canShoot = false; } } }
导航和用来检测的射线:
- bulletAction
子弹的动作类,主要进行触发器的判断
public class bulletAction : MonoBehaviour { public Team team; //子弹的队伍 void OnTriggerEnter(Collider other){ //如果集中的是坦克 if (other.transform.GetComponent<tankData> ()) { //如果击中的坦克是敌方,则触发击中事件,销毁子弹,hp-1 if (other.transform.GetComponent<tankData> ().getTeam () != team) { Singleton<EventManager>.Instance.hitTank (gameObject); other.transform.GetComponent<tankData> ().hp--; if (other.transform.GetComponent<tankData> ().hp <= 0) { Singleton<EventManager>.Instance.destroyTank (other.gameObject); } Destroy (this.gameObject); } } //如果击中的是建筑物,销毁 else if (other.gameObject.tag == "building"){ Destroy (this.gameObject); } } //超出游戏界面进行销毁 void Update () { if (this.transform.position.x > 100 || this.transform.position.x < -100 || this.transform.position.z > 50 || this.transform.position.z < -50) { Destroy (this.gameObject); } } }
- UserGUI
用来进行模式选择,和显示当前战况
public class UserGUI : MonoBehaviour { Director director = Director.getInstance(); //导演对象 GUIStyle style1, style2, style3; //3种GUI样式 void OnGUI(){ //红蓝双方的剩余坦克数 GUI.Label (new Rect (Screen.width / 2 - 30, Screen.height - 60, 30, 50), director.scence.getTeamB().Count.ToString (), style2); GUI.Label (new Rect (Screen.width / 2, Screen.height - 60, 30, 50), director.scence.getTeamR().Count.ToString (), style1); GUI.Label (new Rect (Screen.width / 2 - 5, Screen.height - 60, 10, 50), ":", style3); //准备状态时,对游戏模式进行选择 if (director.scence.getState () == State.Ready) { if (GUI.Button (new Rect (Screen.width / 2 - 200, Screen.height / 2 - 20, 150, 40), "Auto")) { director.scence.startWithAuto (); } else if (GUI.Button (new Rect (Screen.width / 2 + 200, Screen.height / 2 - 20, 150, 40), "Play")) { director.scence.startWithPlay (); } } //游戏中 else if (director.scence.getState () == State.Playing) { //显示红队的成员的HP for (int i = 0; i < director.scence.RedHPs ().Length; i++) { GUI.Label (new Rect (Screen.width - i * 20 - 20, Screen.height - 60, 20, 50), director.scence.RedHPs () [i].ToString (), style1); } //显示蓝队的成员的HP for (int i = 0; i < director.scence.BlueHPs ().Length; i++) { GUI.Label (new Rect (i * 20 + 20, Screen.height - 60, 20, 50), director.scence.BlueHPs () [i].ToString (), style2); } } //游戏结束,显示哪队的和、胜利 else if (director.scence.getState () == State.Win) { GUI.Label (new Rect (Screen.width / 2 - 100, Screen.height / 2 - 30, 200, 60), "Team Red Win", style1); } else{ GUI.Label (new Rect (Screen.width / 2 - 100, Screen.height / 2 - 30, 200, 60), "Team Blue Win", style2); } } //3种样式的初始化 void Start () { style1 = new GUIStyle(); style1.normal.textColor = Color.red; style1.fontStyle = FontStyle.Bold; style1.fontSize = 15; style1.alignment = TextAnchor.MiddleCenter; style2 = new GUIStyle(); style2.normal.textColor = Color.blue; style2.fontStyle = FontStyle.Bold; style2.fontSize = 15; style2.alignment = TextAnchor.MiddleCenter; style3 = new GUIStyle(); style3.normal.textColor = Color.black; style3.fontStyle = FontStyle.Bold; style3.fontSize = 15; style3.alignment = TextAnchor.MiddleCenter; } }
- Singleton
单例模式
public class Singleton<T> : 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; } } }
文章来源: AI坦克对战(实现人机)