AI坦克建模
-
感知
在游戏中,程序是可以获得游戏世界任意信息的,设计一个杀死玩家的算法通常是比较容易的,因此如何限制信息获取是设计不同级别 agent 的核心问题。在AI坦克大战的游戏中,AI坦克获取的信息是通过视觉(Vision)得到的。每个AI坦克获取的信息是导航信息,但是因为有障碍物的阻挡,使得AI坦克不会“一窝蜂”的同时涌向玩家。同时,AI坦克并不能实时瞄准玩家,也增加了游戏的可玩性。 -
思考
主要使用了Unity自带的寻路组件Navigation 进行“思考”寻路。 -
行动
每个AI坦克都会发射子弹,但是不能让AI坦克一直发射子弹,这样就没得玩了。所以会有一个子弹的准备时间,每间隔一段时间再发射子弹。同时地形凹凸不平,也会使得子弹的轨迹有所偏差。
游戏实现
1. 下载资源
从Assets Store
下载tanks tutorial
资源
2.动作状态机
3. 制作地图
将想要的颜色所对应的材质拖到地图上
最终地图为
4. 制作子弹
Bullet
实现了子弹的相关设计,设置了子弹的爆炸范围,对于玩家和敌方坦克,子弹会有不同的伤害
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 子弹设置 √
public class Bullet : MonoBehaviour {
// 设置爆炸范围
public float range = 3.0f;
private TankType tankType;
// 设置坦克的类型
public void setTankType (TankType type) {
tankType = type;
}
// 设置碰撞事件
private void OnCollisionEnter (Collision collision) {
// 打到自己不算
if (collision.transform.gameObject.tag == "Enemy" && this.tankType == TankType.ENEMY ||
collision.transform.gameObject.tag == "Player" && this.tankType == TankType.PLAYER)
return;
Factory factory = Singleton<Factory>.Instance;
ParticleSystem explosion = factory.getParticleSystem ();
explosion.transform.position = gameObject.transform.position;
// 爆炸范围内的所有物体都受到伤害
Collider[] colliders = Physics.OverlapSphere (gameObject.transform.position, range);
foreach (var collider in colliders) {
// 设置范围伤害
float dis = Vector3.Distance (collider.transform.position, gameObject.transform.position);
float damage;
// 玩家和敌人设置不同的伤害
if (collider.tag == "Enemy" && this.tankType == TankType.PLAYER) {
damage = 50.0f / dis;
collider.GetComponent<Tank> ().setHP (collider.GetComponent<Tank> ().getHP () - damage);
} else if (collider.tag == "Player" && this.tankType == TankType.ENEMY) {
damage = 20.0f / dis;
collider.GetComponent<Tank> ().setHP (collider.GetComponent<Tank> ().getHP () - damage);
}
explosion.Play ();
}
if (gameObject.activeSelf) factory.recycleBullet (gameObject);
}
}
5.制作玩家
Player
实现了玩家的设计,设置了一个代理事件,主要控制玩家坦克被销毁;设置了玩家坦克初始的生命值,当坦克的生命值低于0则被销毁;控制坦克的移动;控制坦克的左右转向
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 玩家设置 √
public class Player : Tank {
// 玩家坦克被摧毁
public delegate void DestroyPlayer ();
public static event DestroyPlayer destroyEvent;
// 设置初始生命值
void Start () { setHP (100); }
void Update () {
if (getHP () <= 0) {
this.gameObject.SetActive (false);
destroyEvent ();
}
}
// 前后方向移动
public void moveForWard () {
gameObject.GetComponent<Rigidbody> ().velocity = gameObject.transform.forward * 20;
}
public void moveBackWard () {
gameObject.GetComponent<Rigidbody> ().velocity = gameObject.transform.forward * (-20);
}
// 实现左右转向
public void turn (float turnX) {
float x = gameObject.transform.localEulerAngles.x;
float y = gameObject.transform.localEulerAngles.y + turnX * 2;
gameObject.transform.localEulerAngles = new Vector3 (x, y, 0);
}
}
6.制作敌方坦克
Enemy
实现了敌人坦克的设置。由于用到了工厂模式,所以在坦克被销毁的时候需要进行回收;并且实现了 AI 功能,启动了协程,控制坦克的运动和射击操作;在坦克看到玩家的时候会进行跟踪移动,然后在射程范围内进行攻击。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
// 敌人坦克设置 √
public class Enemy : Tank {
// 敌人坦克被摧毁,进行回收
public delegate void RecycleEnemy (GameObject enemy);
public static event RecycleEnemy recycleEnemy;
// 玩家位置
private Vector3 playerPos;
// 游戏是否结束
private bool gameover;
private void Start () {
playerPos = GameDirector.getInstance ().currentSceneController.getPlayer ().transform.position;
// 启动协程
StartCoroutine (shoot ());
}
void Update () {
playerPos = GameDirector.getInstance ().currentSceneController.getPlayer ().transform.position;
gameover = GameDirector.getInstance ().currentSceneController.GameOver ();
if (!gameover) {
if (getHP () <= 0 && recycleEnemy != null) recycleEnemy (this.gameObject);
else {
// 向玩家移动
NavMeshAgent agent = gameObject.GetComponent<NavMeshAgent> ();
agent.SetDestination (playerPos);
}
} else {
// 游戏结束后停止运动
NavMeshAgent agent = gameObject.GetComponent<NavMeshAgent> ();
agent.velocity = Vector3.zero;
agent.ResetPath ();
}
}
// 控制射击
IEnumerator shoot () {
while (!gameover) {
for (float i = 1; i > 0; i -= Time.deltaTime) yield return 0;
// 射程
if (Vector3.Distance (playerPos, gameObject.transform.position) < 10) shoot (TankType.ENEMY);
}
}
}
7. Factory
实现了工厂模式,来生产和销毁坦克和子弹;主要设计了初始化游戏物体;生产坦克,生产子弹;回收坦克,回收子弹
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum TankType { PLAYER, ENEMY }
// 工厂模式
public class Factory : MonoBehaviour {
public GameObject player;
public GameObject enemy;
public GameObject bullet;
public ParticleSystem explosion;
private List<GameObject> usedTanks;
private List<GameObject> freeTanks;
private List<GameObject> usedBullets;
private List<GameObject> freeBullets;
private GameObject player_t;
private List<ParticleSystem> particles;
// 初始化
private void Awake () {
usedTanks = new List<GameObject> ();
freeTanks = new List<GameObject> ();
usedBullets = new List<GameObject> ();
freeBullets = new List<GameObject> ();
particles = new List<ParticleSystem> ();
player_t = GameObject.Instantiate<GameObject> (player) as GameObject;
player_t.SetActive (true);
player_t.transform.position = Vector3.zero;
}
void Start () { Enemy.recycleEnemy += recycleEnemy; }
// 获取玩家
public GameObject getPlayer () {return player_t;}
// 获取敌人
public GameObject getEnemys () {
GameObject newT = null;
if (freeTanks.Count <= 0) {
newT = GameObject.Instantiate<GameObject> (enemy) as GameObject;
usedTanks.Add (newT);
newT.transform.position = new Vector3 (Random.Range (-100, 100), 0, Random.Range (-100, 100));
} else {
newT = freeTanks[0];
freeTanks.RemoveAt (0);
usedTanks.Add (newT);
}
newT.SetActive (true);
return newT;
}
// 获取子弹
public GameObject getBullets (TankType type) {
GameObject newBullet;
if (freeBullets.Count <= 0) {
newBullet = GameObject.Instantiate<GameObject> (bullet) as GameObject;
usedBullets.Add (newBullet);
newBullet.transform.position = new Vector3 (Random.Range (-100, 100), 0, Random.Range (-100, 100));
} else {
newBullet = freeBullets[0];
freeBullets.RemoveAt (0);
usedBullets.Add (newBullet);
}
newBullet.GetComponent<Bullet> ().setTankType (type);
newBullet.SetActive (true);
return newBullet;
}
// 获取粒子系统
public ParticleSystem getParticleSystem () {
foreach (var particle in particles){
if (!particle.isPlaying) return particle;
}
ParticleSystem newT = GameObject.Instantiate<ParticleSystem> (explosion);
particles.Add (newT);
return newT;
}
// 回收坦克
public void recycleEnemy (GameObject enemyTank) {
usedTanks.Remove (enemyTank);
freeTanks.Add (enemyTank);
enemyTank.GetComponent<Rigidbody> ().velocity = Vector3.zero;
enemyTank.SetActive (false);
}
// 回收子弹
public void recycleBullet (GameObject Bullet) {
usedBullets.Remove (Bullet);
freeBullets.Add (Bullet);
Bullet.GetComponent<Rigidbody> ().velocity = Vector3.zero;
Bullet.SetActive (false);
}
}
GameDirector
实现了总导演的设计,主要是得到实例化
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 总导演 √
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;
}
}
IUserAction
实现了动作的接口;主要有控制玩家坦克的转动以及行进;控制射击;控制游戏是否结束
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 动作接口 √
public interface IUserAction {
// 控制转动和行进
void moveForWard ();
void moveBackWard ();
void turn (float turnX);
// 控制射击
void shoot ();
// 控制游戏结束
bool GameOver ();
}
IUserGUI
实现了游戏界面的设置;主要是设计了监听键盘的输入,来控制坦克的行进以及攻击的操作;并且在游戏结束后显示 Game Over!
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 设置界面 √
public class IUserGUI : MonoBehaviour {
IUserAction action;
void Start () {
action = GameDirector.getInstance ().currentSceneController as IUserAction;
}
void Update () {
if (!action.GameOver ()) {
// 监听键盘
if (Input.GetKey (KeyCode.W)) action.moveForWard ();
if (Input.GetKey (KeyCode.S)) action.moveBackWard ();
if (Input.GetKeyDown (KeyCode.Space)) action.shoot ();
float turnX = Input.GetAxis ("Horizontal");
action.turn (turnX);
}
}
void OnGUI () {
// 游戏结束
if (action.GameOver ()) {
GUIStyle font = new GUIStyle ();
font.fontSize = 50;
font.normal.textColor = Color.red;
GUI.Label (new Rect (Screen.width / 2 - 100, Screen.height / 2 - 50, 200, 50), "Game Over!", font);
}
}
}
SceneController
实现了场景控制器。用来生产玩家和敌人;并且实现了第一视角的设置,将相机的位置始终跟随玩家;可以获取玩家的物体;控制玩家进行移动;控制玩家进行设计;返回游戏的状态并且可以设置游戏的状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 场景控制器 √
public class SceneController : MonoBehaviour, IUserAction {
public GameDirector director;
public GameObject player;
private Factory myFactory;
private GameObject[] enemies;
private int enemyCount = 5;
private bool gameOver = false;
// 初始化
private void Awake () {
director = GameDirector.getInstance ();
director.currentSceneController = this;
myFactory = Singleton<Factory>.Instance;
enemies = new GameObject[enemyCount];
gameOver = false;
}
// 生产玩家和敌人
void Start () {
player = myFactory.getPlayer ();
for (int i = 0; i < enemyCount; ++i) enemies[i] = myFactory.getEnemys ();
Player.destroyEvent += setGameOver;
}
// 设置相机的位置
void Update () {
Camera.main.transform.position = new Vector3 (player.transform.position.x, 20, player.transform.position.z);
}
// 获取玩家游戏物体
public GameObject getPlayer () { return player; }
// 控制玩家移动
public void moveForWard () { player.GetComponent<Player> ().moveForWard (); }
public void moveBackWard () { player.GetComponent<Player> ().moveBackWard (); }
public void turn (float turnX) { player.GetComponent<Player> ().turn (turnX); }
// 控制玩家射击
public void shoot () { player.GetComponent<Player> ().shoot (TankType.PLAYER); }
// 返回游戏状态
public bool GameOver () { return gameOver; }
// 游戏结束
public void setGameOver () { gameOver = true; }
}
将IUserGUI
、SceneController
、Factory
挂载到相机上
Singleton
实现了单实例模式
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 场景单实例模式 √
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));
return instance;
}
}
}
Tank
实现了坦克的相关属性;主要设置了坦克初始的生命值为100;并且设置了 get/set 方法。以及设置被攻击后生命值的减少;射击子弹射击的方向以及力度
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 设置坦克相关属性 √
public class Tank : MonoBehaviour {
private float health = 100.0f;
// 初始化生命值
public Tank () { health = 100.0f; }
// 设置/获取生命值
public void setHP (float health) { this.health = health; }
public float getHP () { return health; }
// 被攻击
public void Attacked () { health -= 20; }
// 设置子弹射击
public void shoot (TankType type) {
GameObject bullet = Singleton<Factory>.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<Rigidbody> ().AddForce (bullet.transform.forward * 20, ForceMode.Impulse);
}
}
8. 导航
之前已经给玩家和敌方坦克添加了Nav Mesh Agent
组件,在菜单栏点击窗口-AI-导航,在右侧窗口点击烘培,点击下方Bake
按钮,生成导航网格图
游戏说明
玩家通过键盘上的wsad
按键控制坦克的移动,玩家是蓝色坦克,敌人是绿色坦克,当玩家暴露在敌人的视野中,敌人就会跟踪玩家,并向玩家发送子弹,玩家要尽可能躲避子弹,按空格键可以向敌人发射子弹。每个坦克都有自己的生命值,当生命值小于零时会被销毁,当玩家坦克被销毁时,游戏结束。
实现效果
- 游戏地图
- 游戏进行中
- 游戏结束
视频效果
项目地址
将Assets
文件夹下载到本地,然后直接通过unity打开即可成功运行,或者新建一个项目,用Assets
覆盖原来的文件夹,打开后即可正常运行。