简介

日轮啊!毁灭这些AI把!

本文为游戏人工智能编程案例精粹笔记。大部分代码最好还是直接下载他的源代码进行阅读。

强烈推荐直接看源码,不要看书,这书可能是因为我看的是翻译版?但是这本真的不行。

操作行为

部分变量介绍

Vector2D m_vVelocity;   // 速度
Vector2D m_vHeading; // 一个标准化向量,指向实体的朝向
Vector2D m_vSide; // 垂直于朝向向量的向量

double m_dMass; // 质量
double m_dMaxSpeed; // 实体的最大速度
double m_dMaxForce; // 实体产生的供以自己动力的最大力
double m_dMaxTurnRate; // 能旋转的最大速率(弧度每秒)

Seek(靠近)

Seek行为返回一个操纵物体到达目标位置的力。

Vector2D SteeringBehaviors::Seek(Vector2D TargetPos)
{
Vector2D DesiredVelocity = Vec2DNormalize(TargetPos - m_pVehicle->Pos()) * m_pVehicle->MaxSpeed();
return (DesiredVelocity - m_pVehicle->Velocity());
}

vsMUET.png

计算预期的速度。这个速度是物体在理想化情况下到达目标位置所需的速度。它是从物体到目标的向量,大小为物体的最大速度。

当然这里的大小不一定非要是物体的最大速度,你也可以再增加一个权重来影响靠近带来的力。主要是为了表达物体有一个移动趋向,具体数值都可以进行配置。

比如说以下代码,会使用权重进行分配。

Vector3 SteerTowards(Vector3 vector)
{
Vector3 v = vector.normalized * settings.maxSpeed - velocity;
return Vector3.ClampMagnitude(v, settings.maxSteerForce);
}

Vector3 offsetToTarget = (target.position - position);
Vector3 acceleration = SteerTowards(offsetToTarget) * settings.targetWeight;

Flee(离开)

Flee和Seek相反。Flee产生一个操控物体离开的力。

其实本质上和Seek完全相同,但是一般这个力不会像Seek一样进行计算,通常会探知周围环境再进行决定离开方向以及力度。

我个人觉得一个比较优秀的AI,就要看他能感知的东西多少,能感知的东西越多,那么他越智能。

Vector2D SteeringBehaviors::Flee(Vector2D TargetPos)
{
const double PanicDistanceSq = 100.0 * 100.0;
if(Vec2DDistanceSq(m_pVehicle->Pos(), TargetPos) > PanicDistanceSq)
{
// 范围之外
return Vector2D(0, 0);
}
Vector2D DesiredVelocity = Vec2DNormalize(m_pVehicle->Pos() - TargetPos) * m_pVehicle->MaxSpeed();
return (DesiredVelocity - m_pVehicle->Velocity());
}

Arrive(抵达)

Seek行为可以让一个物体面对正确方向进行移动,但是这样的移动会产生急停效果,如果想要做一个减速效果的话显然那样是不行的。Arrive行为即可操作物体慢慢减速直到物体抵达目标位置。

// 对距离判定最好进行一个范围判断,毕竟这是float
enum Deceleration{slow = 3, normal = 2, fast = 1};
// 该枚举作用其实就是判断物体接近目标点的行为模式
// 大致作用是用来限制速度,后续会对速度进行一定调整
Vector2D SteeringBehaviors::Arrive(Vector2D TargetPos, Deceleration deceleration)
{
Vector2D offset = TargetPos - m_pVehicle->Pos();
//计算到目标位置的距离
double dist = offset.Length();
if(dist > 0)
{
// 因为枚举是整数int,所以使用该值进行速度调整
const double decelerationTweaker = 0.3;
// 给定预期速度,计算到达目标位置所需速度
double speed = dist / ((double)deceleration * decelerationTweaker);
// 限制最大速度
speed = min(speed, m_pVehicle->MaxSpeed());
// 后续和Seek差不多
// 或许 offset / dist * speed 更清晰一些?
Vector2D DesiredVelocity = offset * speed / dist;
return (DesiredVelocity - m_pVehicle->Velocity());
}
return Vector2D(0, 0);
}

其实个人觉得这其实就是个权重问题,在物体越来越靠近的时候权重会进行相应的减少,导致Seek力度变小,速度也随之减少,这样就形成了减速效果。

Pursuit(追逐)

很多游戏设计的时候都需要一个设定,那就是怪物会去拦截我们的角色,总不能拉着怪物开火车把。当然也不是没有开火车的游戏,在设定方面看需求即可。

想象你是一个小孩,在操场上玩抓人游戏。当你想要抓到某人的时,你不会直接去他们当前的位置。通常你都会预测他们未来的位置,然后移向哪个偏移位置。

vsMaUU.md.png

图就这么一看把。。雀氏找不到高清一点的了,但是我又懒得自己画。知道大致意思即可,就是需要一次预测。

pursuit的成功与否取决于追逐者预测逃避者的运动轨迹有多准。这可以很复杂,但是复杂通常也代表效率会受到影响。

追逐者可能会碰到一种提前结束的情况:如果逃避者在前面,几乎面对物体,那么物体应该直接向逃避者移动。这可以通过点积快速算出。在书中提供的代码,逃避者朝向的反方向和物体的朝向必须在20°20°以内才被认为是面对着的。

一个好的预测难点就是预测多远。很明显,这个"多远"应该正比于追逐者与逃避者的距离,反比于追逐者和逃避者的速度。一旦这个"多远"确定了,我们就可以估算出追逐者Seek的位置。

Vector2D SteeringBehaviors::Pursuit(const Vehicle* evader)
{
Vector2D offset = evader->Pos() - m_pVehicle->Pos();
double relativeHeading = m_pVehicle->Heading().Dot(evader->Heading());
if((offset.Dot(m_pVehicle->Heading()) > 0) && (relativeHeading < -0.95))
{
// acos(0.95) = 18 degs
return Seek(evader->Pos());
}
// 预测逃避者的位置
double lookAheadTime = offser.Length() / (m_pVehicle->MaxSpeed() + evader->Speed());
return Seek(evader->Pos() + evader->Velocity() * lookAheadTime);
}

一些运动模型也可能需要考虑物体转向偏移位置所需的时间。通过给lookAheadTime加上正比于两个朝向向量的点积分和最大转弯率的值,可以简单的实现。

lookAheadTime += TurnAroundTime(m_pVehicle, evader->Pos());

double TurnAroundTime(const Vehicle* pAgent, Vector2D TargetPos)
{
Vector2D offset = Vec2DNormalize(TargetPos - pAgent->Pos());
double dot = pAgent->Heading().Dot(offset);
// 改变这个值得到与其行为
// 物体的最大转弯率越高,这个值越大
// 如果物体正在朝向到目标位置的反方向,那么0.5这个值意味着这个函数返回1s的时间以便让物体转弯
const double coefficient = 0.5;
// 如果目标直接在前面,那么点积为1
// 如果目标直接再后面,那么点积为-1
// 减去1,再除以负的coefficient,得到一个正数值
return (dot - 1.0) * -coefficient;
}

Evade(逃避)

除了逃避者远离预测的位置这一点,evade几乎和pursuit一样。

Vector2D SteeringBehaviors::Evade(const Vehicle* pursuer)
{
Vector2D offsert = pursuer->Pos() - m_pVehicle->Pos();
double lookAheadTime = offset.Length() / (m_pVehicle->MaxSpeed() + puresuer->Speed());
return Flee(pursuer->Pos() + puresuer->Velocity() * lookAheadTime);
}

注意:这次没有必要检查面向方向

Wander(徘徊)

它产生一个操作力,使物体在环境中随机走动。(巡逻罢了)

一个比较简单的做法是每帧都计算出一个随机的驱动力,但这会产生抖动。(当然,一个好的随机算法,或者噪声之类的可以进行优化,但是这会增加CPU的开销)

解决方案是在物体前端突出一个圆圈,目标被限制在该圆圈上,然后我们移向目标。每帧给目标添加一个随机的位移,随着时间的推移,沿着圆周进行移动,以此创建一个没有抖动的往复运动。

vsMd5F.md.png

double m_dWanderRadius;         // 圆的半径
double m_dWanderDistance; // 圆突出在物体前面的距离
double m_dWanderJitter; // 随机位移最大值

Vector2D SteeringBehaviors::Wander()
{
// RandomClamped() 返回-1至1之间的一个数
// 增加一个随机向量到目标位置
m_vWanderTarget += Vector2D(RandomClamped() * m_dWanderJitter, RandomClamped() *m_dWanderJitter);
// m_vWanderTarget是一个点,被限制半径为m_dWanderRadius的圆上,以物体为中心
// 我们每帧都给wander目标位置添加一个小的随机位移

// 把向量重新投影到单位圆上
m_vWanderTarget.Normalize();
// 改变向量的长度为半径长度
m_vWanderTarget *= m_dWanderRadius;
// 移动目标到物体前面WanderDist的位置
Vector2D targetLocal = m_vWanderTarget + Vector2D(m_dWanderDIstance, 0);
// 把目标投影到世界空间
Vector2D targetWorld = PointToWorldSpace(targetLocal, m_pVehicle->Heading(), m_pVehicle->Side(), m_pVehicle->Pos());

return targetWorld - m_pVehicle->Pos();
}

vsM0C4.png

Obstacle Avoidance(避开障碍)

Obstacle Avoidance行为操控物体避开路上的障碍。障碍物是任何一个近似圆周的物体。我们保持长方形区域(碰撞盒)不被碰撞,就可以躲避障碍了。

碰撞盒的宽度等于物体的包围半径,长度正比于物体当前速度。(它移动的越快,检测盒就越长)

vsMlCQ.png

寻求最近的交点

vsM52d.md.png

  1. 物体只考虑那些在检测盒内得障碍物。最初,避开障碍算法迭代游戏世界中所有的障碍,标记那些在检测盒内的障碍物以作进一步分析。
  2. 算法把所有已标记的障碍物转换到物体的局部空间。转换坐标后,那些xx坐标为负值得物体将不被考虑。
  3. 接下来,该算法必须测试障碍物是否和检测盒重叠。使障碍物的包围半径扩大,具体数值为物体包围半径宽度的一半。然后测试该障碍物的yy值是否小于这个值(即障碍物的包围半径加上检测盒宽度的一半)。如果不小于,那么该障碍将不会与检测盒相交,可以不予继续讨论。
  4. 此时,只剩下那些会与检测盒相交的障碍物了。接下来,我们找出离交通工具最近的相交点。我们将再一次在局部空间中计算,33步骤扩大了障碍物的包围半径。我们用简单的线-圆周相交测试方法可以得到被扩大的圆和xx轴的交点。

说实话,上述算法个人觉得没有很必要,如果自己在Unity实现的话没必要如此麻烦,简单的检测即可。如果想追求原理更推荐games104。主要看思想即可。

要是我实现会先Physics.SphereCast()直接判断前方是否存在障碍,之后开始360激光炮硬射。

10.游戏引擎中物理系统的基础理论和算法 | GAMES104-现代游戏引擎:从入门到实践_哔哩哔哩_bilibili

计算操控力

操控力一半分为两部分:侧向操控力和制动操作力。

vsymND.md.png

侧边操控力:障碍物包围半径减去其在局部空间的x值。这会产生一个侧向操控力使其远离障碍物,它会随着障碍物到x轴的距离而减少。该力正比于物体到障碍物的距离(因为物体离障碍物越近,反应越快)

原文说的是减去y值,但是?这合理吗,所以我这里自行做了修改,当然如果这里雀氏是y,那我直接改回来呜呜呜。

制动力:沿着x轴负方向即可,大小正比于物体到障碍的距离。

最后把操控力转换到世界空间即可。

具体算法

Vector2D SteeringBehaviors::ObstacleAvoidance(const std::vector<BaseGameEntity *> &obstacles) 
{
// BaseGameEntity 物体基类
m_dDBoxLength =
Prm.MinDetectionBoxLength + (m_pVehicle->Speed() / m_pVehicle->MaxSpeed()) * Prm.MinDetectionBoxLength;
// 标记在范围内的所有障碍物
m_pVehicle->World()->TagObstaclesWithinViewRange(m_pVehicle, m_DBoxLength);
// 跟踪最近的相交的障碍物(CIB)
BaseGameEntity *closestIntersectingObstacle = NULL;
// 记录CIB被转化的局部坐标
double distToClosestIP = MaxDouble;
// 记录CIB被转化的局部坐标
Vector2D localPosOfClosestObstacle;
std::vector<BaseGameEntity *>::const_iterator curOb = obstacles.begin();
while (curOb != obstacles.end())
{
// 如果该障碍物被标记在范围内
if ((*curOb)->IsTagged())
{
// 计算障碍物在局部空间的位置
Vector2D localPos = PointToLocalSpace((*curOb)->Pos(), m_pVehicle->Heading(), m_pVehicle->Side(),
m_pVehicle->Pos());

// 如果局部空间位置x为负,那么它肯定在物体后面
if (localPos.x >= 0)
{
// 如果物体到x轴距离小于它的半径 + 检查盒宽度的一半,那么有可能相交
double expandedRadius = (*curOb)->BRadius() + m_pVehicle->BRadius();
if (fabs(localPos.y) < expandedRadius) {
// 线圆相交检测
// 圆周的中心是(cX , cY)
// 相交点的公式是x = cX +/-sqrt(r^2-cY^2)
// 注意y = 0,最后我们只需要x的最小正值即可
double cX = localPos.x;
double cY = localPos.y;

// 后半部分书籍中的代码几乎全错,我被迫寻找源码
// 说实话 我都怀疑这本书是翻译是机翻。。

// 计算上面等式的开方
double sqrtPart = sqrt(expandedRadius * expandedRadius - cY * cY);
double ip = cX - sqrtPart;

if (ip <= 0.0)
{
ip = cX + sqrtPart;
}

//记录目前为止最近的
if (ip < distToClosestIP)
{
distToClosestIP = ip;
closestIntersectingObstacle = *curOb;
localPosOfClosestObstacle = localPos;
}

}
}
}
++curOb;
}
// 如果我们找到一个相交的障碍物,计算一个远离它的操控力
Vector2D steeringForce;
if (closestIntersectingObstacle)
{
// 物体离障碍物跃进,操控力就应该越强
double multiplier = 1.0 + (m_dDBoxLength - LocalPosOfClosestObstacle.x) / m_dDBoxLength;
steeringForce.y = (closestIntersectingObstacle->BRadius() - localPosOfClosestObstacle.y) * multiplier;
// 施加一个制动力,它正比于物体到障碍物的距离
const double brakingWeight = 0.2;
steeringForce.x = (closestIntersectingObstacle->BRadius() - localPosOfClosestObstacle.x) * brakingWeight;
}
return VectorToWorldSpace(steeringForce, m_pVehicle->Heading(), m_pVehicle->Side());
}

Wall Avoidance(避开墙)

一堵墙就是一条线段(在3D中是一个多边形),该线段的法线为墙的朝向。

我们在物体前面突出3根触须(这不就是射线),分别测试它们是否和游戏世界中的任何墙相交。

如图所示,墙中间出现的小线就是墙的法线方向。

vsyxKI.md.png

void SteeringBehavior::CreateFeelers()
{
// 射线指向前方
m_Feelers[0] = m_pVehicle->Pos() + m_dWallDetectionFeelerLength * m_pVehicle->Heading();

// 射线指向左边
Vector2D temp = m_pVehicle->Heading();
Vec2DRotateAroundOrigin(temp, HalfPi * 3.5f);
m_Feelers[1] = m_pVehicle->Pos() + m_dWallDetectionFeelerLength/2.0f * temp;

// 射线指向右边
temp = m_pVehicle->Heading();
Vec2DRotateAroundOrigin(temp, HalfPi * 0.5f);
m_Feelers[2] = m_pVehicle->Pos() + m_dWallDetectionFeelerLength/2.0f * temp;
}
Vector2D SteeringBehavior::WallAvoidance(const std::vector<Wall2D> &walls) {
// 射线包含在std::vector m_Feelers
CreateFeelers();

double distToThisIP = 0.0;
double distToClosestIP = MaxDouble;

// 保存一个墙体的向量索引
int closestWall = -1;

Vector2D steeringForce,
point, // 用于存储临时信息
closestPoint; // 持有最接近的交点

// 逐个检查每条射线(翻译的都是触须,但是我不能接受啊)
for (unsigned int flr = 0; flr < m_Feelers.size(); ++flr) {
//遍历墙体,检查相交点
for (unsigned int w = 0; w < walls.size(); ++w) {
// LineIntersection2D() 这个函数很奇怪,我翻了一下源码没有找到
// 猜测是我没下载完?但是不应该啊
// 但是从名字可以看出来这是2D版本的相交点检测
if (LineIntersection2D(m_pVehicle->Pos(),
m_Feelers[flr],
walls[w].From(),
walls[w].To(),
distToThisIP,
point)) {
// 记录目前最近点
if (distToThisIP < distToClosestIP) {
distToClosestIP = distToThisIP;
closestWall = w;
closestPoint = point;
}
}
}

// 如果检测到一个相交点,那么计算出一个使物体远离的力
if (closestWall >= 0) {
// 计算射线超过墙的距离
Vector2D overShoot = m_Feelers[flr] - closestPoint;
// 在墙体法线方向产生一个力,大小为刚才算出来的距离
steeringForce = walls[ClosestWall].Normal() * overShoot.Length();
}
}
return steeringForce;
}

Interpose(插入)

interpose行为返回一个操控力,该力操控物体移到两个物体(或者说是空间中的两个点)的中点。

保镖拿枪保护他的老板或者足球运动员拦截球都是这类行为的典型例子。

像pursuit那样,物体必须估算出这两个物体在时刻T时达到的位置,然后才能移到哪个位置。但是我们如何知道T的最佳值?显然没有办法。所以我们需要经过预测来替代。

计算该力的第一步就是要得到连接两个物体当前位置的线的中点。计算物体到该点的距离,然后除以物体的最大速度,就能得到走完这段距离要花的时间。即我们的T值。

我们使用T来推测出物体未来的位置,然后得到这些预测位置的中点,最后物体使用arrive行为移至该点。

vsWKC8.png

Vector2D SteeringBehavior::Interpose(const Vehicle *AgentA, const Vehicle *AgentB) {
// 首先,我们需要计算出未来T时间,这两个物体的位置
// 物体以最大速度到达目前两个物体重点所需时间近似于T
Vector2D MidPoint = (AgentA->Pos() + AgentB->Pos()) / 2.0;

double TimeToReachMidPoint = Vec2DDistance(m_pVehicle->Pos(), MidPoint) /
m_pVehicle->MaxSpeed();

// 现在我们有了时间T,我们就可以预测未来两个物体的位置
Vector2D APos = AgentA->Pos() + AgentA->Velocity() * TimeToReachMidPoint;
Vector2D BPos = AgentB->Pos() + AgentB->Velocity() * TimeToReachMidPoint;

// 使用预测位置来计算中点,该点即为目标点
MidPoint = (APos + BPos) / 2.0;

return Arrive(MidPoint, fast);
}

Hide(隐藏)

该行为是为了找到一个位置使得物体能够躲在障碍物后以此躲避另一个物体(警察抓小偷)。

当你需要一个能躲开玩家的NPC时或者一个溜到玩家身后的NPC时,可以使用该行为。

书中使用如下方法来实现上面行为。

第一步,对于每个障碍物,计算出一个隐藏点。

vsf9Zn.md.png

给定目标位置、障碍物的位置和半径,该方法计算距离物体的包围半径为distanceFromBoundary且直接背对目标的位置。

它是这么做的,首先标准化"到障碍物"的向量,接着让该向量大小为到障碍物中心的距离,最后把结果加到障碍物位置上。

// 给出一个目标的位置,以及一个障碍物的位置和半径。该方法计算出一个远离其边界的位置,并直接与猎人相对。
Vector2D SteeringBehavior::GetHidingPosition(const Vector2D &posOb, const double radiusOb, const Vector2D &posHunter)
{
// 计算物体离选中的障碍物的包围半径有多远
const double distanceFromBoundary = 30.0;
double distAway = radiusOb + distanceFromBoundary;

// 计算出从目标到物体的方向
Vector2D toOb = Vec2DNormalize(posOb - posHunter);

// 确定大小并加到障碍物的位置,得到隐藏点
return (toOb * distAway) + posOb;
}

第二步,计算出每个隐藏点的距离。然后使用arrive行为移到最处,如果找不到合适的障碍物,物体将会evade(逃避)目标。

Vector2D SteeringBehavior::Hide(const Vehicle *hunter, const vector<BaseGameEntity *> &obstacles)
{
double distToClosest = MaxDouble; // 最接近的距离
Vector2D bestHidingSpot; // 最佳藏身点

std::vector<BaseGameEntity *>::const_iterator curOb = obstacles.begin();
std::vector<BaseGameEntity *>::const_iterator closest;

while (curOb != obstacles.end())
{
// 计算这个障碍物的隐藏点位置
Vector2D hidingSpot = GetHidingPosition((*curOb)->Pos(), (*curOb)->BRadius(), hunter->Pos());

// 找到目前离物体最近的隐藏点
double dist = Vec2DDistanceSq(hidingSpot, m_pVehicle->Pos());

if (dist < distToClosest)
{
distToClosest = dist;
bestHidingSpot = hidingSpot;
closest = curOb;
}
++curOb;
}
// 如果没有找到合适的障碍物,进入躲避
if (distToClosest == MaxFloat)
{
return Evade(hunter);
}
// 前进把!
return Arrive(bestHidingSpot, fast);
}

书上还说了一些可以进行修改的部分,但是我个人觉得没必要,其中大多都是感知方面的内容,比如只有目标在可视范围内才允许隐藏,这些都可以根据需求来具体设计。

Path Following(路径跟随)

该行为产生一个操控力,使物体沿着构成路径的一系列点进行移动。有的时候,路径有起点和重点。而而有的时候路径是循环的,是一个永不结束的封闭路径。

DOTween的DoPath()既视感太强啦。其实魔兽的插旗也可以这么理解,但是插旗可以操作的不止是移动

vsojzV.png

最简单的方案就是设置当前路点为链表中的第一节点,用seek操控靠近,直到到达它的目标距离之内。然后找到下一个路点,靠近它,如此下去,直到当前的路点是最后一个。这时,物体到达最后一个路店,如果路径是封闭的环,那么路点会被再次设为第一个。

//  给定一系列的Vector2D,这个方法产生一个力,使代理按顺序沿着路点移动
// 代理人使用 "寻找 "行为来移动到下一个航点
// 除非它是最后一个航点,在这种情况下,它会 "到达"。
Vector2D SteeringBehavior::FollowPath()
{
// 如果离当前目标足够近,移到下一个目标
if (Vec2DDistanceSq(m_pPath->CurrentWaypoint(), m_pVehicle->Pos()) <
m_dWaypointSeekDistSq) {
m_pPath->SetNextWaypoint();
}

if (!m_pPath->Finished()) {
return Seek(m_pPath->CurrentWaypoint());
} else {
return Arrive(m_pPath->CurrentWaypoint(), normal);
}
}

inline void Path::SetNextWaypoint() {
assert(m_WayPoints.size() > 0);
// 主要是为了看一眼实现循环
if (++curWaypoint == m_WayPoints.end()) {
if (m_bLooped) {
curWaypoint = m_WayPoints.begin();
}
}
}

Offset Pursuit(保持一定偏移的追逐)

相信玩过魔兽等一系列游戏的玩家都有过编队这个习惯,这会保证多个物体之间保持一定距离,不会出现近战跑的飞快,远程还在后面摸鱼的情况。

vsTdoj.md.png

其实就是固定一个领导者和一个固定的偏移量,之后预测下一个位置即可。

Vector2D SteeringBehavior::OffsetPursuit(const Vehicle *leader, const Vector2D offset) {
// 计算出偏移量在世界空间中的位置
Vector2D worldOffsetPos = PointToWorldSpace(offset, leader->Heading(), leader->Side(), leader->Pos());

Vector2D toOffset = worldOffsetPos - m_pVehicle->Pos();

// 预测的时间正比于领导者和追逐者的距离
// 反比于两者速度之和
double lookAheadTime = toOffset.Length() / (m_pVehicle->MaxSpeed() + leader->Speed());

// 前往预测位置
return Arrive(worldOffsetPos + leader->Velocity() * lookAheadTime, fast);
}

组行为

介绍

组行为是考虑游戏世界中一些或者所有的其他物体的操作行为。不少人都提过类鸟群这类的名词,其实就是指这个。

可以看一下这篇是如何实现的,这个up不少东西还是挺有意思的

https://www.youtube.com/watch?v=bqtqltqcQhw

为了得到组行为的操控力,物体将在一个以自我为中心且以一个预定义尺寸为半径的圆形区域内(称为领近半径)考虑所有其他物体。

vsLNIe.md.png

如图所示,白色的物体是我们目前使用的物体,灰色的圆形代表着领近的范围。因此黑色的物体都是它的邻居,但灰色的不是。(可能图片不是很明显,但是我觉得这个圈还是很明显的)

在计算操控力之前,我们必须得到物体的邻居。在实例中使用BaseGameEntity::Tag()方法来标记物体的邻居。这都在函数模板TagNeighbors中完成。

template<class T, class conT>
void TagNeighbors(const T &entity, conT &ContainerOfEntities, double radius)
{
// 迭代所有实体,检查范围
for (typename conT::iterator curEntity = ContainerOfEntities.begin();
curEntity != ContainerOfEntities.end(); ++curEntity) {
// 清楚当前标记
(*curEntity)->UnTag();

Vector2D to = (*curEntity)->Pos() - entity->Pos();

// 考虑边界半径,将其加入到范围内
double range = radius + (*curEntity)->BRadius();

// 如果实体在范围内,进行标记
if (((*curEntity) != entity) && (to.LengthSq() < range * range)) {
(*curEntity)->Tag();
}
}
}

大部分组行为都使用相似的邻近半径,所以我们在调用任何组行为之前,先调用这个方法,从而节约一些时间。

if(On(separation) || On(alignment) || On(cohesion))
{
TagNeighbors(m_pVehicle, m_pVehicle->World()->Agents(), ViewDistance);
}

可以给物体增加可视域(field-of-view)的限制,从而增加组行为的真实性。例如,对于邻近区域内的物体,你可以只标记那些在可视域内的,比方说,在物体朝向270°范围内的。

Separation(分离)

该行为产生一个力,操控物体离开在它的邻近区域中的那些物体。当应用在许多物体上时,他们将四周展开,尽量和其他每个交通工具拉开距离。

vyAcwQ.md.png

说实话,个人觉得这本书的配图有点问题,比如这个图,你给我的反应是修改周围邻居的数据让他远离本物体。但实际上是操控自己。

后面的图我觉得也有同样的问题。

在调用Separation之前,所有在物体邻近区域的物体将会被标记。然后Separation迭代检查每个被标记的物体,标准化物体(中间的物体)到其他被标记物体的向量,接着除以其到邻居的距离,再加到操控力上。

Vector2D SteeringBehavior::Separation(const vector<Vehicle *> &neighbors)
{
Vector2D SteeringForce;
for (unsigned int a = 0; a < neighbors.size(); ++a) {
// 确保自身不会被计算
// 确保被检查的足够近(其实就是被标记)
// m_pTargetAgent1变量在书中未被提到,该变量保存的是跟踪的目标
if ((neighbors[a] != m_pVehicle) && neighbors[a]->IsTagged() && (neighbors[a] != m_pTargetAgent1))
{
Vector2D ToAgent = m_pVehicle->Pos() - neighbors[a]->Pos();

// 力的大小反比于物体到他的邻居的距离
SteeringForce += Vec2DNormalize(ToAgent) / ToAgent.Length();
}
}
return SteeringForce;
}

Alignment(队列)

Alignment行为企图保持物体的朝向和邻居一致。

vyfSB9.md.png

我们通过迭代所有邻居,平均他们的朝向向量来计算一个力,这就是我们需要的朝向,之后再减去物体的朝向即可得到操控力。

Vector2D SteeringBehavior::Alignment(const vector<Vehicle *> &neighbors)
{
Vector2D AverageHeading; // 记录朝向向量平均值
int NeighborCount = 0; // 记录邻居总数目
// 迭代所有被标记的物体,计算总朝向向量
for (unsigned int a = 0; a < neighbors.size(); ++a) {
// 确保自身不会被计算
// 确保被检查的足够近(其实就是被标记)
// m_pTargetAgent1变量在书中未被提到,该变量保存的是跟踪的目标
if ((neighbors[a] != m_pVehicle) && neighbors[a]->IsTagged() &&(neighbors[a] != m_pTargetAgent1))
{
AverageHeading += neighbors[a]->Heading();
++NeighborCount;
}
}
// 计算平均值
if (NeighborCount > 0) {
AverageHeading /= (double) NeighborCount;
AverageHeading -= m_pVehicle->Heading();
}
return AverageHeading;
}

Cohesion(聚集)

Cohesion行为会产生一个操控力,使物体移向邻居的中心。

vy4EeH.png

基本上和其他组行为逻辑没什么区别,通过邻居算出目标位置,之后使用Seek来计算所需操控力。

Vector2D SteeringBehavior::Cohesion(const vector<Vehicle *> &neighbors) {
// 首先找到所有邻居的中心
Vector2D CenterOfMass, SteeringForce;
int NeighborCount = 0;
// 遍历邻居,并对所有位置向量进行求和
for (unsigned int a = 0; a < neighbors.size(); ++a) {
// 确保自身不会被计算
// 确保被检查的足够近(其实就是被标记)
// m_pTargetAgent1变量在书中未被提到,该变量保存的是跟踪的目标
if ((neighbors[a] != m_pVehicle) && neighbors[a]->IsTagged() &&(neighbors[a] != m_pTargetAgent1)) {
CenterOfMass += neighbors[a]->Pos();

++NeighborCount;
}
}

if (NeighborCount > 0) {
CenterOfMass /= (double) NeighborCount;
SteeringForce = Seek(CenterOfMass);
}

// 聚集力通常会比其他两种力大很多,所以选择标准化
// ⬆其实就是规范权重(上面的是代码注释,书中没有这里的标准化)
return Vec2DNormalize(SteeringForce);
}