1、概论
游戏地图中的路径搜索是人工智能领域中理论在游戏中的一个实际应用,游戏地图的寻路是虚拟角色硬要具备的最基本的能力之一。从虚拟角色的行走可以看出该款游戏的品质,因此寻路成为游戏开发中的重要内容之一。再加上现在的游戏玩家对游戏品质要求越来越高,要求游戏更加真实和逼真,这样对寻路算法就提出了更高的要求。寻路就是角色花费最短的时间以一条最佳的方式走到指定的地点。然而,在游戏地图中寻找路径问题并不仅仅是从起点找到终点的通路那么简单。在游戏中需要考虑很多方面的因素:
- 游戏地图数据量很大,需要很大的存储空间
- 游戏题图非常的复杂,包含很多建筑、草地、河流
- 尽量要和玩家的思考的方式一致
一个好的寻路算法需要满足下面的三个条件:
- 可靠
在游戏中存在多种地形,只要涉及到自动寻路功能都需要反复调用寻路模块,所以只要起点到终点存在一条通路,算法必须能够保证能够找到该路径。
- 高效
算法的时间复杂度不能够影响玩家的感知,如果使用起来存在大量延迟,很多玩家是无法接受的。
- 逼真
游戏本身就是对真实生活的模拟,对人物的形态,运动的姿态都需要逼真,让玩家有一种身临其境的感觉。
在寻路算法中游戏领域使用最多是是A*算法,本文重点介绍A*算法。在A*算法之前我们先把图的BFS、DFS、弗洛伊德算法、地杰斯特拉算法简单的介绍一下,然后把A*算法说一下,五种算法对比,很容易就能理解A*算法的原理以及区别。
2、地图模拟
以下算法地图生成均使用同样的地图生成器:
/**
* 图
*
* @author yufei.liu
*/
public class Graph {
/**
* 地图
*/
private int[][] map;
/**
* 随机生成一张地图
*
* @param rowN 行数
* @param colN 列数
*/
public Graph createMap(int rowN, int colN) {
map = new int[rowN][colN];
int obstacleCount = (int) (rowN * colN * 0.05);
Random random = new Random();
for (int k = 0; k < obstacleCount; k++) {
int weight = random.nextInt(4);
int height = random.nextInt(4);
int startX = (int) (rowN * Math.random());
int startY = (int) (colN * Math.random());
for (int i = startX; i <= startX + height; i++) {
if (i >= rowN) {
continue;
}
for (int j = startY; j <= startY + weight; j++) {
if (j >= colN) {
continue;
}
map[i][j] = 1;
}
}
}
return this;
}
}
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 1
0 0 0 0 0 0 1 1 0 0 0 1 1 1 1 0 0 0 0 0
0 0 0 0 0 0 1 1 0 0 0 1 1 1 1 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 1
0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 1
0 1 1 1 0 1 0 0 0 0 1 1 1 1 1 0 0 0 0 1
0 1 1 1 0 1 0 0 0 0 1 1 1 1 0 0 0 0 0 0
0 1 1 1 1 1 1 0 0 0 0 0 1 1 0 0 0 1 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1
0 0 1 0 0 0 0 0 0 0 1 1 1 1 0 0 0 1 1 1
0 0 1 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 1
0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0
0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0
@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 @
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 @
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 @
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 @ 1
0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 @ 1
0 0 0 0 0 0 1 1 0 0 0 1 1 1 1 0 0 0 0 @
0 0 0 0 0 0 1 1 0 0 0 1 1 1 1 0 0 0 0 @
0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 @ 1
0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 @ 1
0 1 1 1 0 1 0 0 0 0 1 1 1 1 1 0 0 0 @ 1
0 1 1 1 0 1 0 0 0 0 1 1 1 1 0 0 0 @ 0 0
0 1 1 1 1 1 1 0 0 0 0 0 1 1 0 0 @ 1 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 @ 1 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 @ 1 1 1
0 0 1 0 0 0 0 0 0 0 1 1 1 1 0 0 @ 1 1 1
0 0 1 0 0 0 0 1 1 1 1 1 1 1 0 0 0 @ @ 1
0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 @
0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 @
0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 @
其中0表示空地,1表示障碍物,@表示一条可行的通路。
3、地图和图的转换
在图中只有结点和边两个概念,边分为有向边和无向边,在地图中有一个一个小格子构成,每一个格子对应图中的一个结点,结点与结点之间的边,在地图上只有“上下左右”(如果允许斜着走也包括左上、右上、左下、右下),在本文中允许斜着走。
地图到图的转换就和上图展示,注意:
- 地图非障碍物节点数等于图中的结点数,如果一个20*20的地图,其中障碍物有180个格子,那么转化成图的结点数只有20个。障碍物是不会出现在图上的,障碍物的作用仅仅在于图不是全连接的。
- 一般来讲,地图生成的图都是无向图,但是一些特殊情形也有可能是有向图,最直接的例子就是单行道。
- 斜着走和直上直下走对应在图中的区别是边的权重不同,一般情况下,斜边的代价是一个小格子的1.414倍。
4、深度优先、广度优先算法
广度优先算法和深度优先算法本质上都是对图的遍历,算法的目的是从图中的一个节点出发搜索连通的节点节点,并且需要保证不重不漏。遍历结束之后一般我们会关心图的深度、连通性这些基本问题。
在遍历的时候我们可以记录节点的父节点,当遍历到指定节点时,反向查找便可以将整条路径得到。
广度优先和深度优先在设计时稍有不同,深度优先一般使用递归写法,但是从本质上讲广度优先的写法更具有普遍性,所以我们需要将深度优先稍加改造,适配广度优先写法。
首先介绍广度优先:
- 广度优先包含两个表:open表、close表,其中open表存放着待搜索结点列表,close表存放着已经搜索过的结点信息
- 最初open表和close表均为空,将起始结点放入open表
- 不断的从open表取出表头结点,放入close表,并将表头结点的子结点顺序放入open表尾
- 再放入close表检查是否是目标结点。如果是。直接退出
- 当open表为空,直接退出,此时没有找到目标结点,说明目标不可达
对于下面这张图:
我们很容易可以得到广度优先的序列:ADEGHI
深度优先每一次都访问的是最近的对象。所以深度优先的流程:
- 深度优先包含两个表:open表、close表,其中open表存放着待搜索结点列表,close表存放着已经搜索过的结点信息
- 最初open表和close表均为空,将起始结点放入open表
- 不断的从open表取出表头结点,放入close表,并将表头结点的子结点逆序,并依次插入open表表头
- 再放入close表检查是否是目标结点。如果是。直接退出
- 当open表为空,直接退出,此时没有找到目标结点,说明目标不可达
所以open表中:
A
DE --------------------------> A
GHE --------------------------> D
HE --------------------------> G
E --------------------------> H
I --------------------------> E
--------------------------> I
列一下代码(https://coding.net/u/yfLiu/p/A-star/git/blob/master/src/main/java/com/lyf/csdn/graph/bdfs/BDFS.java):
/**
* bfs-dfs算法
* 这里实现的dfs是一个非递归版本,和bfs算法保持一致
*
* @author yufei.liu
*/
public class BDFS {
public static final String BFS = "BFS";
public static final String DFS = "DFS";
private Graph graph;
private Point startPoint;
private Point endPoint;
public BDFS(Graph graph, Point startPoint, Point endPoint) {
this.graph = graph;
this.startPoint = startPoint;
this.endPoint = endPoint;
}
/**
* 深度优先或者广度优先
*
* @param type bfs/dfs
*/
public void bdfs(String type) {
LinkedList<Point> open = new LinkedList<>();
LinkedList<Point> close = new LinkedList<>();
Set<Point> openPointSet = new HashSet<>();
Set<Point> closePointSet = new HashSet<>();
Point target = null;
open.addLast(startPoint);
openPointSet.add(startPoint);
while (true) {
if (open.isEmpty()) {
break;
}
Point point = open.removeFirst();
openPointSet.remove(point);
close.add(point);
closePointSet.add(point);
if (point.equals(endPoint)) {
target = point;
break;
}
List<Point> searchPointList= search(point, openPointSet, closePointSet);
if (BFS.equals(type)) {
for (Point item : searchPointList) {
open.addLast(item);
openPointSet.add(item);
}
} else if(DFS.equals(type)) {
for (int i = searchPointList.size() - 1; i >= 0 ; i--) {
open.addFirst(searchPointList.get(i));
openPointSet.add(searchPointList.get(i));
}
} else {
throw new Error();
}
}
if (target == null) {
System.out.println("target is not reachable.");
} else {
LinkedList<Point> stack = new LinkedList<>();
stack.addLast(target);
while (true) {
target = target.getParentPoint();
if (target == null) {
break;
}
stack.addFirst(target);
}
graph.printPath(stack);
}
System.out.println("\n");
}
/**
* 上下左右,以及四个斜边方向搜索
*
* @param point 点
* @return 八个方向的点
*/
private List<Point> search(Point point, Set<Point> openPointSet, Set<Point> closePointSet) {
LinkedList<Point> points = new LinkedList<>();
for (Point item : point.getAllDirectorPoint()) {
if (graph.check(item) && !openPointSet.contains(item) && !closePointSet.contains(item)) {
points.addLast(item);
}
}
return points;
}
}
5、弗洛伊德算法
弗洛伊德算法应该是掌握动态规划的第一个算法,算法伪代码如下:
Floyd-Warshall算法的描述如下:
1 let dist be a |V| × |V| array of minimum distances initialized to ∞ (infinity)
2 for each vertex v
3 dist[v][v] ← 0
4 for each edge (u,v)
5 dist[u][v] ← w(u,v) // the weight of the edge (u,v)
6 for k from 1 to |V|
7 for i from 1 to |V|
8 for j from 1 to |V|
9 if dist[i][j] > dist[i][k] + dist[k][j]
10 dist[i][j] ← dist[i][k] + dist[k][j]
11 end if
import com.lyf.csdn.graph.graph.Graph;
import com.lyf.csdn.graph.graph.Point;
import java.util.ArrayList;
import java.util.List;
/**
* 弗洛伊德算法
*
* @author yufei.liu
*/
public class Floyd {
private Graph graph;
private Point startPoint;
private Point endPoint;
public Floyd(Graph graph, Point startPoint, Point endPoint) {
this.graph = graph;
this.startPoint = startPoint;
this.endPoint = endPoint;
}
@SuppressWarnings("unchecked")
public void work() {
int[][] map = graph.getMap();
int rowN = map.length;
int colN = map[0].length;
int n = 0;
ArrayList<Point> nodeList = new ArrayList<>();
for (int i = 0; i < rowN; i++) {
for (int j = 0; j < colN; j++) {
if (map[i][j] == 0) {
n++;
nodeList.add(new Point(i, j));
}
}
}
double[][] dp = new double[n][n];
Object[][] dpPath = new Object[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
dp[i][j] = distance(nodeList.get(i), nodeList.get(j));
dpPath[i][j] = new ArrayList<Point>();
}
}
for (int k = 0; k < n; k++) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (dp[i][k] >= Integer.MAX_VALUE || dp[k][j] >= Integer.MAX_VALUE) {
continue;
}
double temp = dp[i][k] + dp[k][j];
if (temp < dp[i][j]) {
dp[i][j] = temp;
ArrayList<Point> path = (ArrayList<Point>) dpPath[i][j];
ArrayList<Point> pathItoK = (ArrayList<Point>) dpPath[i][k];
ArrayList<Point> pathKtoJ = (ArrayList<Point>) dpPath[k][j];
path.clear();
path.addAll(pathItoK);
path.add(nodeList.get(k));
path.addAll(pathKtoJ);
}
}
}
}
int startIndex = 0;
int targetIndex = 0;
for (int i = 0; i < nodeList.size(); i++) {
if (nodeList.get(i).equals(startPoint)) {
startIndex = i;
}
if (nodeList.get(i).equals(endPoint)) {
targetIndex = i;
}
}
System.out.println("distance = " + dp[startIndex][targetIndex]);
graph.printPath((List<Point>) dpPath[startIndex][targetIndex]);
}
/**
* 两点之间的距离
*
* @param start 开始结点
* @param end 结束点
* @return 距离
*/
private double distance(Point start, Point end) {
if (Math.abs(start.getRow() - end.getRow()) > 1 || Math.abs(start.getCol() - end.getCol()) > 1) {
return Integer.MAX_VALUE;
}
if (Math.abs(start.getRow() - end.getRow()) == 0 || Math.abs(start.getCol() - end.getCol()) == 0) {
return 1;
}
return 1.414;
}
}
6、迪杰斯特拉算法
dijkstra算法是经典的最短路径算法,用于求解一个节点到其他所有节点的最短路径的问题,它的特点就是:以起始节点为中心,逐层向外面扩展,一致扩展到目标节点为止。也就是以起始节点为树的根节点,然后把距离根节点最近的节点逐个放到树中,知道吧所有的节点(包括目标结点)全部放到树中,这样就找到了起始节点到其他每一个节点的最短路径。它是一种经典的贪婪算法。
dijkstra算法的基本思想如下:
首先要创建两张表,一个open表,一个close表,用来存储节点的信息。其中open表中保存所有已经生成但是没有被考察过的节点,close表中记录已经访问过的节点。
(1)搜索图中距离起始节点最近并且没有被考察过的节点,把这个节点放入open表中等待观察
(2)从open表中找到距离起始节点最近的节点
(3)找出这个节点的所有子节点,然后把这个节点放入close表
(4)考察这个节点的所有子节点。首先计算这些子节点到起始节点的距离值,然后把这些子节点全部放到open表中。
(5)重复第(2)步到第(4)步
import com.lyf.csdn.graph.graph.Graph;
import com.lyf.csdn.graph.graph.Point;
import java.util.LinkedList;
import java.util.List;
/**
* 地杰斯特拉算法
*
* @author yufei.liu
*/
public class Dijkstra {
private Graph graph;
private Point startPoint;
private Point endPoint;
public Dijkstra(Graph graph, Point startPoint, Point endPoint) {
this.graph = graph;
this.startPoint = startPoint;
this.endPoint = endPoint;
}
public void work() {
List<Point> open = new LinkedList<>();
List<Point> close = new LinkedList<>();
int[][] map = graph.getMap();
int rowN = map.length;
int colN = map[0].length;
for (int i = 0; i < rowN; i++) {
for (int j = 0; j < colN; j++) {
if (map[i][j] == 0) {
close.add(new Point(i, j));
}
}
}
close.remove(startPoint);
open.add(startPoint);
Point target = null;
while (true) {
Point st = null;
Point et = null;
double distance = Double.MAX_VALUE;
if (close.isEmpty()) {
break;
}
for (Point openPoint : open) {
for (Point closePoint : close) {
double distanceTemp = distance(openPoint, closePoint) + openPoint.getCurrentDistance();
if (distanceTemp < distance) {
st = openPoint;
et = closePoint;
distance = distanceTemp;
}
}
}
if (distance >= Integer.MAX_VALUE) {
break;
}
et.setParentPoint(st);
et.setCurrentDistance(distance);
close.remove(et);
open.add(et);
if (et.equals(endPoint)) {
target = et;
break;
}
}
if (target == null) {
System.out.println("node is not reachable.");
} else {
List<Point> result = new LinkedList<>();
Point t = target;
result.add(t);
while (true) {
t = t.getParentPoint();
if (t == null) {
break;
}
result.add(t);
}
graph.printPath(result);
}
}
/**
* 两点之间的距离
*
* @param start 开始结点
* @param end 结束点
* @return 距离
*/
private double distance(Point start, Point end) {
if (Math.abs(start.getRow() - end.getRow()) > 1 || Math.abs(start.getCol() - end.getCol()) > 1) {
return Integer.MAX_VALUE;
}
if (Math.abs(start.getRow() - end.getRow()) == 0 || Math.abs(start.getCol() - end.getCol()) == 0) {
return 1;
}
return 1.414;
}
}
7、A*算法