SDL中文论坛

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

[ai] AI攻击

[复制链接]

149

主题

331

帖子

2445

积分

版主

Rank: 7Rank: 7Rank: 7

积分
2445
跳转到指定楼层
楼主
发表于 2020-9-5 19:08:21 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
轮到AI行动时,AI要进行攻击决策。攻击决策包括对哪单位实行攻击,站在哪个格子对该单位实行攻击,用何种武器进行攻击等等。

在讨论AI是如何做攻击决策前先看几个术语。

攻击路径(以下称路径):用于表示一种攻击可能性,由它可引导实际攻击。路径攻击方属于本AI阵营单位,被攻击方是和本AI阵营敌对单位。

这里所说的路径能引导的可能不只是一次实际攻击。为更好说明看以下这个图,

轮到周瑜方攻击。对于属于曹操方阵营的曹纯和魏城这两个单位,要做到攻击得最好,我们往往有个原则,一出手就要对方毙命,一次不行就由多个单位组合起轮攻。这里锁定要攻击单位是曹纯,首先周瑜上去放闪电,其次吕蒙,最后陆逊放魔法。这种组合起来的攻击也称为一个路径,所以路径中的“一”准确说指的是攻击针对的是同一个敌对单位。

攻击路径节拍(以下称路径节拍或节拍):正如以上解释路径时说的,攻击不只是一次实际攻击,把能引导一次实际攻击的称为一个节拍。以上周瑜攻击曹纯称为一个节拍,吕蒙是第二节拍,陆逊是第三节拍,也就是说三个节拍组成了这条路径。对于六角形格子,周围最多站六个单位,以每个单位都能攻击来算,一次路径最多只能是6个节拍。

攻击路径分数(以下称路径分数或分数):每条攻击路径除了攻击单位和被攻击单位外还有一个用于衡量这条路径优劣的指标值:分数。分数是一个数值,可以是浮点数也可以是整数,它是以一定法则联系起数个自变量形成函数、由这个函数计算出的y值。以上周瑜是攻击曹纯还是魏城,由于魏城耐久度高,攻击曹纯这条路径计算出的分数就要比攻击魏城这条路径高。

攻击路径集(以下称路径集):多条路径形成路径集合,这个形成是无序的,就像以vector进行组织。AI最终会选择当中一条作为实际攻击时路径,这条路径往往就是分数最高的路径。

AI形成攻击决策主要是两个方面:1)如何形成路径集;2)对每一条路径,如何得出分数。

二楼:如何形成路径集(2.1)(一个实例[直观理解])
三楼:如何形成路径集(2.2)(代码概述)
四楼:如何形成路径集(2.3)(一个实例[代码理解])
五楼:对每一条路径,如何得出分数
回复

使用道具 举报

149

主题

331

帖子

2445

积分

版主

Rank: 7Rank: 7Rank: 7

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

如何形成路径集(2.2)(代码概述)

一、analyze_targets
  1. void ai_default::analyze_targets(std::vector<attack_analysis>& res)
复制代码
analyze_targets分析出所有攻击路径,res存放这个分析结果。它的执行逻辑

1.1:清空相关缓存
缓存包括us缓存(unit_stats_cache)、城内部队缓存(reside_cache)。
  1. unit_stats_cache().clear();
  2. for (std::map<unit*, std::pair<map_location, unit*>* >::iterator itor = reside_cache_.begin(); itor != reside_cache_.end(); ++ itor) {
  3.         delete itor->second;
  4. }
  5. reside_cache_.clear();
复制代码
1.2:生成本AI势力中依然能实施攻击单位的可移动到信息(脚印信息、端到端信息
  1. ......
  2. calculate_possible_moves2(unit_locs, possible_moves2, srcdst2, dstsrc2, false, false, &get_avoid());
  3. ......
复制代码
1.3:枚举地图上所有敌对单位,针对每个单位分析可攻击路径
  1. for (unit_map::const_iterator j = units_begin(); j != units_.end(); ++j) {
  2.         const map_location& candidate_loc = j->get_location();
  3.         if (current_team().is_enemy(j->second.side()) {
  4.                 // 此单位是本AI势力敌对单位
  5.                 attack_analysis analysis;
  6.                 analysis.target = candidate_loc;
  7.                 ......
  8.                 do_attack_analysis(candidate_loc, ..., res, analysis);
  9.         }
  10. }
复制代码
二、do_attack_analysis
  1. void aspect_attacks::do_attack_analysis(const map_location& loc, const move_map& srcdst, const move_map& dstsrc, const move_map& fullmove_srcdst, const move_map& fullmove_dstsrc, const move_map& enemy_srcdst, const move_map& enemy_dstsrc, const map_location* tiles, bool* used_locations, std::vector<map_location>& units, std::vector<attack_analysis>& result, attack_analysis& cur_analysis) const
复制代码
do_attack_analysis,分析出本阵营单位在某个敌对单位上所有可能的攻击路径。注:这是函数是个递归函数,不考虑递归时,它的每一次执行或在原有路径上新加一切拍形成新的路径,或产生第一节拍形成的新的路径。

A:为什么要有cur_analysis参数?
Q:有些攻击路径不只一个节拍,让此次分析基于它形成多节拍路径。不考虑递归调用,一次do_attack_analysis只是形成一个节拍。例如,给的cur_analysis是空的,那么此次do_attack_anlysis形成是单节拍路径,如果是N节拍,那么此次将形成N+1节拍路径。
注:对路径来说,多一个节拍就意味着又是一条新的路径,因此不考虑递归调用下,一次do_attack_analysis也是形成了一个新路径。

函数主要逻辑
  1. for (size_t i = 0; i != units.size(); ++i) {
  2.         const map_location current_unit = units[i];
  3.         unit_map::iterator unit_itor = units_find(current_unit);
  4.         ......
  5.         int best_rating = 0;
  6.         int cur_position = -1;
  7.         // 以下for循环是为了找出current_unit去攻击loc时,在loc邻靠6个格子中,站哪个格子上时能达到最大攻击输出。
  8.         for (int j = 0; j != 6 ; ++j) {
  9.                 if (used_locations[j]) {
  10.                         continue;
  11.                 }
  12.                 // 计算单位是否有缓慢、背刺、地形等输入量度算出初级优益度:rating
  13.                 rating = ....
  14.                 if (cur_position >= 0 && rating < best_rating) {
  15.                         continue;
  16.                 }
  17.                 // 计算出current_unit在j格子处最优值
  18.                 cur_position = j;
  19.                 best_rating = rating;
  20.         }
  21.         if (cur_postion != -1) {
  22.                 // cur_postion != -1, 表示存在个值得考虑的攻击输出

  23.                 // 这是为再次运行do_attack_analysis做准备。下次运行时已方单位必须少考虑当前这个单位
  24.                 units.erase(units.begin() + i);
  25.                 // 形成一个攻击路径拍子
  26.                 cur_analysis.movements.push_pack(std::pair<map_location, map_location>(current_unit, tiles[cur_position]));
  27.                 cur_analysis.analyze(......);
  28.                 // 形成了一条攻击路径,把这路径汇入路径集
  29.                 result.push_back(cur_analysis);               
  30.                 // 这是为再次运行do_attack_analysis做准备。下次运行时loc旁边的这个cur_postion位置当被占位了,即再有单位要考虑攻击时不能移动到这里
  31.                 used_locations[cur_position] = true;
  32.                 // 再次执行do_attack_analysis,对loc格子进行在当前基础上再深入一层搜索攻击路径
  33.                 do_attack_analysis(loc, ..., titles, used_locations, units, result, cur_analysis);
  34.                 // 复原回该层的现场
  35.                 used_locations[cur_positions] = false;
  36.                 cur_analysis.movements.pop_back();
  37.                 units.insert(units.begin() + i, current_unit);
  38.         }
  39. }
复制代码
回复 支持 反对

使用道具 举报

149

主题

331

帖子

2445

积分

版主

Rank: 7Rank: 7Rank: 7

积分
2445
板凳
 楼主| 发表于 2020-9-5 19:09:35 | 只看该作者
1、根据U和u,找出最优战法时的range;
2、针对U能够动到的range各个格子,找出一个最好的,cur_position记住这个值;
3、以这个最好的形成节拍;

A:已方单位U能攻击敌方单位u的条件?
Q:U--attack-->u,它是形成节拍,而能不能形成这个节拍,它遵循什么条件?
1:U要能够移动到u附近,站在那里以某种战法攻击到。这个不能“移动到附近”有三情况:1)够不到战法距离。如果U最多只能移动和u隔两格,但又只有range=1战法,此次攻击自然不能凑效。2)已有节拍把能攻击到到位置全占了;3)能攻击到的格子全被站了部队。
2:不值得攻击。例如,此个单位是高等级单位,HP只剩1。
在已知U和u条件下,如何找出最优战法?

A:为什么choose_attacker_weapon和choose_defender_weapon实现上很不一样?
Q:choose_attacker_weapon是两种战法都要选,可认为它当中是两个for循环(第二个for循环在choose_defender_weapon内执行)。choose_defender_weapon是在给定了攻击方战法后决定出采用哪种防御战法,只有一个for循环。

A:attack_weight和defend_weight意义
Q:attack_weight决定该战法能不能用于攻击(不能攻击并不意味着它就没用了,它可以用于防御),它的值说来只有两个,0:不用于攻击;非零:可用于攻击,但强制认为非零>0,而典型值就是1.0。
defend_weight决定该战法被用于防御使用时可能性,值越大可能性越高。但并不是说它的值最大它就肯定被作为要使用战法,还要看该战法给攻击方造成的损伤。例如
战法A:给攻方造成损伤400,weight(0.4),===》400*0.4 = 160
战法B:给攻方造成损伤80,weight(1),===》80*1 = 80
虽然战法B比战法A的weight高,但效果还是战法A好,结果使用战法A。

A:battle_context的输入和输出
Q:输入:attack_loc,defend_loc,prev_def。为什么要有prev_def?在AI计算多节拍路径时,第N节拍碰到的防御方已不是defend_loc得到的那unit,而已经被之前N-1个节拍攻击后残剩的unit,要用这个combatant进行计算。注:选择什么战法要是进行模拟攻击,根据攻击后状态来决定谁优认劣,而模拟攻击要使用到prev_def。
输出:四个参数,攻击防御各两个。
unit_stat:此次战斗形成的各个参数。要攻击次数,每次损伤值,使用的战法(索引及结构)等等。
combatant:对unit_stat补充值。攻击后剩余HP,结果是否被中毒,是否被缓慢;

A:do_attack_analysis中构造battle_context时,为何prev_def置为NULL?
Q:原则上说,此个prev_def不能置为NULL,置NULL只有在第一节拍是对的,后面节拍时prev_def不应置为NULL,而是应该反映“N-1次攻击后”的那个状态。
此处置为NULL,是种偷懒作法。
do_attack_analysis能不能拿到pref_def?这是可以的,只要cur_analysis.analyze(...)把分析后的那个pref_def保留下来。但我很纳闷,cur_analysis.analyze(...)函数为何要把所有节拍计算一遍呢,它不保留前面N-1次的计算结果吗?每次都计算N次,是不是太耗CPU了。我认为此处可以优化,优化时再想法抽出prev_def,传给do_attack_analysis。

A:每计算一次battle_context较耗资源,如何减少在这方便CPU消耗?
Q:以<attack_loc, defend_loc>构造一个battle_context以对象,要选择战法,模拟攻击,还是要耗一定CPU的。但分析攻击路径可又是要进行多次构造battle_context以确定攻击结果。在此采用对已出来的battle_context开缓存办法。基于方法是:battle_context除了<attack_loc, defend_loc>构造,它还能以一个更快的方式构造,这种方法就是使用<attack_unit_stats, defend_unit_stats>。
缓存形式:std::map<std::pair<map_location,const unit_type *>, std::pair<battle_context::unit_stats, battle_context::unit_stats> >
key:<防御方所在格子, 攻击方兵种类型指针>
兵种在内存中存储位置,在游戏一初化后就不能再变,它的指针是唯一的。
value:<attack_unit_stats, defend_unit_stats>
由它们可以较快地生成battle_context。
注:由<attack_unit_stats, defend_unit_stats>方式生成的battle_context,要由这个bc得到两个combatant城要经再经过模拟战斗,因而还是须要prev_def。但是,虽然要prev_def,但这种缓存命中方式还是存在缺陷,那就是两者战法比较是以第一个遇到<map_location,const unit_type *>时决定的,此时的prev_def并不见得是那时的pref_def,这叫造成结果存在一定出入。
缓存存放位置:ai_composite对象内的unit_stats_cache_变量。它在ai_composite::play_turn要被反回时clear。
回复 支持 反对

使用道具 举报

149

主题

331

帖子

2445

积分

版主

Rank: 7Rank: 7Rank: 7

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

us缓存(Unit Stats Cache)

AI分析攻击路径时,存在几个需要解决问题。

1)此轮分析状态下,Attacker攻击Defender最好使用何种战法?Defender回击时使用何种战法?知道战法才能确定攻击距离,确定距离需要使用unit_stats。
2)已确定Attacker攻击Defender这个攻击要汇入攻击路径集后,为计算更多此路径“分数”参数需要使用battle_context。
3)在可攻击路径中筛选,已确定Attacker攻击Defender这条攻击路径要用于实际攻击,这时为保证距离一使,它须要知道分析时它是使用何样战法(保证距离一致),需要知道分析时的unit_stats。

由此右见,AI要不断使用<已方部队Attacker, 敌方部队Defender>这样的配对。为此引入us缓存,us缓存两个作用:
1)分析过程需要大量生成battle_context。由attack_loc和defender_loc构造battle_context须要耗不少CPU,引入缓存希望降低CPU。
2)决定A--B攻击路径为实际攻击,为保持距离一致,须要知道分析时它是使用何种战法,这战法就从us缓存中找。

us缓存的key和value
us缓存中value就是以上的std::pair<unit_stats, unit_stats>。key用什么?
在这里key就是作为关键字进行搜索,这个关键字要能表达std::pair<unit_stats, unit_stats>是何处情况下生成的。生成std::pair<unit_stats, unit_stats>时是已方某个部队

攻击敌方某个部队,很容易让想到是用:std::pair<const unit*, const unit*>,当中两个unit*则是两个部队的指针。

以这种方法是能表达唯一性,能根据它搜到指定std::pair<unit_stats, unit_stats>。但为让us缓存更高命中率,采用std::pair<const unit*, const unit_type*>。
first:指向敌方部队指针。
second:已方攻击兵种指针。
注:攻击兵种指针是个全局不变量,以它为指针是安全的。

把second由具体某个部队指针换为该类部队兵种指针,就是为了提高命中率。这里假定该兵种所有部队对该“部队”使用同一战法。

当然,由于部队五维,三个主将能力不同,同一兵种部队能力也表现出很大差别,但在这里做了假定,假定这些差别可以被忽略。

韦诺源码用的是<map_location, const unit_type*>作为key,map_location是被攻击部队所在格子,unit_type*意义一致。改为不用map_location,是对us缓存会有大量的比较

key操作,以指针比较总比map_location要快。

何时清除us缓存中内容

清除us缓存可以两个时机:1)每轮攻击分析后就清除;2)此次AI整个攻击分析后才清除。

第一种:意味着每轮分析开如时它的battle_context集是空的,而分析时生成的很多battle_context之前可能都已经是构造过了的。重新构造,多耗CPU。

第二种:可尽量少构造balle_context,提高缓存使用率。——但这种方法实用下来不可行,使它不可行的原因就是无法保证缓存中项的有效性。让看几种缓存项无效例子。
1)std::pair<const unit*, const unit*>中second指定单位被击溃,attack_num是有效值,weanpon != NULL,但得到的attack结构却是个无效值,像range是""。原因,unit::~unit被调用,从而该unit内的attacks_被析构。unit_stats中存的是指针而不是结构,而且是指定特定部队内的attacks_,attacks_既已被析构,随之指向的将是不可预料值。
2)std::pair<const unit*, const unit*>中的first变得无效。可能是该部队被消灭了,也可能是编写mod造成。例如mod中这样写,赵云部队一旦被击溃,刘备队就被击溃,这种连带的无效使保证这个有效性,很难。

对于这个无效,归结为是已方部队和敌方部队被击溃后,mod可能会写出各式各样情况,像死后原地复活(这种方式下旧unit指针值和新unit指针值可能就是同一个!),为mod编写上灵活,在这里只好牺牲CPU,使用第一种清除时机。
回复 支持 反对

使用道具 举报

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

本版积分规则

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

GMT+8, 2025-5-2 04:04 , Processed in 0.062576 second(s), 23 queries .

Powered by Discuz! X3.3

© 2001-2017 Comsenz Inc.

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