简介
日轮啊!毁灭这些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()); }
|
计算预期的速度。这个速度是物体在理想化情况下到达目标位置所需的速度。它是从物体到目标的向量,大小为物体的最大速度。
当然这里的大小不一定非要是物体的最大速度,你也可以再增加一个权重来影响靠近带来的力。主要是为了表达物体有一个移动趋向,具体数值都可以进行配置。
比如说以下代码,会使用权重进行分配。
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行为即可操作物体慢慢减速直到物体抵达目标位置。
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) { const double decelerationTweaker = 0.3; double speed = dist / ((double)deceleration * decelerationTweaker); speed = min(speed, m_pVehicle->MaxSpeed()); Vector2D DesiredVelocity = offset * speed / dist; return (DesiredVelocity - m_pVehicle->Velocity()); } return Vector2D(0, 0); }
|
其实个人觉得这其实就是个权重问题,在物体越来越靠近的时候权重会进行相应的减少,导致Seek力度变小,速度也随之减少,这样就形成了减速效果。
Pursuit(追逐)
很多游戏设计的时候都需要一个设定,那就是怪物会去拦截我们的角色,总不能拉着怪物开火车把。当然也不是没有开火车的游戏,在设定方面看需求即可。
想象你是一个小孩,在操场上玩抓人游戏。当你想要抓到某人的时,你不会直接去他们当前的位置。通常你都会预测他们未来的位置,然后移向哪个偏移位置。
图就这么一看把。。雀氏找不到高清一点的了,但是我又懒得自己画。知道大致意思即可,就是需要一次预测。
pursuit的成功与否取决于追逐者预测逃避者的运动轨迹有多准。这可以很复杂,但是复杂通常也代表效率会受到影响。
追逐者可能会碰到一种提前结束的情况:如果逃避者在前面,几乎面对物体,那么物体应该直接向逃避者移动。这可以通过点积快速算出。在书中提供的代码,逃避者朝向的反方向和物体的朝向必须在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)) { 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); const double coefficient = 0.5; 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的开销)
解决方案是在物体前端突出一个圆圈,目标被限制在该圆圈上,然后我们移向目标。每帧给目标添加一个随机的位移,随着时间的推移,沿着圆周进行移动,以此创建一个没有抖动的往复运动。
double m_dWanderRadius; double m_dWanderDistance; double m_dWanderJitter;
Vector2D SteeringBehaviors::Wander() { m_vWanderTarget += Vector2D(RandomClamped() * m_dWanderJitter, RandomClamped() *m_dWanderJitter); m_vWanderTarget.Normalize(); m_vWanderTarget *= m_dWanderRadius; 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(); }
|
Obstacle Avoidance(避开障碍)
Obstacle Avoidance行为操控物体避开路上的障碍。障碍物是任何一个近似圆周的物体。我们保持长方形区域(碰撞盒)不被碰撞,就可以躲避障碍了。
碰撞盒的宽度等于物体的包围半径,长度正比于物体当前速度。(它移动的越快,检测盒就越长)
寻求最近的交点
- 物体只考虑那些在检测盒内得障碍物。最初,避开障碍算法迭代游戏世界中所有的障碍,标记那些在检测盒内的障碍物以作进一步分析。
- 算法把所有已标记的障碍物转换到物体的局部空间。转换坐标后,那些x坐标为负值得物体将不被考虑。
- 接下来,该算法必须测试障碍物是否和检测盒重叠。使障碍物的包围半径扩大,具体数值为物体包围半径宽度的一半。然后测试该障碍物的y值是否小于这个值(即障碍物的包围半径加上检测盒宽度的一半)。如果不小于,那么该障碍将不会与检测盒相交,可以不予继续讨论。
- 此时,只剩下那些会与检测盒相交的障碍物了。接下来,我们找出离交通工具最近的相交点。我们将再一次在局部空间中计算,3步骤扩大了障碍物的包围半径。我们用简单的线-圆周相交测试方法可以得到被扩大的圆和x轴的交点。
说实话,上述算法个人觉得没有很必要,如果自己在Unity实现的话没必要如此麻烦,简单的检测即可。如果想追求原理更推荐games104。主要看思想即可。
要是我实现会先Physics.SphereCast()
直接判断前方是否存在障碍,之后开始360激光炮硬射。
10.游戏引擎中物理系统的基础理论和算法 | GAMES104-现代游戏引擎:从入门到实践_哔哩哔哩_bilibili
计算操控力
操控力一半分为两部分:侧向操控力和制动操作力。
侧边操控力:障碍物包围半径减去其在局部空间的x值。这会产生一个侧向操控力使其远离障碍物,它会随着障碍物到x轴的距离而减少。该力正比于物体到障碍物的距离(因为物体离障碍物越近,反应越快)
原文说的是减去y值,但是?这合理吗,所以我这里自行做了修改,当然如果这里雀氏是y,那我直接改回来呜呜呜。
制动力:沿着x轴负方向即可,大小正比于物体到障碍的距离。
最后把操控力转换到世界空间即可。
具体算法
Vector2D SteeringBehaviors::ObstacleAvoidance(const std::vector<BaseGameEntity *> &obstacles) { m_dDBoxLength = Prm.MinDetectionBoxLength + (m_pVehicle->Speed() / m_pVehicle->MaxSpeed()) * Prm.MinDetectionBoxLength; m_pVehicle->World()->TagObstaclesWithinViewRange(m_pVehicle, m_DBoxLength); BaseGameEntity *closestIntersectingObstacle = NULL; double distToClosestIP = MaxDouble; 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());
if (localPos.x >= 0) { double expandedRadius = (*curOb)->BRadius() + m_pVehicle->BRadius(); if (fabs(localPos.y) < expandedRadius) { 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根触须(这不就是射线),分别测试它们是否和游戏世界中的任何墙相交。
如图所示,墙中间出现的小线就是墙的法线方向。
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) { 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) { 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行为移至该点。
Vector2D SteeringBehavior::Interpose(const Vehicle *AgentA, const Vehicle *AgentB) { Vector2D MidPoint = (AgentA->Pos() + AgentB->Pos()) / 2.0;
double TimeToReachMidPoint = Vec2DDistance(m_pVehicle->Pos(), MidPoint) / m_pVehicle->MaxSpeed();
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时,可以使用该行为。
书中使用如下方法来实现上面行为。
第一步,对于每个障碍物,计算出一个隐藏点。
给定目标位置、障碍物的位置和半径,该方法计算距离物体的包围半径为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()既视感太强啦。其实魔兽的插旗也可以这么理解,但是插旗可以操作的不止是移动
最简单的方案就是设置当前路点为链表中的第一节点,用seek操控靠近,直到到达它的目标距离之内。然后找到下一个路点,靠近它,如此下去,直到当前的路点是最后一个。这时,物体到达最后一个路店,如果路径是封闭的环,那么路点会被再次设为第一个。
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(保持一定偏移的追逐)
相信玩过魔兽等一系列游戏的玩家都有过编队这个习惯,这会保证多个物体之间保持一定距离,不会出现近战跑的飞快,远程还在后面摸鱼的情况。
其实就是固定一个领导者和一个固定的偏移量,之后预测下一个位置即可。
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
为了得到组行为的操控力,物体将在一个以自我为中心且以一个预定义尺寸为半径的圆形区域内(称为领近半径)考虑所有其他物体。
如图所示,白色的物体是我们目前使用的物体,灰色的圆形代表着领近的范围。因此黑色的物体都是它的邻居,但灰色的不是。(可能图片不是很明显,但是我觉得这个圈还是很明显的)
在计算操控力之前,我们必须得到物体的邻居。在实例中使用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(分离)
该行为产生一个力,操控物体离开在它的邻近区域中的那些物体。当应用在许多物体上时,他们将四周展开,尽量和其他每个交通工具拉开距离。
说实话,个人觉得这本书的配图有点问题,比如这个图,你给我的反应是修改周围邻居的数据让他远离本物体。但实际上是操控自己。
后面的图我觉得也有同样的问题。
在调用Separation
之前,所有在物体邻近区域的物体将会被标记。然后Separation
迭代检查每个被标记的物体,标准化物体(中间的物体)到其他被标记物体的向量,接着除以其到邻居的距离,再加到操控力上。
Vector2D SteeringBehavior::Separation(const vector<Vehicle *> &neighbors) { Vector2D SteeringForce; for (unsigned int a = 0; a < neighbors.size(); ++a) { 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行为企图保持物体的朝向和邻居一致。
我们通过迭代所有邻居,平均他们的朝向向量来计算一个力,这就是我们需要的朝向,之后再减去物体的朝向即可得到操控力。
Vector2D SteeringBehavior::Alignment(const vector<Vehicle *> &neighbors) { Vector2D AverageHeading; int NeighborCount = 0; for (unsigned int a = 0; a < neighbors.size(); ++a) { 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行为会产生一个操控力,使物体移向邻居的中心。
基本上和其他组行为逻辑没什么区别,通过邻居算出目标位置,之后使用Seek来计算所需操控力。
Vector2D SteeringBehavior::Cohesion(const vector<Vehicle *> &neighbors) { Vector2D CenterOfMass, SteeringForce; int NeighborCount = 0; for (unsigned int a = 0; a < neighbors.size(); ++a) { 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); }
|