本文翻译自这篇文章The Mechanics of Influence Mapping:Representation, Algorithm & Parameters ,主要介绍了势力图的一些基本知识。翻译的不合适的地方,还请谅解。
在很早之前的游戏中势力图就应用在了AI中,追溯其历史可以到10年前的RTS游戏中。从那开始,势力图对于游戏开发者来说就变成了一项基本技术,并且也开始在FPS游戏中流行(比如杀戮地带)。在这篇教程中,你将会了解到一些使用势力图的动机且为什么它如此吸引人。你也将会了解何时使用势力图以及什么样的模型表现最适合你的游戏。之后,你也将会看到在运行时使用势力图需要调整的那些最重要的参数。
动力
势力图基本上通过提供世界的有用信息来帮助AI做出更好的决策。特别的,势力图提供了三种不同类型的信息,这些信息对于决策的制定特别有用:
状况总结——势力图十分擅长在世界中总结小细节并且使他们看一眼就能明白。谁控制了什么区域?领土边界在哪?
在每一块区域中出现了多少敌人?
历史统计——势力图不仅能存储当前状况的信息,而且还能存储过去一段时间内发生了什么。这块区域是否遭到过袭击?
我之前的攻击进行的怎么样?
未来的预测——一个经常被忽略的特性就是势力图还可以帮助预测未来。利用地图,你可以计算出敌人想去
哪且它在未来如何发展它的势力。
正如你能想象到的,势力图的上述三种属性可以通过一种更加智能的方式帮助AI执行威胁分析。因为这些都是我们作为人类玩家考虑的因素,它也应该有助于AI。
为什么不呢?
如果你已经被势力图的优点所说服,你可能应该在实现他们之前花时间暂停一下。有一个不需要他们的原因;如果世界太简单以至于你根本不需要为世界建立地图。比如,使用occasional tree 建立的开放地形。
通常,在以下情况你需要势力图:
1. 如果你的图有变连通,且不仅是一个所有连接都连通的2D网格。
2. 如果你的世界有兴趣特性像是choke-points,大型开放区域,大型障碍。
否则,你可以用简单的距离测试等式在你的AI决策上来推测你的世界。既不需要存储势力图在内存中,也不需要花费计算在上面。
陈述
高精度的网格,粗糙的区域图,路点网络或者一个空间中的粗糙网格;都可以成为一个有效的展示势力图的模型表示。而且还有更多选择。通常有两件事你需要从你的势力图中获取:
空间划分 —— 一种简单有效的划分空间且存储信息的方式。这允许势力图去存储关于通过、聚集等的信息。
连通性(可选)—— 在这些空间划分之间的一种连通性的标示。这些连接允许势力图算法去预测势力
如何散布,以及预测未来会发生什么。
显然,重要的是哪一种是你的游戏的最好选择?通常来说,更高精度的模型表示在底层决策很有用,更大的划分方式更加适合高层决策。
a)2D 网格
这是一种不错的默认的模型表示,如果你能把你的世界映射为一个2D环境。它执行起来非常简单
也非常快。然而,缺点是如果你的2D环境是稀疏的话会浪费内存。
b) 图
如果你已经有一个分层的导航图,那么你可以用它来作为势力图的基础的模型表示。这个方法的优点
是这不是一个非常侵入式的改变且能简单的合并到大部分游戏引擎当中。而缺点则是做出决策时所需要
的细节会有精度不足问题。
c) 路点网络
使用一个3D全路点网络解决了一些2D网格中的问题,你可以简单的缠绕一个路点网络到
多级的建筑上。然而,缺点是它比2D网格和区域图执行起来非常低效。
d) 粗糙的网格
最后一种选择是减少网格的分辨率。不幸的是,这会导致某些网格单元生成在障碍上,
以致在更新势力时出现问题。一种可替换的方案是移除那些分裂的单元,但是这可能导致
丢失连通性。使用这些单元去存储那些非连通的势力也是一种选择,虽然这会导致一个问题,
不能预测未来势力的分布。
算法
从根本上说,这个算法很像照片编辑软件(比如Photoshop)的模糊处理算法。算法开始于在地图中设置一些势力源,然后通过邻接点重复的模糊化这张地图以散布这些势力源。这个算法实际是非常灵活的。虽然它有两个步骤组成,但是你能以你认为恰当的任意顺序执行并定制它们。
1)设置势力
通常来说,第一步是设置一些势力源在你的模型表现中(2D网格等)。在一个势力浮点数组中尽可能简单的存储或更新。唯一的挑战是解决势力值得来源。一般这些来源可以是:
1. 实体,友好的和敌对的士兵,半永久的炮塔等等。
2. 事件像是手雷的爆炸,子弹,受到损害等。
你也需要区分这些势力值来源是附加的(比如附加在已有的势力值上)还是那些基础的势力值。这基本上基于你的游戏是什么样的,不过一些临时的事件像是子弹,它们总是适合作为附加的势力值。
2)传播
传播算法本身是非常简单的,一页代码就可以写完。有很多种不同的方法可以实现且有很多参数也可以工作的很好。。。不过下面是一个很好的例子作为开始:
voidInfluenceMap::propagateInfluence()
{
for (size_t i = 0; i < m_pAreaGraph->getSize(); ++i)
{
float maxInf = 0.0f;
Connections& connections = m_pAreaGraph->getEdgeIndices(i);
for (Connections::const_iterator it = connections.begin();
it != connections.end(); ++it)
{
const AreaConnection& c = m_pAreaGraph->getEdge(*it);
float inf = m_Influences[c.neighbor] * expf(-c.dist * m_fDecay);
maxInf = std::max(inf, maxInf);
}
m_Influences[i] = lerp(m_Influences[i], maxInf, m_fMomentum);
}
}
一些需要注意的要点:
- 如果你想要你的势力值计算正确的话你需要准备双缓存。你可以通过设置一个新的势力值来做到,之后再拷贝到m_Influences数组。如果你要在每一帧执行一个传播步骤,你可以回避掉拷贝过程(在一些附加的过程中做)
- 如果没有双缓存,势力值可能传播的很困难,这基于你图中点的顺序以及你如何处理它们。这会使得势力看上去有一点不规则,但不是什么致命的错误。
- 这里的代码对势力传播用的指数衰减有一些很好的特性。然而,它比线性衰减有一点慢。
- 上面的代码只控制了正势力值得传播。如果你也需要控制负势力值,你可以计算最小的势力值并合并它们。
参数
剩下的挑战就是解决上面代码中的各种参数了。下面是一些你需要关注的最重要的参数。
a) 权重
当你更新势力值时,更新的值应该对已有的值施加多少影响呢?上面的代码使用了线性插值来混合两者,之后依赖权重参数去控制结果。
如果你设置很高的权重(接近1.0),那么算法会偏向于旧有的势力值,这尤其适用于统计那些已经发生的攻击。用它来做一些类似高层战略的地图。相反的,如果你设置很低的权重(接近于0.0),那么算法会偏向于当前计算的势力值,这会导致传播发生的非常快速并且预测会更加精确。把它使用到低层次战略功能,比如预测个人位置。
b) 衰减
势力值根据距离应该衰减的多快呢?在上面的代码块中,这是通过一个乘数乘以距离来控制的,所以你能
控制衰减的快慢。
基本上,你可以使用不同的衰减值,这基于你的势力图的大小。较慢的衰减适用于较大的战略地图,
当地图局部有战略意图时适用于较快的衰减。如果你想要每个单位的势力散布是不同的,那么你需要实现
不同的势力图,或者使用一种定制的算法在每个单元里存储附加的信息。
c) 更新频率
更新频率这个参数你会有较少的控制。这取决于你的AI模块有多少资源可以使用。幸运的是,
势力图可以按比例缩小,但是有一个基本的计算次数需要做以得到比较好质量的信息。
大部分时候,你能以比较少的频率更新高层战略地图,比如0.5Hz到1Hz。那些低层次的个体
战略,考虑使用频率在2Hz到5Hz。你根本不需要它的频率达到30FPS.
结论
1. 建立势力图并使其工作是尤其简单的。甚至少于一页代码
2. 你很可能使用不同的势力图去提供更好的信息给AI
3. 明智的选择地图的模型表示,通过2D网格和你已经有的区域图是最安全的选择
4. 当你调整势力图的参数时,预期会有大量的迭代在你的AI决策上