游戏与常用的五大算法---上篇

    xiaoxiao2022-08-11  11

     前言

          什么时候,我们之间竟然变得这么生疏

          什么时候,我想见到你,却又害怕见到你

          什么时候,才能在我身边,告诉我。其实,你一直都在

                                       -----------《仙剑奇侠传》

    PS:为了方便大家阅读,个人认为比较重要的内容-------红色字体显示

                                          个人认为可以了解的内容-------紫色字体显示

    ---------------------------------------------------------------------------

    -----------------------------------------------分-割-线--------------------------------------------

                最近感觉好忙啊,不过每天也都过得很充实,希望这样保持下去,一直到毕业。好久没有提笔写博客了,尽然已经有半个月之多了,不过今天来讨论一下游戏与算法,主要准备从常用的五大算法入手,顺便讨论一下,游戏与算法之间的关系。其实游戏与算法真的密不可分!如果没有了这些算法,那么游戏几乎就无法运作,加上本身游戏对于性能的要求就很高,所以一款游戏的游戏必然要求有让人拍案叫绝的算法!

                                                              算法之一:分治算法

    、什么是分治算法

          首先来说一说什么是分治法,“分治”二字顾名思义,就是“分而治之”的意思,说的通俗一点就是步步为营各个击破,再来解释分而治之的意思,其实也就是把一个问题(一般来说这个问题都是比较复杂的)分成两个相同或者相似的子问题,再把子问题分成更小的问题,一直这样下去.......直到最后,子问题可以简单地求解,还有一点就是把所有求得的子问题合并就是原问题的解。其实在很多场合下都会使用到分治算法,比如说我们常用的归并排序、快速排序都是很常见的分治思想的体现。

    、核心思想

          说完了分治算法的概念,我们就该谈一谈分治算法的思想及策略

          分治法的思想:将一个难以直接解决的大问题,分解成规模较小的相同问题,接下来就是刚刚说的八个字:步步为营、各个击破。

          怎么样才能达到这种状态呢?我们需要用什么方法呢?首先假设遇到一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决。不过有时候却没有很好地思路去解,这时候如果你发现如果n取得比较小的情况下,很容易解决,那么我们就应该将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。

           不过在使用的时候还要多说几句:

           假设我们遇到一个规模为n的问题,这个问题可分割成k个子问题,1<k≤n,且这些子问题都可解并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法

    、分治算法的适用场景

           知道了分治算法的原理,接下来的自然是归结到一个“用”字上面,怎么使用呢?要使用之前肯定要知道什么样的条件下可以使用或者说是适合使用分治算法。

          1) 该问题的规模缩小到一定的程度就可以容易地解决

          2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质

          3) 利用该问题分解出的子问题的解可以合并为该问题的解;

          4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题

           需要注意的是:第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加,所以说第一条不能作为重要的依据。

          第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;、

          第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法

          第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好

    、实际运用

           具体到实际运用的过程之中,归结到到游戏上面的话,其实用的还是挺常见的。最常见的就是在RGP游戏之中,主角会经常获得道具,有时候我们会想给这些道具按个数的多少拍个序,那么最常见的做法就是按一下数量这个按钮。按下之后就会给这些道具内容进行排序了!一般来说快排是用的最多的,但是归并也很常见,恰好这两者都是分治算法的体现。 

         总结一下在实际过程之中怎么运用,以下三步是分治思想的惯用套路

         实际上就是类似于数学归纳法,找到解决本问题的求解方程公式,然后根据方程公式设计递归程序。 第一步:一定是先找到最小问题规模时的求解方法,一般来说最小问题规模的求解方法是很简单的(就像归并排序之中当问题规模最小的时候,也就是只有一个元素的时候,直接就已经有序了)。 第二步:然后考虑随着问题规模增大时的求解方法,区间划分完了之后,开始考虑规模增大之后应该怎么做,还是以归并排序为例,当划分到每一个元素之后,不能再往下划分了,这时候就需要考虑问题增大时候的求解方法,增大具体方法需要借助另外一个存储空间,这也是归并排序为什么需要O(N)的额外存储空间。 第三步:找到求解的递归函数式后(各种规模或因子),设计递归程序即可。       来看一个具体例子吧,下面是一个归并排序的例子: //---------------------------归并排序之中问题增大时候的求解方法--------------------------------- void Merge(int sourceArr[], int tempArr[], int startIndex, int midIndex, int endIndex) { int i = startIndex, j = midIndex + 1, k = startIndex; while (i != midIndex + 1 && j != endIndex + 1) { if (sourceArr[i] >= sourceArr[j]) tempArr[k++] = sourceArr[j++]; else tempArr[k++] = sourceArr[i++]; } while (i != midIndex + 1) tempArr[k++] = sourceArr[i++]; while (j != endIndex + 1) tempArr[k++] = sourceArr[j++]; for (int index = startIndex; index <= endIndex; ++index) sourceArr[index] = tempArr[index]; } //---------------------------------归并排序划分为子问题------------------------------------------ void MergeSort1(int sourceArr[], int tempArr[], int startIndex, int endIndex) //内部递归使用 { int midIndex = 0; if (startIndex < endIndex) { midIndex = startIndex + (endIndex - startIndex) / 2; MergeSort1(sourceArr, tempArr, startIndex, midIndex); MergeSort1(sourceArr, tempArr, midIndex + 1, endIndex); Merge(sourceArr, tempArr, startIndex, midIndex, endIndex); } } //----------------------------------------优化方法--------------------------------------------- void MergeSort2(int sourceArr[], int tempArr[], int startIndex, int endIndex) { int midIndex = 0; if ((endIndex - startIndex) >= 50) // 大于50个数据的数组进行归并排序 { midIndex = startIndex + (endIndex - startIndex) / 2; MergeSort2(sourceArr, tempArr, startIndex, midIndex); MergeSort2(sourceArr, tempArr, midIndex + 1, endIndex); Merge(sourceArr, tempArr, startIndex, midIndex, endIndex); } else // 小于50个数据的数组进行插入排序 InsertSort(sourceArr + startIndex, endIndex - startIndex + 1); }</span></span></font>

           来看一看优化与不优化两者时间实验结果比较:

           最后再来看一看游戏之中排序的应用(一般是归并排序或者是快速排序)吧,一般来说归并排序在文件的排序用的比较多,而快速排序在大多数情况都适用,如下图所示(图为《仙剑四》买物品的场景,游戏确实有点老了,而且仙剑六都已经出了,仙剑七也正在开发过程之中,但是个人还是认为仙剑四和五前最为经典,所以电脑上一直保留着,自己希望能多多研究这样的经典游戏),对于物品的选择,如果我们物品很多,但是你希望按价格高低排序看一看的,这时候排序就派上用场了,点一下价格,就会按照价格降序排列!

                                                          算法之二:动态规划算法

    、什么是动态规划

          关于什么是动态规划呢?用通俗一点的话来说就是“边走边看”,注意和回溯法这种先把一条道走到黑的方法区别开来,总的来说就是前面的知道了,后面的也可以根据前面的推导出来了。好了通俗的话说到这了,下面用正规一点的语言总结一下:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。

    、核心思想

           其实在刚开始接触的时候,很容易把动态规划与分治算法混在一起,不过这两者还真的有些类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段。不过动态规划之中前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。

           由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。

           与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解),但是分治法不同,分治法一般最后才把这些子问题合并,但是在这之前他们是互不干扰的,所以分治法只要一直往下划分即可。

    、动态规划的适用场景

           动态规划适用的场景还是挺多的,而且什么笔试的时候也很喜欢考,这样的题目都有一个特点,就是如果你知道要使用动态规划区解这个题,那么做起来回很方便,很快速,代码量不多,但却很考验思维。这也是为什么动态规划出现地比较多的原因,甚至在一些什么ACM大赛上,动态规划也是一个易考点。

           高中里我们都学过线性规划,使用来求最优解的方法,动态规划与它也有点类似,所以说动态规划本质上来说还是规划,是不断进行决策的问题,一般用于求解最(优)值;而分治是一种处理复杂问题的方法,不仅仅只用于解决最值问题(而且我们一般也不用它来求最值,你想一串数字如果特别多,你想找一个最大的出来,用了一个排序是不是有一点奢侈呢,比较游戏与效率要求真的很高)。

             所以如果能用动态规划来解决的问题,通常要满足以下三点要求:

            (1)最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。

            (2)无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关

            (3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)

    、实际运用

           前面说了这么多,还是得归结到一个""字上面,什么情况下适用呢?具体到游戏上又应该用在什么什么上面呢?先来说一说怎么用吧!这里我用一下我之前看到的一段总结的比较好的话来说明一下怎么动态规划怎么使用!

          动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。

                                   初始状态→│决策1│→│决策2│→…→│决策n│→结束状态

                                                          动态规划决策过程示意图

        (1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。

        (2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。

        (3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程

        (4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

          一般,只要解决问题的阶段状态状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。

    实际应用中可以按以下几个简化的步骤进行设计:

        (1)分析最优解的性质,并刻画其结构特征。

        (2)递归的定义最优解。

        (3)以自底向上自顶向下记忆化方式(备忘录法)计算出最优值,一般我们可以把需要记忆的内容放在一个全局变量或者一个多维数组之中。

        (4)根据计算最优值时得到的信息,构造问题的最优解。

         不过在具体地操作过程之中还是有几点需要说明一下的:

          动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。

          使用动态规划求解问题,最重要的就是确定动态规划三要素

          (1)问题的阶段             (2)每个阶段的状态         (3)从前一个阶段转化到后一个阶段之间的递推关系。

          递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。

           确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。

                                   f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}

           网上看到一个通用的动态规划算法的通用架子,如下: for(j=1; j<=m; j=j+1) // 第一个阶段 xn[j] = 初始值; for(i=n-1; i>=1; i=i-1)// 其他n-1个阶段 for(j=1; j>=f(i); j=j+1)//f(i)与i有关的表达式 xi[j]=j=max(或min){g(xi-1[j1:j2]), ......, g(xi-1[jk:jk+1])}; t = g(x1[j1:j2]); // 由子问题的最优解求解整个问题的最优解的方案 print(x1[j1]); for(i=2; i<=n-1; i=i+1) { t = t-xi-1[ji]; for(j=1; j>=f(i); j=j+1) if(t=xi[ji]) break; }

           基本用法介绍完了,我们可以来看一看使用动态规划的典型例子,首先就是典型的问题:背包问题。背包问题在我看来就是使用有限的资源,尽可能的创造出更多的价值。背包问题原题是给定n种物品和一背包。物品i的重量是wi,其价值为vi,背包的容量为C。问应如何选择装入背包的物品,使得装入背包中物品的总价值最大?

           接下来我们先把背包问题解决了,然后在说一说在游戏之中背包问题引出的动态规划思想的体现。

           先给出一个具体地背包问题,题目如下:

           有编号分别为a,b,c,d,e的五件物品,它们的重量分别是2,2,6,5,4,它们的价值分别是6,3,5,4,6,现在给你个承重为10的背包,如何让背包里装入的物品具有最大的价值总和

           一看到最大最小值的问题,我们首先应该想一想是否可以使用动态规划解决这个问题呢?一般来说求最值问题,最常用的或者说是最先想到的就应该是动态规划。之前通过上面的分析,我们对于背包问题应该有了一定地思路,不过就是写代码的问题了,关于这道题目的分析过程,这里给出一个链接地址:点这里        上面给出的链接文章之中对于背包问题进行了很好的分析,所以有需要的可以点进去看一下,不过个人感觉他的代码给出的解释太少,所以自己写了一个,大家可以参考参考。

    //--------------------------------------------背包问题------------------------------------------ const int Bag_Capacity = 10; //背包的总容量 const int Weight[] = { 0, 2, 2, 6, 5, 4 }; //用于存放物品重量的数组,其中0号位置没有用到,只是为了方便而已 const int Value[] = { 0, 6, 3, 5, 4, 6 }; //用于存放物品价值的数组,同样的0号位置没有用到 const int nCount = sizeof(Weight) / sizeof(Weight[0]) - 1;//物品的总个数 int GoodList[nCount + 1]; //物品的存在序列(1表示存在,0表示不存在) void Package(int mor[][11], const int Wei[], const int Val[], const int size) { //通常来说背包问题采用自底向上的方式解决比较好,所以我们假定先放的是最后一个物品,也就是Wei[size] //通过自底向上的方式来设置mor这个数组比较好 //首先进行参数检测 if (NULL == mor || NULL == Wei || NULL == Val) return; //在放入第一个元素,也就是Wei[n] for (int index = 0; index <= Bag_Capacity; ++index) { //判断是否可以放背包,index从0到背包的最大容量,可以理解为index是一个试探变量 //因为物品重量都是整数,所以一定存在某一个值正好等于物品重量(在物品重量小于背包重量的前提之下) //如果比背包重量大的话,直接不放如,那么总价值为0(放第一个物品) if (index < Wei[size]) mor[size][index] = 0; else mor[size][index] = Val[size]; } //接下来就是动态规划的体现,对剩下的n-1个物品放入,也就是填充mor数组 for (int row = size - 1; row >= 0; --row) { for (int col = 0; col <= Bag_Capacity; ++col) { if (col < Wei[row]) //这里保持和下面一样就可以了 mor[row][col] = mor[row + 1][col]; else //这里需要理解一下 { mor[row][col] = mor[row + 1][col] > mor[row + 1][col - Wei[row]] + Val[row] ? mor[row + 1][col] : mor[row + 1][col - Wei[row]] + Val[row]; } } } } void GetList(int mor[][11], const int size) { //现在的目的就是为了得到了一个序列,关于物品是否存在的序列 int index = Bag_Capacity; int i = 0; for (i = 1; i <= size - 1; ++i) //判断前n-1个物品是存在 { if (mor[i][index] == mor[i + 1][index]) GoodList[i] = 0; else { GoodList[i] = 1; index = index - Weight[i]; } } //对于最后一个问题,那么只需要判断相应位置是否为0即可 GoodList[i] = mor[i][index] ? 1 : 0; } int main() { int Memory[6][11] = { 0 }; Package(Memory, Weight, Value, nCount); //先把整个过程打印出来 for (int row = 1; row <= nCount; ++row) { for (int col = 0; col <= Bag_Capacity; ++col) printf("%4d", Memory[row][col]); //使用printf在这里比较方便指定行宽 cout << endl; } GetList(Memory, nCount); cout << "最优解为:" << endl; for (int idx = 1; idx <= nCount; ++idx) cout << GoodList[idx]; cout << endl; return 0; }</span></span></span>        关于背包问题在实际游戏之中的运用,我觉得在策略性游戏之中比较有用,记得小学三年级玩的星际争霸的时候,当时还小,而且游戏还是英文的,所以经常打不过电脑。当时就觉得电脑很强(虽然后面打电脑觉得很简单),不过现在看来AI制造部队的时候会不会也是采用类似背包的思想呢?在当前有限的资源下,制造最强战斗力呢?当然还有一个问题,就是资源是不断变化的(除非矿石都已经被采完了),这种情况肯定比背包复杂的不是一点点,所以我认为在这里AI肯定有一套自己的策略用来生产部队。动态规划是一个不错的方法,当然实际肯定会复杂的多。毕竟还有外界因素的影响

           还有一个感觉可能符合的是今年寒假期间刚刚发行的《三国志13》,里面采用了与《三国志12》完全不同的画风,感觉是大地图上宏伟了很多,来看一张截图:

           从上图我们可以看到,这一代玩家可以扮演任意一个角色,而且可以去执行任务。但是需要钱,不同的人执行所需要的金钱也是不同的(同智力成反比),智力越高,所花的金钱越少,所以说这里就需要AI选择了。怎么样花最少的金钱,获得最大的发展。智力就相当于我们上面背包问题里面的重量,执行人物效果又可以对应于背包问题之中的价值。从而选择对于总价值最高的建设方式,尽快提升城市的繁荣程度。个人感觉这一代的AI比上一代的AI明显会思考了很多,而且发展也快了很多。当然游戏里面肯定设计复杂很多,所以说AI的设计真的是一个很大的研究方向,总之应该设计这样的AI,会简单模拟人的思考。用最少的资源,尽快建设城市,训练部队(这两者怎么取舍,这也是一个大问题),而且这些还用到了一些博弈论里面的知识,所以这里就不在赘述了!

                                                                算法之三:贪心算法

    、什么是贪心算法

           刚刚上面讲了动态规划,接下来讲一讲贪心算法。解释一下贪心算法,从字面上先解释一下,所谓贪心就是总是在当前情况下做出最为有利的选择,也就是说它不从整体上考虑。它只是做出了某种意义上的局部最优解

           需要说明的一点就是,贪心算法不像动态规划那样有固定的框架,由于贪心算法没有固定的算法框架,因此怎么样区分有关于贪心算法呢?这就需要一种贪心策略了!利用它来区分各种贪心算法。还有需要说明的就是它与动态规划最本质的区别就是贪心算法不是所有情况下都能得到整体最优解,而且往往来说得到的只是一个近似最优解,所以说如果是求最值的问题上,我们一般不用贪心算法,而是采用动态规划算法。

           另外,贪心策略的选择必须满足无后效性,这是很重要的一点,说的具体一点就是某个状态以后的过程不会影响以前的状态,只与当前状态有关。所以我们在使用贪心算法的时候一点要看一看是否满足无后效性。

    、核心思想

           关于贪心算法,其实没有过多要说的,就简单说一下步骤吧!          第一步:建立数学模型来描述问题。        第二步:把求解的问题分成若干个子问题        第三步:对每一子问题求解,得到子问题的局部最优解        第四步:把子问题的解局部最优解合成原来解问题的一个解。

    、贪心算法的适用场景

           由于贪心算法求出来的解并不是最优解,也就注定在某些要求结果精确的情况之中无法使用,有人可能会认为贪心算法用到的并不多,而且贪心策略的前提就是尽量保证局部最优解可以产生全局最优解,最美好的贪心策略当然就是希望能通过不断地求局部最优解从而得到全局最优解!

           就拿刚刚的背包问题来说,显然使用贪心算法是无法得出答案的(一般情况下不能,不过也有很小的可能恰好是全局最优解),因为贪心策略只能从某一个方向考虑,比如单单以重量(每次选择重量最轻的),或者用价值(每次选择价值最高的),甚至用价格与重量的比值,其实这三者都实际运用过程之中都有问题,基本很难得到最优解。

          一般,对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析,就可做出判断。

           不过还是给出使用贪心算法的一般框架吧:

    //从问题的某一初始解出发; while (能朝给定总目标前进一步) { 利用可行的决策,求出可行解的一个解元素; } //由所有解元素组合成问题的一个可行解;</span></span></span>

          因为用贪心算法只能通过解局部最优解的策略来达到全局最优解,因此,一定要注意判断问题是否适合采用贪心算法策略,找到的解是否一定是问题的最优解。

    、实际运用

          因为在实际过程之中我们都希望通过贪心求得最值,所以说在实际之中运用的不是特别多,最小生成树算是一种。但是在游戏之中贪心算法特别常见!因为对于游戏来说尽可能快求得一个解,从而提高游戏性能显得更为重要,哪怕这个解不是最优解,只要他快,而且最好能让他尽可能的接近最优解的话,那么这样的算法有何尝不是一种好算法呢?在游戏之中贪心算法用的最普遍的就是寻路

      先引用一段网上关于寻路的一段话:

           我们尝试解决的问题是把一个游戏对象(game object)从出发点移动到目的地。路径搜索(Pathfinding)的目标是找到一条好的路径——避免障碍物、敌人,并把代价(燃料,时间,距离,装备,金钱等)最小化。运动(Movement)的目标是找到一条路径并且沿着它行进。把关注的焦点仅集中于其中的一种方法是可能的。一种极端情况是,当游戏对象开始移动时,一个老练的路径搜索器(pathfinder)外加一个琐细的运动算法(movement algorithm)可以找到一条路径,游戏对象将会沿着该路径移动而忽略其它的一切。另一种极端情况是,一个单纯的运动系统(movement-only system)将不会搜索一条路径(最初的“路径”将被一条直线取代),取而代之的是在每一个结点处仅采取一个步骤,同时考虑周围的环境。同时使用路径搜索(Pathfinding)和运动算法(movement algorithm)将会得到最好的效果。

                                                                       A*寻路算法

       接下来就来讲一讲游戏之中常用或者说是2D游戏之中最常用的算法---A*寻路算法!当然寻路算法不止 A* 这一种,还有递归, 非递归, 广度优先, 深度优先, 使用堆栈等等, 有兴趣的可以研究研究~~

       先从背景知识开始吧!在计算机科学中,A*算法广泛应用于寻路和图的遍历。最早是于1968年,由Peter Hart, Nils Nilsson 和Bertram Raphael3人在斯坦福研究院描述了 该算法。是对Dijkstra算法的一种扩展。是一种高效的搜索算法。

                                           寻路的步骤

       总结出下面的寻路六部曲大家先看看下面这张图,因为下面的步骤都是基于这两张图的(一张是开始的图,一张是最终找到了的图)

    第一步:从起点A开始, 把它作为待处理的方格存入一个"开启列表", 开启列表就是一个等待检查方格的列表

    第二步:寻找起点A周围可以到达的方格, 将它们放入"开启列表", 并设置它们的"父方格"为A

    第三步:从"开启列表"中删除起点 A, 并将起点A 加入"关闭列表", "关闭列表"中存放的都是不需要再次检查的方格

           

        注:图中浅绿色描边的方块表示已经加入"开启列表" 等待检查.淡绿色又有点接近淡蓝色描边的起点 A 表示已经放入"关闭列表" , 它不需要再执行检查

            从 "开启列表" 中找出相对最靠谱的方块, 什么是最靠谱? 它们通过公式 F=G+H 来计算,F也叫作启发函数

                    F = G + H

                    G 表示从起点 A 移动到网格上指定方格的移动耗费 (可沿斜方向移动).

                    H 表示从指定的方格移动到终点 B 的预计耗费 (关于H的取法有很多种,最常见的也是用最多的就是曼哈顿算法,两点之间的横坐标之差与纵坐标之差的和,需要注意的是用曼哈顿算法不一定能得到最优路径 而且如果采用曼哈顿算法,那么严格意义上来说只能叫A搜索,不能叫A*搜索,由于采用这个方法说起来简单,实现起来也比较简单,适合初学者,所以本文就采用了曼哈顿算法。A*本身不限制H使用的估计算法,如max(dx,dy)、sqrt(dx*dx+dy*dy)、min(dx,dy)*(0.414)+max(dx+dy)这些都可以(可惜曼哈顿算法dx+dy不在此列),记住一点,只要你能保证H值恒小于实际路径长,A*就是成立的。你甚至可以取一个常数0,这样A*就退化为广搜了)。

            我们还是采用曼哈顿算法来说明吧,因为这样写起来方便,就暂时不去区分A算法与A*算法了!假设横向移动一个格子的耗费为10, 为了便于计算, 沿斜方向移动一个格子耗费是14.。为了更直观的展示如何运算 FGH, 图中方块的左上角数字表示 F, 左下角表示 G, 右下角表示 H。

            从 "开启列表" 中选择 F 值最低的方格 C (绿色起始方块 A 右边的方块), 然后对它进行如下处理:

           第四步:把它从 "开启列表" 中删除, 并放到 "关闭列表" 中

           第五步: 检查它所有相邻并且可以到达 (障碍物和 "关闭列表" 的方格都不考虑) 的方格. 如果这些方格还不在 "开启列表" 里的话, 将它们加入 "开启列表", 计算这些方格的 G,,H 和 F 值各是多少, 并设置它们的 "父方格" 为 C

           第六步: 如果某个相邻方格 D 已经在 "开启列表" 里了, 检查如果用新的路径 (就是经过C 的路径) 到达它的话, G值是否会更低一些,,如果新的G值更低, 那就把它的 "父方格" 改为目前选中的方格 C, 然后重新计算它的 F 值和 G 值 (H 值不需要重新计算, 因为对于每个方块, H 值是不变的).。如果新的 G 值比较高, 就说明经过 C 再到达 D 不是一个明智的选择,,因为它需要更远的路, 这时我们什么也不做.

           上述已构成了一个子问题的求解过程,所以就这样, 我们每次都从 "开启列表" 找出 F 值最小的, 将它从 "开启列表" 中移掉, 添加到 "关闭列表".。再继续找出它周围可以到达的方块,如此循环下去...

           那么什么时候停止呢? —— 当我们发现 "开始列表" 里出现了目标终点方块的时候, 说明路径已经被找到。

           最后一个问题就是如何返回路径呢?

           别忘了,我们还保存了”父节点“呢,最后从目标格开始, 沿着每一格的父节点移动直到回到起始格, 这就是路径

           最后用一张动态图作为结束吧!(关于A*算法的代码,后面会补上!)

       

           其实还有两个常用的算法没有说完,一个是回溯法、一个是分支界限法。这两个算法有些类似,也有区别。所以准备下回一起讲!就暂时写到这吧!

    转载请注明原文地址: https://ju.6miu.com/read-1132667.html
    最新回复(0)