本文内容为坦克对战游戏AI设计。
我们构建 AI 对战坦克有几点目标:
· 使用“感知-思考-行为”模型,建模 AI 坦克
· 场景中要放置一些障碍阻挡对手视线
· 坦克要放置一个矩阵包围盒触发器,保证 AI 坦克能使用射线探测对手方位
· AI 坦克必须在有目标条件下使用导航,并能绕过障碍。
· 实现人机对战
我们使用了资源Tanks! Tutorial(Unity资源商店链接)。
接下来我们构建AI对战坦克。
游戏设计
玩家操纵红色坦克,可以通过WASD按键移动坦克,按空格键发射弹药攻击。地图中有一些蓝色的AI坦克,当玩家出现在AI坦克的视野中,它们就会跟踪玩家并向玩家发射弹药。每辆坦克都有自己的血量值,受到攻击坦克血量会下降,当血量值将为0以下,则坦克被击毁。
如果玩家的坦克被击毁则游戏失败,如果玩家击毁了所有的AI坦克则游戏胜利。
对于“感知-思考-行为”模型——
AI坦克会感知周围是否有玩家存在,进行思考:
1.没有玩家,那么就进行行动巡逻。
2.有玩家,那么就进行行动追捕。
继续进行感知:
1.若玩家进入AI坦克的射击范围,则进行行动射击。
2.若玩家没有进入AI坦克的射击范围,则继续进行行动追捕。
游戏展示
场景布置
我们先简单布置场景:
NavMesh是unity提供的导航寻路功能。
我们先将地图设置为“navigation static”,然后选中,选择最顶部的窗口-AI-导航。在Navigation窗口给地图中的各个对象设置walkable或者not walkable等,然后进行bake烘培,得到描述游戏对象可行走的表面的数据结构Navigation Mesh。
可通过这些三角网格计算其中任意两点之间的最短路径用于游戏对象的导航,作为“感知-思考-行为”模型中的“感知”。
然后我们制作三种预制件——敌方(AI)坦克、玩家坦克和炮弹。
脚本编写
我们考虑游戏的双方:AI坦克和玩家坦克的共性——都有血量。因而我们可以先定义一个共同的基类Tank如下:
public class Tank : MonoBehaviour {
private float hp; // 血量
public float getHp() // 获取血量
{
return hp; // 返回血量
}
public void setHp(float hp) // 设置血量
{
this.hp = hp; // 设置血量
}
}
然后对于坦克的一些事件,我们有接口类IUserAction
,包含坦克前进、后退、转向、射击和游戏状态获取:
public interface IUserAction
{
void moveForward();
void moveBack();
void turn(float offsetX);
void shoot();
bool isGameOver();
}
我们有一个场景控制器SceneController
来掌控全局。因此对于玩家的坦克PlayerTank
,我们只需要在其发现自己血量低于零时,销毁自己并发布相应事件共控制器处理即可:
public class PlayerTank : Tank {
public delegate void destroy();
public static event destroy destroyEvent;
void Start (){
setHp(1000f);
}
void Update (){
if(getHp() <= 0){
this.gameObject.SetActive(false);
if (destroyEvent != null)
destroyEvent();
}
}
}
而对于AI坦克,我们有“思考”。如果其在自己附近没有发现玩家,则进行巡逻。若发现了玩家则开始追捕;一旦追捕使得玩家出现在其一定范围内(射程内),则其开炮射击。这一系列就是对应“行动”。
using UnityEngine.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)
}; // AI坦克初始化的目标点
private int destPoint = 0;
private NavMeshAgent agent; // NavMeshAgent组件
private bool isPatrol = false; // 是否巡逻
private void Awake()
{
destPoint = UnityEngine.Random.Range(0, 13); // 随机选择一个点
}
void Start () {
setHp(100f);
StartCoroutine(shoot()); // 开启协程
agent = GetComponent<NavMeshAgent>(); // 获取NavMeshAgent组件
}
private IEnumerator shoot()
{
while (!gameover) // 游戏未结束
{
for(float i = 1; i > 0; i -= Time.deltaTime)
{
yield return 0;
}
if(Vector3.Distance(transform.position, target) < 20) // 如果距离目标点小于20
{
GameObjectFactory mf = Singleton<GameObjectFactory>.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<Rigidbody>(); // 获取子弹的刚体组件
rb.AddForce(bullet.transform.forward * 20, ForceMode.Impulse); // 给子弹一个向前的力
}
}
}
void Update () {
gameover = Director.getInstance().currentSceneController.isGameOver(); // 获取游戏结束状态
if (!gameover)
{
target = Director.getInstance().currentSceneController.getPlayerPos(); // 获取玩家坦克的位置
if (getHp() <= 0 && recycleEvent != null) // 如果血量小于等于0
{
recycleEvent(this.gameObject); // 回收坦克
}
else
{
if(Vector3.Distance(transform.position, target) <= 30) // 如果距离目标点小于等于30
{
isPatrol = false; // 不巡逻
agent.autoBraking = true; // 自动减速
agent.SetDestination(target); // 设置目标点
}
else
{
patrol(); // 巡逻
}
}
}
else
{
NavMeshAgent agent = GetComponent<NavMeshAgent>(); // 获取NavMeshAgent组件
agent.velocity = Vector3.zero; // 坦克停止
agent.ResetPath(); // 重置路径
}
}
private void patrol()
{
if(isPatrol) // 如果正在巡逻
{
if(!agent.pathPending && agent.remainingDistance < 0.5f) // 如果路径没有挂起并且剩余距离小于0.5
GotoNextPoint(); // 前往下一个点
}
else
{
agent.autoBraking = false; // 不自动减速
GotoNextPoint(); // 前往下一个点
}
isPatrol = true;
}
private void GotoNextPoint()
{
agent.SetDestination(points[destPoint]); // 设置目标点
destPoint = (destPoint + 1) % points.Length; // 选择下一个点
}
}
为了生成坦克(和子弹),我们有工厂类Factory
来进行分配和回收:
public enum tankType : int { Player, Enemy } // 坦克类型
public class Factory : MonoBehaviour {
public GameObject player; // 玩家坦克
public GameObject tank; // AI坦克
public GameObject bullet; // 子弹
public ParticleSystem ps; // 爆炸特效
private Dictionary<int, GameObject> usingTanks; // 使用中的坦克
private Dictionary<int, GameObject> freeTanks; // 未使用的坦克
private Dictionary<int, GameObject> usingBullets; // 使用中的子弹
private Dictionary<int, GameObject> freeBullets; // 未使用的子弹
private List<ParticleSystem> psContainer; // 爆炸特效容器
private void Awake()
{
usingTanks = new Dictionary<int, GameObject>();
freeTanks = new Dictionary<int, GameObject>();
usingBullets = new Dictionary<int, GameObject>();
freeBullets = new Dictionary<int, GameObject>();
psContainer = new List<ParticleSystem>();
}
void Start () {
AITank.recycleEvent += recycleTank; // 注册回收坦克事件
}
public GameObject getPlayer()
{
return player; // 返回玩家坦克
}
public GameObject getTank()
{
if(freeTanks.Count == 0) // 如果未使用的坦克数量为0
{
GameObject newTank = Instantiate<GameObject>(tank); // 实例化坦克
usingTanks.Add(newTank.GetInstanceID(), newTank); // 添加到使用中的坦克
newTank.transform.position = new Vector3(Random.Range(-100, 100), 0, Random.Range(-100, 100)); // 设置坦克位置
return newTank; // 返回坦克
}
foreach (KeyValuePair<int, GameObject> 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) // 如果未使用的子弹数量为0
{
GameObject newBullet = Instantiate(bullet); // 实例化子弹
newBullet.GetComponent<Bullet>().setTankType(type); // 设置子弹类型
usingBullets.Add(newBullet.GetInstanceID(), newBullet); // 添加到使用中的子弹
return newBullet; // 返回子弹
}
foreach (KeyValuePair<int, GameObject> pair in freeBullets) // 遍历未使用的子弹
{
pair.Value.SetActive(true); // 设置子弹为激活状态
pair.Value.GetComponent<Bullet>().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<ParticleSystem>(ps); // 实例化特效
psContainer.Add(newPs); // 添加到特效容器
return newPs; // 返回特效
}
public void recycleTank(GameObject tank)
{
usingTanks.Remove(tank.GetInstanceID()); // 从使用中的坦克中移除
freeTanks.Add(tank.GetInstanceID(), tank); // 添加到未使用的坦克
tank.GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0); // 设置坦克速度为0
tank.SetActive(false); // 设置坦克为未激活状态
}
public void recycleBullet(GameObject bullet)
{
usingBullets.Remove(bullet.GetInstanceID()); // 从使用中的子弹中移除
freeBullets.Add(bullet.GetInstanceID(), bullet); // 添加到未使用的子弹
bullet.GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0); // 设置子弹速度为0
bullet.SetActive(false); // 设置子弹为未激活状态
}
}
我们的子弹Bullet
用完即回收:
public class Bullet : MonoBehaviour {
public float explosionRadius = 3f; // 爆炸半径
private tankType type; // 子弹类型(whois)
public void setTankType(tankType type){
this.type = type; // 设置子弹类型(whois)
}
private void Update()
{
if(this.transform.position.y < 0 && this.gameObject.activeSelf){
Factory mf = Singleton<Factory>.Instance; // 获取工厂
ParticleSystem explosion = mf.getPs(); // 获取粒子系统
explosion.transform.position = transform.position; // 设置粒子系统位置
explosion.Play(); // 播放粒子系统
mf.recycleBullet(this.gameObject); // 回收子弹
}
}
void OnCollisionEnter(Collision other)
{
Factory mf = Singleton<Factory>.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<Tank>().getHp(); // 获取被击中坦克的当前血量
colliders[i].GetComponent<Tank>().setHp(current - hurt); // 设置被击中坦克的血量
}
explosion.Play(); // 播放粒子系统
if (this.gameObject.activeSelf) mf.recycleBullet(this.gameObject); // 回收子弹
}
}
我们有导演类Director
和单例类Singleton
,它们和之前作业中的一样。
场景控制器SceneController
调用Factory
类初始化PlayerTank、AITank和Bullet,还有粒子系统等等游戏中的对象。IUserAction
接口中的函数也在这里实现供“坦克”中调用。
public class SceneController : MonoBehaviour, IUserAction {
public GameObject player; // 玩家坦克
private bool gameOver = false; // 游戏状态
private int enemyCount = 6; // AI坦克数量
private Factory mf; // 工厂
private MainCameraControl cameraControl; // 相机控制
private void Awake()
{
Director director = Director.getInstance(); // 获取导演
director.currentSceneController = this; // 设置当前场景控制器
mf = Singleton<Factory>.Instance; // 获取工厂
player = mf.getPlayer(); // 获取玩家坦克
cameraControl = GetComponent<MainCameraControl>(); // 获取相机控制器
cameraControl.setTarget(player.transform); // 设置相机跟随目标
}
void Start () {
for(int i = 0; i < enemyCount; i++)
{
GameObject gb = mf.getTank(); // 使用工厂生成若干AI坦克
cameraControl.setTarget(gb.transform); // 设置相机跟随目标
}
PlayerTank.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<Rigidbody>().velocity = player.transform.forward * 20;
}
public void moveBack() // 玩家坦克后退
{
player.GetComponent<Rigidbody>().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<Rigidbody>(); // 获取子弹刚体
rb.AddForce(bullet.transform.forward * 20, ForceMode.Impulse); // 给子弹施加力
}
}
最后我们有UserOperater
类来获取玩家的输入,从而通过IUserAction
执行场景控制类中定义的行为:
public class UserOperater : MonoBehaviour {
IUserAction action;
void Start () {
action = Director.getInstance().currentSceneController as IUserAction;
}
void Update () {
if (!action.isGameOver())
{
if (Input.GetKey(KeyCode.W))
{
action.moveForward();
}
if (Input.GetKey(KeyCode.S))
{
action.moveBack();
}
if (Input.GetKeyDown(KeyCode.Space))
{
action.shoot();
}
float offsetX = Input.GetAxis("Horizontal");
action.turn(offsetX);
}
}
}
其中“Horizontal”需要我们在项目设置-输入管理器中做相应的设置,我这里设置的是按照玩家习惯的键盘A和D键。
我们的主摄像机会根据地图中坦克的数量来确定视野大小,并随着玩家的操作移动位置,保证玩家可以看到地图中大多数的AI坦克,做出决策。
参考
https://yuandi-sherry.github.io/2018/06/19/Week15-Tanks/
本人的“3D游戏编程与设计”系列合集,请访问:
https://www.yizuodi.cn/category/3DGame/
Comments | NOTHING
<根据相关法律法规要求,您的评论将在审核后展示。