近来买了一本讲游戏AI的入门书,在这里记录一下的自己的学习过程。不得不吐槽的是,对于游戏中的算法,相关解释给的很是简略而且不准确(或者说是不对,可能是翻译的锅!),而且游戏中给出的是算法的升级版!更坑爹的是,竟然是升级版的简单改造版!这就让读者的理解成本变高,很不友好啊。
今天讲讲追逐算法,最基础的方法莫过于下面的代码了:
if(predatorCol > preyCol)
predatorCol--;
else if(predatorCol < preyCol)
predatorCol++;
if(predatorRow > preyRow)
predatorRow--;
else if(predatorRow < preyRow)
predatorRow++;
这个方法有个问题,就是追逐着会首先按照对角线行走,然后按照直线的方向行走。这在玩家看来,无疑是很狗血的,这样会显得敌人十分愚蠢,因为我们都知道两点之间线段最短,敌人为什么不直接朝我走来呢?
基于此,我们有了接下来要讲的算法,Bresenham算法,这里是在砖块环境下的追逐,即追逐者和猎物的坐标都保存在一个整型的二维数组中。值得一提的是,Bresenham算法是计算机图形学领域使用最广泛的直线扫描转换方法。
这里假设直线的斜率k在0到1之间(如果k > 1, 则采取对应的算法),起点坐标x1,y1,终点坐标x2,y2。横轴间距Δx = x2 - x1 大于 纵轴间距Δy = y2 - y1。因此我们要在这两点这件画一条近似直线的路径,只需要考虑在追逐者“横行”时,需不需要“纵行”即可。假设当前坐标为(xi, yi),那么下一步的坐标可能为(xi + 1, yi)或者(xi + 1, yi + 1)。
上面橙线为起点到终点的直线,我们目前的问题是当追逐者走到横坐标为xi + 1, 纵坐标为yi 还是 yi + 1呢?Bresenham算法是要比较一下dy1与dy2的大小。如果dy1小,则上面的点距直线更近;反之则下面的点距直线更近。
图片来源于:https://blog.csdn.net/natsu1211/article/details/17004375
如果按照上面的方法选好了点,也就能够确定了追逐者的路径。总的来说,整个过程是这样的。误差d,由dy1和dy2转换而来,如图所示。只要d >= 0.5, 即选择上方的点;否则选下方点。初值为0,每横向走一步d = d + k(在坐标轴中,直线每次走的横向距离为单位距离1,纵坐标增加k,k = Δy / Δx),当d >= 0.5,则减去1。
这里呢,我们做个小改进,令e = d - 0.5。相应地,e初值为-0.5,每横向走一步e = e + k,当e >= 0,则减去1。这样的好处在于我们只需要判断e的正负即可,而不必与0.5做比较。
但我们如此就满足了吗!不!再审视一下这个式子,斜率用到了除法,相加用到了浮点运算,为了便于硬件运算和提高速度,我们令e' = e * 2 * Δx。那么e'初值为-Δx,每横向走一步e' = e' + 2 * Δy。当e' >= 0,则减去2 * Δx。
下面是我用C++的简单实现,与书中代码略有不同:
#include<iostream>
#include<cmath>
using namespace std;
int main(){
int map[30][30], pathRow[30], pathCol[30];
int i, j;
int col = 20, row = 2;
int endCol = 4, endRow = 24;
int nextCol = col, nextRow = row;
int deltaRow = endRow - row, deltaCol = endCol - col;
int stepCol, stepRow, currentStep;
int recordCol, recordRow;
float d = 0, k, e;
//初始路径设定
for(i = 0; i < 30; i++){
for(j = 0; j < 30; j++){
map[i][j] = -1;
}
}
for(currentStep = 0; currentStep < 900; currentStep++){
pathRow[currentStep] = -1;
pathCol[currentStep] = -1;
}
currentStep = 0;
//路径方向计算
if(deltaRow < 0) stepRow = -1; else stepRow = 1;
if(deltaCol < 0) stepCol = -1; else stepCol = 1;
recordCol = abs(deltaCol);
recordRow = abs(deltaRow);
deltaRow = abs(deltaRow * 2);
deltaCol = abs(deltaCol * 2);
pathRow[currentStep] = nextRow;
pathCol[currentStep] = nextCol;
currentStep++;
//Bresenham算法
if(deltaCol > deltaRow){
e = -0.5;
k = abs(deltaRow) * 1.0 / abs(deltaCol);
while(nextCol != endCol){
//d = d + k;
e = e + k;
if(e > 0){
nextRow = nextRow + stepRow;
e -= 1;
}
nextCol = nextCol + stepCol;
pathRow[currentStep] = nextRow;
pathCol[currentStep] = nextCol;
currentStep++;
}
}else{
e = -0.5;
k = abs(deltaCol) * 1.0 / abs(deltaRow);
while(nextRow != endRow){
e = e + k;
if(e > 0){
nextCol = nextCol + stepCol;
e -= 1;
}
nextRow = nextRow + stepRow;
pathRow[currentStep] = nextRow;
pathCol[currentStep] = nextCol;
currentStep++;
}
}
//更改地图
//起点为2, 终点为3
for(i = 0; i < currentStep; i++){
if(pathRow[i] == row && pathCol[i] == col)
map[pathRow[i]][pathCol[i]] = 2;
else if(pathRow[i] == endRow && pathCol[i] == endCol)
map[pathRow[i]][pathCol[i]] = 3;
else map[pathRow[i]][pathCol[i]] = 1;
}
//打印
for(i = 0; i < 30; i++){
for(j = 0; j < 30; j++){
if(map[i][j] == -1) cout << "■";
else if(map[i][j] == 2) cout << "☆";
else if(map[i][j] == 3) cout << "★";
else cout << "□";
}
cout << '\n';
}
}
运行结果如下图: