SDL中文论坛

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 5041|回复: 5
打印 上一主题 下一主题

[ai] 函数:find_routes和a_star_searc

[复制链接]

149

主题

331

帖子

2445

积分

版主

Rank: 7Rank: 7Rank: 7

积分
2445
跳转到指定楼层
楼主
发表于 2020-9-5 18:55:38 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 ancientcc 于 2020-9-5 19:04 编辑

两个函数都用了A*搜索算法,看这贴之前需了解A*算法:http://www.libsdl.cn/bbs/forum.p ... &extra=page%3D1

2---4楼:find_routes

5楼:a_star_search
回复

使用道具 举报

149

主题

331

帖子

2445

积分

版主

Rank: 7Rank: 7Rank: 7

积分
2445
沙发
 楼主| 发表于 2020-9-5 18:56:41 | 只看该作者

find_routes概述

find_routes根据单位所在格子和可用移动力计算出单位的可到达位置。

当在游戏中单击鼠标左键选中一个单位,这时地图上显示该单位可到达位置。find_routes就用于计算这些个位置。下图就是鼠标单击(33,12)处时显示的可到达位置,总计有35个(不包括(32,11)和(32,12)这两个站了敌方单位的格子)。


find_routes计算这些个位置时采用A*搜索算法。既然使用了A*算法则需要明确几个问题:

满足什么样条件的结点才能放入OPEN:如果A点是第一次遇到,只要该点不站敌方单位或有移动力移到该点则可放入;如果A点是已经访问过,则此次展开要能比过去更“优”才能放入。

估值函数:到达A点剩余移动力比到达B点剩余移动力多,则认为到达A点价值更高。

已经展开但没有访问的结点集的变量表示:pq

已访问的结点集:这个没有确切定义,nodes很大部分完成这个功能。

以上图为例叙述搜索过程。

第一次
初始状态:
  OPEN: (33,12)
  CLOSE:

正访问结点:(33,12)

进入启发式搜索。具体来说就是判断欲访问结点(33,12)周围六个节点能不能展开,及形成该节点状态。
(32,11):站着个敌方单位,不能展开。(图中也变成可移动框,那是接下的show_attack_options加入的)
(32,12):站着个敌方单位,不能展开。
(33,13):能展开,但由于处在(32,12)的ZOC内,剩余移动力0。该点状态:{C(33,13), P(33,12), 0}。
(34,12):能展开。该点状态:{C(34,12), P(33,12), 6}。
(34,11):能展开。该点状态:{C(34,11), P(33,12), 6}。
(33,11):能展开。该点状态:{C(33,11), P(33,12), 6}。

OPEN: (33,13), (34,12), (34,11), (33,11)
对OPEN中结点进行排序,剩余移动力最大的排在前面。

结末状态:
  (34,12), (34,11), (33,11), (33,13)
  CLOSE: (33,12)


第二次

正访问结点:(34,12)
进入启发式搜索。
(33,12):不能展开。该结点已访问过,而且已访问过的剩余移动力7比到(34,12)都大。
(33,13):不能展开。该结点已访问过,且此次并不比已存在“优”。
(34,13):能展开。该点状态:{C(34,13), P(34,12), 3}。(移动山地花费3点移动力)
(35,13):能展开。该点状态:{C(35,13), P(34,12), 4}。
(35,12):能展开。该点状态:{C(35,12), P(34,12), 5}。
(34,11):不能展开。该结点已访问过,且此次并不比已存在“优”。

结末状态:(OPEN已被排序)
OPEN: (34,11), (33,11), (35,12), (35,13), (34,13), (33,11)
CLOSE:

第三次

第N次也是前面一样流程,在此次详述下for中的四个if判断。

正访问结点:(34,11),它展开出的第一个结点:(33,11),(33,11)已经被访问过。
第一个if:(next_visited && !(n < next)),起点到(34,11)不优于起点到(33,11)。如果到(34,11)要花费移动力不会比到(33,11)少,那可以肯定没必要再寻(34,11)到(33,11)这条路了。
第二个if:(t.movement_left < move_cost || t.turns_left < 0)。t是(33,12)-->(34,11)剩余移动力(注:没到(33,11)),(33,12)单位在(34,11)时已没再有移动力到达(33,11),那么这条路径自然不能成立。
第三个if:(next_visited && !(t < next)):t是(33,12)-->(34,11)-->(33,11),起始经过这条路径到达(33,11)不优于已存的起点到(33,11),那这条路径也不必找了。
第四个if:(next_visited && !(t < next))。这个第三个if是一样判断,但到这时t经过了单位考虑,一旦t位在敌方zoc内,t的剩余移动力归零。以上正是这种情况,(31,11)站着个敌方单位,所以(34,11)到(33,11)后,移动力就归零。——由于个if,!(t < netx)成立,continue。===》在node处依旧保存着curr=(33,11),prev=(33,12), left_movement=0。
回复 支持 反对

使用道具 举报

149

主题

331

帖子

2445

积分

版主

Rank: 7Rank: 7Rank: 7

积分
2445
板凳
 楼主| 发表于 2020-9-5 18:57:08 | 只看该作者

find_routes FAQ

node中in字段

说到in字段则必须说search_counter变量。search_counter是一个全局变量,但在一次find_routes过程中它的值保持不变。search_counter最小值是2(不可能是0和1)。

in变量用于判断某一格子在此次find_routes时是否已被访问过。此次find_routes没被访问过时,值是不确定的,但满足:1)是偶数;2)(in - search_counter <= 1u)成立,这个式子中in可能比search_counter小,但因为是无符号数,<还是成立。

对in变量,要标志node是否访问过,用个bool变量不是更简单,干吗要in又要search_counter,这不是增加复杂度吗?——这么做原因是nodes是个静态变量(node中的元素数是地图格子数,初始化须要点时间)。第N+1次用的是第N次操作结果,N+1次的in值自然也要基于N次in值。

要用bool变量标识的话,逃不掉各个node中开辟个状态变量方法,这个变量在搜索前全部置为false,访问过后置为true。用了以上in+serach_counter方法后,省略了这个全置false过程。

和in状态相关的另一个可能疑问,函数每次搜索前会调用nodes.resize,nodes.resize会不会把所有in复位为0?
——不会。以下是resize的函数实现:
  1. void resize(size_type new_size) { resize(new_size, T()); }
  2. void resize(size_type new_size, const T& x)
  3. {
  4.         if (new_size < size()) {
  5.                 erase(begin() + new_size, end()); // erase区间范围以外的数据,确保区间以外的数据无效
  6.         } else {
  7.                 insert(end(), new_size - size(), x); // 填补区间范围内空缺的数据,确保区间内的数据有效
  8.         }
  9. }
复制代码
回复 支持 反对

使用道具 举报

149

主题

331

帖子

2445

积分

版主

Rank: 7Rank: 7Rank: 7

积分
2445
地板
 楼主| 发表于 2020-9-5 18:57:34 | 只看该作者

find_routes函数

  1. @map:全局唯一的gamemap;
  2. @uinits:全局唯一的unit_map;
  3. @u:要从它开始计算可到达位置的单位;
  4. @loc:一个坐标值,要以它为起点开始计算可到达位置;
  5. @move_left:单位的剩余移动力。对于未移动过单位,它的值就是unit.total_movement;
  6. @[color=Red]destinations[/color][OUT]:存储计算出可到达位置。destinations值上可认为是step数组,struct step定义:{(map_location)  prev, (map_location) curr, (int) move_left};
  7. @force_ignore_zoc:是否强制不考虑zoc。ignore_units=true时,该值忽略;
  8. @allow_teleport:是否要使用传送。拥有传送技能单位该值一般置true,否则false;
  9. @viewing_team:本阵营可看见的其它阵营(包括自己),它经常等于teams;
  10. @additional_turns:附加回合数。1:显示两回内数径,2:显示三回内路径;
  11. @see_all:默认置false;
  12. @ignore_units:是否忽略单位,默认false;

  13. 格子一旦被访问过,它的in值被置为search_counter + 1。
  14. 经过find_routes后形成的destinations内容
  15. 1、自已所在格子。即loc,也会在destinations中,但它的step.prev是个未定义值;
  16. 2、友方阵营单位所在格子。
  17. 3、单位有传输技能时,即allow_teleport,***
  18. 4、敌方阵营单位所在格子不在destinations中,(ignore_units=false)。上面包括两个敌方单位格子,那个后面的show_attack_options(u)添加的。
  19. 5、排序。以[X][Y]次序,x、y都0逐增。

  20. static void find_routes(const gamemap& map, const unit_map& units,
  21.                 const unit& u, const map_location& loc,
  22.                 int move_left, pathfind::paths::dest_vect &destinations,
  23.                 std::vector<team> const &teams,
  24.                 bool force_ignore_zocs, bool allow_teleport, int turns_left,
  25.                 const team &viewing_team,
  26.                 bool see_all, bool ignore_units)
  27. {
  28.         const team& current_team = teams[u.side() - 1];
  29.         std::set<map_location> teleports;
  30.         if (allow_teleport) {
  31.           teleports = pathfind::get_teleport_locations(u, units, viewing_team, see_all, ignore_units);
  32.         }

  33.         const int total_movement = u.total_movement();

  34.         // prepare self-city grid condition
  35.         // 得到属于“本城格子”判断条件
  36.         void* expediting_city_cookie = NULL;
  37.         if (units.expediting()) {
  38.                 expediting_city_cookie = units.expediting_city_node();
  39.         } else if (units.city_from_loc(loc)) {
  40.                 expediting_city_cookie = units.get_cookie(loc);
  41.         }

  42.         std::vector<map_location> locs(6 + teleports.size());
  43.         std::copy(teleports.begin(), teleports.end(), locs.begin() + 6);

  44.         search_counter += 2;
  45.         if (search_counter == 0) search_counter = 2;

  46.         static std::vector<node> nodes;
  47.         nodes.resize(map.w() * map.h());

  48.         indexer index(map.w(), map.h());
  49.         comp node_comp(nodes);

  50.         int xmin = loc.x, xmax = loc.x, ymin = loc.y, ymax = loc.y, nb_dest = 1;

  51.         nodes[index(loc)] = node(move_left, turns_left, map_location::null_location, loc);
  52.         std::vector<int> pq;
  53.         // 调用程序可能把move_left=0传下来,像出征,这时不要让出征到“本城格子”。为不显示“本城格子”是可出征地,办法就是让move_left=0时不执行接下的while循环体
  54.         if (move_left || turns_left) {
  55.                 pq.push_back(index(loc));
  56.         }

  57.         while (!pq.empty()) {
  58.                 node& n = nodes[pq.front()];
  59.                 std::pop_heap(pq.begin(), pq.end(), node_comp);
  60.                 pq.pop_back();
  61.                 n.in = search_counter;

  62.                 get_adjacent_tiles(n.curr, &locs[0]);
  63.                 for (int i = teleports.count(n.curr) ? locs.size() : 6; i-- > 0; ) {
  64.                         if (!locs[i].valid(map.w(), map.h())) continue;

  65.                         node& next = nodes[index(locs[i])];

  66.                         bool next_visited = next.in - search_counter <= 1u;

  67.                         // Classic Dijkstra allow to skip chosen nodes (with next.in==search_counter)
  68.                         // But the cost function and hex grid allow to also skip visited nodes:
  69.                         // if next was visited, then we already have a path 'src-..-n2-next'
  70.                         // - n2 was chosen before n, meaning that it is nearer to src.
  71.                         // - the cost of 'n-next' can't be smaller than 'n2-next' because
  72.                         //   cost is independent of direction and we don't have more MP at n
  73.                         //   (important because more MP may allow to avoid waiting next turn)
  74.                         // Thus, 'src-..-n-next' can't be shorter.
  75.                         if (next_visited) continue;

  76.                         int move_cost;
  77.                         if (expediting_city_cookie && units.get_cookie(locs[i]) == expediting_city_cookie) {
  78.                                 // locs[i]是“本城格子”,消耗移动力0
  79.                                 move_cost = 0;
  80.                         } else {
  81.                                 move_cost = u.movement_cost(map[locs[i]]);
  82.                         }
  83.                                

  84.                         node t = node(n.movement_left, n.turns_left, n.curr, locs[i]);
  85.                         if (t.movement_left < move_cost) {
  86.                                 t.movement_left = total_movement;
  87.                                 t.turns_left--;
  88.                         }

  89.                         // 前半部分作用:有些单位的total_movement可能就小于在该地形上的需要移动力,即使加了一回合还是不够
  90.                         if (t.movement_left < move_cost || t.turns_left < 0) continue;

  91.                         t.movement_left -= move_cost;

  92.                         if (!ignore_units) {
  93.                                 const unit *v =
  94.                                         get_visible_unit(units, locs[i], viewing_team, see_all);
  95.                                 if (v && current_team.is_enemy(v->side())) {
  96.                                         // 正欲展开格子上站着个敌方单位,该格子不进入OPEN
  97.                                         continue;
  98.                                 }

  99.                                 // move cost in any terrain cannot be zero. it is zero indicate loc is self-city grid.
  100.                                 if (move_cost && !force_ignore_zocs && t.movement_left > 0
  101.                                     && pathfind::enemy_zoc(units, teams, locs[i], viewing_team, u.side(), see_all)
  102.                                                 && !u.get_ability_bool("skirmisher", locs[i])) {
  103.                                         // 正欲展开格子在一个敌方单位的ZOC内,一旦移动到该格子剩余移动力置0。
  104.                                         t.movement_left = 0;
  105.                                 }
  106.                         }

  107.                         // 确定要展开一个新的格子后,看是否要扩大容纳可到达位置的“矩形”
  108.                         ++nb_dest;
  109.                         int x = locs[i].x;
  110.                         if (x < xmin) xmin = x;
  111.                         if (xmax < x) xmax = x;
  112.                         int y = locs[i].y;
  113.                         if (y < ymin) ymin = y;
  114.                         if (ymax < y) ymax = y;

  115.                         bool in_list = next.in == search_counter + 1;
  116.                         t.in = search_counter + 1;
  117.                         next = t;

  118.                         // if already in the priority queue then we just update it, else push it.
  119.                         if (in_list) { // never happen see next_visited above
  120.                                 std::push_heap(pq.begin(), std::find(pq.begin(), pq.end(), index(locs[i])) + 1, node_comp);
  121.                         } else {
  122.                                 pq.push_back(index(locs[i]));
  123.                                 std::push_heap(pq.begin(), pq.end(), node_comp);
  124.                         }
  125.                 }
  126.         }

  127.         // Build the routes for every map_location that we reached.
  128.         // The ordering must be compatible with map_location::operator<.
  129.         // 根据容纳可到达位置的“矩形”,以X第一次排序Y第二次排序顺序从nodes生成destinations
  130.         destinations.reserve(nb_dest);
  131.         for (int x = xmin; x <= xmax; ++x) {
  132.                 for (int y = ymin; y <= ymax; ++y)
  133.                 {
  134.                         const node &n = nodes[index(map_location(x, y))];
  135.                         if (n.in - search_counter > 1u) continue;
  136.                         pathfind::paths::step s =
  137.                                 { n.curr, n.prev, n.movement_left + n.turns_left * total_movement };
  138.                         destinations.push_back(s);
  139.                 }
  140.         }
  141. }
复制代码
回复 支持 反对

使用道具 举报

149

主题

331

帖子

2445

积分

版主

Rank: 7Rank: 7Rank: 7

积分
2445
5#
 楼主| 发表于 2020-9-5 18:58:14 | 只看该作者

a_star_search

a_star_search找到一条从起点到终点路径。(shortest_path_calculator:消耗移动力最少)。下图的脚印指出一条路径(从(33,12)到(29,14))。


node中成员数据
g:符合A*算法定义。指示起点到当前点路径代价。在值上差不多等于起点到当前点消耗移动力和,具体对应一路来cost之和。
h:符合A*算法定义。指示当前点到终点路径代价。因为路径没有确定,只好用估值,heuristic函数执行这个估值。
t:g+h。

a_star_search返回的plain_route内容
返回的plain_route指示了一条路径,就由两个变量组成。
1、steps:一个std::vector<map_location>类型变量。其值就是从连缀出的从起到到终点路径。具体到示例图值是8个坐标,(33,12)(34,12)(34,13)(33,14)(32,14)(31,15)(30,14)(29.14).
2、move_cost:steps这条路径消耗的移动力和。具体到示例图值是28。

@calc:对于当见情况,cost_calculator的原型是shortest_path_calculator,对应的cost也就是shortest_path_calculator::cost;
  1. pathfind::plain_route pathfind::a_star_search(const map_location& src, const map_location& dst,
  2.                               double stop_at, const pathfind::cost_calculator *calc, const size_t width,
  3.                             const size_t height, const std::set<map_location>* teleports) {
  4.         //----------------- PRE_CONDITIONS ------------------
  5.         assert(src.valid(width, height));
  6.         assert(dst.valid(width, height));
  7.         assert(calc != NULL);
  8.         assert(stop_at <= calc->getNoPathValue());
  9.         //---------------------------------------------------

  10.         DBG_PF << "A* search: " << src << " -> " << dst << '\n';

  11.         // 对shortest_path_calculator,cost()返回值和src无关,值上大概等于单位在dst这格子上消耗的移动力,和dst是什么地形相关。
  12.         if (calc->cost(dst, 0) >= stop_at) {
  13.                 LOG_PF << "aborted A* search because Start or Dest is invalid\n";
  14.                 pathfind::plain_route locRoute;
  15.                 locRoute.move_cost = int(calc->getNoPathValue());
  16.                 return locRoute;
  17.         }

  18.         if (teleports && teleports->empty()) teleports = NULL;

  19.         std::vector<map_location> locs(teleports ? 6 + teleports->size() : 6 );
  20.         if (teleports) {
  21.                 std::copy(teleports->begin(), teleports->end(), locs.begin() + 6);
  22.         }

  23.         // increment search_counter but skip the range equivalent to uninitialized
  24.         search_counter += 2;
  25.         if (search_counter - bad_search_counter <= 1u)
  26.                 search_counter += 2;

  27.         static std::vector<node> nodes;
  28.         nodes.resize(width * height);  // this create uninitalized nodes

  29.         indexer index(width, height);
  30.         comp node_comp(nodes);

  31.         nodes[index(dst)].g = stop_at + 1;
  32.         nodes[index(src)] = node(0, src, map_location::null_location, dst, true, teleports);

  33.         std::vector<int> pq;
  34.         pq.push_back(index(src));

  35.         while (!pq.empty()) {
  36.                 node& n = nodes[pq.front()];

  37.                 n.in = search_counter;

  38.                 std::pop_heap(pq.begin(), pq.end(), node_comp);
  39.                 pq.pop_back();

  40.                 if (n.t >= nodes[index(dst)].g) {
  41.                         // 正常情况下,这里是退出while出口。随着枚举不断深入,dst会出现在某一次get_adjacent_tiles返回的格子中,到时nodes[index(dst)].g的值就会由初始设置的stop_at改到一个比stop_at小的值(大概等于选择了的src到dst这条路径上消耗移动力之和)
  42.                         break;
  43.                 }

  44.                 get_adjacent_tiles(n.curr, &locs[0]);

  45.                 int i = teleports && teleports->count(n.curr) ? locs.size() : 6;
  46.                 for (; i-- > 0;) {
  47.                         if (!locs[i].valid(width, height)) continue;

  48.                         node& next = nodes[index(locs[i])];

  49.                         // 如果next节点已被访问过,则门限值取next.g,否则取stop_at
  50.                         double thresh = (next.in - search_counter <= 1u) ? next.g : stop_at + 1;
  51.                         // cost() is always >= 1  (assumed and needed by the heuristic)
  52.                         if (n.g + 1 >= thresh) continue;
  53.                         // cost()返回值至少大于等于在locs[i]上的消耗移动力,移动力至少大于等于零
  54.                         // 以下等式不是A*算法中的f(n) = g(n) + h(n),而是类似g(n + 1) = g(n-1) + g(n)
  55.                         double cost = n.g + calc->cost(locs[i], n.g);
  56.                         if (cost >= thresh) continue;

  57.                         bool in_list = next.in == search_counter + 1;

  58.                         next = node(cost, locs[i], n.curr, dst, true, teleports);

  59.                         if (in_list) {
  60.                                 std::push_heap(pq.begin(), std::find(pq.begin(), pq.end(), index(locs[i])) + 1, node_comp);
  61.                         } else {
  62.                                 pq.push_back(index(locs[i]));
  63.                                 std::push_heap(pq.begin(), pq.end(), node_comp);
  64.                         }
  65.                 }
  66.         }

  67.         pathfind::plain_route route;
  68.         if (nodes[index(dst)].g <= stop_at) {
  69.                 DBG_PF << "found solution; calculating it...\n";
  70.                 route.move_cost = static_cast<int>(nodes[index(dst)].g);
  71.                 for (node curr = nodes[index(dst)]; curr.prev != map_location::null_location; curr = nodes[index(curr.prev)]) {
  72.                         route.steps.push_back(curr.curr);
  73.                 }
  74.                 route.steps.push_back(src);
  75.                 std::reverse(route.steps.begin(), route.steps.end());
  76.         } else {
  77.                 LOG_PF << "aborted a* search  " << "\n";
  78.                 route.move_cost = static_cast<int>(calc->getNoPathValue());
  79.         }

  80.         return route;
  81. }
复制代码
回复 支持 反对

使用道具 举报

149

主题

331

帖子

2445

积分

版主

Rank: 7Rank: 7Rank: 7

积分
2445
6#
 楼主| 发表于 2020-9-5 18:59:27 | 只看该作者

AI搜索空位,找到(19,4)。并形成路径(21,4)、(20,4)、(19,4)
但在实际出城时,(20, 5)站着孙坚,被zoc,出征立即结束,于是就看到不断有部队到(20,4)就回城情况。
这种情况会持续到另有一个部队以着另条路径把(19,4)给占掉。例如路径(21,4)、(20,3)、(19,4)。

以上过程形成路径用的是pathfind::a_star_search,使用的成本计算规则是pathfind::shortest_path_calculator;
出城用的是::move_unit;

结论
pathfind::shortest_path_calculator似乎把孙坚这个zoc给忽略了。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|丽谷软件|libsdl.cn

GMT+8, 2025-5-2 01:03 , Processed in 0.064617 second(s), 22 queries .

Powered by Discuz! X3.3

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表