【Unity俯视角射击】我们来做一个《元气骑士》的完整Demo(二)

作者:Yumir
hi~我是Yumir。
今天要分享的内容是进阶版的AI寻路,之前分享过使用A*Pathfinding官方提供的AI脚本进行移动,但是要做一个完整的游戏使用该脚本有诸多不便,于是决定还是利用官方提供的寻路接口自己写一个AI移动脚本。
在线游戏链接:https://connect.unity.com/mg/other/untitled-9864
怪物设计
目前我用到的怪物角色有下图三种,其中三号怪物放大一倍之后作为Boss使用于Boss房。

①野猪怪
一号怪物的状态我用了待机、巡逻(实际上就是乱跑)还有死亡等三个状态,当野猪怪在奔跑途中撞到玩家会对玩家造成伤害。
②吐泡泡的花
二号怪物是在出生点不会乱动的怪物,所以只有待机、攻击、死亡三个状态,当玩家进入二号怪物的攻击范围时二号怪物会向四周发射泡泡保护自己。
③戴面具的人形生物
三号怪是等级比较高的怪物,可以和玩家一样使用武器,所以我给他配了一把和玩家一样的玩具枪,状态机相对也更复杂,有待机、巡逻、追逐、攻击、死亡等五个状态。
④Boss
关于四号怪其实我一开始不打算这样设计的,后来发现元气骑士的新手教程也是把三号怪放大当Boss用,而且在弹幕设计方面我确实并不擅长,所以暂时先这样设计了,可以以后再优化。
Boss使用的武器是权杖,由于本身伤害比较大范围比较广,还进行追逐的话游戏体验就基本没有了,所以Boss的状态有:待机、巡逻、攻击、死亡。在Boss进行攻击之后在“巡逻”和“攻击”中随机一个作为下一个状态。
设置A*寻路
①设置寻路网格
之前的文章已经写过这部分内容,简略的说一下:
1.在官网:https://arongranberg.com/astar/
点击Download选项,在跳转到的页面上选择下载”Free“版本,将下载下来的文件导入到unity中。
2.新建一个空物体,点击”AddComponent“搜索”Pathfinder“添加该组件。添加组件之后面板显示如下,点击图中框选按钮添加Grid Graph(Graphs>Grid Graph)。

3.将”2D“和”Use 2D Physic“勾选,再在第三个红框的位置选择一个Layer,同时将场景中的障碍物的Layer都设置为对应的Layer。

4.点击Scan按钮,生成寻路网格。

②编写寻路脚本
之前的文章用了官方提供的寻路脚本,其实使用官方的提供的路径计算接口自己编写一个寻路脚本也是很简单的。官方也提供了范例脚本,文末会贴上知乎大佬的翻译贴。
寻路这件事其实可以拆解成两个步骤,现代人准备去一个不认识路的目的地的情况下会怎么做?
首先,打开地图进行路线搜索,其实在没有地图app的时代问路的原理也是一样,说白了就是规划路线,然后就是沿着路线向目的地出发了。
AI寻路的道理也一样,先计算路径,再沿路径移动:
1.计算路径
在A*Pathfinding中只要调用Seeker组件里的StartPath方法就可以进行路径的计算。
由于需要使用Seeker组件所以需要先在需要寻路的角色(就是游戏中的怪物)身上添加Seeker组件,然后新建一个脚本AstarAI编写如下代码:
using Pathfinding;
public class AstarAI : MonoBehaviour {
public Transform targetPosition;
public void Start () {
//获取到A*Pathfinding提供的路径计算接口所在的脚本。
Seeker seeker = GetComponent<Seeker>();
//调用路径计算方法,这里需要一个回调方法,但是是测试代码所以没有写内容。
seeker.StartPath(transform.position, targetPosition.position, OnPathComplete);
}
public void OnPathComplete (Path p) {
Debug.Log("Yay, we got a path back. Did it have an error? " + p.error);
}
}
2.沿路径移动

当寻路成功的时候OnPathComplete方法会被调用,我们将获得一个Path,在将他装到兜里之前需要先确认下他是不是寻路成功了。
public void OnPathComplete (Path p) {
if (!p.error) {
path = p;
currentWaypoint = 0;
}
获取到的Path中有一个Vector3类型的列表,这是我们需要的路径点集合,接下来只需要让角色从第一个路径点开始不断朝着下一个路径点移动就可以了,案例代码比较长,直接把我最终的代码放到下文。
由于上面的方法只是在Start中调用StartPath进行一次寻路,目标点是固定不变的。
而游戏中玩家不是站着等怪物找的,也就需要每隔一段时间更新目标位置重新规划路线,考虑到AI状态机的需求,我将上面两个操作分别封装成了方法。
由于玩家可能动也可能不动,所以添加了一个目标点移动距离超过1的条件,这样就不会重复没有必要的路径计算。
public void OnPathComplete(Path p){
if (!p.error){
path = p;
currentWaypoint = 0;
}
}
public void UpdatePath(Vector2 targetPosition){
if (Vector2.Distance(targetPosition, targetLastPosition) > 1){
targetLastPosition = targetPosition;
seeker.StartPath(transform.position, targetPosition, OnPathComplete);
}
}
控制角色向下一个目标点前进的方法:
public void NextTarget(){
if (path == null){ return; }
reachedEndOfPath = false;//标记是否已经到达目标点
float distanceToWaypoint;
while (true){
distanceToWaypoint = Vector3.Distance(transform.position, path.vectorPath[currentWaypoint]);
if (distanceToWaypoint < nextWaypointDistance){
if (currentWaypoint + 1 < path.vectorPath.Count){ currentWaypoint++; }
else{ reachedEndOfPath = true; break; }
}
else{ break; }
}
var speedFactor = reachedEndOfPath ? Mathf.Sqrt(distanceToWaypoint / nextWaypointDistance) : 1f;
if (!reachedEndOfPath){
nextTargetPosition = path.vectorPath[currentWaypoint];
Vector3 dir = (nextTargetPosition - transform.position).normalized;
Vector3 velocity = dir * speed * speedFactor;
transform.position += velocity * Time.deltaTime;
}
else{ path = null; }
}
怪物AI逻辑实现
文章篇幅有限,所以只举例三号怪物的实现过程,因为其他怪的AI逻辑都相对简单,相信看这个文章的朋友看完三号怪物的AI功能实现过程就可以自己做出其他的AI效果了。我在游戏设计上并没有做的很好建议自己扩展设计。

①待机
游戏中怪物是在玩家进入怪物所在的房间才需要开始计算路径的,所以在玩家进入房间前需要一个待机状态,在该状态下怪物的动画状态机播放idle动画,不需要额外设置。
当玩家进入房间时,系统将该房间所有的怪物的标记位“isStart”设置为true,怪物由待机转为巡逻。
void Idle()
{
if (isStart)
{
monsterState = MonsterState.Stroll;
animator.SetBool("run", true);
}
}
②巡逻
设置这个状态主要是希望怪物有“视野”这个设定,如果没有视野的话最后就会变成一堆怪追着玩家和玩家一起绕柱走的状态,游戏体验极差,可以说是最基本的AI需求了。
显然我不可能每个怪物去设置巡逻点,随机寻路目标位置有一个很简单的算法——在圆里随机一个点(然后计算新的路径)。
public void RandomPath(){
var point = Random.insideUnitSphere * randomRadius;
point += transform.position;
UpdatePath(point);
}
同时需要检测怪物是否可以看到玩家:
public void RaycastDetection(){
hit = Physics2D.Raycast(transform.position + Vector3.up, (targetPosition.position - (transform.position + Vector3.up)).normalized, trackingRange, layerMask);
if (hit.transform != null && hit.transform == targetPosition){
seeTarget = true;
Debug.DrawLine(transform.position + Vector3.up, hit.transform.position, Color.red);
}
else{ seeTarget = false; }
}
在巡逻方法中只需要调用控制角色向下一个目标点前进的方法,并且使怪物看向下一个目标点。每隔一段时间随机一个新的目标点并计算路径,当怪物看到玩家(seeTarget==true)时切换到追逐状态。
void Stroll()
{
RaycastDetection();
if (seeTarget){ monsterState = MonsterState.Tracking; }
UpdateLookAt(myAI.nextTargetPosition);
if (Time.time - strolltiming >= strollCD){
strolltiming = Time.time;
myAI.RandomPath();
}
myAI.NextTarget();
}

③追逐
追逐状态下无非就是追到和没追到,这个需要用距离和视线判断,该状态下需要一直调用计算路径方法和往下一个路径点移动的方法,如果和玩家距离小于等于攻击距离就进入攻击状态,如果脱离了视野就回到巡逻状态。
void Tracking(){
myAI.UpdatePath(targetPosition.position);
myAI.NextTarget();
if (Vector2.Distance(transform.position, targetPosition.position) <= attackRange){
monsterState = MonsterState.Attack;
animator.SetBool("run", false);
}
RaycastDetection();
if (!seeTarget){ monsterState = MonsterState.Stroll; }
UpdateLookAt(myAI.nextTargetPosition);
}

④攻击
我在攻击状态下设置了攻击计时器,主要是因为怪物攻击太快的话我打不过,如果你打得过你可以不设(\doge)然后就是根据距离切换回追逐状态,和面向玩家,没啥可说的。
void Attack(){
if (Vector2.Distance(transform.position, targetPosition.position) > attackRange){
monsterState = MonsterState.Tracking;
}
UpdateLookAt(targetPosition.position);
if (Time.time - attacktiming >= attackCD){
attacktiming = Time.time;
weapon.ShootButtonDown();
}
}
⑤死亡
在任何状态下,只要血量为0就会死亡,所以该状态的入口是写在被攻击的方法里面的,三号怪物死亡时会向后弹飞一段距离,这个我也是用寻路实现了:
public override void BeAttack(float data){
base.BeAttack(data);
if (hp <= 0){
myAI.UpdatePath((transform.position - targetPosition.position).normalized * 2 + transform.position);
weapon.gameObject.SetActive(false);
}
}
void Die(){ myAI.NextTarget(); }
可以看到我的BeAttack是Override,因为这个血量为0的状态转换是所有的怪通用的,所以我将通用逻辑都写在了Monster父类中(包括死亡动画以及生成怪物掉落奖励等功能),再在子类中分别处理特殊要求。
public virtual void BeAttack(float data){
hp -= data;
if (hp <= 0)
{
monsterState = MonsterState.Die;
GetComponent<Animator>().SetBool("die", true);
GetComponent<Collider2D>().enabled = false;
room.MonsterDie(this);
for (int i = 0; i < coin; i++){//掉落金币
Instantiate(GameManager.instance.coinPre, transform.position, Quaternion.identity);
}
for (int i = 0; i < magic; i++){//掉落魔晶石
Instantiate(GameManager.instance.mpPre, transform.position, Quaternion.identity);
}
}
else{
GetComponent<Animator>().Play("BeAttack");
GameManager.instance.ShowAttack(data, Camera.main.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0)));
}
}
关于怪物AI我已经倾囊相授啦~接下来是游戏的小地图绘制分享,游戏项目以及资源我会在游戏完成之后整理(放在第四篇文章)需要的同学可以关注一下。
知乎上有大佬翻译了官方教程,下面这篇是本文提到的AI移动脚本,我认为比起去官网翻看相对来说更方便一些,有兴趣可以看看:
https://zhuanlan.zhihu.com/p/69703555

欢迎加入游戏开发群欢乐搅基:1082025059
对游戏开发感兴趣的童鞋可戳这里进一步了解:http://www.levelpp.com/