项目地址:https://github.com/kotomineshiki/AIFindPath
视频地址:多重寻路
综合寻路——包括攻击考量的寻路算法
GamePlay
这是一个《文明》+皇室战争的组合。
UI层使用状态机来实现以下操作
1. 点击棋子再点击格子,棋子移动向格子(AStar算法)
2. 点击棋子会弹出该棋子的属性介绍面板,再点击一次取消
3. 点击格子会弹出该格子的属性介绍面板,再点击一次取消
4. 点击底部召唤板,再点击格子,会在该格子处召唤一个棋子
5. 点击召唤板未点击格子时,会在鼠标停留的格子上出现一个虚影
6. 点击棋子再点击另一个棋子的时候,该棋子移动向另一个棋子,如果他们处于敌对阵营,则该棋子列入攻击列表
一些bug:
AStar算法不要使用多线程,因为如果节点情况有变,则会产生线程合并的冲突导致抖动。
其实Astar算法和作为AI的势能场算法是有冲突的,下次重构的时候应该特别注意只使用势能场。因为势能场本身就已经包括了寻路算法,所以没必要特意写一个寻路算法增加代码复杂度,这是这次经验不足导致的。
两个核心难点
Astar算法
先是自己实现了一遍,后来找到了效率更高的插件就重构了
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyAStar {
public ArrayList openList;
public ArrayList closeList;
public int targetX;
public int targetY;
public Vector2Int start;
public Vector2Int end;
public Stack<string> parentList;//结果栈,存的格式是xy
public List<Vector2Int> resultPath;//结果队列
//public Transform plane;
//public Transform obstacle;
private float alpha = 0;
private float incrementPer = 0;
public void SetStartGrid(int x, int y)
{
start.x = x;
start.y = y;
GridMap.instance.grids[x, y].SetGridType(GridType.Start);
openList.Add(GridMap.instance.grids[x, y]);
}
public void SetEndGrid(int x, int y)
{
end.x = x;
end.y = y;
GridMap.instance.grids[x, y].SetGridType(GridType.End);
}
public void Clear()
{
GridMap.instance.grids[start.x, start.y].SetGridType(GridType.Normal);
GridMap.instance.grids[end.x, end.y].SetGridType(GridType.Normal);//恢复初始状态
for(int i = 0; i < GridMap.instance.row; ++i)
{
for(int j = 0; j < GridMap.instance.column; ++j)
{
GridMap.instance.grids[i, j].parent = null;
GridMap.instance.grids[i, j].f = 0;
GridMap.instance.grids[i, j].g = 0;
GridMap.instance.grids[i, j].h = 0;
}
}
parentList.Clear();
openList.Clear();
closeList.Clear();
resultPath.Clear();
}
public MyAStar()//初始化函数
{
parentList = new Stack<string>();
openList = new ArrayList();
closeList = new ArrayList();
resultPath = new List<Vector2Int>();
Debug.Log("初始化完成");
}
public void Calculate()
{
Debug.Log("开始寻找路径");
//yield return new WaitForSeconds(0.1f);
//openList.Add(grids[startX, startY]);
MyGrid currentGrid = openList[0] as MyGrid;
while (openList.Count > 0 && currentGrid.gridType != GridType.End)
{
currentGrid = openList[0] as MyGrid;
if (currentGrid.gridType == GridType.End)
{
Debug.Log("找到路径");
GenerateResult(currentGrid);
}
for(int i = -1; i <= 1; i++)
{
for(int j = -1; j <= 1; j++)
{
if (i != 0 || j != 0)
{
int x = currentGrid.x + i;
int y = currentGrid.y + j;
if (x >= 0 && y >= 0 && x < GridMap.instance.row && y < GridMap.instance.column
&& GridMap.instance.grids[x, y].landForm != LandForm.Obstacle
&& !closeList.Contains(GridMap.instance.grids[x, y]))
{
int g = currentGrid.g + (int)(Mathf.Sqrt((Mathf.Abs(i) + Mathf.Abs(j))) * 10);
if (GridMap.instance.grids[x, y].g == 0 || GridMap.instance.grids[x, y].g > g)
{
GridMap.instance.grids[x, y].g = g;
GridMap.instance.grids[x, y].parent = currentGrid;
}
GridMap.instance.grids[x, y].h = Manhattan(x, y);
GridMap.instance.grids[x, y].f = GridMap.instance.grids[x, y].g + GridMap.instance.grids[x, y].h;
if (!openList.Contains(GridMap.instance.grids[x, y]))
{
openList.Add(GridMap.instance.grids[x, y]);
}
openList.Sort();
}
}
}
}
closeList.Add(currentGrid);
openList.Remove(currentGrid);
if (openList.Count == 0)
{
Debug.Log("未找到路径");
}
}
}
void GenerateResult(MyGrid currentGrid)
{
if (currentGrid.parent != null)
{
parentList.Push(currentGrid.x + "|" + currentGrid.y);
resultPath.Add(new Vector2Int(currentGrid.x, currentGrid.y));
GenerateResult(currentGrid.parent);
}
}
IEnumerator ShowResult()
{
Debug.Log("显示开始"+parentList.Count);
yield return new WaitForSeconds(0.3f);
incrementPer = 1 / (float)parentList.Count;
while (parentList.Count != 0)
{
Debug.Log("走一步"+ parentList.Count);
string str = parentList.Pop();
yield return new WaitForSeconds(0.3f);
string[] xy = str.Split(new char[]
{
'|'
});
int x = int.Parse(xy[0]);
int y = int.Parse(xy[1]);
alpha += incrementPer;
GridMap.instance.objs[x, y].transform.GetChild(0).GetComponent<MeshRenderer>().material.color
= new Color(1 - alpha, alpha, 0, 1);
}
}
int Manhattan(int x,int y)//曼哈顿距离
{
return (int)(Mathf.Abs(targetX - x) + Mathf.Abs(targetY - y)) * 10;
}
}
AI部分
AI移动倾向需要满足的要求(按照意愿的优先级从高到低):
但友军在和敌人交战的时候,很愿意前去支援
愿意和同阵营的棋子在一起(夹击有加成,更加安全)
愿意前往距离敌军远的、属于敌人的格子(可以占领地盘)
愿意前往比较近的地方,而不愿意前往比较远的地方。
AI攻击需要满足的要求(按照意愿的优先级从高到低):
当有友军在攻击敌军时,会很愿意攻击被友军攻击的敌人
当自己血量较低的时候,不愿意进行攻击,愿意逃跑
当附近有敌人的时候,会进行攻击
实现方法:势能场+贪心算法
实现思路:给每个格子赋予一个优先度属性,每满足一个条件可以增加一些优先度(势能),每次需要做出决断的时候,计算对于该棋子的当前场面上的所有的格子(计算势能场分布),寻找估价最高的格子并选择前往(此处和势能场稍微有些不同,一般的势能场寻路算法是循着导数梯度到达势能的极值点,而因为寻路算法已经选用了AStar算法,就不再需要此处再写了—注意此处隐藏了一个冲突)每一次判断都选用对于当前局面能选择的最优秀的决策。
攻击判定:每到达一个格子会判断是继续行走还是对在该格子攻击范围内的敌人进行攻击。攻击动作也有一个估价函数。
每次完成一次行走或者攻击或者路径被中断时,就会向ChessManager通过观察者模式进行询问,查找下一步该做的事。
势能场介绍:2D的势能场是围绕某一个点(或者设计者期望拥有的属性)进行一个估值,总势能场是所有估值的叠加。AI应该选择当前附近势能变化最快(梯度)的走,也可以选择周围势能最高点作为目的地。对于障碍物,应该把势能设置为(无穷小|无穷大),这样在选择路径的时候,AI会“讨厌”往障碍物处走
using AStar_2D;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Thinking : MonoBehaviour {//这个类是用来估值并决策的
public int[,] Value;//用来计算各个格子的估值的
public int X;
public int Y;
public Camp myself;
public List<Index> answer;
public List<Chess> canAttack;
public ArrayList AttackValue;
// Use this for initialization
void Start() {
X = GameManager.instance.gridMap.gridX;
Y = GameManager.instance.gridMap.gridY;
answer = new List<Index>();
canAttack = new List<Chess>();
myself = this.GetComponent<Chess>().camp;
Value = new int[X, Y];
AttackValue = new ArrayList();
Clear();
//GetComponent<Thinking>().SeekWhatToDo();
//StartCoroutine(trythink());
}
/*IEnumerator trythink()
{
yield return new WaitForSeconds(1);
SeekWhatToDo();
}*/
void Clear()
{
for (int i = 0; i < X; ++i)
{
for (int j = 0; j < Y; ++j)
{
//Debug.Log(i + " s" + j);
Value[i, j] = 0;//全部数据清零
}
}
}
// Update is called once per frame
void Update() {
}
public void SeekWhatToDo()//做出决策
{
Clear();
answer.Clear();
canAttack.Clear();
AttackValue.Clear();
EvaluatingMove();//生成价值矩阵,然后找最大
EvaluatingAttackAction();//寻找攻击价值最高的
ArrayList temp=new ArrayList();
for (int i = 0; i < X; ++i)
{
for (int j = 0; j < Y; ++j)
{
temp.Add(Value[i, j]);
}
}
temp.Sort();
temp.Reverse();
for(int i = 0; i < temp.Count; ++i)
{
Find((int)temp[i]);
}
if(canAttack.Count==0)//无法打的时候走路
this.GetComponent<MyAgent>().setDestination(answer[0]);//前往最优点
else//可以打的时候打人
{
ArrayList temp2 = new ArrayList(AttackValue);
temp2.Sort();
temp2.Reverse();
this.GetComponent<Chess>().Attack(FindAttack((int)temp2[0]));
}
}
void Find(int input)
{
for (int i = 0; i < X; ++i)
{
for (int j = 0; j < Y; ++j)
{
if (Value[i, j] == input&&answer.Contains(new Index(i,j))==false)//值相等且不再列表里
{
answer.Add(new Index(i, j));
}
}
}
}
Chess FindAttack(int input)
{
for(int i = 0; i < canAttack.Count; ++i)
{
if ((int)AttackValue[i] == input)
{
return canAttack[i];
}
}
return null;
}
void EvaluatingAttackAction()//判断打架这个行为是否合算
{
if (Camp.PlayerA == myself)
{
}
else if(Camp.PlayerB==myself)
{
foreach(var i in GameManager.instance.chessManager.playerA)
{
if (this.GetComponent<Chess>().CanAttack(i.GetComponent<Agent>().GetCurrentIndex()))
{
canAttack.Add(i);
}
}//这样获得了可以供攻击的列表,下面判断攻击的价值
for(int i = 0; i < canAttack.Count; ++i)
{
int amount=0;
amount += 110-(int)canAttack[i].hp; //血越少越有打的价值
foreach(var temp in GameManager.instance.chessManager.playerA)
{//如果有友军在打,则应该优先攻打
if (temp.isAttacking == canAttack[i])
{
amount += 40;
}
}
AttackValue.Add(amount);
}
}
}
void EvaluatingMove()
{//AI移动倾向需要满足的要求(按照意愿的优先级从高到低):
EvaluatingRadius(this.GetComponent<Agent>().GetCurrentIndex());
if (Camp.PlayerA == myself)
{
foreach (var i in GameManager.instance.chessManager.playerA)
{
EvaluatingFriend(this.GetComponent<Agent>().GetCurrentIndex());//愿意和同阵营的棋子在一起(夹击有加成,更加安全)
if (i.isAttacking)//友军在和敌人交战的时候,很愿意前去支援
{
EvaluatingAttacking(i.ToAttack[0].GetComponent<Agent>().GetCurrentIndex());
}
}
foreach(var i in GameManager.instance.chessManager.playerB)
{
EvaluatingEnemy(i.GetComponent<Agent>().GetCurrentIndex());
}
for (int i = 0; i < X; ++i)
{
for (int j = 0; j < Y; ++j)
{
if (GameManager.instance.gridMap.tiles[i, j].tileType == Camp.Nobody)//无人占领区有比较大的吸引力
{
Value[i, j] += 2;
}
if (GameManager.instance.gridMap.tiles[i, j].tileType == Camp.PlayerB)//鼓励进攻
{
Value[i, j] += 1;
}
}
}
//愿意前往距离敌军远的、属于敌人的格子(可以占领地盘)
}
else if (Camp.PlayerB == myself)
{
foreach (var i in GameManager.instance.chessManager.playerB)
{
EvaluatingFriend(this.GetComponent<Agent>().GetCurrentIndex());
if (i.isAttacking)//友军在和敌人交战的时候,很愿意前去支援
{
EvaluatingAttacking(i.ToAttack[0].GetComponent<Agent>().GetCurrentIndex());
}
}
foreach (var i in GameManager.instance.chessManager.playerA)
{
EvaluatingEnemy(i.GetComponent<Agent>().GetCurrentIndex());
}
for (int i = 0; i < X; ++i)
{
for (int j = 0; j < Y; ++j)
{
if (GameManager.instance.gridMap.tiles[i, j].tileType == Camp.Nobody)//无人占领区有比较大的吸引力
{
Value[i, j] += 2;
}
if (GameManager.instance.gridMap.tiles[i, j].tileType == Camp.PlayerA)//鼓励进攻
{
Value[i, j] += 1;
}
}
}
//愿意前往距离敌军远的、属于敌人的格子(可以占领地盘)
}
}
void EvaluatingRadius(Index position)//传入自己的地址,以自己为半径来制造一个个等势面
{
for (int distance = 1; distance < 7; ++distance)
{
Index tempA = position + new Index(-distance, distance);
Index tempB = position + new Index(distance, distance);
Index tempC = position + new Index(-distance, -distance);
Index tempD = position + new Index(distance, -distance);
for (int i = tempA.X; i <= tempB.X; ++i)
{//上面一行
if (IsValid(new Index(i, tempA.Y)))
{
// Debug.Log(i+ " "+ tempA.Y);
Value[i, tempA.Y] += 7 - distance;
}
}
for (int i = tempC.X; i <= tempD.X; ++i)
{//下面一行
if (IsValid(new Index(i, tempC.Y)))
{
Value[i, tempC.Y] += 7 - distance;
}
}
for (int i = tempC.Y + 1; i < tempA.Y; ++i)
{//左边
if (IsValid(new Index(tempA.X, i)))
{
Value[tempA.X, i] += 7 - distance;
}
}
for (int i = tempD.Y + 1; i < tempB.Y; ++i)
{//右边
if (IsValid(new Index(tempD.X, i)))
{
Value[tempD.X, i] += 7 - distance;
}
}
}
}
void EvaluatingAttacking(Index position)//传入敌人的地址,敌人周围的格子魅力增加
{
int distance = 1;
Index tempA = position + new Index(-distance, distance);
Index tempB = position + new Index(distance, distance);
Index tempC = position + new Index(-distance, -distance);
Index tempD = position + new Index(distance, -distance);
for (int i = tempA.X; i <= tempB.X; ++i)
{//上面一行
if (IsValid(new Index(i, tempA.Y)))
{
Value[i, tempA.Y] += 10;
}
}
for (int i = tempC.X; i <= tempD.X; ++i)
{//下面一行
if (IsValid(new Index(i, tempC.Y)))
{
Value[i, tempC.Y] += 10 ;
}
}
for (int i = tempC.Y + 1; i < tempA.Y; ++i)
{//左边
if (IsValid(new Index(tempA.X, i)))
{
Value[tempA.X, i] += 10 ;
}
}
for (int i = tempD.Y + 1; i < tempB.Y; ++i)
{//右边
if (IsValid(new Index(tempD.X, i)))
{
Value[tempD.X, i] += 10 ;
}
}
}
void EvaluatingFriend(Index position)//输入一个地址,把周围的格子的魅力+2
{
for(int distance = 1; distance < 5; ++distance)
{
Index tempA = position + new Index(-distance, distance);
Index tempB = position + new Index(distance, distance);
Index tempC = position + new Index(-distance, -distance);
Index tempD = position + new Index(distance, -distance);
for (int i = tempA.X; i <= tempB.X; ++i)
{//上面一行
if(IsValid(new Index(i, tempA.Y)))
{
Value[i, tempA.Y] += 5-distance;
}
}
for (int i = tempC.X; i <= tempD.X; ++i)
{//下面一行
if (IsValid(new Index(i, tempC.Y)))
{
Value[i, tempC.Y] += 5-distance;
}
}
for(int i = tempC.Y + 1; i < tempA.Y; ++i)
{//左边
if(IsValid(new Index(tempA.X, i)))
{
Value[tempA.X, i] += 5-distance;
}
}
for (int i = tempD.Y + 1; i < tempB.Y; ++i)
{//右边
if (IsValid(new Index(tempD.X, i)))
{
Value[tempD.X, i] += 5-distance;
}
}
}
}
void EvaluatingEnemy(Index position)//输入一个地址,把周围的格子的魅力+2
{
for (int distance = 1; distance < 3; ++distance)
{
Index tempA = position + new Index(-distance, distance);
Index tempB = position + new Index(distance, distance);
Index tempC = position + new Index(-distance, -distance);
Index tempD = position + new Index(distance, -distance);
for (int i = tempA.X; i <= tempB.X; ++i)
{//上面一行
if (IsValid(new Index(i, tempA.Y)))
{
Value[i, tempA.Y] += 5 - distance;
}
}
for (int i = tempC.X; i <= tempD.X; ++i)
{//下面一行
if (IsValid(new Index(i, tempC.Y)))
{
Value[i, tempC.Y] += 5 - distance;
}
}
for (int i = tempC.Y + 1; i < tempA.Y; ++i)
{//左边
if (IsValid(new Index(tempA.X, i)))
{
Value[tempA.X, i] += 5 - distance;
}
}
for (int i = tempD.Y + 1; i < tempB.Y; ++i)
{//右边
if (IsValid(new Index(tempD.X, i)))
{
Value[tempD.X, i] += 5 - distance;
}
}
}
}
bool IsValid(Index position)
{
if (position.X >= X||position.X<0) return false;
if (position.Y >= Y || position.Y < 0) return false;
return true;
}
}
为什么要用AStar+势能场
势能场本身其实可以进行寻路,但是在二维的坐标世界中,势能的梯度下降并不是像3维连续的势能的梯度下降,这就导致了一个bug:可能在等势面上(两个同等取值的格子)来回移动。这个是可以避免的(使用一个列表来管理优先前行的位置,前端估值高,后端估值低),发现重复移动则remove掉队首。但是这次真的没时间再做这个了,留给暑假腾讯NextIdea里解决吧。使用AStar还有除此之外的好处:势能场中的势能极值点往往是终点(水往低处流),这样可以获得比势能场寻路更短的路径(但不是最优的路径,因为势能场寻路可以选择被敌人攻击风险最小的方法)。
结语
在这个作业上实在是倾注了太多心血了,前后重构了三次,虽然最后bug还是蛮多的。我之后又重构了一次,想纯粹使用势能场函数进行AI判断,结果虽然也可以,但是bug很多,很容易死机。