作业要求
- 使用“感知-思考-行为”模型,建模 AI 坦克
- 场景中要放置一些障碍阻挡对手视线
- 坦克需要放置一个矩阵包围盒触发器,以保证 AI 坦克能使用射线探测对手方位
- AI 坦克必须在有目标条件下使用导航,并能绕过障碍。(失去目标时策略自己思考)
- 实现人机对战
场景设计
本来是想要沿用巡逻兵的场景布置,不过在商店中发现了一个坦克的资源包,其中包含了一个非常不错的场景,所以这里就直接使用其给出的场景:
场景布置完成之后,我们还需要设置游戏对象的Navigation,如果是障碍物则设置Navigation Area为not walkable,然后进行烘焙,以便AI寻路:
代码解析
在之前的游戏中,我们已经将单例工厂模式运用了很多次了,这里我们的重点是AI坦克的设计。思路和巡逻兵比较类似,如果没有发现玩家,就一直进行巡逻,如果发现了玩家,就进行跟随;在跟随的过程中,如果距离小于某个阈值,就发射炮弹攻击玩家。
其Update函数如下所示:
// Update is called once per frame
void Update ()
{
gameover = GameDirector.getInstance().currentSceneController.isGameOver();
if (!gameover)
{
target = GameDirector.getInstance().currentSceneController.getPlayerPos();
// have been destoryed
if (getHp() <= 0 && recycleEvent != null)
{
recycleEvent(this.gameObject);
}
else
{
// approach the player
if (Vector3.Distance(transform.position, target) <= 30)
{
isPatrol = false;
agent.autoBraking = true;
agent.SetDestination(target);
}
// patrol
else
{
patrol();
}
}
}
else
{
NavMeshAgent agent = GetComponent<NavMeshAgent>();
agent.velocity = Vector3.zero;
agent.ResetPath();
}
}
在执行巡逻函数的时候,我们判断AI坦克是否处于巡逻状态,在没有发现玩家的时候,进行随机移动:
// patrol
private void patrol()
{
if (isPatrol)
{
if(!agent.pathPending && agent.remainingDistance < 0.5f)
GoOn();
}
else
{
agent.autoBraking = false;
GoOn();
}
isPatrol = true;
}
随机移动的函数,是通过选取预设点来实现的:
// move to the next point
private void GoOn()
{
agent.SetDestination(points[destPoint]);
destPoint = (destPoint + 1) % points.Length;
}
然后就是需要我们考虑生命值的问题了,也就是子弹击中坦克之后扣减生命值,这里我们是直接找爆炸范围内的所有碰撞体,如果要设计的更加真实的话,可以考虑击中的部位是坦克的前方还是后方、根据击中的部位不同,造成的伤害也不同:
void OnCollisionEnter(Collision other)
{
GameObjectFactory mf = Singleton<GameObjectFactory>.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);
}
}
然后,通过工厂来统一管理玩家、 AI、子弹、爆炸粒子系统等游戏对象,比如这里的爆炸粒子效果:
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;
}
在添加的时候,这个资源包中也已经给出了现成的粒子效果:
非常方便,我们可以直接利用。
在控制移动方面,我们利用WASD来进行移动,空格进行开火:
if (Input.GetKey(KeyCode.W))
{
action.moveForward();
}
if (Input.GetKey(KeyCode.S))
{
action.moveBackWard();
}
if (Input.GetKeyDown(KeyCode.Space))
{
action.shoot();
}
需要注意的是,这里我们判断了当前坦克是在前进还是后退,如果是在后退的话,为了符合我们的游戏习惯,左右方向需要反向:
if (action.isMovingForward())
action.turn(offsetX);
else
action.turn(-offsetX);
如果不进行反向操作,整个移动过程会变得非常奇怪,不符合一般规则。
最后是在本次的相机设置上,由于它已经有一个关于相机的脚本,所以我们可以直接沿用,并重新自主设计了其size的值,使得整个游戏看起来比较清晰:
可优化部分
- 在之前所说的,我们可以根据子弹打中坦克的部位来设置不同的伤害值,这样会让游戏变得更有趣;
- 另外,我们也可以参考上一次作业,给坦克加上血条,这样就可以很清晰的知道自己的生命值;
- 在控制坦克移动的过程中,有些时候,遇见障碍物的时候转弯幅度会比较大,这一点也是有待优化的部分;
- AI设计上,可以让AI更加智能,比如设计一个根据当前生命值的多少采取不同的策略:如果生命值偏低,那么AI会主动避开战斗,让其他的AI坦克先去围剿玩家,这样会使得游戏更加丰富。